| name | symfony |
| description | Symfony 7+ framework guardrails, patterns, and best practices for AI-assisted development.
Use when working with Symfony projects, or when the user mentions Symfony.
Provides Doctrine ORM, Twig templates, bundles, services, and security guidelines.
|
| license | MIT |
| metadata | {"author":"samuel","version":"1.0","category":"framework","language":"php","extensions":".php"} |
Symfony Framework Guide
Applies to: Symfony 7+, PHP 8.2+, Doctrine ORM, Twig, Messenger
Core Principles
- Convention Over Configuration: Follow Symfony defaults unless there is a strong reason not to
- Dependency Injection: Constructor injection for all services; avoid service locators
- Thin Controllers: Controllers delegate to services; no business logic in controllers
- DTOs at Boundaries: Never expose Doctrine entities directly to API consumers
- Events for Decoupling: Use EventDispatcher and Messenger for side effects
- Explicit Configuration: Use PHP 8 attributes for routing, ORM mapping, and validation
Guardrails
Version and Dependencies
- Target Symfony 7.x with PHP 8.2+
- Use Composer with
composer.lock committed
- Use Symfony Flex for bundle management
- Pin major versions in
composer.json; run composer audit before adding dependencies
Code Style
- All files start with
declare(strict_types=1);
- Use PSR-12 coding standard; run
php-cs-fixer fix before committing
- Run PHPStan at level 8+ (
vendor/bin/phpstan analyse src)
- Use PHP 8 attributes (not annotations) for routing, ORM, validation
- Follow Symfony naming:
PascalCase classes, camelCase methods, snake_case config keys
Controller Rules
- Extend
AbstractController only when you need its shortcuts
- One action per method; keep under 20 lines
- Use
#[Route] attribute on both class and method
- Use
#[MapRequestPayload] for automatic DTO deserialization and validation
- Return
JsonResponse for APIs; never echo or die()
- Use serialization groups to control response shape
Entity Rules
- Use PHP 8 attributes for all Doctrine mapping (
#[ORM\Entity], #[ORM\Column], etc.)
- Always set
repositoryClass on entities
- Use
#[ORM\HasLifecycleCallbacks] sparingly; prefer Doctrine event listeners
- Initialize collections in constructor:
$this->items = new ArrayCollection()
- Use
DateTimeImmutable for all date columns
- Add database indexes for frequently queried columns with
#[ORM\Index]
- Fluent setters return
static for chaining
Service Rules
- One responsibility per service class
- All dependencies via constructor injection (use
readonly promoted properties)
- Never inject
EntityManagerInterface into controllers; inject repositories or services
- Use
#[AsMessageHandler] for async operations
- Tag services only when autowiring cannot resolve
Project Structure
myapp/
├── bin/
│ └── console # Symfony CLI
├── config/
│ ├── packages/ # Per-package YAML config
│ │ ├── doctrine.yaml
│ │ ├── security.yaml
│ │ ├── messenger.yaml
│ │ └── ...
│ ├── routes/ # Route imports
│ ├── routes.yaml
│ ├── services.yaml # Service definitions and autowiring
│ └── bundles.php # Registered bundles
├── migrations/ # Doctrine migrations (never edit after deploy)
├── public/
│ └── index.php # Single entry point
├── src/
│ ├── Controller/ # HTTP controllers (thin)
│ ├── Dto/ # Request/response DTOs
│ ├── Entity/ # Doctrine entities
│ ├── Repository/ # Doctrine repositories
│ ├── Service/ # Business logic
│ ├── EventSubscriber/ # Event subscribers
│ ├── Message/ # Messenger messages
│ ├── MessageHandler/ # Messenger handlers
│ ├── Command/ # Console commands
│ ├── Form/ # Form types (web apps)
│ ├── Security/ # Voters, authenticators
│ └── Kernel.php
├── templates/ # Twig templates
├── tests/
│ ├── Controller/ # Functional tests
│ ├── Service/ # Unit tests
│ └── bootstrap.php
├── translations/ # i18n files
├── var/ # Cache and logs (gitignored)
├── .env # Default env vars (committed)
├── .env.local # Local overrides (gitignored)
├── composer.json
├── phpunit.xml.dist
└── symfony.lock
src/Dto/ keeps request/response data separate from entities
src/Message/ and src/MessageHandler/ follow Messenger conventions
var/ is ephemeral; never store persistent data there
config/packages/ files are loaded by environment (config/packages/test/)
Controllers and Routing
API Controller
#[Route('/api/v1/users')]
class UserController extends AbstractController
{
public function __construct(
private readonly UserService $userService,
) {}
#[Route('', methods: ['GET'])]
public function index(Request $request): JsonResponse
{
$page = $request->query->getInt('page', 1);
$limit = $request->query->getInt('limit', 15);
return $this->json(
$this->userService->getPaginated($page, $limit),
Response::HTTP_OK,
[],
['groups' => 'user:read'],
);
}
#[Route('', methods: ['POST'])
()
{
->(
->userService->(),
::,
[],
[ => ],
);
}
}
Routing Tips
- Prefer attribute routing over YAML for controller routes
- Use YAML routes only for third-party bundle prefixes
- Version API routes:
/api/v1/...
- Type-hint entities in action signatures for automatic
ParamConverter
Doctrine ORM Basics
Entity with Validation
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'users')]
#[ORM\HasLifecycleCallbacks]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM(: , : )
;
(: ::)
\DateTimeImmutable ;
{
->createdAt = ();
}
}
Repository
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class UserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
public function findOneByEmail(string $email): ?User
{
return $this->createQueryBuilder('u')
->andWhere('u.email = :email')
->setParameter(, ())
->()
->();
}
}
Migration Workflow
php bin/console make:migration
php bin/console doctrine:migrations:migrate
php bin/console doctrine:schema:validate
- Always review generated migrations before running
- Every migration must have a working
down() method
- Never edit migrations that have been applied to production
Twig Templates
Layout Pattern
{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}App{% endblock %}</title>
{% block stylesheets %}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>
{# templates/user/index.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Users{% endblock %}
{% block body %}
<h1>Users</h1>
{% for user in users %}
<p>{{ user.name|e }}</p>
{% else %}
<p>No users found.</p>
{% endfor %}
{% endblock %}
- Always escape output (Twig auto-escapes by default; never use
|raw on user data)
- Use
{% include %} for partials, {% embed %} for overridable partials
- Keep logic minimal in templates; compute values in controller or service
Services and Dependency Injection
Service Definition
class UserService
{
public function __construct(
private readonly UserRepository $userRepository,
private readonly EntityManagerInterface $entityManager,
private readonly UserPasswordHasherInterface $passwordHasher,
private readonly EventDispatcherInterface $eventDispatcher,
) {}
public function create(CreateUserDto $dto): User
{
$user = new User();
$user->setEmail($dto->email);
$user->setPassword($this->passwordHasher->hashPassword($user, $dto->password));
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->eventDispatcher->dispatch(new UserCreatedEvent($user));
;
}
}
services.yaml Essentials
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- Rely on autowiring; only add explicit definitions when needed
- Use
#[Autowire] attribute for non-standard parameters
- Use
#[TaggedIterator] to inject all services with a specific tag
Form Handling
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class)
->add('email', EmailType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => User::class]);
}
}
- Always bind forms to a DTO or entity via
data_class
- Validate via constraints on the DTO/entity, not in the form type
- Use CSRF protection (enabled by default for web forms)
- For APIs, prefer
#[MapRequestPayload] over Symfony forms
Security Basics
Voter Pattern
class PostVoter extends Voter
{
public const EDIT = 'POST_EDIT';
public const DELETE = 'POST_DELETE';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::EDIT, self::DELETE], true)
&& $subject instanceof Post;
}
protected function voteOnAttribute(
string $attribute,
mixed $subject,
TokenInterface $token,
): bool {
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
return $subject->() === || ->() === ;
}
}
Security Configuration Checklist
- Use
password_hashers: auto (Symfony picks bcrypt/argon2 automatically)
- Stateless firewalls for APIs (
stateless: true)
- JWT authentication via
lexik/jwt-authentication-bundle for APIs
- CSRF protection enabled for all web forms
- Use
#[IsGranted] attribute on controller actions
- Use Voters for object-level authorization (not
is_granted('ROLE_...') for fine-grained checks)
- Never store plain-text passwords; always use
UserPasswordHasherInterface
Console Commands
#[AsCommand(name: 'app:import-users', description: 'Import users from CSV')]
class ImportUsersCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->success('Import complete.');
return Command::SUCCESS;
}
}
- Use
#[AsCommand] attribute (not configure() for name/description)
- Return
Command::SUCCESS, Command::FAILURE, or Command::INVALID
- Use
SymfonyStyle for consistent output formatting
- Inject services via constructor (commands are services)
Essential CLI Commands
symfony serve
php bin/console cache:clear
php bin/console debug:router
php bin/console debug:container
php bin/console make:entity User
php bin/console make:controller UserController
php bin/console make:migration
php bin/console make:form UserType
php bin/console make:voter PostVoter
php bin/console make:command App:ImportUsers
php bin/console make:subscriber UserEventSubscriber
php bin/console make:message SendWelcomeEmail
php bin/console doctrine:database:create
php bin/console doctrine:migrations:migrate
php bin/console doctrine:schema:validate
php bin/console doctrine:fixtures:load
php bin/console messenger:consume async
php bin/console messenger:failed:show
php bin/console messenger:failed:retry
php bin/phpunit
php bin/phpunit --coverage-html coverage
vendor/bin/phpstan analyse src
vendor/bin/php-cs-fixer fix
composer install --no-dev --optimize-autoloader
php bin/console cache:clear --env=prod
php bin/console cache:warmup --env=prod
Do and Don't
Do
- Use constructor injection with
readonly promoted properties
- Use DTOs for all API request/response payloads
- Use Messenger for async work (emails, notifications, heavy processing)
- Use Voters for authorization logic
- Use Events for decoupling side effects from core logic
- Use serialization groups to control JSON output shape
- Use
DateTimeImmutable for all temporal data
- Run
php bin/console doctrine:schema:validate in CI
Don't
- Don't put business logic in controllers (delegate to services)
- Don't flush
EntityManager inside loops (batch with $em->flush() once)
- Don't use
$_GET, $_POST, $_SERVER (use Request object)
- Don't hardcode config values (use
%env()% syntax or #[Autowire])
- Don't bypass the security component (no manual session/cookie auth)
- Don't create "god services" that handle everything (single responsibility)
- Don't ignore Symfony deprecation warnings (they become errors on upgrade)
Advanced Topics
For detailed patterns and production guidance, see:
- references/patterns.md -- Doctrine advanced patterns, event system, Messenger, API Platform, testing, deployment
External References