16.4 — Cheatsheet Doctrine & Twig
Doctrine — mapping (attributs)
#[ORM\Entity(repositoryClass: BookingRepository::class)]
#[ORM\Table(name: 'bookings')]
#[ORM\Index(columns: ['status'])]
class Booking
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $email;
#[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)]
private ?string $amount = null;
#[ORM\Column(enumType: BookingStatus::class)]
private BookingStatus $status = BookingStatus::Pending;
}Doctrine — relations
// ManyToOne (côté propriétaire, porte la FK)
#[ORM\ManyToOne(inversedBy: 'bookings')]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
// OneToMany (côté inverse)
/** @var Collection<int, Booking> */
#[ORM\OneToMany(targetEntity: Booking::class, mappedBy: 'user', orphanRemoval: true)]
private Collection $bookings; // new ArrayCollection() dans le constructeur
// ManyToMany
#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'services')]
private Collection $tags;
// OneToOne
#[ORM\OneToOne(inversedBy: 'payment')]
private ?Booking $booking = null;Doctrine — persistance & requêtes
$em->persist($entity); $em->flush(); // insert (un seul flush)
$entity->setX(...); $em->flush(); // update (Unit of Work détecte)
$em->remove($entity); $em->flush(); // delete
// Repository (méthodes de base)
$repo->find(42);
$repo->findBy(['status' => X], ['start' => 'ASC'], 20);
$repo->findOneBy(['email' => $e]);
$repo->count(['status' => X]);
// QueryBuilder (+ fetch join anti N+1)
$this->createQueryBuilder('b')
->addSelect('s')->join('b.slot', 's') // charge la relation
->andWhere('b.status = :s')->setParameter('s', $status) // JAMAIS de concat
->orderBy('b.start', 'DESC')
->setFirstResult(($page-1)*$per)->setMaxResults($per)
->getQuery()->getResult();
// Résultats
->getResult(); // Entity[]
->getOneOrNullResult(); // Entity|null
->getSingleScalarResult(); // COUNT / scalaire
->toIterable(); // générateur (gros volumes)Twig — syntaxe
{# affichage / logique / commentaire #}
{{ variable }} {% if x %}...{% endif %} {# note #}
{# accès #}
{{ booking.customerEmail }} {# clé, propriété OU getter #}
{{ booking.status.label }} {# méthode d'enum #}
{# filtres #}
{{ name|upper }} {{ date|date('d/m/Y H:i') }} {{ x|default('—') }}
{{ items|length }} {{ price|number_format(2, ',', ' ') }}
{# fonctions #}
{{ path('route', { id: x }) }} {{ asset('img.png') }} {{ is_granted('EDIT', obj) }}
{# conditions / boucles #}
{% for slot in service.slots %}
{{ loop.index }} — {{ slot.start|date('H:i') }}
{% else %}
Aucun créneau.
{% endfor %}
{# héritage #}
{% extends 'base.html.twig' %}
{% block body %}...{{ parent() }}...{% endblock %}
{# include / composant #}
{% include 'partials/_row.html.twig' with { booking: b } only %}
<twig:StatusBadge :status="booking.status" />
{# global app #}
{{ app.user.email }} {{ app.request.locale }} {{ app.flashes }}Formulaire (Twig)
{{ form_start(form) }}
{{ form_row(form.email) }}
{{ form_widget(form.seats) }}
{{ form_errors(form.seats) }}
{{ form_rest(form) }} {# n'oubliez pas : rend le token CSRF #}
{{ form_end(form) }}Suivant : 16.5 — Pièges & FAQ.