In this step, we will handle the form on the product detail page:
Building a AddToCartType
form
Use the Maker bundle to generate a form class:
$ symfony console make:form AddToCartType OrderItem
The AddToCartType
class defines a form for the OrderItem
entity. Customize it to have the following fields:
- quantity: the number of quantity of products the user wants to purchase,
- add: the submit button to add the product to the cart.
<?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 AddToCartType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('quantity');
$builder->add('add', SubmitType::class, [
'label' => 'Add to cart'
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => OrderItem::class,
]);
}
}
The form fields are bound to the model. The submitted data will be automatically mapped to the model class properties.
Displaying the Form
To display the form to the user, create the form in the ProductController
controller using the FormFactory
factory and pass it to the template:
<?php
namespace App\Controller;
use App\Entity\Product;
use App\Form\AddToCartType;
use App\Repository\ProductRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Class ProductController
* @package App\Controller
*/
class ProductController extends AbstractController
{
/**
* @Route("/product/{id}", name="product.detail")
*/
public function detail(Product $product)
{
$form = $this->createForm(AddToCartType::class);
return $this->render('product/detail.html.twig', [
'product' => $product,
'form' => $form->createView()
]);
}
}
In the template product/detail.html.twig
, display the form by using the form
Twig function:
<div class="col-md-8">
<h1 class="mt-4 mt-md-0">{{ product.name }}</h1>
<h2>{{ product.price }} €</h2>
<hr>
<b>Description: </b>{{ product.description }}
{{ form_start(form, {'attr': {'class': 'mt-4 p-4 bg-light'}}) }}
<div class="form-group">
{{ form_label(form.quantity) }}
{{ form_widget(form.quantity, {
'attr': {
'class': 'form-control ' ~ (form.quantity.vars.valid ? '' : 'is-invalid')
}
}) }}
<div class="invalid-feedback">
{{ form_errors(form.quantity) }}
</div>
</div>
{{ form_widget(form.add, {'attr': {'class': 'btn btn-warning w-25'}}) }}
{{ form_end(form) }}
</div>
Validating the Model
We need to add some validation constraints on the OrderItem
model to ensure that the submitted quantity is:
- not empty and,
- greater than or equal to 1.
<?php
namespace App\Entity;
use App\Repository\OrderItemRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass=OrderItemRepository::class)
*/
class OrderItem
{
// ...
/**
* @ORM\Column(type="integer")
* @Assert\NotBlank()
* @Assert\GreaterThanOrEqual(1)
*/
private $quantity;
// ...
}
Handling the Form
Now, we can handle the form submission and add the product to the cart by using the CartManager
manager.
<?php
namespace App\Controller;
use App\Entity\Product;
use App\Form\AddToCartType;
use App\Manager\CartManager;
use App\Repository\ProductRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Class ProductController
* @package App\Controller
*/
class ProductController extends AbstractController
{
// ...
/**
* @Route("/product/{id}", name="product.detail")
*/
public function detail(Product $product, Request $request, CartManager $cartManager)
{
$form = $this->createForm(AddToCartType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$item = $form->getData();
$item->setProduct($product);
$cart = $cartManager->getCurrentCart();
$cart
->addItem($item)
->setUpdatedAt(new \DateTime());
$cartManager->save($cart);
return $this->redirectToRoute('product.detail', ['id' => $product->getId()]);
}
return $this->render('product/detail.html.twig', [
'product' => $product,
'form' => $form->createView()
]);
}
}
When the form is submitted, the OrderItem
object is updated according to the submitted data. We retrieve it by using the Form::getData()
method and we don't forget to link the product to the OrderItem
object. Then, we add the item to the current cart and persist it in the session and database by using the CartManager::save()
method. Finally, we redirect the user to the product page.
If the form is not valid, we display the page with the form containing the submitted values and error messages.
For the moment, the user can add products to the cart but cannot review their cart because the page does not yet exist. Let's create the cart page in the next step.
Top comments (4)
I think there is a problem here
The productManager is called without arguments, thus throwing an exception when trying to acces 'product.detail' route.
The exception is :
Type error: Too few arguments to function App\Manager\CartManager::__construct(), 0 passed in C:\Projets Symfony\projetValentin\var\cache\dev\ContainerEq4lpi1\getServiceLocator_Frm8o4fService.php on line 9 and exactly 3 expected
Hello. I use the autowiring of Symfony, so I don't need to manage service dependencies.
To enable it, make sure that your
config/services.yaml
file configuration is like that:Of course !
How did I not think about that before ?
Been doing symfony for 4 years now and still forgetting basics lol .
Thx for the help. Much appreciated !
Hey Quentin, I'm really enjoying your tutorial!
I encountered a question while working on creating a form for the "OrderItem" entity. In the "AddToCartType.php" file, my form structure looked like this:
However, I ran into some errors and later realized that your implementation of the "buildForm" function is different:
I'm curious why the structure is different in your tutorial. Did you manually modify this function?