Symfony Framework Guide¶
Framework: Symfony 6.3+ Language: PHP 8.1+ Type: Full-Stack Web Framework Use Cases: Enterprise Applications, APIs, Microservices, Complex Web Apps
Overview¶
Symfony is a set of reusable PHP components and a web application framework. Known for its stability, flexibility, and performance, it's widely used for enterprise-level applications and provides the foundation for many other PHP projects including Laravel and Drupal.
Project Structure¶
myapp/
├── bin/
│ └── console # CLI tool
├── config/
│ ├── packages/ # Package configurations
│ │ ├── doctrine.yaml
│ │ ├── security.yaml
│ │ └── ...
│ ├── routes/ # Route definitions
│ │ └── api.yaml
│ ├── routes.yaml
│ ├── services.yaml # Service definitions
│ └── bundles.php
├── migrations/ # Doctrine migrations
├── public/
│ └── index.php # Entry point
├── src/
│ ├── Controller/ # Controllers
│ ├── Entity/ # Doctrine entities
│ ├── Repository/ # Doctrine repositories
│ ├── Service/ # Business logic
│ ├── EventSubscriber/ # Event subscribers
│ ├── Command/ # Console commands
│ ├── Form/ # Form types
│ ├── Security/ # Security (voters, authenticators)
│ └── Kernel.php
├── templates/ # Twig templates
├── tests/
│ ├── Controller/
│ ├── Service/
│ └── bootstrap.php
├── translations/ # Translation files
├── var/
│ ├── cache/
│ └── log/
├── vendor/
├── .env
├── .env.local
├── composer.json
├── phpunit.xml.dist
└── symfony.lock
Dependencies¶
composer.json¶
{
"name": "mycompany/myapp",
"type": "project",
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/doctrine-bundle": "^2.10",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.15",
"lexik/jwt-authentication-bundle": "^2.19",
"nelmio/api-doc-bundle": "^4.11",
"symfony/console": "6.3.*",
"symfony/dotenv": "6.3.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "6.3.*",
"symfony/messenger": "6.3.*",
"symfony/property-access": "6.3.*",
"symfony/property-info": "6.3.*",
"symfony/runtime": "6.3.*",
"symfony/security-bundle": "6.3.*",
"symfony/serializer": "6.3.*",
"symfony/uid": "6.3.*",
"symfony/validator": "6.3.*",
"symfony/yaml": "6.3.*"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.2",
"symfony/browser-kit": "6.3.*",
"symfony/css-selector": "6.3.*",
"symfony/maker-bundle": "^1.50",
"symfony/phpunit-bridge": "^6.3"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"scripts": {
"test": "bin/phpunit",
"analyse": "vendor/bin/phpstan analyse src",
"cs-fix": "vendor/bin/php-cs-fixer fix"
}
}
Core Patterns¶
Entity (Doctrine ORM)¶
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'users')]
#[ORM\Index(columns: ['role', 'is_active'])]
#[ORM\HasLifecycleCallbacks]
#[UniqueEntity(fields: ['email'], message: 'This email is already registered.')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Assert\Length(max: 255)]
private string $name;
#[ORM\Column(length: 180, unique: true)]
#[Assert\NotBlank]
#[Assert\Email]
private string $email;
#[ORM\Column]
private string $password;
#[ORM\Column(length: 50)]
#[Assert\Choice(choices: ['admin', 'user', 'guest'])]
private string $role = 'user';
#[ORM\Column]
private bool $isActive = true;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private \DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
#[ORM\ManyToOne(targetEntity: Organization::class, inversedBy: 'users')]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?Organization $organization = null;
#[ORM\OneToMany(mappedBy: 'author', targetEntity: Post::class, orphanRemoval: true)]
private Collection $posts;
public function __construct()
{
$this->posts = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable();
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = strtolower($email);
return $this;
}
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
public function getRole(): string
{
return $this->role;
}
public function setRole(string $role): static
{
$this->role = $role;
return $this;
}
public function getRoles(): array
{
return ['ROLE_' . strtoupper($this->role)];
}
public function getUserIdentifier(): string
{
return $this->email;
}
public function eraseCredentials(): void
{
// Clear temporary sensitive data
}
public function isActive(): bool
{
return $this->isActive;
}
public function setIsActive(bool $isActive): static
{
$this->isActive = $isActive;
return $this;
}
/**
* @return Collection<int, Post>
*/
public function getPosts(): Collection
{
return $this->posts;
}
}
Repository¶
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<User>
*/
class UserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
public function save(User $user, bool $flush = true): void
{
$this->getEntityManager()->persist($user);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(User $user, bool $flush = true): void
{
$this->getEntityManager()->remove($user);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* @return User[]
*/
public function findActiveUsers(): array
{
return $this->createQueryBuilder('u')
->andWhere('u.isActive = :active')
->setParameter('active', true)
->orderBy('u.createdAt', 'DESC')
->getQuery()
->getResult();
}
public function findByRole(string $role): array
{
return $this->createQueryBuilder('u')
->andWhere('u.role = :role')
->andWhere('u.isActive = :active')
->setParameter('role', $role)
->setParameter('active', true)
->getQuery()
->getResult();
}
public function findOneByEmail(string $email): ?User
{
return $this->createQueryBuilder('u')
->andWhere('u.email = :email')
->setParameter('email', strtolower($email))
->getQuery()
->getOneOrNullResult();
}
public function createPaginatedQueryBuilder(): QueryBuilder
{
return $this->createQueryBuilder('u')
->leftJoin('u.organization', 'o')
->addSelect('o')
->orderBy('u.createdAt', 'DESC');
}
}
Controller (API)¶
<?php
declare(strict_types=1);
namespace App\Controller\Api;
use App\Dto\CreateUserDto;
use App\Dto\UpdateUserDto;
use App\Entity\User;
use App\Service\UserService;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Attributes as OA;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Serializer\SerializerInterface;
#[Route('/api/v1/users')]
#[OA\Tag(name: 'Users')]
class UserController extends AbstractController
{
public function __construct(
private readonly UserService $userService,
private readonly SerializerInterface $serializer,
) {}
#[Route('', name: 'api_users_index', methods: ['GET'])]
#[OA\Response(
response: 200,
description: 'Returns list of users',
content: new OA\JsonContent(
type: 'array',
items: new OA\Items(ref: new Model(type: User::class, groups: ['user:read']))
)
)]
public function index(Request $request): JsonResponse
{
$page = $request->query->getInt('page', 1);
$limit = $request->query->getInt('limit', 15);
$users = $this->userService->getPaginated($page, $limit);
return $this->json($users, Response::HTTP_OK, [], ['groups' => 'user:read']);
}
#[Route('/{id}', name: 'api_users_show', methods: ['GET'])]
#[OA\Response(
response: 200,
description: 'Returns a single user',
content: new OA\JsonContent(ref: new Model(type: User::class, groups: ['user:read']))
)]
public function show(User $user): JsonResponse
{
return $this->json($user, Response::HTTP_OK, [], ['groups' => 'user:read']);
}
#[Route('', name: 'api_users_create', methods: ['POST'])]
#[IsGranted('ROLE_ADMIN')]
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: CreateUserDto::class)))]
#[OA\Response(response: 201, description: 'User created')]
public function create(#[MapRequestPayload] CreateUserDto $dto): JsonResponse
{
$user = $this->userService->create($dto);
return $this->json($user, Response::HTTP_CREATED, [], ['groups' => 'user:read']);
}
#[Route('/{id}', name: 'api_users_update', methods: ['PUT', 'PATCH'])]
#[IsGranted('ROLE_ADMIN')]
public function update(User $user, #[MapRequestPayload] UpdateUserDto $dto): JsonResponse
{
$user = $this->userService->update($user, $dto);
return $this->json($user, Response::HTTP_OK, [], ['groups' => 'user:read']);
}
#[Route('/{id}', name: 'api_users_delete', methods: ['DELETE'])]
#[IsGranted('ROLE_ADMIN')]
public function delete(User $user): JsonResponse
{
$this->userService->delete($user);
return $this->json(null, Response::HTTP_NO_CONTENT);
}
}
DTOs with Validation¶
<?php
declare(strict_types=1);
namespace App\Dto;
use Symfony\Component\Validator\Constraints as Assert;
final readonly class CreateUserDto
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Length(max: 255)]
public string $name,
#[Assert\NotBlank]
#[Assert\Email]
public string $email,
#[Assert\NotBlank]
#[Assert\Length(min: 8)]
#[Assert\Regex(
pattern: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/',
message: 'Password must contain uppercase, lowercase, number, and special character'
)]
public string $password,
#[Assert\NotBlank]
#[Assert\Choice(choices: ['admin', 'user', 'guest'])]
public string $role = 'user',
#[Assert\Positive]
public ?int $organizationId = null,
) {}
}
Service Layer¶
<?php
declare(strict_types=1);
namespace App\Service;
use App\Dto\CreateUserDto;
use App\Dto\UpdateUserDto;
use App\Entity\User;
use App\Event\UserCreatedEvent;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
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->setName($dto->name);
$user->setEmail($dto->email);
$user->setRole($dto->role);
$hashedPassword = $this->passwordHasher->hashPassword($user, $dto->password);
$user->setPassword($hashedPassword);
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->eventDispatcher->dispatch(new UserCreatedEvent($user));
return $user;
}
public function update(User $user, UpdateUserDto $dto): User
{
if ($dto->name !== null) {
$user->setName($dto->name);
}
if ($dto->email !== null) {
$user->setEmail($dto->email);
}
if ($dto->password !== null) {
$hashedPassword = $this->passwordHasher->hashPassword($user, $dto->password);
$user->setPassword($hashedPassword);
}
if ($dto->role !== null) {
$user->setRole($dto->role);
}
$this->entityManager->flush();
return $user;
}
public function delete(User $user): void
{
$this->entityManager->remove($user);
$this->entityManager->flush();
}
public function getPaginated(int $page, int $limit): array
{
$offset = ($page - 1) * $limit;
return $this->userRepository->createPaginatedQueryBuilder()
->setFirstResult($offset)
->setMaxResults($limit)
->getQuery()
->getResult();
}
}
Event and Subscriber¶
<?php
// src/Event/UserCreatedEvent.php
declare(strict_types=1);
namespace App\Event;
use App\Entity\User;
use Symfony\Contracts\EventDispatcher\Event;
class UserCreatedEvent extends Event
{
public const NAME = 'user.created';
public function __construct(
public readonly User $user,
) {}
}
<?php
// src/EventSubscriber/UserEventSubscriber.php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Event\UserCreatedEvent;
use App\Message\SendWelcomeEmail;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\MessageBusInterface;
class UserEventSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly MessageBusInterface $messageBus,
private readonly LoggerInterface $logger,
) {}
public static function getSubscribedEvents(): array
{
return [
UserCreatedEvent::class => 'onUserCreated',
];
}
public function onUserCreated(UserCreatedEvent $event): void
{
$user = $event->user;
$this->logger->info('User created', ['user_id' => $user->getId()]);
$this->messageBus->dispatch(new SendWelcomeEmail($user->getId()));
}
}
Message and Handler (Messenger)¶
<?php
// src/Message/SendWelcomeEmail.php
declare(strict_types=1);
namespace App\Message;
class SendWelcomeEmail
{
public function __construct(
public readonly int $userId,
) {}
}
<?php
// src/MessageHandler/SendWelcomeEmailHandler.php
declare(strict_types=1);
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]
class SendWelcomeEmailHandler
{
public function __construct(
private readonly UserRepository $userRepository,
private readonly MailerInterface $mailer,
) {}
public function __invoke(SendWelcomeEmail $message): void
{
$user = $this->userRepository->find($message->userId);
if ($user === null) {
return;
}
$email = (new Email())
->from('noreply@example.com')
->to($user->getEmail())
->subject('Welcome!')
->html('<p>Welcome to our platform!</p>');
$this->mailer->send($email);
}
}
Security Configuration¶
# config/packages/security.yaml
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/api/login
stateless: true
json_login:
check_path: /api/login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
jwt: ~
access_control:
- { path: ^/api/login, roles: PUBLIC_ACCESS }
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
Custom Voter¶
<?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;
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;
}
/** @var Post $post */
$post = $subject;
return match ($attribute) {
self::EDIT, self::DELETE => $this->canModify($post, $user),
default => false,
};
}
private function canModify(Post $post, User $user): bool
{
// Author can modify their own posts
if ($post->getAuthor() === $user) {
return true;
}
// Admins can modify any post
return $user->getRole() === 'admin';
}
}
Testing¶
Functional Test¶
<?php
declare(strict_types=1);
namespace App\Tests\Controller;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class UserControllerTest extends WebTestCase
{
private KernelBrowser $client;
private EntityManagerInterface $entityManager;
private string $token;
protected function setUp(): void
{
$this->client = static::createClient();
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
// Create admin user and get JWT token
$this->token = $this->getAuthToken();
}
public function testListUsers(): void
{
$this->client->request(
'GET',
'/api/v1/users',
[],
[],
['HTTP_AUTHORIZATION' => 'Bearer ' . $this->token]
);
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/json');
$content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertIsArray($content);
}
public function testCreateUser(): void
{
$this->client->request(
'POST',
'/api/v1/users',
[],
[],
[
'HTTP_AUTHORIZATION' => 'Bearer ' . $this->token,
'CONTENT_TYPE' => 'application/json',
],
json_encode([
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'Password123!',
'role' => 'user',
])
);
$this->assertResponseStatusCodeSame(201);
/** @var UserRepository $repository */
$repository = static::getContainer()->get(UserRepository::class);
$user = $repository->findOneByEmail('john@example.com');
$this->assertNotNull($user);
$this->assertEquals('John Doe', $user->getName());
}
public function testCreateUserWithInvalidData(): void
{
$this->client->request(
'POST',
'/api/v1/users',
[],
[],
[
'HTTP_AUTHORIZATION' => 'Bearer ' . $this->token,
'CONTENT_TYPE' => 'application/json',
],
json_encode([
'name' => '',
'email' => 'invalid',
'password' => 'short',
'role' => 'invalid',
])
);
$this->assertResponseStatusCodeSame(422);
}
public function testUnauthorizedAccess(): void
{
$this->client->request('GET', '/api/v1/users');
$this->assertResponseStatusCodeSame(401);
}
private function getAuthToken(): string
{
// Create admin user if not exists
$user = new User();
$user->setName('Admin');
$user->setEmail('admin@test.com');
$user->setPassword('$2y$13$...'); // Pre-hashed password
$user->setRole('admin');
$this->entityManager->persist($user);
$this->entityManager->flush();
// Get JWT token
$this->client->request(
'POST',
'/api/login',
[],
[],
['CONTENT_TYPE' => 'application/json'],
json_encode(['username' => 'admin@test.com', 'password' => 'password'])
);
$content = json_decode($this->client->getResponse()->getContent(), true);
return $content['token'];
}
}
Unit Test¶
<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\Dto\CreateUserDto;
use App\Entity\User;
use App\Event\UserCreatedEvent;
use App\Repository\UserRepository;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class UserServiceTest extends TestCase
{
private UserService $service;
private MockObject $userRepository;
private MockObject $entityManager;
private MockObject $passwordHasher;
private MockObject $eventDispatcher;
protected function setUp(): void
{
$this->userRepository = $this->createMock(UserRepository::class);
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$this->service = new UserService(
$this->userRepository,
$this->entityManager,
$this->passwordHasher,
$this->eventDispatcher,
);
}
public function testCreateUser(): void
{
$dto = new CreateUserDto(
name: 'John Doe',
email: 'john@example.com',
password: 'Password123!',
role: 'user',
);
$this->passwordHasher
->expects($this->once())
->method('hashPassword')
->willReturn('hashed_password');
$this->entityManager
->expects($this->once())
->method('persist')
->with($this->isInstanceOf(User::class));
$this->entityManager
->expects($this->once())
->method('flush');
$this->eventDispatcher
->expects($this->once())
->method('dispatch')
->with($this->isInstanceOf(UserCreatedEvent::class));
$user = $this->service->create($dto);
$this->assertEquals('John Doe', $user->getName());
$this->assertEquals('john@example.com', $user->getEmail());
$this->assertEquals('user', $user->getRole());
}
}
Commands¶
# Installation
composer create-project symfony/skeleton myapp
cd myapp
composer require webapp # Install common bundles
# Development server
symfony serve
# or
php -S localhost:8000 -t public
# Console commands
php bin/console make:entity User
php bin/console make:controller UserController
php bin/console make:form UserType
php bin/console make:migration
php bin/console make:subscriber UserEventSubscriber
php bin/console make:message SendWelcomeEmail
php bin/console make:voter PostVoter
php bin/console make:command App:ImportUsers
# 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
# Cache
php bin/console cache:clear
php bin/console cache:warmup
# Messenger (Queue)
php bin/console messenger:consume async
php bin/console messenger:failed:show
php bin/console messenger:failed:retry
# Debug
php bin/console debug:router
php bin/console debug:container
php bin/console debug:config security
# Testing
php bin/phpunit
php bin/phpunit --filter=UserServiceTest
php bin/phpunit --coverage-html coverage
# Code quality
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
Best Practices¶
Do¶
- ✓ Use dependency injection via constructor
- ✓ Use DTOs for data transfer
- ✓ Use Symfony Messenger for async operations
- ✓ Use Doctrine repositories for database queries
- ✓ Use Voters for authorization logic
- ✓ Use Events for decoupling
- ✓ Use PHP 8 attributes for mapping and validation
- ✓ Use serialization groups for API responses
- ✓ Configure services properly in services.yaml
Don't¶
- ✗ Don't use Doctrine entities directly in controllers (use DTOs)
- ✗ Don't bypass the security system
- ✗ Don't hardcode configuration values
- ✗ Don't flush EntityManager in loops
- ✗ Don't use
$_GET,$_POSTdirectly - ✗ Don't create services that do everything (Single Responsibility)
- ✗ Don't ignore deprecation warnings
Framework Comparison¶
| Feature | Symfony | Laravel | Slim |
|---|---|---|---|
| Learning Curve | Steep | Moderate | Easy |
| Performance | Excellent | Good | Excellent |
| Flexibility | High | Moderate | High |
| Enterprise Ready | Yes | Yes | Limited |
| Components | Reusable | Integrated | Minimal |
| Configuration | YAML/PHP | PHP | PHP |
| Best For | Enterprise | Full-stack | Microservices |
References¶
- Symfony Documentation
- Symfony Best Practices
- Doctrine ORM
- API Platform (REST/GraphQL on Symfony)
- Symfony Casts
- PHP-FIG PSR Standards