Shopping : le panier

Dans cet article on va afficher les produits, permettre leur sélection et utiliser un panier pour mémoriser et comptabiliser les achats.

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

Affichage de la boutique

La première chose à faire est d’afficher les produits sur la page d’accueil. Pour le moment on a la page d’accueil standard de Laravel avec la vue welcome. On va supprimer cette vue qui ne nous sert à rien et on va recycler la vue home.

Dans le contrôleur HomeController on va prévoir ce code (on supprime au passage le middleware guest) :

<?php

namespace App\Http\Controllers;

use App\Models\Product;

class HomeController extends Controller
{
    /**
     * Show home page
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $products = Product::whereActive(true)->get();

        return view('home', compact('products'));
    }
}

Pour cette application j’ai décidé de me passer de repositories, c’est un choix délibéré. On a une totale liberté sur l’organisation de son code.

On change la route pour l’accueil :

Route::get('/', 'HomeController@index')->name('home');

Comme on a peu de produits on se passe de pagination. On charge donc tous les produits actifs dans une variable $products que l’on envoie dans la vue home. On change le code de cette vue pour utiliser le layout et afficher les produits :

@extends('layouts.app')

@section('content')
<div class="container">
  
  <div class="row">
    <div class="col s12 cards-container">
      @foreach($products as $product)
        <div class="card">
          <div class="card-image">
            @if($product->quantity)
              <a href="#">
            @endif
              <img src="/images/thumbs/{{ $product->image }}">
            @if($product->quantity) </a> @endif
          </div>          
          <div class="card-content center-align">
            <p>{{ $product->name }}</p>
            @if($product->quantity)
              <p><strong>{{ number_format($product->price, 2, ',', ' ') }} € TTC</strong></p>
            @else
              <p class="red-text"><strong>Produit en rupture de stock</strong></p>
            @endif
          </div>
        </div>
      @endforeach
    </div>
  </div>

</div>
@endsection

Et pour la répartition des produits sur la page un peu de style pour un effet Masonry (sass/app.scss) :

.cards-container {
  .card {
    display: inline-block;
    overflow: visible;
    width: 100%;
  }
}

@media only screen and (max-width: 600px) {
  .cards-container {
    -webkit-column-count: 1;
    -moz-column-count: 1;
    column-count: 1;
  }
}
@media only screen and (min-width: 601px) {
  .cards-container {
    -webkit-column-count: 2;
    -moz-column-count: 2;
    column-count: 2;
  }
}
@media only screen and (min-width: 993px) {
  .cards-container {
    -webkit-column-count: 3;
    -moz-column-count: 3;
    column-count: 3;
  }
}
@media only screen and (min-width: 1200px) {
  .cards-container {
    -webkit-column-count: 4;
    -moz-column-count: 4;
    column-count: 4;
  }
}

On se retrouve avec 1 à 4 produits par ligne selon la largeur de l’affichage :

On complète aussi les liens dans la barre du layout :

<a href="{{ route('home') }}" class="brand-logo"><img src="/images/logo1.png" width="210px" alt="Logo"></a>
<a href="{{ route('home') }}" data-target="mobile" class="sidenav-trigger"><i class="material-icons">menu</i></a>

Si un produit est en rupture de stock c’est signalé :

Le détail des produits

Quand on clique sur un produit il faut afficher une page avec l’image haute définition, la description, la quantité désirée et un bouton pour ajouter au panier.

On va créer un contrôleur pour les produits :

php artisan make:controller ProductController

Avec ce code :

<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    /**
     * Display the specified resource.
     *
     * @param  \App\Models\Product  $product
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request, Product $produit)
    {
        if($produit->active || $request->user()->admin) {
            return view('products.show', compact('produit'));
        }

        return redirect(route('home'));
    }
}

On affiche la page produit que si celui-ci est actif ou si c’est un administrateur qui veut la voir.

On prévoit la route :

Route::name('produits.show')->get('produits/{produit}', 'ProductController');

On crée la vue dans un dossier :

Avec ce code :

@extends('layouts.app')

@section('content')
<div class="container">

  <div class=row>
    <div class="col s12 m6">
      <img style="width: 100%" src="/images/{{ $produit->image }}">
    </div>
    <div class="col s12 m6">
      <h4>{{ $produit->name }}</h4>
      <p><strong>{{ number_format($produit->price, 2, ',', ' ') }} € TTC</strong></p>
      <p>{{ $produit->description }}</p>
      <form  method="POST" action="#">
        @csrf
        <div class="input-field col">
          <input type="hidden" id="id" name="id" value="{{ $produit->id }}">
          <input id="quantity" name="quantity" type="number" value="1" min="1">
          <label for="quantity">Quantité</label>        
          <p>
            <button class="btn waves-effect waves-light" style="width:100%" type="submit" id="addcart">Ajouter au panier
              <i class="material-icons left">add_shopping_cart</i>
            </button>
          </p>    
        </div>    
      </form>
    </div>
  </div>

</div>
@endsection

Dans la vue home on ajoute le lien pour afficher la page d’un produit :

<a href="{{ route('produits.show', $product->id) }}">

Maintenant le clic sur un produit ouvre sa page de détail :

Il ne nous reste plus qu’à faire fonctionner le panier !

Mise en place du panier

Pour le panier on va utiliser ce package.

composer require darryldecode/cart

On crée un contrôleur de ressource :

php artisan make:controller CartController --resource

On n’utilisera que 4 méthodes donc voici les routes :

Route::resource('panier', 'CartController')->only(['index', 'store', 'update', 'destroy']);

On va compléter la vue products.show avec une vue modale, le lien du formulaire, et du Javascript pour le traitement :

@extends('layouts.app')

@section('content')
<div class="container">

  @if(session()->has('cart'))
    <div class="modal">
      <div class="modal-content center-align">
        <h5>Produit ajouté au panier avec succès</h5>
        <hr>
        <p>Il y a {{ $cartCount }} @if($cartCount > 1) articles @else article @endif dans votre panier pour un total de <strong>{{ number_format($cartTotal, 2, ',', ' ') }} € TTC</strong> hors frais de port.</p>
        <p><em>Vous avez la possibilité de venir chercher vos produits sur place, dans ce cas vous cocherez la case correspondante lors de la confirmation de votre commande et aucun frais de port ne vous sera facturé.</em></p>
        <div class="modal-footer">     
          <button class="modal-close btn waves-effect waves-light left" id="continue">
            Continuer mes achats
          </button>
          <a href="{{ route('panier.index') }}" class="btn waves-effect waves-light">
            <i class="material-icons left">check</i>
            Commander          
          </a>
        </div>
      </div>
    </div>
  @endif

  <div class=row>
    <div class="col s12 m6">
      <img style="width: 100%" src="/images/{{ $produit->image }}">
    </div>
    <div class="col s12 m6">
      <h4>{{ $produit->name }}</h4>
      <p><strong>{{ number_format($produit->price, 2, ',', ' ') }} € TTC</strong></p>
      <p>{{ $produit->description }}</p>
      <form  method="POST" action="{{ route('panier.store') }}">
        @csrf
        <div class="input-field col">
          <input type="hidden" id="id" name="id" value="{{ $produit->id }}">
          <input id="quantity" name="quantity" type="number" value="1" min="1">
          <label for="quantity">Quantité</label>        
          <p>
            <button class="btn waves-effect waves-light" style="width:100%" type="submit" id="addcart">Ajouter au panier
              <i class="material-icons left">add_shopping_cart</i>
            </button>
          </p>    
        </div>    
      </form>
    </div>
  </div>

</div>
@endsection

@section('javascript')
  <script>
    @if(session()->has('cart'))
      document.addEventListener('DOMContentLoaded', () => {      
        const instance = M.Modal.init(document.querySelector('.modal'));
        instance.open();    
      });
    @endif    
  </script>
@endsection

Dans le contrôleur CartController on code la méthode store :

use App\Models\Product;
use Cart;

...

public function store(Request $request)
{
    $product = Product::findOrFail($request->id);
      
    Cart::add([
        'id' => $product->id,
        'name' => $product->name,
        'price' => $product->price,
        'quantity' => $request->quantity,
        'attributes' => [],
        'associatedModel' => $product,
      ]
    );

    return redirect()->back()->with('cart', 'ok');
}

On récupère le produit concerné et on renseigne le panier, on renvoie la même vue en flashant la variable cart.

On va aussi envoyer les informations de quantité de produits dans le panier et de coût total mais comme ça va servir dans plusieurs situations on passe par un composeur de vue dans AppServiceProvider :

use Illuminate\Support\Facades\View;
use Cart;

...

public function boot()
{
    View::composer(['layouts.app', 'products.show'], function ($view) {
        $view->with([
            'cartCount' => Cart::getTotalQuantity(), 
            'cartTotal' => Cart::getTotal(),
        ]);
    });
}

Maintenant quand on utilise le bouton « Ajouter au panier » au retour ça ouvre la page modale :

Voyons un peu comment agit le Javascript dans cette vue…

Au chargement de la vue on a :

@if(session()->has('cart'))
  document.addEventListener('DOMContentLoaded', () => {      
    const instance = M.Modal.init(document.querySelector('.modal'));
    instance.open();    
  });
@endif

Si on a flashé la variable cart dans la session on ouvre la page modale avec la librairie de Materialize.

Si on clique sur « Continuer mes achats » la page modale se ferme et on se retrouve avec la vue du produit.

Maintenant ce qui serait bien c’est d’informer l’utilisateur qu’il a un panier actif avec le nombre de produits dedans. On va le faire dans la barre de menu (layout) en prévoyant les deux emplacement (barre normale et barre latérale pour mobiles) :

<ul class="right hide-on-med-and-down">
@if($cartCount)
  <li>
    <a class="tooltipped" href="{{ route('panier.index') }}" data-position="bottom" data-tooltip="Voir mon panier"><i class="material-icons left">shopping_cart</i>Panier({{ $cartCount }})</a>
  </li>
@endif
@guest  

...

<ul class="sidenav" id="mobile">
  @if($cartCount)
    <li>
      <a class="tooltipped" href="{{ route('panier.index') }}" data-position="bottom" data-tooltip="Voir mon panier">Panier({{ $cartCount }})</a>
    </li>
  @endif
  @guest

Le panier

Maintenant on va coder pour voir le panier. On crée une vue :

Avec ce code :

@extends('layouts.app')

@section('content')
<div class="container">

  <div class="row">
    <div class="col s12">
      <div class="card">        
        <div class="card-content">
          <div id="wrapper">          
            @if($total)
              <span class="card-title">Mon panier</span>            
              @foreach ($content as $item)
                <hr><br>
                <div class="row">
                  <form action="{{ route('panier.update', $item->id) }}" method="POST">
                    @csrf
                    @method('PUT')
                    <div class="col m6 s12">{{ $item->name }}</div>
                    <div class="col m3 s12"><strong>{{ number_format($item->quantity * $item->price, 2, ',', ' ') }} €</strong></div>
                    <div class="col m2 s12">
                      <input name="quantity" type="number" style="height: 2rem" min="1" value="{{ $item->quantity }}">
                    </div>
                  </form>
                  <form action="{{ route('panier.destroy', $item->id) }}" method="POST">
                    @csrf
                    @method('DELETE')
                    <div class="col m1 s12"><i class="material-icons deleteItem" style="cursor: pointer">delete</i></div>
                  </form>              
                </div>
              @endforeach
              <hr><br>
              <div class="row" style="background-color: lightgrey">
                <div class="col s6">
                  Total TTC (hors livraison)
                </div>
                <div class="col s6">
                  <strong>{{ number_format($total, 2, ',', ' ') }} €</strong>
                </div>
              </div>
            @else
            <span class="card-title center-align">Le panier est vide</span>
            @endif
          </div>        
          <div id="loader" class="hide">
            <div class="loader"></div>
          </div>
        </div>
        <div id="action" class="card-action">
          <p>
            <a  href="{{ route('home') }}">Continuer mes achats</a>
            @if($total)
              <a href="#">Commander</a>
            @endif
          </p>
        </div>
      </div>
    </div>
  </div>

</div>
@endsection

@section('javascript')
  <script>

    document.addEventListener('DOMContentLoaded', () => {
      const quantities = document.querySelectorAll('input[name="quantity"]');
      quantities.forEach( input => {
        input.addEventListener('input', e => {
          if(e.target.value < 1) {
            e.target.value = 1;
          } else {
            e.target.parentNode.parentNode.submit();
            document.querySelector('#wrapper').classList.add('hide');
            document.querySelector('#action').classList.add('hide');
            document.querySelector('#loader').classList.remove('hide');
          }
        });
      }); 

      const deletes = document.querySelectorAll('.deleteItem');
      deletes.forEach( icon => {
        icon.addEventListener('click', e => {
          e.target.parentNode.parentNode.submit();
          document.querySelector('#wrapper').classList.add('hide');
          document.querySelector('#loader').classList.remove('hide');
        });
      }); 
    });
    
  </script>
@endsection

Et dans le contrôleur CartController on complète la méthode index :

public function index()
{
    $content = Cart::getContent();
    $total = Cart::getTotal();

    return view('cart.index', compact('content', 'total'));
}

Maintenant quand on clique sur « Commander » (ou si on clique sur le lien du panier dans le menu) on obtient la page avec le contenu du panier :

Là l’utilisateur peut modifier les quantités et même supprimer un produit.

Pour que ça fonctionne on va compléter le contrôleur CartController :

public function update(Request $request, $id)
{
    Cart::update($id, [
        'quantity' => ['relative' => false, 'value' => $request->quantity],
    ]);

    return redirect(route('panier.index'));
}

public function destroy($id)
{
    Cart::remove($id);

    return redirect(route('panier.index'));
}

Pour la mise à jour de la quantité d’un produit il faut bien préciser relative à false sinon ça change relativement à la valeur mémorisée or là on envoie la nouvelle quantité.

Pour faire patienter l’utilisateur pendant les mises à jour et pour éviter qu’il clique encore quelque part on fait apparaître un loader. Pour que ça fonctionne on va ajouter un peu de style dans app.scss (n’oubliez pas de relancer la compilation) :

#loader {
  margin: 40px;
}
.loader {
  margin: auto;
  border: 16px solid #e3e3e3;
  border-radius: 50%;
  border-top: 16px solid #1565c0;
  width: 120px;
  height: 120px;
  animation: spin 2s linear infinite;
}
@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

Une petite animation fait patienter l’utilisateur :

Le Javascript a plusieurs fonctions :

  • mettre en route l’animation
  • empêcher la saisie d’une valeur inférieure à 1
  • lancer la soumission des formulaires

Conclusion

On a maintenant une boutique qui sait afficher ses produits, montrer le détail d’un produit, permet d’ajouter un produit au panier en choisissant la quantité voulue, d’afficher le panier en pouvant jouer sur les quantités. Dans le prochain article on mettra en place l’enregistrement d’une commande.

 

 

Print Friendly, PDF & Email

25 commentaires sur “Shopping : le panier

  1. Bonsoir bestmomo, j’ai un problème au niveau de la mise en place du panier,le bouton ajouter panier renvoie une erreur :  »The post method is not supported for this route . Supported methods GET,HEAD » ainsi que tout le reste

  2. bonsoir,

    je viens vers toi car je désespère vraiment, je n’arrive pas utiliser la fonction card
    J’ai bien installé darryldecode/cart et écrit la même chose que toi dans le cardcontroller, mais rien y fait.  » Cart::  » est souligné en rouge (mon code ne le comprends pas) et lorsque j’essaye d’ajouter un produit a mon panier j’obtient ce message d’erreur :
    Darryldecode\Cart\Exceptions\InvalidItemException
    validation.required

    Je pensais que mon problème venait de ma version de laravel (7) qui n’aurait pas encore implémenté la fonction Cart mais si toi tu as réussi je ne comprends pas pourquoi mon code ne le reconnais pas

    Ps : j’ai tenté d’ajouter les codes nécessaires dans Providers Array et Aliases du fichier config\app.php. Mais rien n’y fait
    j’ai suivit les instruction du Git : https://github.com/darryldecode/laravelshoppingcart

  3. Bonjour Bestmomo,
    Saurais tu comment je pourrais bloquer le panier pour qu’il n’y est qu’une seul fois le même id dans le panier.
    je pensais à cart::get($itemid) pour savoir si le produit est déjà dans le panier et ensuite retourner une notification de type « Véhicule déjà dans le panier » sinon je fais un add….?

      1. c’est cela mais le fait est que mes produits sont des voitures et que forcément je ne peux ajouter deux fois la même voiture avec le même numéro de serie ou la plaque d’immatriculation. C’est pour cela que j’aimerai bloqué la qty à 1 max…

  4. Bonjour à tous,
    Je vous remercie pour tout
    j’ai encore une erreur « Class ‘App\Http\Controllers\Product’ not found »
    pointe sur cette fonction:

    public function store(Request $request)
    {
    $product = Product::findOrFail($request->id);
    Cart::add([
    'id' => $product->id,
    'name' => $product->name,
    'price' => $product->price,
    'quantity' => $request->quantity,
    'attributes' => [],
    'associatedModel' => $product,
    ]
    );
    return redirect()->back()->with('cart', 'ok');
    }

    merci de m’aide

  5. Bonsoir BestMomo, Je sais pas ce qui ne va pas chez moi mais aucune des images du dossier image ne s’affiche sur mon site. Et pourtant j’ai la même architecture que le projet zippé. Est-ce que vous avez une idée de comment je peux régler ce problème s’il vous plait ? Merci

        1. Bonjour BestMomo, je viens de voir que le code que j’ai copié-coller hier ne s’est pas affiché. Je m’en excuse sincèrement. Le code généré dans l’inspecteur de code est le même que celui dans l’éditeur de texte: . Est-ce que c’est normal ? quel devrait être le code ? PS: J’ai le même problème avec tous mes navigateurs à savoir : firefox, opéra et google chrome.

          Besoin de votre aide s’il vous plait. merci

          1. Voici le code que j’essaie de copier-coller mais qui ne s’affichepas : . Merci et encore une fois désolé

          2. Bonjour,
            Je peux quand même voir le code dans le message, en fait il apparaît public dans l’url. Normalement ça doit pas arriver si on a bien mis le fichier .htaccess dans le root.

          3. Salut BestMomo, Je n’ai pas bien compris ce que vous voulez dire par mettre le fichier .htaccess dans le root mais néanmoins j’ai pu trouver une autre solution pour afficher les images. il s’agit de l’utilisation de la fonction asset() et cela a marché. Merci beaucoup pour votre disponibilité. Vous êtes le meilleur.

          4. Bonjour Hamdi j’ai même problème que toi les images de mes produits s’affichent pas vous avez utilisez la fonction asset() comment?

    1. Salut,

      Comme on n’a pas encore codé la création d’adresse pour le client il faut se limiter aux clients créés avec le seeder pour passer une commande sinon on tombe forcément sur cette erreur. J’aurais sans doute dû prévoir la création du compte client avec cette création d’adresse avant d’aborder la commande mais bon, maintenant c’est fait…

Laisser un commentaire