원클릭으로
controller-action
Écrit un contrôleur Symfony — AbstractController, helpers (render/json/redirectToRoute), mappage (#[MapQueryParameter],
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Écrit un contrôleur Symfony — AbstractController, helpers (render/json/redirectToRoute), mappage (#[MapQueryParameter],
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | controller-action |
| description | Écrit un contrôleur Symfony — AbstractController, helpers (render/json/redirectToRoute), mappage (#[MapQueryParameter], |
| user_invocable | true |
| allowed-tools | ["Read","Write","Edit","Glob","Grep","Bash(ls:*)","Bash(find:*)","Bash(cat:*)","Bash(git:*)","Bash(symfony:*)","Bash(bin/console:*)","Bash(composer:*)","Bash(vendor/bin/*:*)","Bash(./vendor/bin/*:*)"] |
Utilise quand tu ajoutes ou révises une action HTTP qui lit une
Requestet renvoie uneResponse(HTML, JSON, fichier, stream, redirection). Pas quand tu veux juste poser/ajuster du routing (path, methods, requirements,host,schemes, génération d'URL) →/symfony:routing-definecouvre tout le registre#[Route]. Pas quand tu fais du CRUD sur une Resource Sylius — leResourceControllervendor gère index/create/update/show/delete.
Tu crées ou révises un contrôleur Symfony. Un contrôleur est une fonction PHP qui lit une Request et renvoie une Response — rien d'autre. Tu fais passer tout le métier par un service et tu t'appuies sur les helpers de AbstractController plutôt que de réinventer l'accès à Twig, au routeur, au security, à la session.
composer.json à la racine du projet.symfony/framework-bundle dans les dépendances.
symfony/framework-bundle dans composer.json. On continue quand même ou on change d'approche ? » et attendre la réponse.#[Route] (standard Symfony 6+) ou YAML/XML sous config/routes/ (legacy, à ne pas introduire dans un projet neuf).sylius/sylius est présent → signaler en une ligne que Sylius expose un ResourceController générique pour le CRUD des Resources ; un contrôleur custom n'est justifié que pour une action hors CRUD (dashboard, webhook, export).AbstractController : donne accès à render(), json(), redirectToRoute(), createNotFoundException(), addFlash(), getUser(), denyAccessUnlessGranted(), isGranted(), sans les injecter à la main. Pas besoin d'extender Controller (déprécié) ni d'implémenter quoi que ce soit.Response (ou une sous-classe : JsonResponse, RedirectResponse, BinaryFileResponse, StreamedResponse). Une méthode qui ne renvoie pas de Response casse le kernel, sauf à déclencher un event kernel.view.#[Route] sur la méthode publique. Le préfixe commun (URL ou nom) va sur un #[Route] au niveau de la classe. Toujours nommer la route (name: 'product_show') — les URLs se construisent par nom, pas par chaîne.methods: ['GET'], methods: ['POST'], etc. Ne jamais laisser une action CRUD accepter ANY. Pour un form, methods: ['GET', 'POST'] (affichage + soumission, cf. /symfony:form-handle)./symfony:service-define).declare(strict_types=1) en tête de chaque fichier, types de retour explicites (: Response, : JsonResponse).$this->container ni de getContainer() : anti-pattern. Tout ce qui serait tiré du container est déjà exposé par AbstractController ou injectable.final : un contrôleur n'a pas vocation à être étendu. Marquer la classe final.// src/Controller/ProductController.php
declare(strict_types=1);
namespace App\Controller;
use App\Service\ProductFinder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/product', name: 'product_')]
final class ProductController extends AbstractController
{
public function __construct(
private readonly ProductFinder $finder,
) {}
#[Route('/{id}', name: 'show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function show(int $id): Response
{
$product = $this->finder->find($id) ?? throw $this->createNotFoundException();
return $this->render('product/show.html.twig', [
'product' => $product,
]);
}
}
Le contrôleur est enregistré automatiquement comme service (resource App\: de services.yaml, cf. /symfony:service-define). L'autowiring résout ProductFinder via le constructeur.
#[Route('/article/{slug}', name: 'article_show', methods: ['GET'])]
public function show(string $slug): Response { /* ... */ }
Le type PHP convertit automatiquement (int, string, bool, \DateTimeImmutable via value resolver).
#[MapEntity] (implicite via type-hint)#[Route('/product/{id}', name: 'product_show', methods: ['GET'])]
public function show(Product $product): Response
{
return $this->render('product/show.html.twig', ['product' => $product]);
}
doctrine/doctrine-bundle requis. Par défaut, matche sur {id}. Pour un slug ou un critère custom → #[MapEntity(mapping: ['slug' => 'slug'])], détails dans /symfony:doctrine-query.
#[MapQueryParameter] (Symfony 6.3+)use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
#[Route('/search', name: 'search', methods: ['GET'])]
public function search(
#[MapQueryParameter] string $q = '',
#[MapQueryParameter] int $page = 1,
): Response {
// $q et $page viennent de la query string, typés, avec défauts.
}
Remplace $request->query->get('q') + cast manuel. Les contraintes de validation (#[Assert\Length], #[Assert\Positive]) s'appliquent sur le paramètre.
#[MapQueryString] — bind la query string sur un DTOfinal class SearchCriteria
{
public function __construct(
public string $q = '',
#[Assert\Positive] public int $page = 1,
) {}
}
#[Route('/search', name: 'search', methods: ['GET'])]
public function search(#[MapQueryString] SearchCriteria $criteria): Response { /* ... */ }
#[MapRequestPayload] — bind le body JSON/form sur un DTOfinal class CreateProductRequest
{
public function __construct(
#[Assert\NotBlank] public string $name,
#[Assert\PositiveOrZero] public int $priceCents,
) {}
}
#[Route('/api/products', name: 'api_product_create', methods: ['POST'])]
public function create(
#[MapRequestPayload] CreateProductRequest $payload,
ProductCreator $creator,
): JsonResponse {
$product = $creator->create($payload);
return $this->json($product, Response::HTTP_CREATED);
}
Valide automatiquement (400 si contraintes KO), dispensant d'un FormType pour les API JSON.
Request brutÀ utiliser quand l'action lit plusieurs champs hétérogènes ou les headers :
public function action(Request $request): Response
{
$ua = $request->headers->get('User-Agent');
$body = $request->getContent();
$ip = $request->getClientIp();
// ...
}
Préférer #[MapQueryParameter] / #[MapRequestPayload] quand le format est connu.
AbstractController| Besoin | Helper |
|---|---|
| Rendre un template Twig | $this->render('path.html.twig', [...]) |
| Réponse JSON | $this->json($data, $status) |
| Redirection vers une route | $this->redirectToRoute('name', [...], $status) |
| Redirection vers une URL brute | $this->redirect($url, $status) |
| 404 | throw $this->createNotFoundException('msg?') |
| 403 | throw $this->createAccessDeniedException('msg?') |
| Check droit | $this->denyAccessUnlessGranted('ROLE', $subject) |
| Utilisateur connecté | $this->getUser() |
| Message flash | $this->addFlash('success', 'key') |
| Fichier téléchargé | $this->file($path, $name) |
| Stream | new StreamedResponse(callable) |
| Forward (interne) | $this->forward('OtherController::action', [...]) |
#[Route('/api/products/{id}', name: 'api_product_show', methods: ['GET'])]
public function show(int $id, ProductFinder $finder): JsonResponse
{
$product = $finder->find($id) ?? throw $this->createNotFoundException();
return $this->json($product, context: ['groups' => ['product:read']]);
}
Les groups s'appuient sur le Serializer (cf. /symfony:serializer-use). Pour les gros volumes, préférer API Platform.
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
#[Route('/invoice/{id}/pdf', name: 'invoice_pdf', methods: ['GET'])]
public function pdf(Invoice $invoice, InvoiceRenderer $renderer): Response
{
$path = $renderer->toTempPdf($invoice);
return $this->file($path, "facture-{$invoice->getNumber()}.pdf", ResponseHeaderBag::DISPOSITION_ATTACHMENT);
}
use Symfony\Component\HttpFoundation\StreamedResponse;
#[Route('/export/orders.csv', name: 'orders_export', methods: ['GET'])]
public function export(OrderExporter $exporter): StreamedResponse
{
$response = new StreamedResponse(function () use ($exporter): void {
$exporter->writeCsv(fopen('php://output', 'wb'));
});
$response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
$response->headers->set('Content-Disposition', 'attachment; filename="orders.csv"');
return $response;
}
return $this->redirect('https://example.com', Response::HTTP_MOVED_PERMANENTLY);
N'utiliser redirect() que pour les URLs externes. Pour une route interne : redirectToRoute() (refactor-safe).
#[Route('/health', name: 'health', methods: ['GET'])]
final class HealthController extends AbstractController
{
public function __invoke(): JsonResponse
{
return $this->json(['status' => 'ok']);
}
}
Pratique pour les actions uniques (health-check, webhook). Le nom de classe suffit en routing.
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/admin/products', name: 'admin_product_index', methods: ['GET'])]
#[IsGranted('ROLE_ADMIN')]
public function index(): Response { /* ... */ }
Préférer l'attribut #[IsGranted] à denyAccessUnlessGranted() quand la règle est statique. Pour une règle liée à l'objet (#[IsGranted('edit', subject: 'product')]), passer par un Voter.
$session = $request->getSession(); // depuis la Request
$this->isCsrfTokenValid('delete'.$id, $token); // helper AbstractController
Ne pas injecter SessionInterface directement — l'obtenir depuis la Request.
throw $this->createNotFoundException().throw $this->createAccessDeniedException() (ou #[IsGranted] en amont).#[MapRequestPayload] lève automatiquement une HttpException 422 si validation KO, ou 400 si JSON malformé. Pour custom : throw new BadRequestHttpException('...').throw new HttpException(Response::HTTP_GONE).Un test fonctionnel par action publique :
public function testShowReturnsProductPage(): void
{
$client = static::createClient();
$client->request('GET', '/product/42');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('h1', 'Widget');
}
public function testShowReturns404WhenMissing(): void
{
$client = static::createClient();
$client->request('GET', '/product/99999');
self::assertResponseStatusCodeSame(404);
}
Les tests de form → /symfony:form-handle. Les tests d'API JSON → assertions sur $client->getResponse()->getContent() + json_decode.
return : une action qui ne retourne rien renvoie null → LogicException: The controller must return a "Response" object.redirectToRoute avec une route inexistante : échec silencieux au build d'URL → RouteNotFoundException. Toujours utiliser le nom déclaré dans #[Route(name: ...)].methods: oublié : l'action accepte GET/POST/PUT/DELETE/… par défaut, ouvrant des surfaces non voulues (CSRF GET, mutations idempotentes). Toujours borner.EntityManagerInterface dans le contrôleur : symptôme d'une logique métier qui devrait être dans un service. Déplacer.render() avec un FormType : passer $form (et non $form->createView() depuis Symfony 6.2). Voir /symfony:form-handle.#[Route('/product/')] : Symfony traite /product et /product/ comme différents. Choisir une convention (sans slash final) et la maintenir.@Route (annotations) dans un projet Symfony 6+ : doctrine/annotations a été retiré des dépendances Flex. Utiliser les attributs PHP natifs #[Route].services.yaml : inutile, le resource App\: couvre src/Controller/. Un contrôleur n'a pas besoin de public: true (le ControllerResolver le détecte via le tag controller.service_arguments posé par autoconfigure).symfony console debug:router # toutes les routes
symfony console debug:router product_show # détail d'une route
symfony console router:match /product/42 # quelle route pour cette URL
symfony console debug:container App\\Controller\\ProductController
vendor/bin/phpstan analyse src/Controller
Product, Order, Customer…), ne pas réécrire un contrôleur CRUD : le ResourceController de sylius/resource-bundle gère index/create/update/show/delete avec templates, repository, factory configurables dans _sylius.yaml.Sylius\Bundle\ResourceBundle\Controller\ResourceController uniquement pour surcharger finement une action d'une resource ; sinon la configuration suffit.symfony console debug:router | grep sylius_.#[MapEntity] / #[MapQueryParameter] / #[MapRequestPayload] ou Request brut ?final sous src/Controller/, extends AbstractController, declare(strict_types=1).#[Route] au niveau classe pour préfixe + nom, #[Route] sur chaque méthode.symfony console debug:router la route apparaît.curl, navigateur, symfony console router:match).Afficher :
#[MapRequestPayload], #[IsGranted], …)./symfony:form-render), service applicatif (/symfony:service-define), sérialisation API (/symfony:serializer-use), traitement de form (/symfony:form-handle), tests fonctionnels./symfony:controller-action ProductController show — scaffolde l'action show dans ProductController (classe créée si absente).
/symfony:controller-action src/Controller/OrderController.php — audit du fichier (routes, méthodes HTTP bornées, logique métier déléguée, helpers utilisés, sécurité).
/symfony:controller-action sans argument — demande l'URL, la méthode HTTP et le comportement attendu.
Cadrage d'un article (sujet, thèse, audience, chapitrage, frontmatter). Détecte la stack (Astro, Hugo, Jekyll, MDX). Produit `docs/story/a-<NNN>-<slug>/plan.md`. Déclenche sur "idée d'article", "plan d'article", "j'écris sur…".
Retouche chirurgicale d'une portion d'article publié (chapitre, section, paragraphe). Lit le `plan.md` associé, respecte la voix, propage à la traduction. Déclenche sur "retravaille cette section", "réécris ce chapitre", "resserre ce paragraphe".
Rédige un article depuis le `plan.md` sous `docs/story/a-<NNN>-<slug>/` — fichier dans la collection détectée (Astro, Hugo, Jekyll, MDX), schéma + traduction. Déclenche sur "rédige depuis ce plan", "écris l'article", "draft l'article".
Crée une entité Sylius traduisible (pattern personal translations) : AbstractTranslation, TranslatableInterface, TranslatableTrait, locale fallback, ajout programmatique. Pour des libellés UI statiques → `/sylius:translation`.
Crée ou modifie une entité Doctrine (Symfony/Sylius) — ORM, champs, relations, types custom. Déclenche sur "créer entité", "relation ManyToOne", "UniqueEntity", "mapping Doctrine". Impose make:entity et snake_case BDD.
Conçoit une classe FormType Symfony — AbstractType, buildForm, configureOptions, types de champs (ChoiceType, EntityType…). Déclenche sur "créer FormType", "buildForm", "data_class", "EntityType". Impose make:form.