playlist Gestion du panier

Publié il y a 13 jours dans la série : Rails
Nicolas Cavigneaux
Votre formateur
Nicolas Cavigneaux

A la recherche d'un langage polyvalent, j'ai fait la découverte de Ruby en 2003. J'ai donc très vite commencé à utiliser Ruby au quotidien pour des tâches diverses et variées (scripting, applications lourdes…).

Courant 2004, une vague de fraicheur est apparue avec l'arrivée de Ruby on Rails qui m'a de suite conquis. J'ai donc décidé de participer activement à la communauté (forums, patches, librairies, …). En 2010, je fais la rencontre de Martin Catty et retrouve dans sa vision la rigueur et les bonnes pratiques que j'aime mettre en place, le déclic a donc été immédiat.

Synbioz met en place des solutions robustes sur la base d'outils modernes et funs, je veux faire partie de l'aventure.

Catégories : Développement

Cette vidéo a pour objectif d'expliquer comment mettre en place un panier e-commerce en Rails et notamment comment utiliser les sessions.
Afficher le transcript complet de la vidéo

Bonjour et bienvenue dans cette vidéo consacrée à Ruby on Rails. Dans cet épisode nous allons voir comment mettre en place la gestion d'un panier pour permettre à nos utilisateurs de choisir les produits qu'ils souhaitent acheter.


Création du panier


On veut donc conserver dans notre application les produits que les utilisateurs ont sélectionné. Pour cela, on va garder ces informations en base de données dans une table dédiée et on gardera l'identifiant du panier de l'utilisateur courant en session pour pouvoir le retrouver facilement.


Créons donc le panier :

$ bin/rails generate scaffold Cart
$ bin/rails db:migrate


Récupération du panier courant


Dans Rails, on a accès à la session de l'utilisateur courant grâce à la méthode session. Cette méthode retourne un objet qui se comporte comme un Hash. Créons un module qui va être dédié à la récupération du panier. On pourra ensuite inclure ce module dans chaque contrôleur qui nécessite un accès au panier.


Pour mettre en place ce genre de module ré-utilisable, Rails met à disposition un répertoire app/controllers/concerns. C'est ici que nous allons ajouter notre fichier current_cart.rb :

module CurrentCart
  private

  def set_cart
    @cart = Cart.find_by(id: session[:cart_id]) || Cart.create
    session[:cart_id] ||= @cart.id
  end
end


On a donc créé un module dédié à la gestion du panier de l'utilisateur courant. Ce module ne contient qu'une méthode définie comme privée. Il n'y a aucune raison que cette méthode utilitaire puisse être rendue disponible en tant qu'action dans un contrôleur.


Cette méthode set_cart se charge d'essayer de retrouver en base de donnée un panier dont l'id est celui contenu en session pour la clé :cart_id. Cette valeur peut potentiellement être nil c'est pourquoi on utilise find_by et non pas find. find lève une exception si l'enregistrement ne peut pas être trouvé, find_by va lui simplement retourner nil.


On peut donc chaîner avec une condition "ou" qui dit «ok si tu n'as pas trouvé de panier, crées en un».


Finalement, on met à jour la valeur en session sauf si elle existe déjà grâce au ||=.


Association de produits au panier


Très bien, on sait récupérer ou créer un panier maintenant mais en l'état ça ne va pas nous emmener bien loin. Il nous est impossible de spécifier quels produits sont dans ce panier…


Il va nous falloir un modèle supplémentaire dont le but va être de stocker une référence aux produits ajoutés à un panier donné. On veut aussi savoir en quelle quantité un produit donné a été ajouté :

$ bin/rails generate scaffold LineItem cart:belongs_to product:belongs_to quantity:integer


On va éditer la nouvelle migration pour spécifier qu'on veut que la quantité soit à 1 par défaut :

t.integer :quantity, default: 1


Si on regarde cette migration de plus près on a bien notre belongs_to qui apparaît et signifie que dans cette table, on aura une colonne cart_id qui référence un panier donné. Idem pour product.


On peut maintenant jouer nos migrations :

$ bin/rails db:migrate


Si on jette un œil au modèle LineItem qui a été généré pour nous, on voit que deux lignes belongs_to apparaissent. Une pour le panier et une pour le produit associé à cette LineItem. C'est ce qui permet à Rails de savoir qu'un LineItem est associé à un panier et à un produit. Par ces simples déclarations, il est maintenant possible de récupérer le panier ou le produit associé à un LineItem depuis le code.


Il faut aussi savoir qu'ajouter un belongs_to dans un modèle, ajoute automatiquement une validation sur la présence de cet élément. Il est possible de la désactiver en passant l'option optional à true au belongs_to concerné.


Lançons une console pour vérifier tout cela :

$ bin/rails console

LineItem.new.product
LineItem.create.errors.messages


On peut donc bien accéder au produit d'un LineItem qui est ici vide puisque c'est un objet qu'on vient de créer sans lui passer d'arguments.


Si on tente une création, on a bien les messages d'erreur nous précisant qu'on doit affecter un panier et un produit.


Mais est-ce qu'on peut accéder à un LineItem depuis un panier grâce à ces belongs_to ?

Cart.new.line_items


Non, une exception est levée nous disant que cette méthode n'existe pas. Les relations sont à définir explicitement dans les deux sens. On va donc ajouter la relation à LineItem dans le model Cart :

has_many :line_items, dependent: :destroy


C'est la méthode has_many qui permet de définir une relation 1-N, notre modèle Cart sait maintenant qu'il peut avoir N LineItems associés. Vous noterez qu'on a passé l'option dependent: :destroy dont le but est de supprimer en cascade les LineItems associés à un panier si ce dernier est supprimé. Ça nous évite de nous retrouver avec des lignes orphelines.


Faisons de même pour le modèle Product :

has_many :line_items, dependent: :nullify


Cette fois l'option dependent est différente. On ne veut pas supprimer des LineItems si un produit qui y est utilisé venait à être supprimé. On veut plutôt faire en sorte que la référence qui apparaît dans le LineItem soit remise à zéro. Ça nous évitera de pointer vers une produit qui n'existe plus tout en gardant la ligne dans le panier ce qui nous permettra de préciser que le produit n'existe plus.


Ajouter un produit au panier depuis l'interface


On va maintenant modifier l'interface de notre application pour permettre aux utilisateurs d'ajouter des produits à leur panier d'un simple clic.


Ajoutons donc un bouton à côté de chaque produit qui appellera l'action create du controller LineItems en prenant soin de bien passer l'identifiant du produit en paramètre. 


Éditons le fichier app/views/catalog/index.html.erb :

<%= button_to 'Ajouter au panier', line_items_path(product_id: product), class: 'btn btn-primary' %>


Si on recharge la page, notre bouton est là. L'helper button_to crée une balise form qui va contacter en POST l'URL spécifiée. On va donc bien arriver sur notre action create.


Modifions cette action pour faire usage de l'identifiant produit passé en paramètre :

# app/controllers/line_items_controller.rb
include CurrentCart

before_action :set_cart, only: [:create]

# …

product = Product.find(params[:product_id])
@line_item = @cart.line_items.new(product: product)

# …

format.html { redirect_to @line_item.cart, notice: 'Line item was successfully created.' }


On a donc inclue notre module de gestion du panier et on a ensuite ajouté un callback qui appelle sa méthode set_cart avant d'entrer dans l'action create.


Dans cette action, on récupère le produit sur la base de l'identifiant passé en paramètre par le bouton d'ajout puis on construit un LineItem lié au panier courant disponible dans @cart. Il ne nous reste plus qu'à passer le produit concerné en paramètre de la méthode new.


Finalement on a changé la redirection pour qu'elle nous emmène sur la page de détail du panier.


Si on essaie d'ajouter un produit, tout semble fonctionner. Toutefois la vue générée par le scaffold pour la page de détail du panier n'est pas adaptée, changeons cette page pour un titre et l'affichage de la liste des produits du panier :

<p id="notice"><%= notice %></p>
<h2>Panier</h2>
<ul>
  <% @cart.line_items.each do |item| %>
    <li><%= item.product.title %></li>
  <% end %>
</ul>
<p><%= link_to "Retour aux produits", root_path %></p>


On a donc simplement bouclé sur les LineItems du panier courant pour afficher leur titre.


Si on y réfléchit bien, on a oublié de gérer un cas. Si j'essaie d'ajouter à nouveau le même produit, que se passe-t-il ?


Notre produit est dupliqué alors qu'on voudrait simplement incrémenter le compteur lié à chaque produit pour un panier donné.


Pour ce faire, nous allons avoir besoin d'une méthode qui vérifie si pour un panier et un produit donné un LineItem existe déjà, si oui, on incrémente son compteur, sinon on crée la ligne. Cette méthode aurait sa place dans le modèle Cart, ajoutons là :

def add_product(product)
  item = line_items.find_by(product: product)

  if item
    item.quantity += 1
  else
    item = line_items.new(product: product)
  end

  item
end


On modifie ensuite notre action contrôleur pour utiliser cette nouvelle méthode :

@line_item = @cart.add_product(product)


Et finalement on affiche cette information dans la page panier :

<li><%= item.product.title %> (x <%= item.quantity %>)</li>


On peut maintenant essayer d'ajouter deux fois le même produit:


Et ça fonctionne !


Test fonctionnel


Pour terminer sereinement, on va ajouter un test fonctionnel sur notre contrôleur qui s'assure que ce comportement continuera à être respecté lors des développements futurs. On édite le fichier test/controllers/line_items_controller_test.rb :

test "should create line_item" do
  tshirt = products(:one)

  assert_difference('LineItem.count') do
    post line_items_url, params: { product_id: tshirt.id }
  end

  assert_redirected_to cart_url(LineItem.last.cart)
  follow_redirect!

  assert_select 'li', "#{tshirt.title} (x 1)"

  post line_items_url, params: { product_id: tshirt.id }
  follow_redirect!

  assert_select 'li', "#{tshirt.title} (x 2)"
end


Dans la prochaine vidéo, nous rendrons notre application plus robuste face aux erreurs et manipulations de paramètres et ajouterons quelques fonctionnalités à notre panier.


À bientôt !

6/7 dans la sérieRails