Laravel 7

Shopping : les statistiques

Pour compléter le projet de boutique en ligne je vous propose dans cet article de mettre en place quelques statistiques : le nombre de commandes et de nouveaux clients. On doit pouvoir choisir l’année concernée.

Vous pouvez télécharger un ZIP du projet ici.

Un package

Il existe quelques packages pour dessiner des graphes mais celui que je préfère est Laravel Charts :

La version 7 vient tout juste d’être lancée avec un remaniement de fond en particulier le choix de Chartisan pour le frontend.

Il est facile à installer :

composer require consoletvs/charts:7.*

Si ça coince commencez par mettre à jour vos librairies (il faut aussi PHP >= 7.4) :

composer update

Contrôleur et route

On va créer un contrôleur :

php artisan make:controller Back\StatisticsController

On le rend invocable :

<?php

namespace App\Http\Controllers\Back;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class StatisticsController extends Controller
{
    public function __invoke(Request $request)
    {  
        // On va coder ici
    }
}

On ajoute une route :

Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::name('statistics')->get('statistiques/{year}', 'StatisticsController');

On prévoit le paramètre year pour l’année.

Le menu

On ajoute l’item dans le menu de l’administration (back.layout) avec l’année actuelle par défaut comme paramètre :

<x-menu-item 
  :href="route('statistics', now()->year)" 
  icon="chart-bar"
  :active="currentRouteActive('statistics')">
  Statistiques
</x-menu-item>

Les commandes

On va créer la classe pour le graphe des commandes :

php artisan make:chart OrdersChart

On l’enregistre dans AppServiceProvider :

use ConsoleTVs\Charts\Registrar as Charts;
use App\Charts\OrdersChart;

...

public function boot(Charts $charts)
{
    $charts->register([
        OrdersChart::class
    ]);

On a un code par défaut dans la classe OrdersChart mais plutôt que de tout coder dans cette classe on va en créer une autre abstraite parce qu’on va avoir du code commun avec les nouveaux utilisateurs qu’on va voir plus loin dans cet article.

On crée la classe CommonChart :

Avec ce code :

<?php

namespace App\Charts;

use ConsoleTVs\Charts\BaseChart;
use Chartisan\PHP\Chartisan;

abstract class CommonChart extends BaseChart
{
    protected function chartisan($model, $title)
    {
        $year = request()->year;

        $datas = $this->datas($year, $model);

        return Chartisan::build()
            ->labels($datas->pluck('month_name')->toArray())
            ->dataset($title , $datas->pluck('data')->toArray());
    }

    protected function datas($year, $model)
    {
        return $model->selectRaw('
            count(*) data, 
            month(created_at) month, 
            monthname(created_at) month_name
        ')
        ->whereYear('created_at', $year)
        ->groupBy('month', 'month_name')
        ->orderBy('month', 'asc')
        ->get();
    }
}

Cette classe est abstraite parce qu’elle n’a pas vocation a être instanciée.

On commence par récupérer l’année dans la requête :

$year = request()->year;

Ensuite on récupère les données :

$datas = $this->datas($year, $model);

C’est une fonction qui assure cette tâche :

protected function datas($year, $model)
{
    return $model->selectRaw('
        count(*) data, 
        month(created_at) month, 
        monthname(created_at) month_name
    ')
    ->whereYear('created_at', $year)
    ->groupBy('month', 'month_name')
    ->orderBy('month', 'asc')
    ->get();
}

On a besoin de compter les enregistrements par mois, on a aussi besoin du nom de chaque mois pour l’afficher. On classe aussi par mois.

On peut alors générer les étiquettes et le dataset pour le graphe :

return Chartisan::build()
    ->labels($datas->pluck('month_name')->toArray())
    ->dataset($title , $datas->pluck('data')->toArray());

Du coup la classe OrdersChart est légère :

<?php

declare(strict_types = 1);

namespace App\Charts;

use Chartisan\PHP\Chartisan;
use Illuminate\Http\Request;
use App\Models\Order;

class OrdersChart extends CommonChart
{
    public function handler(Request $request): Chartisan
    {
        return $this->chartisan(new Order, 'Commandes');
    }
}

La vue

On crée la vue :

@extends('back.layout') 

@section('main') 

  <div class="container-fluid">    

    <div class="col-12">
      <div class="card">
        <div class="card-body">
          <div class="form-group">
            <label for="customRange1">Année : &nbsp</label>
            @foreach ($years as $year)
              <div class="custom-control custom-radio custom-control-inline">
                <input type="radio" id="{{ $year }}" name="year" class="custom-control-input" value="{{ $year }}" @if($actualYear == $year) checked @endif>
                <label class="custom-control-label" for="{{ $year }}">{{ $year }}</label>
              </div>               
            @endforeach
          </div>
        </div>
      </div>
    </div>

    <div class="col-12">
      <div class="card">
        <div id="ordersChart" style="height: 300px;" class="card-body">          
        </div>
      </div>
    </div>

  </div>

@endsection

@section('js')
  <script src="https://unpkg.com/chart.js/dist/Chart.min.js"></script>
  <script src="https://unpkg.com/@chartisan/chartjs/dist/chartisan_chartjs.js"></script>
  <script>
    $(function() {

      const OrdersChart = new Chartisan({
        el: '#ordersChart',
        url: "@chart('orders_chart')" + '?year={{ $actualYear }}',
        hooks: new ChartisanHooks()
          .colors(['#c33'])
          .responsive()
          .beginAtZero()
      });
  
      $('input').change(function() { 
        let year = $("input[name='year']:checked").val();
        let param = '?year=' + year;;     
        OrdersChart.update({
          url: "@chart('orders_chart')" + param
        });
        window.history.replaceState('', '', '/admin/statistiques/' + year);
      });
    });
  </script>
@endsection

On ajoute aussi le titre dans config.titles :

return [

    ...

    'statistics' => 'Statistiques',

La librairie est facile à utiliser, on peut ajuster quelques options, ici la couleur, le fait que ça commence à 0…

On met aussi à jour l’adresse pour être cohérent.

On a aussi automatiquement la création d’une route côté serveur :

C’est cette route qu’on appelle pour créer et actualiser le graphe.

On va aussi coder le contrôleur StatisticsController pour appeler et nourrir cette vue :

<?php

namespace App\Http\Controllers\Back;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Order;

class StatisticsController extends Controller
{
    public function __invoke(Request $request)
    {  
        $actualYear = $request->year;

        // Années disponibles
        $years = range(Order::oldest()->first()->created_at->year, now()->year);

        return view('back.statistics.index', compact(
            'years',
            'actualYear'
        ));
    }
}

Et ça devrait marcher :

On peut maintenant obtenir les statistiques des commandes pour chaque année. Le seul problème c’est que les mois sont en anglais alors on va régler ça dans AppSerciveProvider :

use DB;

...

public function boot(Charts $charts)
{

    DB::statement("SET lc_time_names = 'fr_FR'");

On a maintenant les mois en français :

On a un tooltip assez élégant au survol :

Les nouveaux clients

Pour les statistiques des nouveaux clients on crée aussi une classe :

php artisan make:chart UsersChart

Avec ce code :

<?php

declare(strict_types = 1);

namespace App\Charts;

use Chartisan\PHP\Chartisan;
use Illuminate\Http\Request;
use App\Models\User;

class UsersChart extends CommonChart
{
    public function handler(Request $request): Chartisan
    {
        return $this->chartisan(new User, 'Nouveaux clients');
    }
}

On voit ici l’intérêt de la classe CommonChart qu’on a créée précédemment.

On complète ainsi la vue back.statistics.index :

@extends('back.layout') 

@section('main') 

  <div class="container-fluid">    

    ...    

    <div class="col-12">
      <div class="card">
        <div id="usersChart" style="height: 300px;" class="card-body">          
        </div>
      </div>
    </div>

  </div>

@endsection

@section('js')
  
      ...

      const UsersChart = new Chartisan({
        el: '#usersChart',
        url: "@chart('users_chart')" + '?year={{ $actualYear }}',
        hooks: new ChartisanHooks()
          .colors(['#3c3'])
          .responsive()
          .beginAtZero()
      });
    
      $('input').change(function() { 
        let year = $("input[name='year']:checked").val();
        let param = '?year=' + year;;     
        OrdersChart.update({
          url: "@chart('orders_chart')" + param
        });
        UsersChart.update({
          url: "@chart('users_chart')" + param
        });
        window.history.replaceState('', '', '/admin/statistiques/' + year);
      });
    });
  </script>
@endsection

On a maintenant aussi les statistiques pour les nouveaux clients :

Conclusion

On a vu dans cet article qu’il est facile d’ajouter des statistiques à notre boutique, surtout avec ce package et cette librairie. Je n’ai montré qu’une approche sommaire et on peut bien améliorer cet aspect de maintes manières.

Print Friendly, PDF & Email

39 commentaires

  • softcode

    Bonsoir best momo j’ai rencontré un problème au niveau de l’installation quand j’ai essayé de lancer la commande composer require consoletvs/charts:7.* j’ai eu cette erreur:

    – Root composer.json requires consoletvs/charts 7.*, found consoletvs/charts[dev-main, 6.5.6, 6.6.0] but it does not match the constraint.

    Use the option –with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.

    j’ai même mis à jour composer mais aucun succes ça ne marche pas toujours, svp vous pouvez m’aider à cela ? merci

  • DIM

    Salut best , tu peux nous donner un script js qui prend en compte l’année chécké , et qui met à jour les données par rapport à cette année . mais avec la bibliothèque chartjs de laravel-charts .
    vu que la bibliothèque chartisan n’existe plus .

    en resumé le meme travail que ce projet ci mais cette fois avec la bibliothèque chartjs
    Merci

      • DIM

        Salut , je te soihaite de passer de passer de très bonnes vacances.
        best j’aimerai refaire la procédure pour avoir les statistiques par an , le problème est qu’ avec le composant menu-item.blade.php que nous avons créer dans le projet du blog , je n’arrive à prendre en compte le paramètre year , il est dit que le paramètre est manquant , ça se presente comme ceci:
        back.menu-item

        ‘role’ => ‘user’,
        ‘route’ => « ‘statistics’, now()->year »,
        ‘icon’=> ‘chart-bar’,

        on me dit que le paramètre year est manquant

        la route est invokable
        Route::get(‘admin/statistiques/{year}’, StatisticsController::class)->name(‘statistics’);
        que dois-je améliorer dans mon composant pour prendre en compte l’id ?
        merci et désolé vraiment de te pousser à réfléchir sur le code .

          • DIM

            salut , alors premièrement je récupère les années comprises entre la plus vieille et l’année en cours, ensuite je les affiche dans des boutons radio , au clic sur une année je souhaite n’obtenir que les statistiques de l’année en question. mais j’aimerai qu’au chargement de la page récupérer les années de l’année en cours , d’où le now()->year que j’éssaie d’introduire sans succès en me basant sur le composant du menu-item.

            Best j’ai aussi éssayé ce script pour la librairie chartjs de laravel-chart pour éssayer de mettre à jour les résultats sur le clic d’une année.

            script

            $(function() {

            $(‘input’).change(function() {
            let year = $(« input[name=’year’]:checked »).val();
            let param = ‘?year=’ + year;
            var url = {{ $maleChart->id }}_api_url;

            {{$maleChart->id}}_refresh(
            url + param);

            window.history.replaceState( »,  », ‘/admin/statistiques/’ + year);

            });

            });
            script.

            je n’ai pas encore un niveau acceptable en javascript , j’ai vraiment besoin de ton aide .

            Merci et bien de choses à toi.

          • bestmomo

            J’ai un peu relu l’article et le code. Pour le chargement de l’année en cours pourquoi ne pas utiliser le code de l’article qui fonctionne bien ? Dans le composant menu-item il y a la propriété href qui définit le lien.

            En ce qui concerne la bibliothèque Laravel Charts j’ai regardé dans le projet du créateur et j’ai vu qu’il a tout changé parce qu’il a dû supprimer la version 7 que j’avais utilisée. Du coup le code de l’article ne correspond plus du tout pour la création des graphiques. Il faut repartir sur la nouvelle version ou sut autre chose…

  • DIM

    bestmomo salut, j’ai une erreur à ce niveau
    use ConsoleTVs\Charts\Registrar il est dit que cette classe n’existe pas et après quelques recherches j’ai appris que la version 7 qui utilisait cette classe a été annulé comment la remplacer dans ma version 6.6.0 ?
    merci

  • jondelweb

    Salut Best Momo

    (C’est encore moi! )

    J’arrive au bout mais j’ai encore un petit problème de taille !

    J’ai cette erreur :

    Call to undefined method App\Charts\OrdersChart::__set_state()

    Et du coup ben je peux plus lancer mon appli…

    Cette erreur ce trouve dans le fichier : routes-v7.php

    Je précise que même si je fais un « composer dumpautoload » il n’y a rien qui change…

    Une idée ?

  • Ferfalam

    Bonsoir BESTMOMO. Merci pour ce tutoriel c’est super instructif j’adore. Encore Merci. Je sais pas si c’est un oublie ou tu veux nous laisser gérer par nous même mais dans le dashboard quand on appuie sur appuie sur plus d’informations rien ne se passe cette partie du site n’a pas été faite. Merci de le voir je t’e prie.

  • fgb

    Bonjour et merci pour le tuto.
    je suis un novice et j’ai essayé de charger le projet du tuto pour bien comprendre et je tombe sur cette erreur:
    *********
    λ php artisan migrate

    Illuminate\Database\QueryException

    SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘forge.shops’ doesn’t exist (SQL: select * from `shops` limit 1)

    at C:\laragon\www\shopping23\vendor\laravel\framework\src\Illuminate\Database\Connection.php:671
    667| // If an exception occurs when attempting to run a query, we’ll format the error
    668| // message to include the bindings with SQL, which will make this exception a
    669| // lot more helpful to the developer instead of just the database’s errors.
    670| catch (Exception $e) {
    > 671| throw new QueryException(
    672| $query, $this->prepareBindings($bindings), $e
    673| );
    674| }
    675|

    • A table was not found: You might have forgotten to run your migrations. You can run your migrations using `php artisan migrate`.
    https://laravel.com/docs/master/migrations#running-migrations

    1 [internal]:0
    Illuminate\Foundation\Application::Illuminate\Foundation\{closure}(Object(App\Providers\AppServiceProvider))

    2 C:\laragon\www\shopping23\vendor\laravel\framework\src\Illuminate\Database\Connection.php:331
    PDOException::(« SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘forge.shops’ doesn’t exist »)

    C:\laragon\www\shopping23
    *****************
    pouver vous m’orienter?

          • fgb

            Merci, voici la nouvelle erreur:

            λ php artisan migrate

            Illuminate\Contracts\Container\BindingResolutionException

            Target [Illuminate\Contracts\Auth\Access\Gate] is not instantiable.

            at C:\laragon\www\shopping23\vendor\laravel\framework\src\Illuminate\Container\Container.php:1013
            1009| } else {
            1010| $message = « Target [$concrete] is not instantiable. »;
            1011| }
            1012|
            > 1013| throw new BindingResolutionException($message);
            1014| }
            1015|
            1016| /**
            1017| * Throw an exception for an unresolvable primitive.

            1 C:\laragon\www\shopping23\vendor\laravel\framework\src\Illuminate\Container\Container.php:814
            Illuminate\Container\Container::notInstantiable(« Illuminate\Contracts\Auth\Access\Gate »)

            2 C:\laragon\www\shopping23\vendor\laravel\framework\src\Illuminate\Container\Container.php:687
            Illuminate\Container\Container::build(« Illuminate\Contracts\Auth\Access\Gate »)

          • fgb

            λ composer dumpautoload
            Generating optimized autoload files> Illuminate\Foundation\ComposerScripts::postAutoloadDump
            > @php artisan package:discover –ansi

            Illuminate\Contracts\Container\BindingResolutionException

            Target [Illuminate\Contracts\Auth\Access\Gate] is not instantiable.

            at C:\laragon\www\shopping23\vendor\laravel\framework\src\Illuminate\Container\Container.php:1013
            1009| } else {
            1010| $message = « Target [$concrete] is not instantiable. »;
            1011| }
            1012|
            > 1013| throw new BindingResolutionException($message);
            1014| }
            1015|
            1016| /**
            1017| * Throw an exception for an unresolvable primitive.

            1 C:\laragon\www\shopping23\vendor\laravel\framework\src\Illuminate\Container\Container.php:814
            Illuminate\Container\Container::notInstantiable(« Illuminate\Contracts\Auth\Access\Gate »)

            2 C:\laragon\www\shopping23\vendor\laravel\framework\src\Illuminate\Container\Container.php:687
            Illuminate\Container\Container::build(« Illuminate\Contracts\Auth\Access\Gate »)
            Script @php artisan package:discover –ansi handling the post-autoload-dump event returned with error code 1

          • fgb

            Merci,
            j’ai reinstallé ,
            ajusté
            if(app()->runningInConsole()) {
            return;
            }
            et la migration c’est bien passée!

  • kopatik

    Merci pour cette belle série de tutos, je l’ai suivie depuis le début et tout fonctionne !!
    Vraiment appréciable d’avoir des tutos de cette qualité en français.

    PS : ne pas oublier d’enregistrer dans AppServiceProvider la classe du graphe des utilisateurs, UsersChart 😉

Laisser un commentaire