- Building Forms
- Creating the Controller
- Rendering the Cart Page
- Handling the Form
- Adding a Link to the 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
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
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
]);
}
}
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 allOrderItem
items of anOrder
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
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
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
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
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
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 %}
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>
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>
You already can see the cart page on http://localhost:8000/cart.
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()
]);
}
}
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 %}
Now, the navbar contains a button to the cart page.
What's about the Remove and Clear buttons? We will manage them in the next two steps.
Top comments (1)
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