Creating the Cart Page | Building a Shopping Cart with Symfony
Tags: php, symfony, tutorial, form
Published: 2020-12-21 12:55:46

- [Building Forms](#building-forms)
  - [The `CartItemType` Form](#cart-item-type)
  - [The `CartType` Form](#cart-type)
- [Creating the Controller](#controller)
- [Rendering the Cart Page](#cart-page)
  - [List of items](#list-of-items)
  - [The Cart summary](#cart-summary)
- [Handling the Form](#handling-form)
- [Adding a Link to the Cart Page](#link-to-cart-page)

----

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 <a name="building-forms"></a>
--------------

### The `CartItemType` Form <a name="cart-item-type"></a>

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
```

Customize it to have the fields we need:

```php
<?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
        ]);
    }
}
```

### The `CartType` Form <a name="cart-type"></a>

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
```

Customize it to have the fields we need:

```php
<?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,
        ]);
    }
}
```

Creating the Controller <a name="controller"></a>
----------------------------------------
Create the `CartController` controller via the Maker:

```
$ symfony console make:controller CartController
```

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

```php
<?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',
        ]);
    }
}
```

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
<?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()
        ]);
    }
}

```

Rendering the Cart Page <a name="cart-page"></a>
-----------------------
In the `cart/index.html.twig` file, add the two-column layout grid below:

```twig
{% 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 %}
```

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

### List of items <a name="list-of-items"></a>
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:

```twig
<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>
```
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 <a name="cart-summary"></a>
In the right column, add the cart summary:

```twig
<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>
```

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1wre5wq78heni068d2oh.png)

Handling the Form <a name="handling-form"></a>
----------------------
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
<?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()
        ]);
    }
}
```

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 <a name="link-to-cart-page"></a>
------------------------------
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:

```twig
{% 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 %}
```

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/kak6ghngflf34l8v7o19.png)

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