DEV Community

Cover image for Creating the Cart Page | Building a Shopping Cart with Symfony
Quentin Ferrer
Quentin Ferrer

Posted on

Creating the Cart Page | Building a Shopping Cart with Symfony


The cart page will allow the user to manage the products it wants to purchase. The user will be able to:

  • Update the quantity of products in the cart,
  • Remove products from the cart,
  • Clear the cart,
  • See the list of products in the cart,
  • See the quantity of products in the cart,
  • See the summary of the cart.

Building Forms

The CartItemType Form

The CartItemType form will manage the form fields for an OrderItem object of an Order. It will contain the following fields:

  • quantity: the number of quantity of the product the customer wants to purchase,
  • remove: a submit button to remove the product from the cart.

Use the Maker to generate the form:

$ symfony console make:form CartItemType OrderItem
Enter fullscreen mode Exit fullscreen mode

Customize it to have the fields we need:

<?php

namespace App\Form;

use App\Entity\OrderItem;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CartItemType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('quantity')
            ->add('remove', SubmitType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => OrderItem::class
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

The CartType Form

The CartType form will be the main form and manage all items in the cart. It will contain the following fields:

  • items: a collection of CartItemType form type. It will allow us to modify all OrderItem items of an Order right inside the cart form itself,
  • save: a submit button to save the cart,
  • clear: a submit button to clear the cart.

Use the Maker to generate this class:

$ symfony console make:form CartType Order
Enter fullscreen mode Exit fullscreen mode

Customize it to have the fields we need:

<?php

namespace App\Form;

use App\Entity\Order;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CartType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('items', CollectionType::class, [
                'entry_type' => CartItemType::class
            ])
            ->add('save', SubmitType::class)
            ->add('clear', SubmitType::class);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Order::class,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating the Controller

Create the CartController controller via the Maker:

$ symfony console make:controller CartController
Enter fullscreen mode Exit fullscreen mode

The command creates a CartController class under the src/Controller/ directory and a template file to templates/cart/index.html.twig.

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class CartController extends AbstractController
{
    /**
     * @Route("/cart", name="cart")
     */
    public function index(): Response
    {
        return $this->render('cart/index.html.twig', [
            'controller_name' => 'CartController',
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

In the CartController controller, implement the index() method:

  • Get the current cart using the CartManager and,
  • Create the CartType form with the cart as form data and,
  • Pass the form view and the cart to the Twig template cart/index.html.twig
<?php

namespace App\Controller;

use App\Form\CartType;
use App\Manager\CartManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * Class CartController
 * @package App\Controller
 */
class CartController extends AbstractController
{
    /**
     * @Route("/cart", name="cart")
     */
    public function index(CartManager $cartManager): Response
    {
        $cart = $cartManager->getCurrentCart();
        $form = $this->createForm(CartType::class, $cart);

        return $this->render('cart/index.html.twig', [
            'cart' => $cart,
            'form' => $form->createView()
        ]);
    }
}

Enter fullscreen mode Exit fullscreen mode

Rendering the Cart Page

In the cart/index.html.twig file, add the two-column layout grid below:

{% extends 'base.html.twig' %}

{% block title %}Cart{% endblock %}

{% block body %}
    <div class="container mt-4">
        <h1>Your Cart</h1>
        {% if cart.items.count > 0 %}
            <div class="row mt-4">
                <!-- List of items -->
                <div class="col-md-8"></div>
                <!-- Summary -->
                <div class="col-md-4"></div>
            </div>
        {% else %}
            <div class="alert alert-info">
                Your cart is empty. Go to the <a href="{{ path('home') }}">product list</a>.
            </div>
        {% endif %}
    </div>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

If the cart is empty, we add a link to the product list.

List of items

In the left column, render the cart form (list of items in the cart) by using the form_start(), form_end(), form_widget() and form_errors() Twig functions:

<div class="col-md-8">
    {{ form_start(form) }}
    <div class="card">
        <div class="card-header bg-dark text-white d-flex">
            <h5>Items</h5>
            <div class="ml-auto">
                {{ form_widget(form.save, {'attr': {'class': 'btn btn-warning'}}) }}
                {{ form_widget(form.clear, {'attr': {'class': 'btn btn-light'}}) }}
            </div>
        </div>
        <ul class="list-group list-group-flush">
            {% for item in form.items %}
                <li class="list-group-item d-flex">
                    <div class="flex-fill mr-2">
                        <img src="https://via.placeholder.com/200x150" width="64" alt="Product image">
                    </div>
                    <div class="flex-fill mr-2">
                        <h5 class="mt-0 mb-0">{{ item.vars.data.product.name }}</h5>
                        <small>{{ item.vars.data.product.description[:50] }}...</small>
                        <div class="form-inline mt-2">
                            <div class="form-group mb-0 mr-2">
                                {{ form_widget(item.quantity, {
                                    'attr': {
                                        'class': 'form-control form-control-sm ' ~ (item.quantity.vars.valid ? '' : 'is-invalid')
                                    }
                                }) }}
                                <div class="invalid-feedback">
                                    {{ form_errors(item.quantity) }}
                                </div>
                            </div>
                            {{ form_widget(item.remove, {'attr': {'class': 'btn btn-dark btn-sm'}}) }}
                        </div>
                    </div>
                    <div class="flex-fill mr-2 text-right">
                        <b>{{ item.vars.data.product.price }}</b>
                    </div>
                </li>
            {% endfor %}
        </ul>
    </div>
    {{ form_end(form, {'render_rest': false}) }}
</div>
Enter fullscreen mode Exit fullscreen mode

As we don't use Javascript, we need to put the Save button at the beginning of the form to avoid submitting another submit button (such as the Delete button) when the customer press Enter. This is because when we have multiple submit buttons and you press Enter, the form will, by default, use the first submit button it finds.

The Cart summary

In the right column, add the cart summary:

<div class="col-md-4">
    <div class="card mt-4 mt-md-0">
        <h5 class="card-header bg-dark text-white">Summary</h5>
        <ul class="list-group list-group-flush">
            <li class="list-group-item d-flex justify-content-between">
                <div><b>Total</b></div>
                <span><b>{{ cart.total }}</b></span>
            </li>
        </ul>
        <div class="card-body">
            <a href="#" class="btn btn-warning w-100">Checkout</a>
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

You already can see the cart page on http://localhost:8000/cart.

Alt Text

Handling the Form

In the CartController controller, update the index() method and handle the form to map the submitted data to the form data, the cart as Order entity in this case.

<?php

namespace App\Controller;

use App\Form\CartType;
use App\Manager\CartManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * Class CartController
 * @package App\Controller
 */
class CartController extends AbstractController
{
    /**
     * @Route("/cart", name="cart")
     */
    public function index(CartManager $cartManager, Request $request): Response
    {
        $cart = $cartManager->getCurrentCart();

        $form = $this->createForm(CartType::class, $cart);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $cart->setUpdatedAt(new \DateTime());
            $cartManager->save($cart);

            return $this->redirectToRoute('cart');
        }

        return $this->render('cart/index.html.twig', [
            'cart' => $cart,
            'form' => $form->createView()
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

When the form is submitted, we have just to save the cart in session and database and then redirect the customer to the cart page. That's all.

In fact, the Order entity is automatically updated according to the submitted data. The data for the items field is used to construct an ArrayCollection of OrderItem entities. Each of them has been updated with the quantity chosen by the customer. The collection is then set on the items property of the Order.

For the moment, we only manage the edition of items in the cart. We will manage the deletion of each of them and the clearing of the cart later.

Adding a Link to the Cart Page

The customer needs a button to access the cart. Add a Cart button to the navbar in the base layout base.html.twig into the header block:

{% block header %}
    <nav class="navbar navbar-dark bg-dark sticky-top">
        {# ... #}
        <div class="navbar-nav">
            <a href="{{ path('cart') }}" class="btn btn-light">
                Cart
            </a>
        </div>
    </nav>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Now, the navbar contains a button to the cart page.

Alt Text

What's about the Remove and Clear buttons? We will manage them in the next two steps.

Top comments (1)

Collapse
 
webdesigncuba profile image
David Cordero Rosales

Good morning:
I am following your article but at the time of testing the cart it reflects this error
Cannot autowire argument $cartManager of "App\Controller\Frontend\CartController::index()": it references class "App\Manager\CartManager" but no such service exists.

I don't know if I forgot something