Symfony PHP Framework Guide — Enterprise Component Architecture
Symfony is a set of reusable PHP components and a full-stack web framework — known for its flexibility, modularity, and use in enterprise projects and major CMS platforms like Drupal.
What You’ll Learn
By the end of this guide, you’ll understand Symfony’s component architecture, use Doctrine ORM and Twig templating, work with the service container, and build enterprise-grade applications.
Why Symfony Matters
Symfony’s component architecture means you can use individual pieces (HttpFoundation, Routing, Console) in any PHP project without the full framework. At DodaTech, we use Symfony components in Doda Browser’s sync service and Durga Antivirus Pro’s API layer. Drupal, Magento, and phpBB all use Symfony components — making it the most influential PHP framework.
Symfony Architecture
flowchart TD
A[Request] --> B[Front Controller]
B --> C[Router]
C --> D[Controller]
D --> E[Doctrine ORM]
D --> F[Twig View]
E --> G[(Database)]
F --> H[Response]
Key Concepts
Bundles — Modular Features
Everything in Symfony is a bundle — a reusable package containing controllers, models, views, config, and assets:
composer create-project symfony/skeleton myapp
composer require doctrine makerDoctrine ORM
// src/Entity/Product.php
#[Entity(repositoryClass: ProductRepository::class)]
class Product
{
#[Id, GeneratedValue, Column]
private ?int $id = null;
#[Column(length: 100)]
private string $name;
#[Column(type: "decimal", precision: 10, scale: 2)]
private float $price;
#[ManyToOne(targetEntity: Category::class)]
private ?Category $category = null;
}Doctrine uses PHP 8 attributes for mapping — clean, readable, and type-safe.
Service Container with Autowiring
// config/services.yaml
services:
App\Service\ProductService:
autowire: true
arguments:
$apiKey: '%env(API_KEY)%'Symfony’s DI container automatically resolves dependencies — if your service needs EntityManagerInterface, Symfony figures out how to provide it.
Twig Templating
{# templates/product/index.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<h1>Products</h1>
{% for product in products %}
<div>{{ product.name }} - {{ product.price|currency }}</div>
{% endfor %}
{% endblock %}Twig is secure by design — automatic escaping, sandbox mode, and template inheritance.
Console Commands
Symfony’s Console component lets you build CLI commands with minimal boilerplate:
// src/Command/ImportProductsCommand.php
namespace App\Command;
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\Output\OutputInterface;
use App\Repository\ProductRepository;
#[AsCommand(name: 'app:import-products')]
class ImportProductsCommand extends Command
{
public function __construct(
private ProductRepository $repo
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('file', InputArgument::REQUIRED, 'Path to CSV file');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$file = $input->getArgument('file');
if (!file_exists($file)) {
$output->writeln("<error>File not found: $file</error>");
return Command::FAILURE;
}
$handle = fopen($file, 'r');
$count = 0;
while (($row = fgetcsv($handle)) !== false) {
$this->repo->saveProduct($row);
$count++;
}
fclose($handle);
$output->writeln("<info>Imported $count products.</info>");
return Command::SUCCESS;
}
}Expected output when running:
php bin/console app:import-products data/products.csvImported 47 products.The Console component auto-registers commands tagged with #[AsCommand] — no manual wiring needed. This pattern powers DodaZIP’s batch file processing and Durga Antivirus Pro’s scheduled scan tasks.
Symfony Security: Voters
Symfony’s security system uses voters to make authorization decisions. Each voter votes ACCESS_GRANTED, ACCESS_DENIED, or ACCESS_ABSTAIN:
// src/Security/Voter/ProductVoter.php
namespace App\Security\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use App\Entity\Product;
use App\Entity\User;
class ProductVoter extends Voter
{
const VIEW = 'view';
const EDIT = 'edit';
const DELETE = 'delete';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE])
&& $subject instanceof Product;
}
protected function voteOnAttribute(
string $attribute,
mixed $subject,
TokenInterface $token
): bool {
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
// Admin can do anything
if (in_array('ROLE_ADMIN', $user->getRoles())) {
return true;
}
/** @var Product $product */
$product = $subject;
return match ($attribute) {
self::VIEW => true, // Everyone can view
self::EDIT => $user === $product->getOwner(),
self::DELETE => $user === $product->getOwner(),
default => false,
};
}
}Use the voter in a controller:
#[Route('/products/{id}/edit', name: 'product_edit')]
public function edit(Product $product): Response
{
$this->denyAccessUnlessGranted('edit', $product);
// Only the owner reaches here
}How Symfony Works Under the Hood
Every Symfony request passes through the HttpKernel — the heart of the framework:
flowchart TD
A[index.php] --> B[HttpKernel::handle()]
B --> C[Get Request]
C --> D[resolveController]
D --> E[Event: kernel.request]
E --> F[Event: kernel.controller]
F --> G[Call Controller]
G --> H[Event: kernel.view]
H --> I[Event: kernel.response]
I --> J[Return Response]
index.php loads the autoloader, creates the kernel with environment and debug mode, and calls
$kernel->handle($request).HttpKernel::handle() wraps the entire lifecycle. It dispatches events at every stage:
kernel.request— early processing, routing, firewall checkskernel.controller— after the controller is resolved, before executionkernel.view— if the controller returns a non-Response object (e.g., array), a view listener converts itkernel.response— after the controller returns a Response, but before it’s sent
Dependency Injection Container — the
services.yamlconfiguration is compiled into PHP code cached invar/cache/. During compilation, Symfony processes autowiring, autoconfigure tags, compiler passes, and extensions. The cached container is loaded on every request, which is why config changes require clearing the cache.Compiler passes — during container compilation, registered compiler passes can modify service definitions. For example, the
makerbundle adds itself, Doctrine’sregisterResolveTargetEntityPasshandles target entity resolution, and Twig’stwig.loaderregisters template paths.
This architecture is why Symfony can run large enterprise applications with hundreds of bundles — the compiled container is efficient, and the event system allows any bundle to hook into any part of the lifecycle.
Common Mistakes
1. Not using autowiring — Manually wiring services defeats Symfony’s biggest productivity feature. Use autowiring and autoconfigure.
2. Putting logic in Doctrine entities — Entities should be plain PHP objects. Business logic belongs in services or domain models.
3. Ignoring Symfony’s profiler — The Symfony debug toolbar and profiler show database queries, memory usage, and performance — invaluable for debugging.
4. Overriding too many defaults — Symfony’s defaults are well-tested. Only override when you have a specific reason.
Practice Questions
1. What is a Symfony bundle?
A reusable package containing controllers, entities, views, configuration, and assets — similar to a plugin. Everything in Symfony is a bundle, including core features.
2. How does autowiring work in Symfony?
The dependency injection container automatically resolves constructor dependencies by type-hint. If a service needs EntityManagerInterface, Symfony creates and injects it.
3. What makes Twig secure?
Automatic output escaping prevents XSS, sandbox mode restricts template operations, and template inheritance prevents layout injection.
4. Challenge: Create a Symfony controller that returns a JSON response with query results from Doctrine.
#[Route('/api/products', name: 'api_products')]
public function list(ProductRepository $repo): JsonResponse
{
return $this->json($repo->findAll());
}FAQ
Real-World Task: Build a REST API with Validation
Create a product API endpoint with request validation, JSON responses, and proper error handling. This pattern matches the API layer in Doda Browser’s bookmark sync service.
Step 1: Create a DTO (Data Transfer Object) for the request:
// src/DTO/CreateProductRequest.php
namespace App\DTO;
use Symfony\Component\Validator\Constraints as Assert;
class CreateProductRequest
{
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 100)]
public string $name;
#[Assert\NotBlank]
#[Assert\PositiveOrZero]
public float $price;
#[Assert\NotBlank]
#[Assert\Type('integer')]
#[Assert\PositiveOrZero]
public int $stock;
#[Assert\Type('integer')]
public ?int $categoryId = null;
}Step 2: Create a controller that uses the DTO:
// src/Controller/Api/ProductController.php
namespace App\Controller\Api;
use App\DTO\CreateProductRequest;
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
#[Route('/api/products')]
class ProductController extends AbstractController
{
public function __construct(
private EntityManagerInterface $em,
private ValidatorInterface $validator
) {}
#[Route('', methods: ['POST'])]
public function create(Request $request): JsonResponse
{
$dto = new CreateProductRequest();
$data = json_decode($request->getContent(), true);
$dto->name = $data['name'] ?? '';
$dto->price = $data['price'] ?? 0;
$dto->stock = $data['stock'] ?? 0;
$dto->categoryId = $data['category_id'] ?? null;
$errors = $this->validator->validate($dto);
if (count($errors) > 0) {
$errorMessages = [];
foreach ($errors as $error) {
$errorMessages[$error->getPropertyPath()] = $error->getMessage();
}
return $this->json(['errors' => $errorMessages], Response::HTTP_BAD_REQUEST);
}
$product = new Product();
$product->setName($dto->name);
$product->setPrice($dto->price);
$product->setStock($dto->stock);
$this->em->persist($product);
$this->em->flush();
return $this->json($product, Response::HTTP_CREATED);
}
#[Route('', methods: ['GET'])]
public function list(): JsonResponse
{
$products = $this->em
->getRepository(Product::class)
->findBy([], ['name' => 'ASC']);
return $this->json($products);
}
}Expected output — GET /api/products:
[
{"id": 1, "name": "Widget", "price": 9.99, "stock": 42, "category": null},
{"id": 2, "name": "Gadget", "price": 14.99, "stock": 18, "category": null}
]Expected output — POST /api/products with invalid data {"price": -5}:
{
"errors": {
"name": "This value should not be blank.",
"price": "This value should be either positive or zero.",
"stock": "This value should not be blank."
}
}Challenge: Add a PUT /api/products/{id} endpoint that uses Symfony’s DenyAccessUnlessGranted() with the ProductVoter from earlier — only the product owner can update.
What’s Next
| Lesson | Description |
|---|---|
| https://tutorials.dodatech.com/backend/php/laravel/ | Modern PHP framework comparison |
| https://tutorials.dodatech.com/backend/php/yii/ | High-performance framework |
| https://tutorials.dodatech.com/backend/nodejs/express/ | Node.js Express.js framework |
| PHP | Advanced PHP concepts |
| Doctrine | Doctrine ORM deep dive |
What’s Next
Congratulations on completing this Symfony tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro