PHP 8.4+ · LTS 7.4

Symfony 8 — aide-mémoire et correspondances

Votre point de repère.

Cette piste est votre référence : elle sert d'ancrage aux comparaisons des deux autres. Elle rassemble les commandes et les configurations du quotidien, plus une table de correspondance vers Laravel et TypeScript. Utilisez la recherche pour naviguer entre les trois pistes sur un même concept.

01

Installation & serveur

Le CLI, Flex et les squelettes

symfony new mon-projet --webappApplication complète : Twig, Doctrine, formulaires, sécurité, Maker.
symfony new mon-apiSkeleton minimal, pour une API ou un microservice.
symfony new mon-projet --version=7.4 --webappCible la LTS plutôt que la dernière stable.
composer require webappTransforme un skeleton en application web complète.
symfony server:start -dServeur en arrière-plan, HTTPS et HTTP/2.
symfony server:logLogs applicatifs et serveur en direct.
symfony console cache:clearComme bin/console, avec les variables des services Docker.
symfony check:securityCVE connues dans composer.lock.
symfony check:requirementsPrérequis de la machine.
Alias FlexInstalle
composer require ormDoctrine ORM + migrations
composer require maker --devMakerBundle
composer require securitySecurityBundle
composer require form validatorformulaires + validation
composer require apiAPI Platform
composer require messengerbus de messages
composer require debug --devprofiler + barre de debug
composer require test-pack --devPHPUnit et le pont Symfony
02

La console

Les debug:* sont la vraie documentation

php bin/console listToutes les commandes, par namespace.
php bin/console aboutVersion, environnement, chemins, état du cache.
php bin/console debug:routerToutes les routes.
php bin/console router:match /articles/12Quelle route répond, et pourquoi.
php bin/console debug:autowiring mailerQuels type-hints sont injectables.
php bin/console debug:container --show-privateTous les services du conteneur.
php bin/console debug:config frameworkConfiguration effective, valeurs résolues.
php bin/console config:dump-reference doctrineToutes les clés possibles, documentées.
php bin/console debug:event-dispatcherÉcouteurs enregistrés, par priorité.
php bin/console debug:dotenvD'où vient chaque variable.
php bin/console debug:firewall main --eventsDétail d'un pare-feu.
php bin/console cache:clearLe premier réflexe quand un comportement semble figé.
php bin/console lint:twig templatesValide les templates avant de déployer.
php bin/console lint:containerVérifie que tous les arguments se résolvent.
À retenirAucun des deux autres écosystèmes n'offre un équivalent à debug:container et debug:autowiring. C'est le prix de la configuration explicite — et son bénéfice.
03

MakerBundle

Les générateurs

CommandeGénère
make:controllercontrôleur + template
make:entityentité + repository, en interactif
make:crud ArticleCRUD complet : contrôleur, formulaire, templates
make:form ArticleTypeclasse de formulaire
make:migrationmigration par diff des entités
make:user · make:authsocle de sécurité
make:registration-form · make:reset-passwordparcours complets
make:voterautorisation
make:command app:xcommande console
make:subscriber · make:listenerévénements
make:messagemessage + handler Messenger
make:validatorcontrainte personnalisée
make:twig-componentcomposant Twig UX
make:testunitaire, fonctionnel ou Panther
04

Routing & contrôleurs

Attributs et mapping automatique

Routes par attributs
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;

#[Route('/articles', name: 'app_article_')]
class ArticleController extends AbstractController
{
    #[Route('', name: 'index', methods: ['GET'])]
    public function index(
        #[MapQueryParameter] int $page = 1,
    ): Response { … }

    #[Route('/{slug}', name: 'show', methods: ['GET'])]
    public function show(
        #[MapEntity(mapping: ['slug' => 'slug'])] Article $article,
    ): Response { … }

    #[Route('/{id}', requirements: ['id' => Requirement::DIGITS])]
    public function byId(int $id): Response { … }

    #[Route('/api', methods: ['POST'])]
    public function create(
        #[MapRequestPayload] CreateArticleDto $dto,
    ): JsonResponse { … }
}
Raccourcis d'AbstractController
$this->render('x.html.twig', []);
$this->json($data, 200, [], ['groups' => ['article:read']]);
$this->redirectToRoute('app_article_index');
$this->addFlash('success', 'Enregistré.');
$this->getUser();
$this->denyAccessUnlessGranted('EDIT', $article);
throw $this->createNotFoundException();
$this->generateUrl('app_article_show', ['id' => $id]);
05

Doctrine

Entités, repository, QueryBuilder

Entité
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
#[ORM\Index(columns: ['status', 'published_at'])]
class Article
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Assert\NotBlank]
    private string $title;

    #[ORM\Column(enumType: ArticleStatus::class)]
    private ArticleStatus $status = ArticleStatus::Draft;

    #[ORM\ManyToOne(inversedBy: 'articles')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    private User $author;

    #[ORM\OneToMany(mappedBy: 'article', targetEntity: Comment::class,
                    orphanRemoval: true, cascade: ['persist'])]
    private Collection $comments;
}
Repository — la requête vit ici
public function findPublishedWithAuthors(int $limit = 10): array
{
    return $this->createQueryBuilder('a')
        ->addSelect('u')                       // évite le N+1
        ->join('a.author', 'u')
        ->andWhere('a.publishedAt IS NOT NULL')
        ->andWhere('a.status = :status')
        ->setParameter('status', ArticleStatus::Published)
        ->orderBy('a.publishedAt', 'DESC')
        ->setMaxResults($limit)
        ->getQuery()
        ->getResult();
}
php bin/console make:migrationDiff entre les entités et le schéma réel.
php bin/console doctrine:migrations:migrateApplique les migrations en attente.
php bin/console doctrine:migrations:statusOù en est la base.
php bin/console doctrine:schema:validateCohérence mapping ↔ base.
php bin/console doctrine:query:dql "SELECT a FROM App\Entity\Article a"Teste une DQL sans écrire de code.
php bin/console doctrine:fixtures:loadVide la base avant de charger. --append pour conserver.
À retenirLe compteur Doctrine de la barre de debug est votre premier détecteur de N+1. Au-delà d'une dizaine de requêtes sur une page simple, il y a quelque chose à joindre avec addSelect().
06

Formulaires & validation

FormType et contraintes

Le cycle complet
$form = $this->createForm(ArticleType::class, $article);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
    $this->em->persist($article);
    $this->em->flush();

    $this->addFlash('success', 'Article enregistré.');

    return $this->redirectToRoute('app_article_index');
}

return $this->render('article/new.html.twig', ['form' => $form]);
Contraintes courantes
#[Assert\NotBlank(message: 'Le titre est obligatoire.')]
#[Assert\Length(min: 3, max: 255)]
private string $title;

#[Assert\Email(mode: 'strict')]
private string $email;

#[Assert\Choice(choices: ['draft', 'published'])]
private string $status;

#[Assert\GreaterThan('today')]
private \DateTimeImmutable $eventDate;

#[Assert\Valid]
private Address $address;

#[Assert\File(maxSize: '2M', mimeTypes: ['image/jpeg', 'image/png'])]
private ?UploadedFile $cover = null;

// Sur la classe
#[UniqueEntity(fields: ['slug'], message: 'Ce slug existe déjà.')]
07

Sécurité

Pare-feux, access_control, Voters

config/packages/security.yaml
security:
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

    providers:
        app_user_provider:
            entity: { class: App\Entity\User, property: email }

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                enable_csrf: true
            logout:
                path: app_logout

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/profil, roles: IS_AUTHENTICATED_FULLY }
Voter et points d'appel
class ArticleVoter extends Voter
{
    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, ['EDIT', 'DELETE'])
            && $subject instanceof Article;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();

        return $user instanceof User && $subject->getAuthor() === $user;
    }
}

#[IsGranted('EDIT', subject: 'article')]
public function edit(Article $article): Response { … }

$this->denyAccessUnlessGranted('EDIT', $article);
public function profile(#[CurrentUser] User $user): Response { … }
08

Services & injection

Autowiring et attributs

Les attributs qui remplacent le YAML
class InvoiceGenerator
{
    public function __construct(
        private readonly EntityManagerInterface $em,
        #[Autowire('%app.upload_dir%')] private readonly string $uploadDir,
        #[Autowire(env: 'STRIPE_KEY')] private readonly string $stripeKey,
        #[Autowire(service: 'monolog.logger.billing')] private readonly LoggerInterface $log,
    ) {}
}

#[AutoconfigureTag('app.exporter')]
interface ExporterInterface { … }

class ExportManager
{
    public function __construct(
        #[AutowireIterator('app.exporter')] private iterable $exporters,
    ) {}
}

#[AsDecorator(decorates: MailerInterface::class)]
class LoggingMailer implements MailerInterface { … }

#[When(env: 'dev')]
class FakePaymentGateway implements PaymentGatewayInterface { … }
09

Messenger & Scheduler

Asynchrone et planification

Message et handler
final readonly class SendInvoiceEmail
{
    public function __construct(public int $invoiceId) {}
}

#[AsMessageHandler]
final class SendInvoiceEmailHandler
{
    public function __invoke(SendInvoiceEmail $message): void { … }
}

$bus->dispatch(new SendInvoiceEmail($id));
$bus->dispatch(new SendInvoiceEmail($id), [new DelayStamp(60000)]);
php bin/console messenger:consume async -vvWorker en avant-plan, avec détail.
php bin/console messenger:consume async --time-limit=3600 --memory-limit=128MRéglages de production.
php bin/console messenger:stop-workersAprès chaque déploiement.
php bin/console messenger:failed:showMessages en échec.
php bin/console messenger:failed:retry -vvRejoue les échecs.
php bin/console debug:schedulerProchaines exécutions planifiées.
10

Tests

WebTestCase et assertions

Test fonctionnel
class ArticleControllerTest extends WebTestCase
{
    public function testIndexAfficheLesArticles(): void
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/articles');

        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Articles');
        $this->assertCount(10, $crawler->filter('article.card'));
    }

    public function testSoumissionFormulaire(): void
    {
        $client = static::createClient();
        $client->loginUser($this->getUser());
        $client->submitForm('Enregistrer', ['article[title]' => 'Bonjour']);

        $this->assertResponseRedirects();
    }
}
php bin/phpunit --testdoxSortie lisible, une phrase par test.
php bin/console --env=test doctrine:database:createBase dédiée aux tests.
composer require --dev dama/doctrine-test-bundleChaque test dans une transaction annulée.
composer require --dev zenstruck/foundryFactories expressives, dans l'esprit Laravel.
11

Déploiement

La séquence de production

Sur le serveur
git pull origin main
composer install --no-dev --optimize-autoloader --classmap-authoritative

composer dump-env prod

APP_ENV=prod php bin/console cache:clear
APP_ENV=prod php bin/console cache:warmup
APP_ENV=prod php bin/console doctrine:migrations:migrate -n
APP_ENV=prod php bin/console asset-map:compile
APP_ENV=prod php bin/console messenger:stop-workers
Point de contrôleAttendu
Racine du vhost/chemin/public
APP_ENV / APP_DEBUGprod / 0
Droitsvar/ inscriptible par le user PHP-FPM
OPcacheactivé, validate_timestamps=0, preload
Workerssupervisés, avec --time-limit
Profilerabsent des dépendances de production
12

Table de correspondance

Symfony → Laravel → TypeScript

La vue d'ensemble des trois pistes. La colonne TypeScript n'a de sens que pour les concepts qui traversent la frontière client/serveur.

Outillage et structure

SymfonyLaravelTypeScript / front
bin/consolephp artisannpm run / scripts
cache:clearoptimize:clearsupprimer node_modules/.vite
debug:routerroute:list
debug:containeraucun équivalent
aucun équivalenttinkerconsole du navigateur
Flex + recipesauto-discovery des providersnpm install
bundlespackages + ServiceProvidermodules ES
composer.lockcomposer.lockpackage-lock.json
PHPStan / PsalmPHPStan / Larastantsc --noEmit
PHP-CS-FixerPintESLint + Prettier

Concepts applicatifs

SymfonyLaravelTypeScript / front
Doctrine (Data Mapper)Eloquent (Active Record)interface + client d'API
Repositoryscopes sur le modèlemodule api/
Entity + #[ORM\Column]modèle + migrationinterface ou schéma Zod
TwigBladeJSX
FormTypeaucun équivalentétat de formulaire + Zod
#[Assert]Form Requestschéma Zod
VotersPolicies
role_hierarchyaucun équivalent
MessengerQueues + Jobs
SchedulerScheduler + cron unique
EventDispatcherEvents / ListenersCustomEvent / émetteur
Serializer + #[Groups]API Resourcesz.infer du schéma
enum: stringenum PHP + castunion de littéraux
?string?stringstring | null
Collections DoctrineCollections LaravelArray.prototype / T[]
Fixtures / FoundryFactoriesfixtures de test / MSW
AssetMapper / EncoreViteVite
ProfilerTelescopeReact DevTools + Redux DevTools
Comment lire ce tableauLes cases « aucun équivalent » sont les plus instructives : elles signalent là où votre intuition Symfony vous trompera. Pas de composant Form en Laravel, pas de hiérarchie de rôles, pas d'introspection du conteneur. À l'inverse, tinker et les factories n'ont pas d'équivalent natif Symfony — ce sont des habitudes à prendre, pas à transposer.