Agent skill
symfony
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.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/symfony
Metadata
Additional technical details for this skill
- author
- samuel
- version
- 1.0
- category
- framework
- language
- php
- extensions
- .php
SKILL.md
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.lockcommitted - Use Symfony Flex for bundle management
- Pin major versions in
composer.json; runcomposer auditbefore adding dependencies
Code Style
- All files start with
declare(strict_types=1); - Use PSR-12 coding standard; run
php-cs-fixer fixbefore committing - Run PHPStan at level 8+ (
vendor/bin/phpstan analyse src) - Use PHP 8 attributes (not annotations) for routing, ORM, validation
- Follow Symfony naming:
PascalCaseclasses,camelCasemethods,snake_caseconfig keys
Controller Rules
- Extend
AbstractControlleronly 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
JsonResponsefor APIs; neverechoordie() - Use serialization groups to control response shape
Entity Rules
- Use PHP 8 attributes for all Doctrine mapping (
#[ORM\Entity],#[ORM\Column], etc.) - Always set
repositoryClasson entities - Use
#[ORM\HasLifecycleCallbacks]sparingly; prefer Doctrine event listeners - Initialize collections in constructor:
$this->items = new ArrayCollection() - Use
DateTimeImmutablefor all date columns - Add database indexes for frequently queried columns with
#[ORM\Index] - Fluent setters return
staticfor chaining
Service Rules
- One responsibility per service class
- All dependencies via constructor injection (use
readonlypromoted properties) - Never inject
EntityManagerInterfaceinto 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 entitiessrc/Message/andsrc/MessageHandler/follow Messenger conventionsvar/is ephemeral; never store persistent data thereconfig/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'])]
#[IsGranted('ROLE_ADMIN')]
public function create(#[MapRequestPayload] CreateUserDto $dto): JsonResponse
{
return $this->json(
$this->userService->create($dto),
Response::HTTP_CREATED,
[],
['groups' => 'user:read'],
);
}
}
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\Column(length: 180, unique: true)]
#[Assert\NotBlank]
#[Assert\Email]
private string $email;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private \DateTimeImmutable $createdAt;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
}
// Getters and fluent setters (return static)
}
Repository
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/** @extends ServiceEntityRepository<User> */
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('email', strtolower($email))
->getQuery()
->getOneOrNullResult();
}
}
Migration Workflow
php bin/console make:migration # Generate from entity diff
php bin/console doctrine:migrations:migrate # Apply migrations
php bin/console doctrine:schema:validate # Check mapping vs DB
- 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
|rawon 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));
return $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->getAuthor() === $user || $user->getRole() === 'admin';
}
}
Security Configuration Checklist
- Use
password_hashers: auto(Symfony picks bcrypt/argon2 automatically) - Stateless firewalls for APIs (
stateless: true) - JWT authentication via
lexik/jwt-authentication-bundlefor 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);
// Implementation here
$io->success('Import complete.');
return Command::SUCCESS;
}
}
- Use
#[AsCommand]attribute (notconfigure()for name/description) - Return
Command::SUCCESS,Command::FAILURE, orCommand::INVALID - Use
SymfonyStylefor consistent output formatting - Inject services via constructor (commands are services)
Essential CLI Commands
# Development
symfony serve # Local dev server
php bin/console cache:clear # Clear cache
php bin/console debug:router # List all routes
php bin/console debug:container # List all services
# Code Generation
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
# Database
php bin/console doctrine:database:create
php bin/console doctrine:migrations:migrate
php bin/console doctrine:schema:validate
php bin/console doctrine:fixtures:load
# Messenger
php bin/console messenger:consume async
php bin/console messenger:failed:show
php bin/console messenger:failed:retry
# Testing and Quality
php bin/phpunit
php bin/phpunit --coverage-html coverage
vendor/bin/phpstan analyse src
vendor/bin/php-cs-fixer fix
# Production
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
readonlypromoted 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
DateTimeImmutablefor all temporal data - Run
php bin/console doctrine:schema:validatein CI
Don't
- Don't put business logic in controllers (delegate to services)
- Don't flush
EntityManagerinside loops (batch with$em->flush()once) - Don't use
$_GET,$_POST,$_SERVER(useRequestobject) - 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
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?