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

29 commentaires

  • 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 😉

Leave a Reply