Files
ai-web/.agents/skills/php-pro/references/symfony-patterns.md
jiangdong.cheng aa16c9f8c2
Some checks failed
Tests / PHP 8.2 (push) Has been cancelled
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
init
2026-02-11 17:28:36 +08:00

11 KiB

Symfony Patterns

Dependency Injection

<?php

declare(strict_types=1);

namespace App\Service;

use App\Repository\UserRepositoryInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\MailerInterface;

final readonly class UserService
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private MailerInterface $mailer,
        private LoggerInterface $logger,
    ) {}

    public function createUser(string $email, string $password): User
    {
        $user = new User($email, password_hash($password, PASSWORD_ARGON2ID));

        $this->userRepository->save($user);
        $this->logger->info('User created', ['email' => $email]);

        return $user;
    }
}

Service Configuration (services.yaml)

# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
        bind:
            string $projectDir: '%kernel.project_dir%'
            bool $isDebug: '%kernel.debug%'

    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    # Interface binding
    App\Repository\UserRepositoryInterface:
        class: App\Repository\DoctrineUserRepository

    # Service with specific configuration
    App\Service\PaymentService:
        arguments:
            $apiKey: '%env(PAYMENT_API_KEY)%'
            $timeout: 30

    # Tagged services
    App\EventSubscriber\:
        resource: '../src/EventSubscriber/'
        tags: ['kernel.event_subscriber']

Controllers with Attributes

<?php

declare(strict_types=1);

namespace App\Controller;

use App\DTO\CreateUserRequest;
use App\Entity\User;
use App\Service\UserService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[Route('/api/users', name: 'api_users_')]
final class UserController extends AbstractController
{
    public function __construct(
        private readonly UserService $userService,
    ) {}

    #[Route('', name: 'list', methods: ['GET'])]
    #[IsGranted('ROLE_USER')]
    public function list(): JsonResponse
    {
        $users = $this->userService->getAllUsers();

        return $this->json($users, Response::HTTP_OK, [], [
            'groups' => ['user:read'],
        ]);
    }

    #[Route('', name: 'create', methods: ['POST'])]
    #[IsGranted('ROLE_ADMIN')]
    public function create(
        #[MapRequestPayload] CreateUserRequest $request
    ): JsonResponse {
        $user = $this->userService->createUser(
            $request->email,
            $request->password
        );

        return $this->json($user, Response::HTTP_CREATED, [], [
            'groups' => ['user:read'],
        ]);
    }

    #[Route('/{id}', name: 'show', methods: ['GET'])]
    public function show(User $user): JsonResponse
    {
        $this->denyAccessUnlessGranted('view', $user);

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

DTOs with Validation

<?php

declare(strict_types=1);

namespace App\DTO;

use Symfony\Component\Validator\Constraints as Assert;

final readonly class CreateUserRequest
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Email]
        public string $email,

        #[Assert\NotBlank]
        #[Assert\Length(min: 8, max: 100)]
        #[Assert\PasswordStrength(minScore: Assert\PasswordStrength::STRENGTH_MEDIUM)]
        public string $password,

        #[Assert\NotBlank]
        #[Assert\Length(min: 2, max: 100)]
        public string $name,

        #[Assert\Choice(choices: ['admin', 'user', 'moderator'])]
        public string $role = 'user',
    ) {}
}

final readonly class UpdateUserRequest
{
    public function __construct(
        #[Assert\Email]
        public ?string $email = null,

        #[Assert\Length(min: 2, max: 100)]
        public ?string $name = null,

        #[Assert\Type('bool')]
        public ?bool $isActive = null,
    ) {}
}

Event Subscribers

<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use App\Event\UserRegisteredEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\MailerInterface;

final readonly class UserSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private MailerInterface $mailer,
        private LoggerInterface $logger,
    ) {}

    public static function getSubscribedEvents(): array
    {
        return [
            UserRegisteredEvent::class => [
                ['sendWelcomeEmail', 10],
                ['logRegistration', 5],
            ],
        ];
    }

    public function sendWelcomeEmail(UserRegisteredEvent $event): void
    {
        $user = $event->getUser();
        // Send email logic
        $this->logger->info('Welcome email sent', ['user_id' => $user->getId()]);
    }

    public function logRegistration(UserRegisteredEvent $event): void
    {
        $this->logger->info('User registered', [
            'user_id' => $event->getUser()->getId(),
            'email' => $event->getUser()->getEmail(),
        ]);
    }
}

Custom Events

<?php

declare(strict_types=1);

namespace App\Event;

use App\Entity\User;
use Symfony\Contracts\EventDispatcher\Event;

final class UserRegisteredEvent extends Event
{
    public function __construct(
        private readonly User $user,
        private readonly \DateTimeImmutable $occurredAt = new \DateTimeImmutable(),
    ) {}

    public function getUser(): User
    {
        return $this->user;
    }

    public function getOccurredAt(): \DateTimeImmutable
    {
        return $this->occurredAt;
    }
}

// Dispatching events
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final readonly class UserService
{
    public function __construct(
        private EventDispatcherInterface $eventDispatcher,
    ) {}

    public function registerUser(string $email, string $password): User
    {
        $user = new User($email, $password);
        // ... save user

        $this->eventDispatcher->dispatch(new UserRegisteredEvent($user));

        return $user;
    }
}

Console Commands

<?php

declare(strict_types=1);

namespace App\Command;

use App\Service\UserService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'app:user:create',
    description: 'Create a new user',
)]
final class CreateUserCommand extends Command
{
    public function __construct(
        private readonly UserService $userService,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addArgument('email', InputArgument::REQUIRED, 'User email')
            ->addArgument('password', InputArgument::REQUIRED, 'User password')
            ->addOption('admin', 'a', InputOption::VALUE_NONE, 'Make user admin');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $email = $input->getArgument('email');
        $password = $input->getArgument('password');
        $isAdmin = $input->getOption('admin');

        try {
            $user = $this->userService->createUser($email, $password, $isAdmin);

            $io->success(sprintf('User created with ID: %d', $user->getId()));

            return Command::SUCCESS;
        } catch (\Exception $e) {
            $io->error($e->getMessage());
            return Command::FAILURE;
        }
    }
}

Voters (Authorization)

<?php

declare(strict_types=1);

namespace App\Security\Voter;

use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

final class PostVoter extends Voter
{
    public const VIEW = 'view';
    public const EDIT = 'edit';
    public const DELETE = 'delete';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE])
            && $subject instanceof Post;
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token
    ): bool {
        $user = $token->getUser();

        if (!$user instanceof User) {
            return false;
        }

        /** @var Post $post */
        $post = $subject;

        return match ($attribute) {
            self::VIEW => $this->canView($post, $user),
            self::EDIT => $this->canEdit($post, $user),
            self::DELETE => $this->canDelete($post, $user),
            default => false,
        };
    }

    private function canView(Post $post, User $user): bool
    {
        return $post->isPublished() || $this->isOwner($post, $user);
    }

    private function canEdit(Post $post, User $user): bool
    {
        return $this->isOwner($post, $user);
    }

    private function canDelete(Post $post, User $user): bool
    {
        return $this->isOwner($post, $user) || $user->hasRole('ROLE_ADMIN');
    }

    private function isOwner(Post $post, User $user): bool
    {
        return $post->getAuthor()->getId() === $user->getId();
    }
}

Message Handler (Messenger)

<?php

declare(strict_types=1);

namespace App\Message;

final readonly class SendWelcomeEmail
{
    public function __construct(
        public int $userId,
    ) {}
}

namespace App\MessageHandler;

use App\Message\SendWelcomeEmail;
use App\Repository\UserRepositoryInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

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

    public function __invoke(SendWelcomeEmail $message): void
    {
        $user = $this->userRepository->find($message->userId);

        if (!$user) {
            return;
        }

        // Send email logic
    }
}

// Dispatching messages
use Symfony\Component\Messenger\MessageBusInterface;

$this->messageBus->dispatch(new SendWelcomeEmail($user->getId()));

Quick Reference

Component Purpose File Location
Controller HTTP handlers src/Controller/
Service Business logic src/Service/
Repository Data access src/Repository/
Event Domain events src/Event/
EventSubscriber Event handlers src/EventSubscriber/
Command CLI commands src/Command/
Voter Authorization src/Security/Voter/
Message Async messages src/Message/
MessageHandler Message handlers src/MessageHandler/
DTO Data transfer src/DTO/