Managing Products | Building a Shopping Cart with Symfony
Tags: php, symfony, tutorial, doctrine
Published: 2020-12-21 12:52:57

- [Creating the Entity](#create-entity)
- [Migrating the Database](#migrate-database)
- [Generating Fake Products](#generate-fake-products)
 - [Creating the Product Fixtures](#create-product-fixtures)
 - [Loading Data Fixtures](#load-data-fixtures)
- [Creating the Product List Page](#create-product-list-page)
 - [Updating the Home Controller](#update-home-controller)
 - [Rendering the Product List Page](#render-product-list-page)
- [Creating the Product Detail Page](#create-product-detail-page)
 - [Creating the Product Controller](#create-product-controller)
 - [Rendering the Product Detail Page](#render-product-detail-page)
- [Adding a Link to the Product Page](#add-link-to-product-page)
 - [Updating the Home Page](#update-home-page)


-------

A shopping cart without products does not make sense. In this part, to simplify the project, we will load fake products instead of managing them from an admin backend. We will also create a product list page and a product details page to allow users to add products to the cart.

**We will not manage product stocks.**

Creating the Entity <a name="create-entity"></a>
--------------------

A Product can be described with a few properties:
- Name
- Description
- Price

Use the Maker bundle to generate a class that represents a Product:

```
$ symfony console make:entity Product
```

The command is interactive, let's add the fields we need:
- name, string, 255, no
- description, text, no
- price, float, no

A `Product` entity has been stored under the `App\Entity` namespace and a `ProductRepository` repository under the `App\Repository`.

```php
<?php

namespace App\Entity;

use App\Repository\ProductRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=ProductRepository::class)
 */
class Product
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * @ORM\Column(type="text")
     */
    private $description;

    /**
     * @ORM\Column(type="float")
     */
    private $price;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(string $description): self
    {
        $this->description = $description;

        return $this;
    }

    public function getPrice(): ?float
    {
        return $this->price;
    }

    public function setPrice(float $price): self
    {
        $this->price = $price;

        return $this;
    }
}
```

Migrating the Database <a name="migrate-database"></a>
----------------------
For now, the database is empty. We need to create the database tables related to the entity.

One more time, use the Maker bundle to generate a migration file:

```
$ symfony console make:migration
```

A migration file has been stored under the `migrations/` directory. 

```php
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
 * Auto-generated Migration: Please modify to your needs!
 */
final class Version20201215122946 extends AbstractMigration
{
    public function getDescription() : string
    {
        return '';
    }

    public function up(Schema $schema) : void
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->addSql('CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, description LONGTEXT NOT NULL, price DOUBLE PRECISION NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
    }

    public function down(Schema $schema) : void
    {
        // this down() migration is auto-generated, please modify it to your needs
        $this->addSql('DROP TABLE product');
    }
}
```

We can now ready to update the local database schema:

```
$ symfony console doctrine:migrations:migrate
```

Generating Fake Products <a name="generate-fake-products"></a>
------------------------

### Creating the Product Fixtures <a name="create-product-fixtures"></a>
To add some fake products to your database, create a `ProductFixtures` class via the Maker bundle:

```
$ symfony console make:fixtures ProductFixtures
```

In the `load()` method, add ten products with fake data:

```php
<?php

namespace App\DataFixtures;

use App\Entity\Product;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

/**
 * Class ProductFixtures
 */
class ProductFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        for ($i = 1; $i <= 10; $i++) {
            $product = new Product();
            $product
                ->setName('Product ' . $i)
                ->setDescription('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua')
                ->setPrice(mt_rand(10, 600));

            $manager->persist($product);
        }

        $manager->flush();
    }
}
```

### Loading Data Fixtures <a name="load-data-fixtures"></a>
Load the fixtures by executing this command:

```
$ symfony console doctrine:fixtures:load
```

Creating the Products List Page <a name="create-product-list-page"></a>
--------------------------------
The list of products page will be the homepage.

### Updating the Home Controller <a name="update-home-controller"></a>

In the `HomeController` controller, implement the `index()` method:
- Fetch all products by using the `ProductRepository` and pass them to the Twig template `products/index.html.twig`.

```php
<?php

namespace App\Controller;

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

class HomeController extends AbstractController
{
    /**
     * @Route("/", name="home")
     */
    public function index(ProductRepository $productRepository): Response
    {
        return $this->render('home/index.html.twig', [
            'products' => $productRepository->findAll(),
        ]);
    }
}
```

### Rendering the Product List Page <a name="render-product-list-page"></a>

The last step is to update the `templates/home/index.html.twig` file:

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

{% block body %}
<div class="container">
    <h1 class="mt-3 mb-4">Products</h1>
    <div class="row">
        {% for product in products %}
            <div class="col-md-4">
                <div class="card mb-4">
                    <img src="https://via.placeholder.com/200x150" alt="{{ product.name }}" class="card-img-top">
                    <div class="card-body">
                        <h5 class="card-title">{{ product.name }}</h5>
                        <p class="card-text">{{ product.description }}</p>
                        <div class="d-flex justify-content-between align-item-center">
                            <a href="#" class="btn btn-dark">View details</a>
                            <span class="h5 mt-auto">{{ product.price }} €</span>
                        </div>
                    </div>
                </div>
            </div>
        {% endfor %}
    </div>
</div>
{% endblock %}
```

Now, you can see the list of products on the homepage http://localhost:8000.

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

Creating the Product Detail Page <a name="create-product-detail-page"></a>
--------------------------------
The detail page will be used to view the details of a product and allow a user to add one or more items to the cart.

### Creating the Product Controller <a name="create-product-controller"></a>
Create the `ProductController` controller via the Maker bundle:

```
$ symfony console make:controller ProductController
```

The command creates a `ProductController` class under the `src/Controller/` directory and a template file to `templates/product/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 ProductController extends AbstractController
{
    /**
     * @Route("/product", name="product")
     */
    public function index(): Response
    {
        return $this->render('product/index.html.twig', [
            'controller_name' => 'ProductController',
        ]);
    }
}
```
Rename the `index()` method with a `detail()` method in the `ProductController` and implement it:
- Define a route with a product ID parameter,
- Get the product and pass it to the Twig template `product/detail.html.twig`.

```php
<?php

namespace App\Controller;

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

class ProductController extends AbstractController
{
    /**
     * @Route("/product/{id}", name="product.detail")
     */
    public function index(Product $product): Response
    {
        return $this->render('product/detail.html.twig', [
            'product' => $product,
        ]);
    }
}
```

Thanks to the `ParamConverters`, Symfony will automatically query for the `Product` entity and pass it as an argument to the controller. It will also show a 404 page if no entity can be found.

### Rendering the Product Detail Page <a name="render-product-detail-page"></a>
Rename the template `product/index.html.twig` file with `product/detail.html.twig` and design the product page:

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

{% block title %}{{ product.name }}{% endblock %}

{% block body %}
    <div class="container">
        <div class="row mt-3 mb-4">
            <div class="col-md-4">
                <img src="https://via.placeholder.com/600x400" alt="{{ product.name }}" class="img-fluid">
            </div>
            <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 class="mt-4 p-4 bg-light" method="post">
                    <div class="form-group">
                        <label for="quantity">Quantity</label>
                        <input type="number" class="form-control" id="quantity">
                    </div>
                    <button class="btn btn-warning">Add to Cart</button>
                </form>
            </div>
        </div>
    </div>
{% endblock %}
```

Now you can see the detail of the products. For example, to see the Product 1, go to the page http://localhost:8000/product/1.

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

Note that the form will be handled later.

Adding a Link to the Product Page <a name="add-link-to-product-page"></a>
---------------------------------

### Updating the Home Page <a name="update-home-page"></a>
The *View details* button gets nowhere. Open the template `home/index.html.twig` and replace the hyperlink with the route to the product page:

```twig
<a href="{{ path('product.detail', {id: product.id}) }}" class="btn btn-dark">
    View details
</a>
```

It's done! Go to the homepage and click on the *View details* button of a product and take a look at your product page.

Let's manage the cart to the next step.