Skip to content
Back to skills

Symfony

ASecurity

Symfony is a PHP framework for building web applications and APIs from reusable components: routing and controllers, dependency injection, Doctrine ORM, validation, security and the Messenger queue. Use when a user asks to create or upgrade a Symfony project, write controllers, entities or migrations, validate request payloads, run background jobs with Messenger, build a REST API with API Platform, or debug services and routes with bin/console.

  • 142 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 6, 2026
developmentgophpbashsqlgitapidatabasesecurity

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add TerminalSkills/skills --skill symfony --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Symfony?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Symfony
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/terminalskills-symfony/badge)](https://www.skillsdirectory.com/skills/terminalskills-symfony)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: symfony
description: >-
  Symfony is a PHP framework for building web applications and APIs from
  reusable components: routing and controllers, dependency injection, Doctrine
  ORM, validation, security and the Messenger queue. Use when a user asks to
  create or upgrade a Symfony project, write controllers, entities or
  migrations, validate request payloads, run background jobs with Messenger,
  build a REST API with API Platform, or debug services and routes with
  bin/console.
license: Apache-2.0
compatibility: "Symfony 8.1 needs PHP 8.4+ and Composer 2; the 7.4 LTS runs on PHP 8.2+"
metadata:
  author: terminal-skills
  version: 1.1.0
  category: development
  repository: https://github.com/symfony/symfony
  tags:
    - php
    - framework
    - enterprise
    - api
    - doctrine
---

# Symfony — Enterprise PHP Framework

## Overview

Symfony is a PHP framework built from decoupled components, wired together by a dependency-injection container and configured with PHP attributes. A new project starts as a minimal skeleton; features (Doctrine ORM, Serializer, Validator, Security, Messenger, API Platform) are added with `composer require`, and Symfony Flex recipes register and configure each one. Current stable is **8.1** (PHP 8.4+, maintained until January 2027); **7.4** is the long-term-support line (PHP 8.2+, security fixes until November 2029).

## Instructions

### Installation

```bash
composer create-project symfony/skeleton:"8.1.*" checkout-api    # or "7.4.*" for the LTS
cd checkout-api
composer require orm serializer validator messenger mailer       # API building blocks
composer require --dev maker
composer require webapp                   # OR the full server-rendered stack: Twig, forms, security, profiler
composer require api                      # OR API Platform for REST/GraphQL resources
# Optional Symfony CLI: local HTTPS server, `symfony new`, `symfony check:requirements`
brew install symfony-cli/tap/symfony-cli  # macOS/Linux; Windows: scoop install symfony-cli
symfony serve -d                          # without the CLI: php -S 127.0.0.1:8000 -t public
```

Put machine-specific settings in `.env.local` (git-ignored), for example `DATABASE_URL="postgresql://app:${DB_PASSWORD}@127.0.0.1:5432/checkout?serverVersion=16&charset=utf8"`. The skeleton also ships an `AGENTS.md` with the project's conventions for coding agents — read it.

### Controllers and Routing

```php
<?php
// src/Controller/UserController.php
namespace App\Controller;

use App\Dto\CreateUser;
use App\Entity\User;
use App\Message\SendWelcomeEmail;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/api/users')]
final class UserController extends AbstractController
{
    public function __construct(
        private readonly UserRepository $users,
        private readonly EntityManagerInterface $em,
        private readonly MessageBusInterface $bus,
    ) {}

    #[Route('', methods: ['GET'])]
    public function list(#[MapQueryParameter] int $page = 1, #[MapQueryParameter] int $limit = 20): JsonResponse
    {
        return $this->json($this->users->findPaginated($page, $limit), context: ['groups' => ['user:list']]);
    }

    #[Route('', methods: ['POST'])]
    public function create(#[MapRequestPayload] CreateUser $input): JsonResponse
    {
        $user = new User($input->name, $input->email);
        $this->em->persist($user);
        $this->em->flush();
        $this->bus->dispatch(new SendWelcomeEmail($user->getId()));

        return $this->json($user, 201, context: ['groups' => ['user:detail']]);
    }

    #[Route('/{id}', requirements: ['id' => '\d+'], methods: ['GET'])]
    public function show(User $user): JsonResponse
    {
        return $this->json($user, context: ['groups' => ['user:detail']]);
    }
}

// src/Dto/CreateUser.php
namespace App\Dto;

use App\Entity\User;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;

#[UniqueEntity(fields: ['email'], entityClass: User::class)]
final readonly class CreateUser
{
    public function __construct(
        #[Assert\NotBlank, Assert\Length(min: 2, max: 100)]
        public string $name,
        #[Assert\NotBlank, Assert\Email]
        public string $email,
    ) {}
}
```

`#[MapRequestPayload]` deserializes and validates the JSON body (malformed JSON → 400, violations → 422). A `User` argument is loaded from the `{id}` placeholder; an unknown id gives 404.

### Doctrine Entity

```php
<?php
// src/Entity/User.php
namespace App\Entity;

use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'users')]
class User
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    #[Groups(['user:list', 'user:detail'])]
    private ?int $id = null;

    #[ORM\Column]
    #[Groups(['user:detail'])]
    private \DateTimeImmutable $createdAt;

    public function __construct(
        #[ORM\Column(length: 100)]
        #[Groups(['user:list', 'user:detail'])]
        private string $name,
        #[ORM\Column(unique: true)]
        #[Groups(['user:detail'])]
        private string $email,
    ) {
        $this->createdAt = new \DateTimeImmutable();
    }

    public function getId(): ?int { return $this->id; }
    public function getName(): string { return $this->name; }
    public function getEmail(): string { return $this->email; }
    public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
}

// src/Repository/UserRepository.php — a method of the class that extends ServiceEntityRepository
public function findPaginated(int $page, int $limit): array
{
    return $this->createQueryBuilder('u')->orderBy('u.id', 'ASC')
        ->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
        ->getQuery()->getResult();
}
```

```bash
php bin/console make:migration                   # diff entities against the database -> migrations/VersionYYYYMMDDHHMMSS.php
php bin/console doctrine:migrations:migrate -n   # apply; run the same command on deploy
```

### Messenger (Async Processing)

```php
<?php
// src/Message/SendWelcomeEmail.php
namespace App\Message;

use Symfony\Component\Messenger\Attribute\AsMessage;

#[AsMessage('async')]                      // route to the "async" transport
final readonly class SendWelcomeEmail
{
    public function __construct(public int $userId) {}
}

// src/MessageHandler/SendWelcomeEmailHandler.php
namespace App\MessageHandler;

use App\Message\SendWelcomeEmail;
use App\Repository\UserRepository;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Mime\Email;

#[AsMessageHandler]
final class SendWelcomeEmailHandler
{
    public function __construct(
        private readonly UserRepository $users,
        private readonly MailerInterface $mailer,
    ) {}

    public function __invoke(SendWelcomeEmail $message): void
    {
        $user = $this->users->find($message->userId);
        if (null === $user) { return; }    // deleted before the worker got to it
        $this->mailer->send((new Email())->from('hello@northwind.dev')->to($user->getEmail())
            ->subject('Welcome to Northwind')
            ->text(sprintf('Hi %s, your account is ready.', $user->getName())));
    }
}
```

```yaml
# config/packages/messenger.yaml
framework:
    messenger:
        failure_transport: failed
        transports:
            async: '%env(MESSENGER_TRANSPORT_DSN)%'    # doctrine://default, amqp://…, redis://…
            failed: 'doctrine://default?queue_name=failed'
```

```bash
composer require symfony/doctrine-messenger      # each transport is its own package (also amqp-messenger, redis-messenger)
php bin/console messenger:consume async -vv --time-limit=3600    # worker; keep it alive with systemd or Supervisor
php bin/console messenger:failed:show            # inspect failures; messenger:failed:retry to re-run them
```

### Inspect Before You Guess

```bash
php bin/console about                      # Symfony/PHP versions, environment, end-of-maintenance date
php bin/console debug:router               # every route with method and path
php bin/console debug:autowiring mailer    # which type-hints can be injected
php bin/console lint:container             # catches wiring errors without running the app
```

## Examples

### Example 1: A validated JSON endpoint

**User request:** "Add `POST /api/users` to our Symfony API. Reject bad input with proper error responses."

With the controller and `CreateUser` DTO above, no manual `json_decode()` or validator call is needed:

```
$ curl -s -i -X POST http://127.0.0.1:8000/api/users -H 'Content-Type: application/json' \
    -H 'Accept: application/json' -d '{"name":"Dana Whitfield","email":"dana@northwind.dev"}'
HTTP/1.1 201 Created
{"id":1,"createdAt":"2026-10-01T17:18:37+00:00","name":"Dana Whitfield","email":"dana@northwind.dev"}

$ curl -s -i -X POST http://127.0.0.1:8000/api/users -H 'Content-Type: application/json' \
    -H 'Accept: application/json' -d '{"name":"D","email":"not-an-email"}'
HTTP/1.1 422 Unprocessable Content
{"type":"https:\/\/symfony.com\/errors\/validation","title":"Validation Failed","status":422,
 "detail":"name: This value is too short. It should have 2 characters or more.\nemail: This value is not a valid email address.", …}
```

Posting the same email again returns 422 with `email: This value is already used.`; a body that is not JSON returns 400. `GET /api/users?page=1&limit=10` returns `[{"id":1,"name":"Dana Whitfield"}]` because only `user:list` fields are serialized.

### Example 2: Send the welcome email in the background

**User request:** "Signup is slow because we send the email inline. Move it to a queue."

The controller dispatches `SendWelcomeEmail`; `#[AsMessage('async')]` routes it to the `async` transport, so the request returns immediately. Create the queue table and run a worker:

```
$ php bin/console make:migration && php bin/console doctrine:migrations:migrate -n
$ php bin/console messenger:consume async --limit=1 -vv
 [OK] Consuming messages from transport "async".
[info] Received message App\Message\SendWelcomeEmail
[info] Message Symfony\Component\Mailer\Messenger\SendEmailMessage handled by Symfony\Component\Mailer\Messenger\MessageHandler::__invoke
[info] Message App\Message\SendWelcomeEmail handled by App\MessageHandler\SendWelcomeEmailHandler::__invoke
[info] App\Message\SendWelcomeEmail was handled successfully (acknowledging to transport).
[info] Worker stopped due to maximum count of 1 messages processed
```

## Guidelines

1. **Dependency injection** — let Symfony autowire constructor arguments; use `#[Autowire]` for parameters and env vars, and interfaces for swappable implementations. YAML service definitions are the last resort.
2. **Attributes only** — `#[Route]`, `#[Groups]`, `#[Assert\…]`, `#[AsMessageHandler]`. Doctrine annotations and the `Annotation\` namespaces of older tutorials are gone; import from `…\Attribute\…`.
3. **Doctrine migrations** — change the schema with `make:migration` (or `doctrine:migrations:diff`) and `doctrine:migrations:migrate`; never `doctrine:schema:update --force` in production.
4. **Serialization groups** — `#[Groups]` decides which fields each endpoint exposes (list vs detail); without groups every getter is serialized.
5. **Validation** — put constraints on a request DTO and bind it with `#[MapRequestPayload]` / `#[MapQueryString]`. Clients must send `Accept: application/json` to get JSON errors; otherwise the error page is HTML.
6. **Messenger for async** — dispatch messages for heavy work (emails, reports). Messages carry ids, not entities. Workers load code once: run `messenger:stop-workers` on every deploy so the process manager restarts them.
7. **API Platform** — for CRUD-heavy REST/GraphQL APIs, `composer require api` and mark entities with `#[ApiResource]` instead of hand-writing controllers.
8. **Security voters** — keep authorization in voters and call them with `#[IsGranted('EDIT', subject: 'post')]` or `denyAccessUnlessGranted()`; do not scatter role checks through controllers.
9. **Events** — use `#[AsEventListener]` for cross-cutting concerns instead of calling services from every controller.
10. **Secrets and production** — `.env` is committed and holds defaults only; real secrets go to `.env.local` or `bin/console secrets:set`. Deploy with `APP_ENV=prod`, `composer install --no-dev --optimize-autoloader` and `composer dump-env prod`; debug mode exposes stack traces.
11. **Makers are interactive** — `bin/console make:*` prompts on a terminal; in scripts and agent sessions pass every argument and `--no-interaction`, or write the class by hand.
12. **When not to use** — for a single script or a tiny service, standalone components (`symfony/console`, `symfony/http-client`) without the full framework are enough.

Files in this skill

  • SKILL.md5.6 KB
  • _scores.json1.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…