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.
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 Flex | Installe |
|---|---|
composer require orm | Doctrine ORM + migrations |
composer require maker --dev | MakerBundle |
composer require security | SecurityBundle |
composer require form validator | formulaires + validation |
composer require api | API Platform |
composer require messenger | bus de messages |
composer require debug --dev | profiler + barre de debug |
composer require test-pack --dev | PHPUnit et le pont Symfony |
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.debug:container et debug:autowiring. C'est le prix de la configuration explicite — et son bénéfice.MakerBundle
Les générateurs
| Commande | Génère |
|---|---|
make:controller | contrôleur + template |
make:entity | entité + repository, en interactif |
make:crud Article | CRUD complet : contrôleur, formulaire, templates |
make:form ArticleType | classe de formulaire |
make:migration | migration par diff des entités |
make:user · make:auth | socle de sécurité |
make:registration-form · make:reset-password | parcours complets |
make:voter | autorisation |
make:command app:x | commande console |
make:subscriber · make:listener | événements |
make:message | message + handler Messenger |
make:validator | contrainte personnalisée |
make:twig-component | composant Twig UX |
make:test | unitaire, fonctionnel ou Panther |
Routing & contrôleurs
Attributs et mapping automatique
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 { … }
}$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]);Doctrine
Entités, repository, QueryBuilder
#[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;
}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.addSelect().Formulaires & validation
FormType et contraintes
$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]);#[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à.')]Sécurité
Pare-feux, access_control, Voters
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 }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 { … }Services & injection
Autowiring et attributs
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 { … }Messenger & Scheduler
Asynchrone et planification
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.Tests
WebTestCase et assertions
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.Déploiement
La séquence de production
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ôle | Attendu |
|---|---|
| Racine du vhost | /chemin/public |
APP_ENV / APP_DEBUG | prod / 0 |
| Droits | var/ inscriptible par le user PHP-FPM |
| OPcache | activé, validate_timestamps=0, preload |
| Workers | supervisés, avec --time-limit |
| Profiler | absent des dépendances de production |
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
| Symfony | Laravel | TypeScript / front |
|---|---|---|
bin/console | php artisan | npm run / scripts |
cache:clear | optimize:clear | supprimer node_modules/.vite |
debug:router | route:list | — |
debug:container | aucun équivalent | — |
| aucun équivalent | tinker | console du navigateur |
| Flex + recipes | auto-discovery des providers | npm install |
| bundles | packages + ServiceProvider | modules ES |
composer.lock | composer.lock | package-lock.json |
| PHPStan / Psalm | PHPStan / Larastan | tsc --noEmit |
| PHP-CS-Fixer | Pint | ESLint + Prettier |
Concepts applicatifs
| Symfony | Laravel | TypeScript / front |
|---|---|---|
| Doctrine (Data Mapper) | Eloquent (Active Record) | interface + client d'API |
| Repository | scopes sur le modèle | module api/ |
Entity + #[ORM\Column] | modèle + migration | interface ou schéma Zod |
| Twig | Blade | JSX |
| FormType | aucun équivalent | état de formulaire + Zod |
#[Assert] | Form Request | schéma Zod |
| Voters | Policies | — |
role_hierarchy | aucun équivalent | — |
| Messenger | Queues + Jobs | — |
| Scheduler | Scheduler + cron unique | — |
| EventDispatcher | Events / Listeners | CustomEvent / émetteur |
Serializer + #[Groups] | API Resources | z.infer du schéma |
enum: string | enum PHP + cast | union de littéraux |
?string | ?string | string | null |
| Collections Doctrine | Collections Laravel | Array.prototype / T[] |
| Fixtures / Foundry | Factories | fixtures de test / MSW |
| AssetMapper / Encore | Vite | Vite |
| Profiler | Telescope | React DevTools + Redux DevTools |
tinker et les factories n'ont pas d'équivalent natif Symfony — ce sont des habitudes à prendre, pas à transposer.