Разработка бэкенда сайта на PHP (Symfony)
Symfony — не быстрый старт. Это фундамент для проектов, которые должны жить долго, масштабироваться и поддерживаться командами разного состава. Высокий порог входа окупается предсказуемостью архитектуры, строгой типизацией и тем, что компоненты Symfony используются внутри Laravel, Drupal, Magento и десятков других систем — это индикатор их качества.
Symfony выбирают для: сложных монолитов с богатой доменной логикой, DDD-проектов, высоконагруженных API, enterprise-систем с долгосрочной поддержкой.
Архитектура компонентов
Symfony строится вокруг контейнера зависимостей (Service Container) и конфигурации через атрибуты PHP 8+. Всё — сервис, всё — внедряется автоматически:
namespace App\Service;
use App\Repository\ProductRepository;
use App\Event\ProductCreatedEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Psr\Cache\CacheItemPoolInterface;
final class ProductService
{
public function __construct(
private readonly ProductRepository $productRepository,
private readonly EventDispatcherInterface $dispatcher,
private readonly CacheItemPoolInterface $cache,
) {}
public function create(CreateProductDto $dto): Product
{
$product = new Product(
name: $dto->name,
price: Money::of($dto->price, 'USD'),
category: $dto->categoryId
? $this->productRepository->findCategoryOrFail($dto->categoryId)
: null,
);
$this->productRepository->save($product, flush: true);
$this->dispatcher->dispatch(new ProductCreatedEvent($product));
// Инвалидация кеша
$this->cache->deleteItem("product_{$product->getId()}");
return $product;
}
}
Контроллеры и маршруты
namespace App\Controller\Api\V1;
use App\Dto\CreateProductDto;
use App\Service\ProductService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/api/v1/products', name: 'api_products_')]
final class ProductController extends AbstractController
{
public function __construct(private readonly ProductService $productService) {}
#[Route('', name: 'list', methods: ['GET'])]
public function list(ProductListQuery $query): JsonResponse
{
$result = $this->productService->getPaginated($query);
return $this->json($result, context: ['groups' => ['product:list']]);
}
#[Route('', name: 'create', methods: ['POST'])]
#[IsGranted('ROLE_ADMIN')]
public function create(
#[MapRequestPayload] CreateProductDto $dto
): JsonResponse {
$product = $this->productService->create($dto);
return $this->json($product, status: 201, context: ['groups' => ['product:detail']]);
}
#[Route('/{id}', name: 'show', methods: ['GET'])]
public function show(Product $product): JsonResponse // ParamConverter автоматически
{
return $this->json($product, context: ['groups' => ['product:detail']]);
}
}
#[MapRequestPayload] — автоматическая десериализация + валидация через Symfony Validator:
namespace App\Dto;
use Symfony\Component\Validator\Constraints as Assert;
final class CreateProductDto
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 255)]
public readonly string $name,
#[Assert\Positive]
public readonly float $price,
#[Assert\Positive]
#[Assert\IsNull]
public readonly ?int $categoryId = null,
#[Assert\Length(max: 5000)]
public readonly ?string $description = null,
) {}
}
Doctrine ORM
Doctrine — полноценный ORM с Unit of Work паттерном. Принципиальное отличие от Active Record (Eloquent): Entity не знает о базе данных, репозиторий управляет persisting:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: ProductRepository::class)]
#[ORM\Table(name: 'products')]
#[ORM\Index(columns: ['slug'], name: 'idx_products_slug')]
#[ORM\Index(columns: ['category_id', 'is_active'], name: 'idx_products_cat_active')]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['product:list', 'product:detail'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['product:list', 'product:detail'])]
private string $name;
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
#[Groups(['product:list', 'product:detail'])]
private string $price;
#[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'products')]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['product:detail'])]
private ?Category $category = null;
#[ORM\Column(type: 'json')]
private array $attributes = [];
// getters/setters...
}
Репозиторий с DQL:
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
class ProductRepository extends ServiceEntityRepository
{
public function findActiveByCategory(int $categoryId, int $page = 1, int $limit = 20): array
{
return $this->createQueryBuilder('p')
->leftJoin('p.category', 'c')
->addSelect('c')
->where('p.isActive = :active')
->andWhere('p.category = :categoryId')
->setParameter('active', true)
->setParameter('categoryId', $categoryId)
->orderBy('p.createdAt', 'DESC')
->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit)
->getQuery()
->getResult();
}
}
Безопасность и аутентификация
Symfony Security — один из самых гибких механизмов среди PHP-фреймворков:
# config/packages/security.yaml
security:
password_hashers:
App\Entity\User:
algorithm: bcrypt
cost: 12
providers:
app_provider:
entity:
class: App\Entity\User
property: email
firewalls:
api:
pattern: ^/api
stateless: true
jwt: ~
access_control:
- { path: ^/api/auth, roles: PUBLIC_ACCESS }
- { path: ^/api/admin, roles: ROLE_ADMIN }
- { path: ^/api, roles: ROLE_USER }
JWT через lexik/jwt-authentication-bundle:
#[Route('/api/auth/login', methods: ['POST'])]
public function login(
#[MapRequestPayload] LoginDto $dto,
UserRepository $users,
UserPasswordHasherInterface $hasher,
JWTTokenManagerInterface $jwtManager
): JsonResponse {
$user = $users->findOneByEmail($dto->email);
if (!$user || !$hasher->isPasswordValid($user, $dto->password)) {
throw new UnauthorizedHttpException('', 'Invalid credentials');
}
return $this->json(['token' => $jwtManager->create($user)]);
}
Messenger и очереди
Symfony Messenger поддерживает: синхронный режим, AMQP (RabbitMQ), Redis Streams, SQS:
// Сообщение
final class SendEmailNotification
{
public function __construct(
public readonly int $userId,
public readonly string $template,
public readonly array $context = []
) {}
}
// Обработчик
#[AsMessageHandler]
final class SendEmailNotificationHandler
{
public function __invoke(SendEmailNotification $message): void
{
$user = $this->userRepository->find($message->userId);
$this->mailer->sendTemplate($user->getEmail(), $message->template, $message->context);
}
}
// Диспетчеризация
$this->bus->dispatch(new SendEmailNotification($user->getId(), 'welcome'));
// Конфигурация транспорта
# config/packages/messenger.yaml
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
routing:
'App\Message\SendEmailNotification': async
Кеширование
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
final class ProductService
{
public function getById(int $id): Product
{
return $this->cache->get("product_{$id}", function (ItemInterface $item) use ($id) {
$item->expiresAfter(3600);
$item->tag(["product_{$id}", 'products']);
return $this->productRepository->find($id) ?? throw new NotFoundException();
});
}
public function invalidate(int $id): void
{
$this->cache->invalidateTags(["product_{$id}"]);
}
}
Сроки разработки
Symfony требует больше времени на настройку, но это инвестиция в поддержку:
- Архитектура + DDD domain layer — 1–2 недели
- Entities + Doctrine migrations — 1 неделя
- API + Security + DTO — 2–3 недели
- Messenger + интеграции — 1–2 недели
- Тесты (PHPUnit + Foundry) — 1–2 недели
Сложный корпоративный сайт или портал: 8–16 недель. Symfony окупается на проектах с планируемым ростом, сложной доменной логикой и командой, которая в нём работает.







