Website Backend Development with PHP (Symfony)
Symfony is not a fast start. It's a foundation for projects that must live long, scale, and be maintained by teams of different compositions. High entry threshold is paid off by architecture predictability, strict typing, and the fact that Symfony components are used inside Laravel, Drupal, Magento — this is an indicator of their quality.
Symfony is chosen for: complex monoliths with rich domain logic, DDD projects, high-load APIs, enterprise systems with long-term support.
Component Architecture
Symfony is built around a dependency container (Service Container) and PHP 8+ attribute configuration. Everything is a service, everything is auto-injected:
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;
}
}
Controllers and Routes
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
{
return $this->json($product, context: ['groups' => ['product:detail']]);
}
}
Doctrine ORM
Doctrine is a full-featured ORM with Unit of Work pattern. Unlike Eloquent's Active Record, entities don't know about the database:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: ProductRepository::class)]
#[ORM\Table(name: 'products')]
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;
}
Messenger and Queues
Symfony Messenger supports sync mode, AMQP, 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);
}
}
Development Timelines
Symfony requires more setup time but provides mature infrastructure:
- Architecture + DDD domain layer — 1–2 weeks
- Entities + Doctrine migrations — 1 week
- API + Security + DTO — 2–3 weeks
- Messenger + integrations — 1–2 weeks
- Tests (PHPUnit + Foundry) — 1–2 weeks
Complex corporate website or portal: 8–16 weeks. Symfony pays off on projects with planned growth, complex domain logic, and a team working in it.







