Les 8: Winkelmantje
HomeLes 8

🛒 Winkelmantje

⏱ Geschatte leestijd: 50 minuten · 7 stappen

Een winkelmantje slaat tijdelijk gegevens op terwijl de gebruiker door de webshop bladert. Symfony gebruikt sessions om data per gebruiker te bewaren tussen pagina-aanvragen. Je bouwt een CartSessionStorage service die de winkelmandje-logica beheert, en koppelt dit aan een checkout die de bestelling in de database opslaat.

📐 Architectuur van het winkelmantje

Session
Bewaard in de browser van de gebruiker, tijdelijk
CartSessionStorage
Service klasse die de cart leest/schrijft via de session
CartController
Toont het winkelmantje, verwerkt add/remove
Purchase + OrderLine
Entities die de bestelling permanent opslaan in de database

1 Sessions in Symfony

Een session slaat gegevens op die aan één gebruiker zijn gekoppeld — ook als die meerdere pagina-aanvragen doet. Symfony gebruikt RequestStack om bij de huidige sessie te komen.

Sessions lezen en schrijven via RequestStack
use Symfony\Component\HttpFoundation\RequestStack;

class MijnService
{
    private $session;

    public function __construct(RequestStack $requestStack)
    {
        // Haal de huidige sessie op
        $this->session = $requestStack->getSession();
    }

    public function opslaanInSessie(): void
    {
        // Sla een waarde op in de sessie
        $this->session->set('naam', 'Jan');
        $this->session->set('winkelmandje', [1 => 2, 3 => 1]); // product_id => aantal
    }

    public function lezenUitSessie(): mixed
    {
        // Lees een waarde uit de sessie (met standaardwaarde)
        return $this->session->get('naam', 'onbekend');
    }

    public function verwijderenUitSessie(): void
    {
        // Verwijder een sleutel uit de sessie
        $this->session->remove('naam');
    }
}
set('key', $val)

Waarde opslaan

get('key', default)

Waarde ophalen

remove('key')

Waarde verwijderen

2 Fixtures: testdata laden

Fixtures zijn PHP-klassen die testdata in de database laden. Handig voor ontwikkeling: je hoeft niet telkens handmatig producten aan te maken.

Terminal — fixtures installeren
# Installeer de fixtures-bundle
composer require --dev orm-fixtures

# Laad de fixtures (wist bestaande data!)
php bin/console doctrine:fixtures:load
src/DataFixtures/ProductFixtures.php
<?php

namespace App\DataFixtures;

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

class ProductFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        // Maak 5 testproducten aan
        $producten = [
            ['naam' => 'Laptop Pro',     'prijs' => 999.99,  'beschrijving' => 'Krachtige laptop voor ontwikkelaars'],
            ['naam' => 'Muis Wireless',  'prijs' => 29.95,   'beschrijving' => 'Ergonomische draadloze muis'],
            ['naam' => 'Toetsenbord',    'prijs' => 79.00,   'beschrijving' => 'Mechanisch toetsenbord'],
            ['naam' => 'Monitor 27"',    'prijs' => 349.00,  'beschrijving' => '4K UHD monitor'],
            ['naam' => 'USB-C Hub',      'prijs' => 49.95,   'beschrijving' => '7-in-1 USB-C hub'],
        ];

        foreach ($producten as $data) {
            $product = new Product();
            $product->setNaam($data['naam']);
            $product->setPrijs($data['prijs']);
            $product->setBeschrijving($data['beschrijving']);
            $manager->persist($product);
        }

        $manager->flush();
    }
}
⚠️ Let op: doctrine:fixtures:load wist standaard alle bestaande data. Gebruik --append om te voorkomen dat bestaande data wordt verwijderd.

3 CartSessionStorage service

Je bouwt een service die alle winkelmantje-logica bevat. Een service is een herbruikbare PHP-klasse die je via dependency injection in controllers en andere services kunt gebruiken. Symfony maakt de service automatisch beschikbaar via autowiring.

src/Service/CartSessionStorage.php
<?php

namespace App\Service;

use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

class CartSessionStorage
{
    private const CART_KEY = 'shopping_cart';

    private $session;
    private EntityManagerInterface $em;

    public function __construct(RequestStack $requestStack, EntityManagerInterface $em)
    {
        $this->session = $requestStack->getSession();
        $this->em = $em;
    }

    // Voeg een product toe aan het winkelmantje
    public function addProductToCart(int $productId): void
    {
        $cart = $this->getCart();
        // Als product al in cart: verhoog het aantal
        $cart[$productId] = ($cart[$productId] ?? 0) + 1;
        $this->session->set(self::CART_KEY, $cart);
    }

    // Verwijder een product uit het winkelmantje
    public function removeProductFromCart(int $productId): void
    {
        $cart = $this->getCart();
        unset($cart[$productId]);
        $this->session->set(self::CART_KEY, $cart);
    }

    // Geef het winkelmantje terug als [Product => aantal] array
    public function getShoppingCart(): array
    {
        $result = [];
        foreach ($this->getCart() as $productId => $aantal) {
            $product = $this->em->getRepository(Product::class)->find($productId);
            if ($product) {
                $result[] = ['product' => $product, 'aantal' => $aantal];
            }
        }
        return $result;
    }

    // Aantal items in het winkelmantje
    public function getNumberOfProductsInCart(): int
    {
        return array_sum($this->getCart());
    }

    // Totaalprijs berekenen
    public function getTotalPrice(): float
    {
        $total = 0;
        foreach ($this->getCart() as $productId => $aantal) {
            $product = $this->em->getRepository(Product::class)->find($productId);
            if ($product) {
                $total += $product->getPrijs() * $aantal;
            }
        }
        return $total;
    }

    // Winkelmantje leegmaken
    public function clearShoppingCart(): void
    {
        $this->session->remove(self::CART_KEY);
    }

    private function getCart(): array
    {
        return $this->session->get(self::CART_KEY, []);
    }
}
💡 Autowiring: Symfony registreert klassen in src/Service/ automatisch als service. Je hoeft niets in services.yaml te configureren. Voeg de service gewoon toe als parameter in de constructor van een controller.

4 CartController — winkelmantje tonen en beheren

De CartController gebruikt de CartSessionStorage service om het winkelmantje te tonen en producten toe te voegen of te verwijderen.

src/Controller/CartController.php
<?php

namespace App\Controller;

use App\Service\CartSessionStorage;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class CartController extends AbstractController
{
    // Symfony injecteert CartSessionStorage automatisch via autowiring
    public function __construct(private CartSessionStorage $cart) {}

    #[Route('/winkelmandje', name: 'cart_index')]
    public function index(): Response
    {
        return $this->render('cart/index.html.twig', [
            'items'  => $this->cart->getShoppingCart(),
            'totaal' => $this->cart->getTotalPrice(),
            'aantal' => $this->cart->getNumberOfProductsInCart(),
        ]);
    }

    #[Route('/winkelmandje/toevoegen/{id}', name: 'cart_add')]
    public function add(int $id): Response
    {
        $this->cart->addProductToCart($id);
        $this->addFlash('success', 'Product toegevoegd aan het winkelmandje!');
        return $this->redirectToRoute('cart_index');
    }

    #[Route('/winkelmandje/verwijderen/{id}', name: 'cart_remove')]
    public function remove(int $id): Response
    {
        $this->cart->removeProductFromCart($id);
        return $this->redirectToRoute('cart_index');
    }
}
templates/cart/index.html.twig
{% for item in items %}
<div class="product-rij">
    <span>{{ item.product.naam }}</span>
    <span>{{ item.aantal }}x</span>
    <span>€ {{ (item.product.prijs * item.aantal)|number_format(2, ',', '.') }}</span>
    <a href="{{ path('cart_remove', {id: item.product.id}) }}">Verwijder</a>
</div>
{% else %}
    <p>Je winkelmandje is leeg.</p>
{% endfor %}

<p><strong>Totaal: € {{ totaal|number_format(2, ',', '.') }}</strong></p>

<a href="{{ path('cart_checkout') }}" class="btn btn-primary">Afrekenen</a>

5 Entities: Purchase en OrderLine

Om een bestelling permanent op te slaan heb je twee entities nodig die een OneToMany relatie hebben: één Purchase (de bestelling) heeft meerdere OrderLines (één per product).

📊 ERD — Entiteitsrelaties
Product
id
naam
beschrijving
prijs
ManyToOne
OneToMany
OrderLine
id
product →
purchase →
aantal
prijs
ManyToOne
OneToMany
Purchase
id
datum
totaalprijs
orderLines →
src/Entity/Purchase.php
<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Purchase
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private int $id;

    #[ORM\Column(type: 'datetime')]
    private \DateTime $datum;

    #[ORM\Column(type: 'float')]
    private float $totaalprijs;

    // Eén bestelling heeft meerdere orderregels
    #[ORM\OneToMany(targetEntity: OrderLine::class, mappedBy: 'purchase', cascade: ['persist'])]
    private Collection $orderLines;

    public function __construct()
    {
        $this->datum = new \DateTime();
        $this->orderLines = new ArrayCollection();
    }

    public function addOrderLine(OrderLine $line): void
    {
        $this->orderLines->add($line);
        $line->setPurchase($this);
    }
    // ... getters en setters
}
src/Entity/OrderLine.php
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class OrderLine
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private int $id;

    // Elke orderregel verwijst naar één product
    #[ORM\ManyToOne(targetEntity: Product::class)]
    private Product $product;

    // Elke orderregel hoort bij één bestelling
    #[ORM\ManyToOne(targetEntity: Purchase::class, inversedBy: 'orderLines')]
    private Purchase $purchase;

    #[ORM\Column]
    private int $aantal;

    #[ORM\Column(type: 'float')]
    private float $prijs;
    // ... getters en setters
}

6 Checkout — bestelling opslaan

Bij de checkout zet je de sessie-data om naar echte database-records. Je maakt een Purchase aan, voegt voor elk cart-item een OrderLine toe en slaat alles op via Doctrine.

src/Controller/CartController.php — checkout methode toevoegen
use App\Entity\OrderLine;
use App\Entity\Purchase;
use Doctrine\ORM\EntityManagerInterface;

#[Route('/winkelmandje/afrekenen', name: 'cart_checkout', methods: ['POST'])]
public function checkout(EntityManagerInterface $em): Response
{
    $items = $this->cart->getShoppingCart();

    if (empty($items)) {
        $this->addFlash('error', 'Je winkelmandje is leeg!');
        return $this->redirectToRoute('cart_index');
    }

    // Maak een nieuwe bestelling aan
    $purchase = new Purchase();
    $purchase->setTotaalprijs($this->cart->getTotalPrice());

    // Voeg voor elk product een orderregel toe
    foreach ($items as $item) {
        $orderLine = new OrderLine();
        $orderLine->setProduct($item['product']);
        $orderLine->setAantal($item['aantal']);
        $orderLine->setPrijs($item['product']->getPrijs());
        $purchase->addOrderLine($orderLine);
    }

    // Sla de bestelling op (cascade persist zorgt voor de orderlines)
    $em->persist($purchase);
    $em->flush();

    // Winkelmantje leegmaken
    $this->cart->clearShoppingCart();

    $this->addFlash('success', 'Bestelling geplaatst! Bedankt voor je aankoop.');
    return $this->redirectToRoute('cart_confirmation', ['id' => $purchase->getId()]);
}
✓ cascade: ['persist'] op de OneToMany relatie zorgt ervoor dat je de OrderLines niet apart hoeft te persisteren. Alleen persist($purchase) is nodig — Doctrine slaat alle gekoppelde OrderLines automatisch op.

7 Oefenen: schrijf addProductToCart

Schrijf de methode addProductToCart(int $productId) voor de CartSessionStorage service. De methode moet het product aan de sessie toevoegen (of het aantal verhogen als het er al in zit).

src/Service/CartSessionStorage.php
Klik op "Controleer" om je code te testen...

🛒 Demo: winkelmantje simulatie

Klik op "Toevoegen" om te zien hoe een winkelmantje werkt. De data wordt opgeslagen in localStorage (simulatie van een sessie).

Laptop Pro
€ 999,99
Muis Wireless
€ 29,95
Toetsenbord
€ 79,00

🛒 Winkelmandje (0 items)

Nog geen producten

📋 Samenvatting

  • Sessions bewaren data per gebruiker: session->set(), get(), remove() via RequestStack
  • Fixtures laden testdata in de database: composer require --dev orm-fixtures
  • CartSessionStorage is een service die alle winkelmandje-logica bevat via sessions
  • Services worden automatisch geïnjecteerd via Symfony autowiring (constructor parameter)
  • Purchase (OneToMany) en OrderLine (ManyToOne) slaan de bestelling permanent op
  • Bij checkout: cart-items omzetten naar Doctrine entities, persist() + flush(), cart leegmaken
🎓

Gefeliciteerd!

Je hebt alle 8 lessen van SymfonyLearn doorlopen! Van installatie en routes tot CRUD en een volledig winkelmantje — je hebt de basisconcepten van Symfony in de vingers.

Klaar voor een echt project? Combineer alles wat je geleerd hebt en bouw je eigen Symfony-webshop!

🧠

Kennischeck

Test of je de stof begrepen hebt

Laatste les! Markeer hem als voltooid.