↑
Documentation Index
Introduction
Installing & Setting UP the Symfony
What Is Composer?
Architecture
Components
Service Container
Events & Event Listeners
Expression Language
Bundles
Controllers
Routing
View Engine
Doctrine ORM
Forms
Understanding CSRF (Cross-Site Request Forgery)
Validation
File Uploading
AJAX Control
Cookies and Sessions Management
Internationalization
Logging
Email Management
Unit Testing
Advanced Concepts
Introduction to Symfony
What Is Symfony?
Symfony is a powerful, full-stack, enterprise-grade PHP framework used for building modern web applications.
Symfony is widely used in industry, notably by:
Drupal
Magento
OroCRM
Laravel (uses Symfony components)
It is ideal for:
Enterprise apps
Long-term maintainable systems
Modular and scalable architectures
APIs and web services
Installing Symfony
First install the Symfony CLI tool:
curl -sS https://get.symfony.com/cli/installer | bash
symfony -V
Create a new Symfony project:
symfony new my_project --webapp
This generates a full MVC skeleton with:
my_project/
├── assets/
├── bin/
├── config/
├── migrations/
├── public/
├── src/
├── templates/
├── tests/
└── var/
Symfony Directory Structure (Overview)
src/ — PHP code (controllers, services, entities).
templates/ — Twig template files.
config/ — application configuration.
public/ — web root (index.php lives here).
var/ — cache and logs.
vendor/ — installed dependencies.
Your First Symfony Route
Symfony uses attributes (preferred), annotations, or YAML/XML for routing.
Create a simple controller:
<?php
// src/Controller/HomeController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class HomeController extends AbstractController
{
#[Route('/', name: 'home')]
public function index(): Response
{
return new Response('Hello from Symfony!');
}
}
symfony serve
Visit: http://127.0.0.1:8000
Templates with Twig
Symfony uses the Twig template engine for HTML.
Create a template:
<!-- templates/home.html.twig -->
<h1>Hello {{ name }}!</h1>
#[Route('/hello/{name}', name: 'hello')]
public function hello(string $name): Response
{
return $this->render('home.html.twig', [
'name' => $name,
]);
}
Symfony and MVC
Model: Entities + Doctrine ORM
View: Twig templates
Controller: Classes inside src/Controller
Request → Controller → Service/Model → Response → Browser
Database Support with Doctrine ORM
Symfony integrates tightly with Doctrine (ORM).
symfony console make:entity
<?php
// src/Entity/Product.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 100)]
private string $name;
#[ORM\Column]
private float $price;
// getters and setters...
}
symfony console make:migration
symfony console doctrine:migrations:migrate
Dependency Injection Container
Symfony is built around a powerful Service Container .
Almost everything (mailer, logger, custom services) is registered as a service.
Services are autowired automatically based on type hints.
<?php
class SluggerService
{
public function slugify(string $s): string
{
return strtolower(str_replace(' ', '-', $s));
}
}
#[Route('/slug/{text}')]
public function slug(SluggerService $slugger, string $text): Response
{
return new Response($slugger->slugify($text));
}
Symfony Flex and Bundles
Bundles are reusable modules, similar to Django apps or Laravel packages.
Symfony Flex automates installation and configuration.
composer require annotations
composer require twig
composer require symfony/orm-pack
Debugging Tools
Debug Toolbar : shows log, memory, performance, routing info.
Profiler : detailed debugging UI at /_profiler.
Useful commands:
symfony console debug:router
symfony console debug:config
symfony console debug:container
Installing & Setting Up the Symfony Framework
Overview of Symfony Installation
Symfony is a powerful PHP framework for building modern, scalable web applications.
Installation can be done in two main ways:
via the Symfony CLI (recommended)
via Composer create-project
Symfony requires:
PHP ≥ 8.1
Composer
OpenSSL extension
pdo_mysql or pdo_pgsql (if using databases)
web server (local or built-in)
Optional tools:
Symfony CLI
Docker
Xdebug
Installing the Symfony CLI
The Symfony CLI is the best way to create, serve, and debug Symfony apps.
Download (Linux):
wget https://get.symfony.com/cli/installer -O - | bash
Move it globally:
mv ~/.symfony5/bin/symfony /usr/local/bin/symfony
Verify installation:
symfony -v
You may install via package manager:
macOS: Homebrew
Windows: Scoop
Creating a New Symfony Project
Create a new web application (recommended):
symfony new myapp --webapp
This installs:
HTTP Kernel
Routing
.env configuration
Twig templating
Doctrine ORM
Debugging tools (Profiler)
Create a minimal project (without web tools):
symfony new myapp
You can also use Composer directly:
composer create-project symfony/skeleton myapp
Understanding the Symfony Directory Structure
myapp/
├── config/ # configuration for routing, services, packages
├── public/ # web root (index.php lives here)
├── src/ # controllers, entities, services
├── templates/ # Twig templates
├── migrations/ # database migration files
├── var/ # cache and logs
├── vendor/ # Composer dependencies
└── .env # environment variables
Most development happens in:
Running Symfony's Local Web Server
Use Symfony CLI's built-in server:
symfony serve -d
This launches the server in the background.
View status:
symfony server:status
Stop the server:
symfony server:stop
You can also use PHP’s built-in server:
php -S localhost:8000 -t public/
Configuring Environment Variables
Symfony uses an .env file for development configuration.
You can define:
database credentials
debug settings
mailer configuration
API keys
Example .env fragment:
APP_ENV=dev
APP_SECRET=abc123
DATABASE_URL="mysql://user:pass@localhost:3306/mydb"
For production, use .env.local and server-level env variables.
Installing Useful Symfony Bundles
composer require symfony/orm-pack
Maker Bundle (code generator):
composer require symfony/maker-bundle --dev
Twig (templates):
composer require twig
Security (authentication):
composer require symfony/security-bundle
API Platform:
composer require api
Testing Your Symfony Setup
https://localhost:8000
Symfony debug toolbar should be visible (in dev environment).
Test that routing works:
symfony console debug:router
Check environment status:
symfony console about
Using Symfony's Console Tool
symfony console list
symfony console cache:clear
symfony console make:controller
symfony console make:entity
symfony console debug:container
These tools save significant development time.
Configuring a Database Connection
DATABASE_URL="mysql://user:pass@localhost:3306/mydb"
Test DB connectivity:
symfony console doctrine:query:sql "SELECT 1"
Generate entity + migration:
symfony console make:entity
symfony console make:migration
symfony console doctrine:migrations:migrate
Configuring Web Server (Apache / Nginx)
Symfony uses public/index.php as front controller.
Apache Virtual Host example:
<VirtualHost *:80>
DocumentRoot /var/www/myapp/public
DirectoryIndex index.php
<Directory /var/www/myapp/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Nginx example:
server {
root /var/www/myapp/public;
index index.php;
location / {
try_files $uri /index.php$is_args$args;
}
}
Verifying Production-Ready Configuration
Symfony offers a readiness check:
symfony check:requirements
Optimize the app for production:
symfony console cache:clear --env=prod
symfony console cache:warmup --env=prod
Install optimized Composer dependencies:
composer install --no-dev --optimize-autoloader
What Is Composer?
Overview of Composer
Composer is the official dependency manager for PHP — similar to:
npm for JavaScript
pip for Python
cargo for Rust
bundler for Ruby
It allows PHP projects to install and manage:
libraries
frameworks (Symfony, Laravel, etc.)
components
autoloaders
project dependencies
Composer is essential for modern PHP development — most frameworks require it.
It installs packages from Packagist , the default PHP package registry.
Installing Composer
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
Install globally:
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
Verify installation:
composer --version
How Composer Works
Composer uses two primary files in your project:
composer.json describes required libraries
composer.lock locks the exact installed versions
When running:
composer install
Composer will:
download dependencies
resolve version conflicts
install packages to vendor/
generate the autoloader
All dependencies go into the vendor/ directory.
The composer.json File
{
"name": "myapp/project",
"require": {
"symfony/http-foundation": "^7.0",
"twig/twig": "^3.0"
}
}
You can also specify:
autoload rules
PHP version constraints
scripts to run
dev dependencies
Installing Dependencies
Install all dependencies:
composer install
Add a new dependency:
composer require symfony/console
Add a dev-only dependency:
composer require --dev phpunit/phpunit
Update dependencies:
composer update
Remove a dependency:
composer remove twig/twig
The Autoloader
Composer automatically generates an autoloader file:
vendor/autoload.php
Load it in your PHP code:
<?php
require __DIR__ . "/vendor/autoload.php";
?>
This makes all installed packages available without manual require calls.
Supports PSR-4 autoloading for your own namespaces:
{
"autoload": {
"psr-4": {
"App\\\\": "src/"
}
}
}
After editing composer.json autoload section:
composer dump-autoload
Scripts in Composer
You can define automation scripts in composer.json:
{
"scripts": {
"test": "phpunit",
"start": "php -S localhost:8000 -t public"
}
}
Run scripts with:
composer run test
composer run start
Composer Version Constraints Explained
Common version patterns:
^2.0 → allow updates except major breaking changes
~2.0 → allow updates only in minor versions
2.0.* → patch updates only
>=2.0 → any version ≥ 2.0
Example:
"require": {
"monolog/monolog": "^3.0"
}
This allows version 3.0, 3.1, 3.2… but not 4.0 .
Global vs Local Composer Installation
Local — installed inside each project (vendor/)
Global — installed once for the whole system
Global installation example:
composer global require laravel/installer
Useful for CLI tools like Laravel installer or PHPUnit.
Packagist — The PHP Package Repository
By default, Composer installs packages from:
https://packagist.org
Packagist hosts:
Symfony components
Laravel components
Doctrine
Monolog
Twig
Guzzle
You can browse it for any PHP library you need.
Updating Composer Itself
Update to latest version:
composer self-update
Rollback to previous version:
composer self-update --rollback
Common Problems and Solutions
php -d memory_limit=-1 /usr/local/bin/composer update
Slow installs: use Composer v2 — it’s much faster.
Autoload not updated:
composer dump-autoload
Conflicts: Composer tells you what packages are incompatible.
Lock file ignored:
composer install
Never use composer update in production.
Symfony Architecture
Overview of Symfony’s Architecture
Symfony follows a modular, decoupled architecture built around reusable components.
It is designed with:
MVC principles (Model–View–Controller)
Dependency Injection
Event Dispatcher pattern
Service Container as the core runtime engine
Bundles for modular features
This architecture makes Symfony:
highly extensible
testable
maintainable
suitable for both microservices and large enterprise apps
High-Level Request Lifecycle
Symfony transforms each HTTP request into a controller response using the HttpKernel workflow.
Key concepts involved:
Front Controller
Routing
Controller Execution
Events (RequestEvent, ViewEvent, ResponseEvent)
Front Controller (public/index.php)
All requests go through one file:
<?php
require dirname(__DIR__).'/vendor/autoload.php';
$kernel = new Kernel('dev', true);
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
?>
This ensures:
centralized error handling
environment control (dev/prod)
security filters
consistent bootstrapping
Routing Component
The Routing component maps a URL to a controller.
Example:
<?php
#[Route('/hello/{name}', name: 'hello')]
function hello($name) { ... }
?>
Symfony loads routes from:
annotations / attributes (PHP)
YAML files
XML files
PHP config files
HttpKernel & Event Dispatcher
Symfony’s request handling is event-driven.
HttpKernel dispatches events such as:
kernel.request
kernel.controller
kernel.view
kernel.response
kernel.exception
Developers can attach listeners/subscribers to customize the process.
Examples:
JSON response formatters
Authentication listeners
Error page handlers
Profiling tools
The Controller Layer
Controllers are responsible for:
processing the request
interacting with models/services
returning a Response object
Typical controller:
<?php
class HomeController extends AbstractController {
#[Route('/')]
public function index() {
return $this->render('home.html.twig');
}
}
?>
Symfony controllers can:
render templates
redirect users
return JSON
access services from the DI container
View Layer (Twig Templating)
Symfony uses Twig as the default templating engine.
Responsibilities:
output HTML
format data
extend layouts
sanitize user input
Example template:
{% extends 'base.html.twig' %}
{% block body %}
<h1>Hello {{ name }}!</h1>
{% endblock %}
Model Layer (Doctrine ORM)
Symfony typically uses Doctrine as ORM (Object Relational Mapping).
Doctrine Maps PHP classes (Entities) to database tables.
Example entity:
<?php
#[ORM\Entity]
class Product {
#[ORM\Id, ORM\GeneratedValue]
private int $id;
#[ORM\Column(length: 255)]
private string $name;
}
?>
Doctrine handles:
database migrations
relations (OneToMany, ManyToMany)
repositories (data access objects)
transaction management
Service Container (Dependency Injection)
At the core of Symfony is the dependency injection container .
It creates and manages objects ("services") for the entire app.
Services include:
database connections
mailer
loggers
security services
custom app services
Example service registration (services.yaml):
services:
App\Service\Mailer:
arguments:
$adminEmail: '%env(ADMIN_EMAIL)%'
Service injection in controllers:
public function send(Mailer $mailer) {
$mailer->send("hello@test.com");
}
Bundles: Modular Symfony Extensions
Bundles are reusable packages containing:
controllers
routes
services
configs
templates
Core Symfony bundles include:
FrameworkBundle
SecurityBundle
TwigBundle
DoctrineBundle
MonologBundle
Bundles are automatically registered in config/bundles.php.
Configuration System
Symfony Flex
Symfony Components
Overview of Symfony Components
Symfony Components are a collection of decoupled, reusable PHP libraries that solve common web development problems.
You can use Symfony Components:
inside the Symfony Framework
inside other frameworks (Laravel, Drupal, Magento)
in plain PHP projects
in microservices or CLI scripts
Benefits of using components:
well-tested and stable
PSR-compliant
independent (use only what you need)
robust documentation
Examples of popular components:
Routing
EventDispatcher
HttpFoundation
Console
Yaml
DomCrawler
The HttpFoundation Component
Replaces raw PHP superglobals ($_GET, $_POST, $_COOKIE, etc.) with object-oriented Request & Response classes.
Example — creating a response:
<?php
use Symfony\Component\HttpFoundation\Response;
$response = new Response("Hello World", 200);
$response->send();
?>
This component is the foundation of Symfony’s HTTP layer.
The Routing Component
Maps URLs to specific controllers or callbacks.
Example:
<?php
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
$routes = new RouteCollection();
$routes->add('hello', new Route('/hello/{name}'));
?>
Used widely in APIs, microservices, and framework-less apps.
The EventDispatcher Component
Implements the Observer pattern.
Allows different parts of an app to react to events without tight coupling.
Example — dispatching an event:
<?php
$dispatcher->dispatch(new UserRegisteredEvent($user));
?>
Used internally by Symfony’s HttpKernel, Security, and Messenger systems.
The DependencyInjection Component
Powers the Service Container .
Handles automatic dependency resolution and object creation.
Example service definition (YAML):
services:
App\Service\Mailer:
arguments:
$host: "smtp.example.com"
Provides strong decoupling and testability.
The Console Component
Used to build command-line tools.
Symfony Console is one of the most popular CLI libraries in PHP.
Example command class:
<?php
use Symfony\Component\Console\Command\Command;
class HelloCommand extends Command {
protected function execute($input, $output) {
$output->writeln("Hello!");
return Command::SUCCESS;
}
}
?>
Symfony’s bin/console is powered by this component.
The Finder Component
Searches for files and directories.
Example — find all .php files:
<?php
use Symfony\Component\Finder\Finder;
$finder = new Finder();
$finder->files()->in('src')->name('*.php');
?>
Useful for CLI tools, build scripts, and automated tasks.
The Yaml Component
Loads and dumps YAML configuration files.
Sample usage:
<?php
use Symfony\Component\Yaml\Yaml;
$data = Yaml::parseFile('config.yaml');
?>
Used heavily in Symfony’s configuration system.
The Serializer Component
Converts PHP objects to JSON, XML, YAML, or arrays.
Used in APIs and message processing.
<?php
$serializer->serialize($user, 'json');
?>
Highly customizable with normalizers and encoders.
The Validator Component
Provides object validation using constraints.
Example:
<?php
use Symfony\Component\Validator\Constraints as Assert;
class User {
#[Assert\NotBlank]
public $name;
}
?>
Used in form handling, API validation, and data processing.
The Form Component
Handles form creation, rendering, submission, and validation.
Example form type:
<?php
class ContactType extends AbstractType {
public function buildForm($builder, $options) {
$builder
->add('name')
->add('email')
->add('message');
}
}
?>
Useful in CRUD apps or admin panels.
Other Useful Symfony Components
Symfony Component
Description
Translation
Internationalization (i18n) and localization support.
Cache
Cache adapters for Redis, Memcached, and filesystem caching.
Process
Execute and manage shell commands from PHP.
Security
Authentication and access control system.
Messenger
Message bus for CQRS, queues, and async processing.
HttpClient
Fast, modern HTTP client alternative to Guzzle.
Mime
Email handling and MIME type utilities.
DomCrawler
HTML and DOM parsing API.
CssSelector
CSS selector engine for querying DOM elements.
Filesystem
Convenient file and directory management tools.
How Symfony Uses Components Internally
Symfony Framework = collection of components + glue code.
Example internal usage:
Routing resolves URLs
HttpKernel orchestrates the request lifecycle
EventDispatcher triggers events
HttpFoundation standardizes requests/responses
DependencyInjection builds the service container
Each component is tested separately and follows semantic versioning.
Using Components Without the Framework
You can install any component with Composer:
composer require symfony/http-foundation
Use case examples:
custom microframework
CLI utilities
API microservices
legacy PHP modernization
email processing tools
Symfony Components are framework-agnostic.
Symfony Service Container
Introduction to the Service Container
The Service Container is the core of Symfony’s architecture. It manages:
object creation (services)
dependency injection
configuration of services
performance optimizations (caching + compiling)
Instead of manually instantiating objects using new, Symfony delegates object management to the container.
Advantages:
loose coupling
cleaner architecture
better testability
automatic dependency resolution
Every major part of Symfony (routing, Twig, security, logging) is organized as a service managed by this container.
What Is a Service?
A service is simply a PHP object that performs a reusable task in your application.
Examples of services:
Mailer service
Logger
Database connection
API call client
Authentication manager
Custom service example:
<?php
namespace App\Service;
class Mailer {
public function send(string $email, string $message) {
// ...
}
}
?>
The Service Container instantiates and manages this class automatically.
How Dependency Injection Works
Dependencies are automatically passed into the constructor of a service.
Example service with dependencies:
<?php
namespace App\Service;
use Psr\Log\LoggerInterface;
class ReportGenerator {
public function __construct(
private LoggerInterface $logger
) {}
public function generate() {
$this->logger->info("Report generated.");
}
}
?>
Symfony automatically resolves LoggerInterface and injects the correct service into ReportGenerator.
Service Registration
Symfony automatically registers most classes in src/ as services (autoconfiguration + autowiring ).
Manual service definition (YAML):
# config/services.yaml
services:
App\Service\Mailer:
arguments:
$host: '%env(SMTP_HOST)%'
Services can be registered via YAML, XML, or PHP config.
Autowiring
Autowiring automatically finds and injects services based on type hints.
Example controller using autowired service:
<?php
class ReportController extends AbstractController {
public function index(ReportGenerator $generator) {
$generator->generate();
return new Response("Done");
}
}
?>
No need for configuration — Symfony sees the type ReportGenerator and injects it.
Autoconfiguration
Autoconfiguration tells Symfony to automatically apply:
tags (e.g. event subscribers)
interfaces (e.g. command handlers)
service behaviors
Enabled by default in services.yaml:
_defaults:
autowire: true
autoconfigure: true
Example: a class implementing EventSubscriberInterface is automatically tagged as an event subscriber.
Service Visibility (Public vs Private)
Private services cannot be fetched directly from the container.
Public services can be accessed via the container (discouraged).
By default, all custom services are private — good practice.
To make a service public:
services:
App\Service\Mailer:
public: true
Best practice: use dependency injection , not $container->get().
Accessing the Container
Direct access to the container in controllers is discouraged:
$mailer = $this->container->get(Mailer::class);
Correct method: inject the service in your controller constructor or action.
For rare cases, you can inject the container itself:
services:
App\Controller\SpecialController:
arguments: ['@service_container']
But this breaks dependency inversion — avoid it.
Service Tags
Tags tell the container that a service has a special role.
Examples of tags:
kernel.event_listener
kernel.event_subscriber
console.command
doctrine.repository_service
Example (YAML):
services:
App\Event\UserRegisteredSubscriber:
tags: ['kernel.event_subscriber']
Autoconfiguration applies many tags automatically.
Service Aliases
Aliases provide alternative names for services.
Example:
services:
mailer: '@App\Service\Mailer'
Now mailer refers to the same service as App\Service\Mailer.
Container Compilation
Symfony compiles and optimizes the Service Container into plain PHP.
Generated container files live in:
var/cache/dev/
Compilation benefits:
faster service resolution
removal of unused services
inlined definitions
cached autowiring metadata
Recompile manually:
symfony console cache:clear
Debugging Services
symfony console debug:container
Find services containing name:
symfony console debug:container mail
Inspect service details:
symfony console debug:container App\Service\Mailer
Symfony Events & Event Listeners
Introduction to Symfony’s Event System
Symfony uses an event-driven architecture at the heart of its HTTP Kernel.
This architecture enables different parts of the framework (and your app) to react to events during the request–response lifecycle.
Core ideas:
Events represent something that happens (request received, controller chosen, response sent…)
Event listeners react to those events
Event subscribers register multiple listeners
The EventDispatcher triggers the events
The system allows:
decoupling
clean cross-cutting logic
plug-in architecture
What Is the EventDispatcher?
The EventDispatcher component is responsible for:
registering event listeners
dispatching events
triggering listener callbacks
Example in plain PHP:
<?php
use Symfony\Component\EventDispatcher\EventDispatcher;
$dispatcher = new EventDispatcher();
$dispatcher->addListener('user.registered', function () {
echo "Event caught!";
});
$dispatcher->dispatch(new Event(), 'user.registered');
?>
Symfony uses this internally for routing, controllers, responses, exceptions, security, etc.
Symfony’s Built-In Kernel Events
Symfony emits many core events during request processing.
Most important events:
kernel.request
kernel.controller
kernel.controller_arguments
kernel.view
kernel.response
kernel.terminate
kernel.exception
Example lifecycle:
Request → kernel.request → kernel.controller → controller → response
↑ ↓
listeners kernel.view
You can hook into any of these with listeners or subscribers.
Creating a Custom Event
Define your own event class:
<?php
namespace App\Event;
use Symfony\Contracts\EventDispatcher\Event;
class UserRegisteredEvent extends Event {
public function __construct(
public readonly string $email
) {}
}
?>
This encapsulates the data that listeners will handle.
Dispatching an Event
Inject the EventDispatcher into your service or controller:
<?php
use App\Event\UserRegisteredEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class UserService {
public function __construct(private EventDispatcherInterface $dispatcher) {}
public function register(string $email) {
// ... save user ...
$this->dispatcher->dispatch(new UserRegisteredEvent($email));
}
}
?>
Dispatching triggers all listeners registered for this event.
Creating an Event Listener
An event listener is a class with a single public method reacting to an event:
<?php
namespace App\EventListener;
use App\Event\UserRegisteredEvent;
class WelcomeEmailListener {
public function onUserRegistered(UserRegisteredEvent $event) {
// send email
}
}
?>
Register the listener (YAML):
services:
App\EventListener\WelcomeEmailListener:
tags:
- { name: 'kernel.event_listener', event: 'App\Event\UserRegisteredEvent', method: 'onUserRegistered' }
Symfony automatically calls onUserRegistered() when the event is dispatched.
Creating an Event Subscriber
A subscriber is a class that listens to multiple events.
Implement EventSubscriberInterface:
<?php
namespace App\EventSubscriber;
use App\Event\UserRegisteredEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class UserSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [
UserRegisteredEvent::class => 'onRegister',
'kernel.response' => 'onResponse',
];
}
public function onRegister(UserRegisteredEvent $event) { /* ... */ }
public function onResponse($event) { /* ... */ }
}
?>
Register subscriber (YAML):
services:
App\EventSubscriber\UserSubscriber:
tags:
- { name: 'kernel.event_subscriber' }
Subscribers are better than listeners when:
you handle many events
you want clearer organization
you want automatic tagging via autoconfiguration
Event Priorities
Multiple listeners can react to the same event.
You can control execution order using priority (higher = earlier):
services:
App\EventListener\FirstListener:
tags:
- { name: kernel.event_listener, event: App\Event\UserRegisteredEvent, priority: 20 }
App\EventListener\SecondListener:
tags:
- { name: kernel.event_listener, event: App\Event\UserRegisteredEvent, priority: 10 }
Example:
priority 20 → runs first
priority 10 → runs second
Modifying Responses with Events
You can modify or replace the response using kernel.response:
<?php
class AddHeaderListener {
public function onKernelResponse(ResponseEvent $event) {
$response = $event->getResponse();
$response->headers->set('X-App', 'Demo');
}
}
?>
Useful for:
security headers
performance headers
response normalization
Handling Exceptions with Events
kernel.exception allows custom exception handling:
<?php
class ExceptionListener {
public function onKernelException(ExceptionEvent $event) {
$event->setResponse(new Response("Custom error!", 500));
}
}
?>
Use cases:
API JSON error formatting
Custom error pages
Logging exceptions
Symfony Expression Language
What Is the Symfony Expression Language?
The Expression Language is a component that allows you to write small, dynamic expressions inside configuration files, annotations/attributes, security rules, or service definitions.
It provides:
a small, safe scripting language
type-safe evaluation
variables and functions
integration with Symfony Security and DI Container
Its primary use cases:
Security (access control)
Routing expressions
Service configuration
Validation
Workflow component
The Expression Language allows logic like:
user.isAdmin() and request.getPath() matches "^/admin"
Basic Syntax of Expressions
The Expression Language syntax is similar to JavaScript / C-like expressions.
Supported operations:
Arithmetic : 1 + 2 * 5
Comparisons : a == b, x > 10
Logical : and, or, not
Function calls : min(1, 2)
Property access : user.name
Method calls : user.getProfile()
Example:
user.age >= 18 and user.hasVerifiedEmail()
Using Expression Language in PHP Code
You can evaluate expressions directly:
<?php
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
$el = new ExpressionLanguage();
echo $el->evaluate('1 + 2 * 10'); // 21
?>
Expressions can reference variables:
<?php
$el->evaluate('user.age >= 18', [
'user' => $user
]);
?>
Expressions in Security (Most Common Use Case)
Expressions are heavily used in security.yaml for access control.
Example:
security:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/profile, allow_if: "user.isActive() and user.age >= 18" }
Other examples:
is_granted('ROLE_ADMIN')
is_granted('EDIT', object)
user == post.getAuthor()
request.getMethod() == 'POST'
Expressions in Service Container
You can use expressions in service definitions:
services:
App\Service\Cache:
arguments:
$timeout: "@=parameter('cache_timeout') * 2"
Notice the @= prefix — this tells Symfony to evaluate it as an expression.
Expressions can also access services:
$handler: "@=service('logger').getHandlers()[0]"
Available functions:
service()
parameter()
constant()
env()
Expressions in Workflow Component
Expressions can limit transitions:
framework:
workflows:
article_process:
transitions:
publish:
guard: "user.isEditor() and not subject.isLocked()"
subject = the entity in the workflow (e.g. Article).
Expressions in Routing
Routing conditions allow request-based filtering:
homepage:
path: /
controller: App\Controller\HomeController::index
condition: "context.getMethod() in ['GET', 'HEAD']"
Useful for API method checks or host-based routing.
Creating Custom Functions for Expression Language
You can register custom functions to expand the language:
<?php
$el->register('range', function ($arg) {
return sprintf('range(%s)', $arg);
}, function ($values, $arg) {
return range($arg, $arg * 2);
});
?>
Now usable as:
range(5)
Expression Caching
Expressions can be compiled into PHP code for performance:
<?php
$phpCode = $el->compile('user.age * 2', ['user']);
echo $phpCode;
?>
You can store compiled expressions to accelerate runtime execution.
Symfony Bundles
What Is a Symfony Bundle?
A Bundle in Symfony is a directory containing a set of files (PHP classes, controllers, services, config, templates, etc.) that implement a specific feature.
Bundles are similar to:
Plugins (WordPress)
Modules (Drupal)
Gems (Ruby on Rails)
Bundles allow:
code reusability
easy packaging of features
sharing of code between applications
organization of large projects
In older Symfony versions (2 & 3), everything was a bundle.
In modern Symfony (4+), apps usually have only:
AppBundle → removed in Symfony 4
src/ directory contains the app code
Third-party bundles installed via Composer
Today, bundles are mostly for:
reusable packages
third-party integrations (Doctrine, Twig, Security, Messenger)
major app modules in large teams
Bundle Structure
A typical bundle structure looks like:
MyBundle/
├── DependencyInjection/
│ ├── Configuration.php
│ └── MyBundleExtension.php
├── Resources/
│ ├── config/
│ ├── views/
│ └── translations/
├── Tests/
├── MyBundle.php
Key components:
MyBundle.php : the bundle class
DependencyInjection/ : service, config loading
Resources/views : Twig templates
Resources/config : YAML/XML/PHP configs
Tests/ : PHPUnit tests
Core Symfony Bundles
Symfony comes with several built-in bundles:
Symfony Bundle
Description
FrameworkBundle
Provides the core framework behavior and services.
TwigBundle
Enables Twig templating support.
DoctrineBundle
Integrates the Doctrine ORM and database layer.
SecurityBundle
Provides authentication and authorization features.
WebProfilerBundle
Offers the debug toolbar and profiling tools.
MonologBundle
Handles logging using Monolog integration.
MakerBundle
Provides command-line tools for code scaffolding (generators).
These are registered automatically by Symfony Flex.
Installing Third-Party Bundles
composer require friendsofsymfony/user-bundle
Symfony Flex usually auto-registers the bundle in config/bundles.php.
Example entry:
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
FOS\UserBundle\FOSUserBundle::class => ['all' => true],
];
Flex also installs default configuration for the bundle.
Creating Your Own Bundle
<?php
namespace App\MyBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class MyBundle extends Bundle {}
?>
Add to bundles.php:
return [
App\MyBundle\MyBundle::class => ['all' => true],
];
Bundle structure normally goes under:
src/MyBundle
Then add:
controllers
services
configs
views
Bundle Configuration with Extension Classes
Bundles typically have a configuration loader in:
DependencyInjection/MyBundleExtension.php
Example Extension:
<?php
class MyBundleExtension extends Extension {
public function load(array $configs, ContainerBuilder $container) {
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.yaml');
}
}
?>
This allows external configuration from config/packages/my_bundle.yaml.
Configuring a Bundle Externally
Most third-party bundles expose configuration options:
my_bundle:
enable_feature: true
api_key: "%env(API_KEY)%"
Symfony uses Configuration.php in the bundle to validate configuration:
$treeBuilder->getRootNode()
->children()
->booleanNode('enable_feature')->defaultFalse()->end()
->scalarNode('api_key')->isRequired()->end()
->end();
Bundle Best Practices (Modern Symfony)
DO NOT create a bundle for normal application code — use src/.
Create a bundle when:
the code must be reused across projects
you want to publish a package for the Symfony ecosystem
a feature needs to be fully isolated (rare)
Bundle classes should be small — most logic lives in:
services
controllers
configs
templates
Do not overload the bundle with unnecessary files.
Follow Symfony coding standards (PSR-4, PSR-12).
Examples of Popular Third-Party Bundles
Bundle
Description
FOSUserBundle
User management (registration, login, profiles).
ApiPlatformBundle
Automatic REST and GraphQL API generation.
EasyAdminBundle
Admin dashboard and CRUD backend generation.
LiipImagineBundle
Advanced image manipulation and caching.
HWIOAuthBundle
OAuth client integration (Google, Facebook, GitHub…).
KnpPaginatorBundle
Pagination utilities for Doctrine queries and arrays.
SymfonyCasts VerifyEmailBundle
Email verification workflow for user registration.
How Bundles Integrate into the Kernel
Symfony loads bundles during Kernel boot:
<?php
class Kernel extends BaseKernel {
public function registerBundles(): iterable {
// Loaded from config/bundles.php
}
}
?>
Each bundle may:
add services
add configuration
override templates
extend behavior of other bundles
Bundles interact via events, DI configuration, and routing.
Bundle Overrides (Resource Inheritance)
You can override bundle templates:
templates/bundles/TwigBundle/Exception/error404.html.twig
Or override controllers and services via DI tags or config.
This makes bundles extremely flexible and customizable.
Symfony Controllers
What Is a Symfony Controller?
A controller in Symfony is a PHP callable (usually a class method) that:
receives the incoming HTTP request
performs logic (fetch data, call services, process forms…)
returns a Response object
Controllers are the “C” in the MVC pattern.
Symfony encourages thin controllers, fat services :
Controllers: glue code, orchestrate
Services: business logic
Minimum controller example:
<?php
use Symfony\Component\HttpFoundation\Response;
function home() {
return new Response("Hello Symfony!");
}
?>
Controller Classes
Most controllers are classes extending AbstractController.
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class HomeController extends AbstractController {
public function index(): Response {
return new Response("Welcome!");
}
}
?>
AbstractController provides helpers:
render()
redirectToRoute()
addFlash()
json()
getUser()
Routing to Controllers
Routes map URLs to controllers.
Using PHP attributes:
<?php
use Symfony\Component\Routing\Annotation\Route;
class HomeController extends AbstractController {
#[Route('/', name: 'home')]
public function index() { ... }
}
?>
Equivalent YAML:
home:
path: /
controller: App\Controller\HomeController::index
Route placeholders automatically map to arguments:
#[Route('/user/{id}')]
public function show(int $id) { ... }
Returning Responses
Controllers must return a Response object.
Basic response:
return new Response("OK");
JSON response:
return $this->json(['status' => 'ok']);
Redirect response:
return $this->redirectToRoute('home');
Rendering a Twig template:
return $this->render('home/index.html.twig', [
'name' => 'Junzhe'
]);
Request Object Injection
Symfony automatically injects the Request object:
use Symfony\Component\HttpFoundation\Request;
public function search(Request $req) {
$term = $req->query->get('q');
}
Useful request helpers:
$req->query → GET params
$req->request → POST params
$req->cookies
$req->files
$req->getContent() → raw body
$req->isMethod('POST')
Autowiring Services into Controllers
You can autowire services into action arguments:
public function index(LoggerInterface $logger) {
$logger->info("Visiting index");
...
}
Or into the controller constructor (recommended for repeated dependencies):
public function __construct(private Mailer $mailer) {}
Symfony resolves dependencies automatically via type-hints.
Handling Route Parameters
Route parameters are automatically mapped:
#[Route('/post/{slug}')]
public function view(string $slug) {}
With constraints:
#[Route('/user/{id<\d+>}')]
public function show(int $id) {}
Optional parameters:
#[Route('/blog/{page?1}')]
public function blog(int $page) {}
Param Converters (Doctrine Integration)
If you pass an Entity type, Symfony loads it automatically:
#[Route('/product/{id}')]
public function show(Product $product) {
return $this->json($product);
}
Symfony queries the repository:
$productRepository->find($id)
If no entity → throws 404 automatically.
Using Flash Messages
Flash messages persist for exactly one request.
$this->addFlash('success', 'User created!');
Display in Twig:
{% for msg in app.flashes('success') %}
<div class="alert alert-success">{{ msg }}</div>
{% endfor %}
Returning JSON Responses
return $this->json([
'user' => 'Junzhe',
'score' => 100
]);
Symfony handles:
JSON encoding
setting Content-Type
HTTP status codes
Controller Traits and Base Features
AbstractController provides:
createForm()
render()
json()
redirectToRoute()
addFlash()
denyAccessUnlessGranted()
It also gives access to:
Doctrine
UrlGenerator
Security
ParameterBag
API Controllers (Stateless)
API controllers typically:
return JSON
avoid sessions
disable CSRF
consume JSON request bodies
Example:
#[Route('/api/data', methods:['POST'])]
public function api(Request $req) {
$data = json_decode($req->getContent(), true);
return $this->json(['received' => $data]);
}
Symfony Routing
What Is Routing in Symfony?
The Symfony Routing component maps incoming URLs to:
a specific controller ,
with specific parameters ,
and optional constraints or requirements .
Routing is one of the first steps in the HTTP request lifecycle.
Routing supports:
static paths
dynamic placeholders
host-based routing
HTTP methods
default values
requirements (regex)
custom conditions (Expression Language)
Defining Routes Using PHP Attributes (Recommended)
Modern Symfony uses PHP 8 attributes to define routes directly above actions:
<?php
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class HomeController extends AbstractController {
#[Route('/', name: 'home')]
public function index() { ... }
}
?>
Advantages:
closest to controller code
no extra files needed
easy to read and maintain
Defining Routes in YAML
Routes can also be defined in config/routes.yaml:
home:
path: /
controller: App\Controller\HomeController::index
Useful for large enterprise apps or shared bundles.
Defining Routes in XML
<route id="home" path="/">
<default key="_controller">App\Controller\HomeController::index</default>
</route>
Mostly for older projects or bundle authors.
Dynamic Route Parameters
Routes can have placeholders:
#[Route('/user/{id}', name: 'user_show')]
public function show(int $id) { ... }
Symfony automatically converts the placeholder into a controller argument.
Optional parameters:
#[Route('/blog/{page?1}')]
public function blog(int $page) {}
Default values (YAML):
blog:
path: /blog/{page}
controller: App\Controller\BlogController::index
defaults:
page: 1
Parameter Requirements (Regex Constraints)
Use requirements to constrain parameters:
#[Route('/user/{id<\d+>}')]
public function show(int $id) {}
Examples:
id: digits only
slug: [a-z0-9-]+
date: \d{4}-\d{2}-\d{2}
HTTP Method Constraints
Limit routes to specific HTTP methods:
#[Route('/submit', methods: ['POST'])]
public function submit() { ... }
Options include:
GET
POST
PUT
PATCH
DELETE
HEAD
YAML equivalent:
submit:
path: /submit
controller: App\Controller\FormController::submit
methods: [POST]
Host-Based Routing
You can match routes based on the domain:
#[Route(host: '{subdomain}.example.com', path: '/')]
public function index(string $subdomain) {}
Useful for:
multi-tenant SaaS apps
per-locale domains
API subdomains
YAML:
tenant_home:
path: /
host: "{tenant}.example.com"
controller: App\Controller\TenantController::index
Route Conditions (Expression Language)
Routes can include runtime logic:
home:
path: /
controller: App\Controller\HomeController::index
condition: "context.getMethod() in ['GET', 'HEAD']"
Examples:
restrict API access based on IP
method-based redirects
locale-specific routing
Another example:
admin:
path: /admin
controller: App\Controller\AdminController::index
condition: "request.headers.get('X-ADMIN') == '1'"
Route Priority (Order of Matching)
Routes are matched in the order they are defined.
More specific routes should come before generic ones.
Example — wrong order:
product:
path: /product/{slug}
product_new:
path: /product/new
Correct order:
product_new:
path: /product/new
product:
path: /product/{slug}
Generating URLs in Controllers and Twig
$url = $this->generateUrl('user_show', ['id' => 5]);
In Twig :
{{ path('user_show', { id: 5 }) }}
Absolute URL:
{{ url('user_show', { id: 5 }) }}
Loading Routes from Controllers Automatically
In config/routes/annotations.yaml:
controllers:
resource: ../../src/Controller/
type: attribute
Symfony will scan all controller files and import routes automatically.
Debugging Routes
List all available routes:
symfony console debug:router
Show detailed info for one route:
symfony console debug:router user_show
Match a URL to see which route triggers:
symfony console router:match /user/10
Symfony View Engine (Twig Templating)
What Is the Symfony View Engine?
Symfony uses Twig as its default templating (view) engine.
Twig is a flexible, secure, and fast templating language designed to:
render HTML pages,
embed variables,
create loops/conditionals,
extend layouts,
escape outputs safely,
integrate with Symfony (routing, assets, forms).
Twig files have the extension .twig and live in the templates/ directory.
The View Engine is responsible for converting template files into actual HTML sent to the browser.
Serving a Twig Template from a Controller
Use the controller’s render() method:
<?php
return $this->render('home/index.html.twig', [
'name' => 'Junzhe'
]);
?>
The template contains placeholders for variables:
<h1>Hello {{ name }}!</h1>
Twig Template Syntax Basics
Twig uses three types of delimiters:
{{ ... }} → print a variable
{% ... %} → logic (loops, conditions, imports)
{# ... #} → comments
Example:
{# Comment #}
{% if user %}
Hello {{ user.name }}
{% else %}
Hello Guest
{% endif %}
Template Inheritance (Layouts)
Twig supports layout inheritance via {% extends %}.
Base layout:
{# templates/base.html.twig #}
<html>
<body>
{% block body %}{% endblock %}
</body>
</html>
Child template:
{% extends 'base.html.twig' %}
{% block body %}
<h1>Welcome!</h1>
{% endblock %}
This allows consistent headers/footers across your site.
Including Templates
You can reuse pieces of templates:
{% include 'partials/menu.html.twig' %}
Good for:
navigation menus
footers
forms
shared UI blocks
Variables in Twig
Variables are passed from controllers:
return $this->render('page.html.twig', ['age' => 22]);
Access in Twig:
{{ age }}
Objects support attribute access automatically:
{{ user.name }}
{{ user.getEmail() }}
Loops and Conditionals
{% for product in products %}
<li>{{ product.name }} — {{ product.price }}€</li>
{% endfor %}
{% if products is empty %}
No products found.
{% endif %}
Twig supports powerful loop variables:
loop.index
loop.first
loop.last
Filters
Filters transform output:
{{ name|upper }}
{{ content|striptags }}
{{ list|length }}
Common filters:
upper
lower
date
json_encode
escape
merge
Functions
Twig provides built-in functions:
path() → generate URLs
url() → absolute URLs
asset() → link static assets (CSS, JS)
dump() → debug data
<a href="{{ path('home') }}">Home</a>
Twig and Symfony Forms
Twig integrates tightly with Symfony Forms:
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.email) }}
{{ form_end(form) }}
Symfony provides themes (Bootstrap, Foundation) to style forms.
Custom Twig Extensions
You can add:
custom filters
custom functions
custom global variables
<?php
class AppExtension extends AbstractExtension {
public function getFilters() {
return [new TwigFilter('reverse', fn($s) => strrev($s))];
}
}
?>
Usage:
{{ 'hello'|reverse }}
Using Assets (CSS, JS, Images)
<link rel="stylesheet" href="{{ asset('style.css') }}">
Assets live in public/.
Error Pages via Twig
Symfony loads templates from:
templates/bundles/TwigBundle/Exception/
Common editable templates:
error.html.twig
error404.html.twig
error500.html.twig
Debugging Twig Templates
Use {{ dump() }} inside Twig to inspect variables.
Enable Symfony Profiler for rich debugging of:
rendered templates
variables
performance
CLI debug command:
symfony console debug:twig
Symfony Doctrine ORM
What Is Doctrine ORM?
Doctrine ORM is Symfony’s default Object–Relational Mapper .
It maps PHP objects (Entities) to database tables automatically.
Doctrine provides:
database abstraction,
entity lifecycle management,
automatic SQL generation,
migrations,
repositories for querying,
relationship mapping.
Key benefits:
no need to write SQL manually,
database-agnostic (MySQL, PostgreSQL, SQLite...),
clean domain-driven model.
Installing Doctrine ORM
composer require symfony/orm-pack
composer require --dev symfony/maker-bundle
Creates configuration under config/packages/doctrine.yaml.
Creating Your First Entity
symfony console make:entity Product
Example entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Product
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private int $id;
#[ORM\Column(length: 255)]
private string $name;
#[ORM\Column]
private float $price;
public function getId(): int { return $id; }
}
?>
Doctrine annotations/attributes map fields to table columns.
Running Migrations
Analyze entity changes → generate SQL:
symfony console make:migration
Apply migrations:
symfony console doctrine:migrations:migrate
Doctrine keeps track of database schema state automatically.
Reading & Writing Data (EntityManager)
Doctrine uses an EntityManager to manage entity persistence.
Saving an entity:
<?php
$product = new Product();
$product->setName('Laptop');
$product->setPrice(999);
$em->persist($product);
$em->flush();
?>
persist() marks entity for saving.
flush() performs SQL execution.
Doctrine Repositories
Each Entity automatically gets a repository.
Inject repository into a controller or service:
public function list(ProductRepository $repo) {
$products = $repo->findAll();
}
Common query helpers:
find($id)
findOneBy([...])
findAll()
findBy([...])
Custom Repository Methods
<?php
class ProductRepository extends ServiceEntityRepository
{
public function cheapProducts() {
return $this->createQueryBuilder('p')
->where('p.price < :limit')
->setParameter('limit', 100)
->getQuery()
->getResult();
}
}
?>
Uses Doctrine QueryBuilder.
Doctrine Relationships
Doctrine supports:
OneToOne
OneToMany
ManyToOne
ManyToMany
Example: One Product belongs to one Category:
#[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'products')]
private Category $category;
Inverse side:
#[ORM\OneToMany(mappedBy: 'category', targetEntity: Product::class)]
private Collection $products;
Lazy Loading vs Eager Loading
Doctrine loads related entities lazily by default.
Eager load with JOIN:
$qb->addSelect('c')
->leftJoin('p.category', 'c');
Important for performance when loading relationships.
Lifecycle Callbacks
Hooks for entity events (prePersist, postUpdate, etc.).
#[ORM\HasLifecycleCallbacks]
class Product {
#[ORM\PrePersist]
public function onCreate() {
$this->createdAt = new \DateTime();
}
}
Using the QueryBuilder
Fluent SQL-like query builder:
$qb = $repo->createQueryBuilder('p')
->where('p.price > :min')
->setParameter('min', 50)
->orderBy('p.price', 'DESC')
->getQuery()
->getResult();
More readable and safer than DQL.
Doctrine Migrations
Doctrine tracks database schema via migration files:
symfony console make:migration
Apply migrations:
symfony console doctrine:migrations:migrate
Migrations are versioned and rollbackable.
Flushing Strategy
Doctrine tracks changes internally in the Unit of Work.
Best practices:
Call flush() only when needed (not after each persist).
Batch operations → flush in chunks.
Avoid flushing inside loops.
Entity Validation (with Symfony Validator)
Doctrine works together with Symfony Validator:
#[Assert\NotBlank]
#[ORM\Column(length: 255)]
private string $name;
Validated automatically in forms and API requests.
Advanced Topics
Entity Inheritance (mapped superclass, single table inheritance)
Caching (metadata + query cache)
Custom DQL functions
Custom Type Mapping (JSON, UUID)
Soft Deletes (via Gedmo extensions)
What Are Symfony Forms?
The Symfony Form component provides a structured and secure way to handle:
HTML form rendering
form submission
data validation
mapping submitted data to objects
CSRF protection
Forms in Symfony separate:
Form definition (fields, types)
Form rendering (Twig templates)
Form processing (request handling)
This ensures clean, reusable and secure form handling.
Installing the Form Component
composer require symfony/form
composer require symfony/validator
composer require symfony/twig-bundle
validator is optional but recommended.
Creating a Form Type
symfony console make:form ContactFormType
Example form type:
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class ContactFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('name', TextType::class)
->add('email', EmailType::class)
->add('message', TextareaType::class);
}
}
?>
Each add() call defines one form field.
Rendering the Form in a Controller
Create and render the form:
<?php
public function contact(Request $request) {
$form = $this->createForm(ContactFormType::class);
return $this->render('contact/form.html.twig', [
'form' => $form->createView(),
]);
}
?>
createView() converts the form into a Twig-friendly view object.
Rendering the Form in Twig
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.email) }}
{{ form_row(form.message) }}
{{ form_end(form) }}
The form_row() helper renders:
Handling Form Submission
<?php
$form = $this->createForm(ContactFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData(); // associative array or object
// process form data...
}
?>
handleRequest() populates the form using POST data.
Mapping Forms to Entities
Forms can be mapped directly to Doctrine entities.
Example entity:
<?php
class Product {
private string $name;
private float $price;
}
?>
Bind form to entity:
$product = new Product();
$form = $this->createForm(ProductType::class, $product);
After submit → Doctrine entity automatically filled:
$em->persist($product);
$em->flush();
Form Field Options
All fields accept configuration options:
$builder->add('name', TextType::class, [
'label' => 'Full Name',
'required' => false,
'attr' => ['class' => 'form-control'],
]);
Common options:
label
required
attr (HTML attributes)
help
constraints (validation)
Built-in Form Field Types
Core field types include:
TextType
EmailType
PasswordType
ChoiceType
TextareaType
DateType
CheckboxType
IntegerType
MoneyType
FileType
Each type has specialized rendering + validation behavior.
ChoiceType (Dropdowns, Radios, Checkboxes)
$builder->add('color', ChoiceType::class, [
'choices' => [
'Red' => 'red',
'Blue' => 'blue',
'Green' => 'green',
]
]);
Multiple selection:
'expanded' => true, // radio/checkboxes
'multiple' => true
Validation Integration
Forms integrate with Symfony Validator.
Example constraint:
use Symfony\Component\Validator\Constraints as Assert;
$builder->add('email', EmailType::class, [
'constraints' => [
new Assert\NotBlank(),
new Assert\Email(),
]
]);
Invalid forms automatically display error messages in Twig.
File Uploads with FileType
$builder->add('photo', FileType::class, [
'mapped' => false,
'required' => false,
]);
mapped: false means → field does not map to an entity property.
Access file in controller:
$file = $form->get('photo')->getData();
CSRF Protection
Enabled by default for all forms.
Provides hidden input:
<input type="hidden" name="_token" value="..." />
Prevents cross-site request forgery attacks.
Rendering Individual Controls
You can render inputs manually:
{{ form_label(form.name) }}
{{ form_widget(form.name) }}
{{ form_errors(form.name) }}
Useful for custom Bootstrap layouts.
Handling Forms in API Applications
Forms can handle API (JSON) data:
$form->submit(json_decode($request->getContent(), true));
Useful for validation-heavy APIs.
CollectionType (Dynamic Forms)
Used for repeated/array-like form fields:
$builder->add('tags', CollectionType::class, [
'entry_type' => TextType::class,
'allow_add' => true,
'allow_delete' => true,
]);
Useful for:
product tags
multiple phone numbers
dynamic list inputs
Understanding CSRF (Cross-Site Request Forgery)
What Is CSRF?
CSRF (Cross-Site Request Forgery) is a web security attack where a malicious website tricks a logged-in user into performing unintended actions on another website.
It exploits:
the user’s legitimate login session,
browser auto-sending cookies,
hidden requests disguised as normal actions.
CSRF does not steal data; instead, it forces the victim to execute actions without their consent .
Examples:
changing password
sending money
deleting an account
posting content
How CSRF Works (Attack Scenario)
CSRF relies on the fact that browsers automatically include cookies.
Attack scenario:
1. User logs into yourbank.com; session cookie is stored.
2. User visits a malicious site evil.com.
3. evil.com secretly sends a request to:
https://yourbank.com/transfer?to=attacker&amount=5000
4. Browser automatically attaches the user’s cookies.
5. The bank thinks it's a genuine user action.
Why Are Cookies Automatically Sent?
Browsers always send cookies for a domain when requesting resources from that domain.
This means:
even requests from other sites (malicious) still include your valid session cookies.
This behavior is how CSRF becomes possible.
CSRF vs XSS
Both are web vulnerabilities, but completely different:
XSS (Cross-Site Scripting) → attacker injects JavaScript into your site.
CSRF → attacker tricks your users into unintentionally sending requests.
CSRF does not require JS; even HTML forms can trigger it.
How CSRF Tokens Prevent Attacks
CSRF tokens work by embedding a secret random value inside each form.
This value is known only to:
the server
the genuine page generated for the user
Example form:
<form method="post">
<input type="hidden" name="_token" value="4f7a98dcc..." />
<button>Submit</button>
</form>
When the form is submitted:
the server compares the submitted token with the expected token stored in the session.
Since attackers cannot read the form’s token, they cannot forge valid requests.
CSRF Tokens in Symfony
Symfony enables CSRF protection for all forms by default.
Hidden token automatically added:
<input type="hidden" name="_token" value="...">
Controller automatically validates the token during handleRequest().
You can manually validate tokens as well:
<?php
$csrf->isTokenValid(new CsrfToken('delete_item', $submittedToken));
?>
What CSRF Does Not Protect Against
CSRF does not protect against:
XSS
session hijacking
SQL injection
MITM attacks
It only prevents unauthorized cross-site actions .
Other Defenses Against CSRF
Aside from tokens, additional protection techniques include:
SameSite cookies (modern browsers prevent cross-site sending)
Double Submit Cookies
Checking HTTP Referer
Using Authorization headers for APIs (cookies avoided)
Modern default protection: SameSite=Lax or Strict.
Why CSRF Is Still Important
Even with modern SameSite cookie defaults, CSRF is still relevant because:
browsers behave inconsistently
developers often misconfigure cookies
APIs using cookies remain vulnerable
legacy browsers/systems still exist
Therefore → CSRF tokens are still recommended and widely used.
Symfony Validation
What Is Symfony Validation?
The Symfony Validator component provides a structured system to verify that data meets certain rules before being processed or saved.
Validation is commonly used for:
form input validation
API request validation
entity property validation
custom object validation
Advantages:
declarative constraints
automatic error messages
integration with Symfony Forms
supports both attributes and YAML/XML/PHP config
Installing the Validator Component
composer require symfony/validator
Validation config is placed in config/packages/validator.yaml.
Works seamlessly with forms and Doctrine entities.
Adding Validation Using PHP Attributes
Most modern Symfony apps use PHP attributes.
Example:
<?php
use Symfony\Component\Validator\Constraints as Assert;
class UserInput
{
#[Assert\NotBlank]
public string $name;
#[Assert\Email]
public string $email;
#[Assert\Length(min: 8)]
public string $password;
}
?>
Each #[Assert\...] defines a rule for the property.
Validating Objects Manually
Inject the ValidatorInterface:
<?php
use Symfony\Component\Validator\Validator\ValidatorInterface;
public function submit(ValidatorInterface $validator)
{
$input = new UserInput();
$input->name = '';
$input->email = 'wrong email';
$input->password = '123';
$errors = $validator->validate($input);
if (count($errors) > 0) {
return (string) $errors;
}
}
?>
Each error describes:
the violated constraint,
the field,
the error message.
Validation with Symfony Forms
Forms automatically use validation constraints.
Example entity with constraints:
<?php
class Product
{
#[Assert\NotBlank]
#[Assert\Length(min: 3)]
private string $name;
#[Assert\Positive]
private float $price;
}
?>
Form controller:
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// valid data
}
Errors automatically appear in Twig:
{{ form_errors(form.name) }}
Common Validation Constraints
String constraints
NotBlank
NotNull
Length
Email
Number constraints
Range
Positive
Negative
LessThan
GreaterThan
Date constraints
Date
DateTime
LessThanOrEqual
GreaterThanOrEqual
Collection constraints
File constraints
Entity constraints
Custom Error Messages
#[Assert\NotBlank(message: 'Name cannot be empty')]
private string $name;
All constraints accept custom messages.
Validating Nested Objects
<?php
class Order
{
#[Assert\Valid]
public Customer $customer;
}
?>
This ensures that constraints inside Customer are also validated.
Using Groups for Conditional Validation
Validation groups allow different rule sets depending on context.
Example:
#[Assert\NotBlank(groups: ['create'])]
public string $password;
Controller validation with groups:
$errors = $validator->validate($user, null, ['create']);
Useful for:
Entity creation vs edition
User registration vs login
Step-based forms
Validation Using YAML or XML
You can define validation rules without touching PHP code.
Example YAML:
App\Entity\User:
properties:
email:
- Email: ~
- NotBlank: ~
age:
- Range: { min: 18 }
This is useful for reusable bundles or clean domain models.
Custom Validation Constraints
Create your own constraints:
symfony console make:validator StrongPassword
Structure:
a constraint class
a validator class containing logic
Example validation logic:
if (!preg_match('/[A-Z]/', $value)) {
$this->context->buildViolation('Must contain uppercase')->addViolation();
}
Using the Validation Profiler
The Symfony Profiler shows:
validated values
constraints executed
violation messages
Helps troubleshoot form or API validation issues.
Symfony File Uploading
Overview of File Uploading in Symfony
Symfony provides a clean and secure workflow for file uploads using:
Form component (FileType)
UploadedFile class
CSRF protection
Validator (File / Image constraints)
Uploaded files are typically stored in:
public/uploads/
or a custom directory defined in .env
Symfony does NOT automatically save uploaded files.
You must:
handle uploaded data manually,
move the file to a permanent location,
store file paths in the database (not the file itself).
Creating a Form for File Uploading
A file input uses FileType:
<?php
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\File;
$builder->add('image', FileType::class, [
'label' => 'Upload Image',
'mapped' => false,
'required' => false,
'constraints' => [
new File([
'maxSize' => '5M',
'mimeTypes' => ['image/jpeg', 'image/png'],
'mimeTypesMessage' => 'Please upload a valid JPG or PNG image.',
])
]
]);
?>
mapped: false means the file field does not correspond to an entity property.
File uploads require the form to have:
{{ form_start(form, { 'attr': { 'enctype': 'multipart/form-data' } }) }}
Rendering the Upload Form in Twig
{{ form_start(form, { attr: { enctype: 'multipart/form-data' } }) }}
{{ form_row(form.image) }}
<button class="btn btn-primary">Upload</button>
{{ form_end(form) }}
Twig automatically handles:
CSRF field
file input rendering
Processing the Uploaded File in the Controller
On submit, Symfony wraps uploaded files into the UploadedFile class.
<?php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\File\UploadedFile;
$form = $this->createForm(ProductType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile $file */
$file = $form->get('image')->getData();
if ($file) {
$newFilename = uniqid().'.'.$file->guessExtension();
$file->move(
$this->getParameter('uploads_directory'),
$newFilename
);
// Store $newFilename in the database if needed
}
}
?>
The move() method:
moves the file to your uploads folder,
generates a safe filename.
Configuring Upload Directory
Add this to config/services.yaml:
parameters:
uploads_directory: '%kernel.project_dir%/public/uploads'
Now you can inject it via:
$this->getParameter('uploads_directory');
Displaying Uploaded Files
If you store the filename in the DB:
<img src="/uploads/{{ product.imageFilename }}" width="200" />
The file is publicly available in public/uploads/.
File Validation Rules
Common validators:
maxSize
mimeTypes
minWidth (for image)
minHeight
detectCorrupted
Image example:
new Assert\Image([
'maxSize' => '10M',
'minWidth' => 400,
'minHeight' => 300,
])
Handling Multiple File Uploads
$builder->add('photos', FileType::class, [
'multiple' => true,
'mapped' => false,
]);
Controller:
<?php
$files = $form->get('photos')->getData();
foreach ($files as $file) {
$filename = uniqid().'.'.$file->guessExtension();
$file->move($uploadDir, $filename);
}
?>
Deleting Uploaded Files
Symfony does NOT auto-delete files.
You must delete manually:
unlink($this->getParameter('uploads_directory').'/'.$product->getImageFilename());
Good practice: create a dedicated FileManager service.
Upload Error Handling
Symfony throws exceptions if:
file size exceeds server php.ini limits
upload directory is not writable
file is corrupted
In Twig, errors appear automatically:
{{ form_errors(form.image) }}
Symfony AJAX Control
What Is AJAX in Symfony?
AJAX (Asynchronous JavaScript and XML) refers to sending HTTP requests from JavaScript without reloading the page.
Modern Symfony projects use:
fetch() API
Axios
jQuery AJAX (legacy)
Stimulus + Symfony UX
AJAX is typically used for:
dynamic page updates
form submissions
loading partial HTML
REST API endpoints
live search / autocomplete
Symfony controllers can return JSON, partial HTML, or any custom HTTP response for AJAX.
Creating a Route for AJAX
AJAX routes are standard Symfony routes:
<?php
#[Route('/ajax/product/info', name: 'ajax_product_info', methods: ['POST'])]
public function info(Request $req): Response {
$id = $req->request->get('id');
return $this->json(['price' => 199, 'stock' => 5]);
}
?>
AJAX routes often use:
POST for data submission
GET for data loading
Use $this->json() to return JSON quickly.
Sending AJAX Requests Using fetch()
Example using JavaScript fetch():
fetch('/ajax/product/info', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: 10 })
})
.then(r => r.json())
.then(data => {
console.log(data.price);
});
JSON is the most common format for AJAX exchanges.
CSRF Protection for AJAX
Symfony protects against CSRF, including AJAX.
You must send a token with requests modifying data.
Generate token in Twig:
<script>
let csrf = "{{ csrf_token('ajax_action') }}";
</script>
Send it with AJAX request:
fetch('/ajax/action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
_token: csrf,
id: 123
})
});
Validate token in controller:
<?php
if (!$csrf->isTokenValid(new CsrfToken('ajax_action', $req->get('_token')))) {
return $this->json(['error' => 'invalid_csrf'], 400);
}
?>
Returning JSON Responses
Most AJAX endpoints return JSON:
return $this->json([
'status' => 'ok',
'username' => 'Junzhe'
]);
JSON responses automatically set Content-Type: application/json.
Returning Partial HTML for Dynamic Updates
Some AJAX operations return small HTML fragments:
<?php
return $this->render('product/_row.html.twig', [
'product' => $product
]);
?>
In JavaScript:
fetch('/ajax/product/row?id=5')
.then(r => r.text())
.then(html => {
document.querySelector('#products').innerHTML = html;
});
This allows dynamic table updates, search results, etc.
Sending Form Data via AJAX
let form = document.querySelector('#contact');
let data = new FormData(form);
fetch('/ajax/contact', {
method: 'POST',
body: data
});
Symfony handles form validation normally:
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
return $this->json(['success' => true]);
}
Useful for:
async form submissions
inline validation
file upload previews
Handling File Uploads via AJAX
Use FormData to upload files:
let formData = new FormData();
formData.append('photo', fileInput.files[0]);
formData.append('_token', csrf);
fetch('/ajax/upload', {
method: 'POST',
body: formData
});
Symfony processes uploads via UploadedFile normally.
Detecting AJAX Requests in Symfony
Symfony detects AJAX using:
if ($request->isXmlHttpRequest()) {
// AJAX request
}
Primarily useful for returning different responses for AJAX vs full page loads.
Returning Error Responses
Return HTTP status codes:
return $this->json(['error' => 'Forbidden'], 403);
Error format popular in APIs:
{ "errors": ["Price must be positive"] }
Twig can display errors dynamically after AJAX submission.
Using Stimulus & Symfony UX for AJAX Enhancements
Symfony UX integrates AJAX naturally via Stimulus controllers.
Stimulus example:
// assets/controllers/search_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
search(e) {
fetch('/ajax/search?q=' + e.target.value)
.then(r => r.text())
.then(html => this.resultsTarget.innerHTML = html);
}
}
Used for:
live search
table filtering
modal loading
in-place editing
Symfony Cookies and Session Management
Overview of Cookies and Session Management in Symfony
Symfony provides built-in tools for managing:
Cookies — small key/value pairs stored in the user’s browser.
Sessions — server-side storage linked to a specific user.
Use cases include:
remembering user preferences
tracking login sessions
temporary storage across requests
flash messages
Symfony uses the Session component to manage server-side sessions and the Cookie class to create/modify cookies.
Understanding Cookies
Cookies are stored in the user's browser and sent with each request to the server.
They contain name/value pairs and optional properties:
expiration time
path and domain
secure flag
httpOnly flag
SameSite policy
Cookies are used for:
persistent preferences
remember-me tokens
tracking
frontend storage
Important: Never store sensitive data in cookies .
Creating a Cookie in Symfony
Cookies are added to the Response object:
<?php
use Symfony\Component\HttpFoundation\Cookie;
$response = new Response();
$response->headers->setCookie(
Cookie::create('theme', 'dark')
->withExpires(strtotime('+1 year'))
);
return $response;
?>
Symfony provides fluent methods:
withExpires()
withSecure()
withHttpOnly()
withSameSite('Strict')
Reading Cookies
Access cookies from the Request object:
$theme = $request->cookies->get('theme', 'light');
The second argument is the default value.
Deleting Cookies
$response->headers->clearCookie('theme');
Deletes by sending an expired cookie.
Understanding Session Management
Sessions store data on the server for each user.
A session is identified by a session ID stored in a cookie (PHPSESSID).
Sessions are useful for:
authentication state
shopping carts
user preferences
temporary values
Symfony sessions are available via the Request object or SessionInterface.
Writing Data to Session
$session = $request->getSession();
$session->set('user_id', 42);
Session data persists until:
user logs out
session expires
session is cleared
Reading Session Data
$userId = $session->get('user_id', null);
If key does not exist → default value returned.
Removing Session Data
$session->remove('user_id');
To clear everything:
$session->clear();
Using Flash Messages (Session-Based)
Flash messages last for exactly one request.
Setting a flash message:
$this->addFlash('success', 'Profile updated!');
Display in Twig:
{% for msg in app.flashes('success') %}
<div class="alert alert-success">{{ msg }}</div>
{% endfor %}
Session Storage Backends
Symfony can store session data in various backends:
native filesystem (default)
Redis
Memcached
database
Configured in framework.yaml:
framework:
session:
handler_id: 'snc_redis.session.handler'
Preventing Session Fixation
Always regenerate session ID when user logs in:
$session->migrate();
This prevents attackers from stealing known session IDs.
Cookies Security Best Practices
Enable httpOnly (prevents JavaScript access).
Use secure flag on HTTPS:
->withSecure(true)
Use SameSite=Lax or Strict to mitigate CSRF.
Never store passwords or sensitive info in cookies.
Session Best Practices
Store minimal data in session — avoid large objects.
Regenerate session ID after login.
Use Redis for high-traffic applications.
Do not store sensitive information unencrypted.
Use flash messages for one-time events.
Symfony Internationalization (i18n)
What Is Internationalization (i18n)?
Internationalization (i18n) is the process of preparing an application to support multiple languages and regional formats.
In Symfony, i18n includes:
translation of text
translation of form labels & validation messages
date and number formatting
locale switching (language selector)
message pluralization
Symfony uses the Translation Component for managing language files and dynamic translations.
Installing and Enabling the Translation Component
composer require symfony/translation
Symfony Flex enables translation support automatically by adding:
framework:
translator: { fallbacks: ['en'] }
The fallback locale is used when a translation for the user’s locale is missing.
Translation File Structure
Translation files are placed in:
translations/
File naming convention:
messages.en.yaml
messages.fr.yaml
validators.de.yaml
forms.zh_CN.yaml
Example translation file:
# translations/messages.fr.yaml
welcome: "Bienvenue"
logout: "Se déconnecter"
Using Translations in Twig
Twig provides the trans filter:
{{ 'welcome'|trans }}
{{ 'logout'|trans }}
With parameters:
{{ 'greeting'|trans({ '%name%': 'Junzhe' }) }}
Using custom translation domain:
{{ 'button.save'|trans({}, 'forms') }}
Using Translations in Controllers and Services
<?php
use Symfony\Contracts\Translation\TranslatorInterface;
public function index(TranslatorInterface $translator)
{
$message = $translator->trans('welcome');
$withName = $translator->trans('greeting', ['%name%' => 'Junzhe']);
}
?>
Translation domain:
$translator->trans('save', domain: 'forms');
Pluralization
Symfony supports plural forms based on locale.
YAML example:
apple_count:
one: "You have one apple"
other: "You have %count% apples"
Usage in Twig:
{{ 'apple_count'|trans({ '%count%': amount }, count=amount) }}
Usage in PHP:
$translator->trans('apple_count', ['%count%' => $n], null, locale: null, count: $n);
Translation Domains
Each translation can belong to a domain:
messages (default)
validators
forms
security
Example:
# translations/validators.es.yaml
This value should not be blank: "Este valor no debe estar vacío"
Setting and Detecting Locale
Locales can be set in:
.env
URL query
session
user preferences
browser language
Default locale (config/packages/framework.yaml):
framework:
default_locale: en
Changing locale dynamically:
$request->getSession()->set('_locale', 'fr');
Reading current locale:
{{ app.request.locale }}
Locale Switcher (Language Selector)
<a href="{{ path(app.request.attributes.get('_route'), {_locale: 'en'}) }}">English</a>
<a href="{{ path(app.request.attributes.get('_route'), {_locale: 'fr'}) }}">Français</a>
Requires routes to include _locale:
app_home:
path: '/{_locale}/home'
defaults: { _locale: 'en' }
Translating Form Labels
Form labels automatically use translation keys when provided:
$builder->add('name', TextType::class, [
'label' => 'form.name'
]);
Twig form rendering automatically uses translations:
{{ form_label(form.name) }}
Translating Validation Messages
<?php
use Symfony\Component\Validator\Constraints as Assert;
class Product
{
#[Assert\NotBlank(message: 'product.name.not_blank')]
public string $name;
}
?>
Corresponding translation:
# translations/validators.de.yaml
product.name.not_blank: "Name darf nicht leer sein"
Translating Flash Messages
Flash messages can be translated directly in Twig:
{% for msg in app.flashes('success') %}
{{ msg|trans }}
{% endfor %}
Or before adding:
$this->addFlash('success', $translator->trans('profile.saved'));
Symfony Logging
What Is Logging in Symfony?
Logging is the process of recording important events that occur during the execution of an application.
Symfony uses the Monolog library by default to provide powerful and flexible logging features.
Logs are essential for:
debugging issues
monitoring system behavior
auditing user actions
diagnosing production errors
Symfony automatically logs framework-level events, HTTP requests, routing issues, and errors.
Installing Monolog (Usually Installed by Default)
Most Symfony projects already have Monolog pre-installed.
If not installed:
composer require symfony/monolog-bundle
Configuration file:
config/packages/monolog.yaml
Understanding Log Levels
Symfony (via Monolog) supports PSR-3 log levels:
Log Level
Description
DEBUG
Detailed debugging information.
INFO
Key runtime events.
NOTICE
Normal but significant events.
WARNING
Potential issues or irregular states.
ERROR
An error occurred, but the application can still continue running.
CRITICAL
Critical conditions requiring immediate attention.
ALERT
Action must be taken immediately.
EMERGENCY
The system is unusable.
Log visibility depends on environment configuration.
Logging in a Controller or Service
<?php
use Psr\Log\LoggerInterface;
public function index(LoggerInterface $logger)
{
$logger->info('User visited the homepage');
$logger->debug('Debug data: ', ['user' => 'Junzhe']);
$logger->error('Something went wrong!');
}
?>
Each method corresponds to a log level.
Customizing Log Messages
Include context information:
$logger->warning('Low disk space', [
'free_space_mb' => 120,
'path' => '/var/www'
]);
Context is automatically JSON-encoded in logs.
Where Logs Are Stored
var/log/dev.log
var/log/prod.log
The logs differ by environment:
dev — verbose logs for debugging
prod — performance-optimized logs
Configuring Monolog Handlers
Handlers define how logs are processed and stored.
Common handlers:
Handler
Description
stream
Writes logs to a file.
rotating_file
Creates daily or size-based rotating log files.
fingers_crossed
Buffers logs and releases them only when an error occurs.
syslog
Sends logs to the system logger.
slack
Sends log messages to a Slack channel.
email
Sends error logs via email.
Example in monolog.yaml:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/app.log"
level: debug
Daily Log Rotation
monolog:
handlers:
rotating:
type: rotating_file
path: "%kernel.logs_dir%/app.log"
max_files: 30
level: info
Keeps logs manageable over time.
Using the Fingers Crossed Handler
Buffers logs until a serious error occurs.
Configuration:
monolog:
handlers:
filtered:
type: fingers_crossed
action_level: error
handler: stream_handler
stream_handler:
type: stream
path: "%kernel.logs_dir%/error.log"
Ideal for production: keeps logs clean until something goes wrong.
Logging from Twig Templates
Not recommended for production but possible:
{% do logger.info('Rendering homepage template') %}
Requires logger to be passed as a Twig global.
Better to log from controllers or services.
Channel-Based Logging
Channels provide logical separation for logs.
Example channels:
doctrine
security
mailer
http_client
Custom channels:
monolog:
channels: ['checkout', 'billing']
Inject channel-specific loggers:
use Psr\Log\LoggerInterface;
public function pay(LoggerInterface $checkoutLogger)
{
$checkoutLogger->info('Payment started');
}
Logging Exceptions
Symfony automatically logs uncaught exceptions in prod.log.
You can manually log exceptions:
try {
// risky action
} catch (\Throwable $e) {
$logger->error('Payment failed', ['exception' => $e]);
}
Exceptions include stack traces for debugging.
Viewing Logs in the Symfony Profiler
In dev mode, logs are displayed in:
Web Debug Toolbar
Profiler (Logs tab)
Shows:
log levels
message context
originating file & line
timing
Symfony Email Management
What Is Email Management in Symfony?
Symfony provides a powerful Mailer Component to send emails using different transport layers.
Modern email handling in Symfony includes:
building rich email messages
sending via SMTP or third-party APIs
sending transactional emails
sending templated emails (Twig)
attaching files & inline images
failover transports
async email sending (Messenger)
The Mailer component replaces the old SwiftMailer library.
Installing the Symfony Mailer Component
composer require symfony/mailer symfony/twig-bundle
This installs:
Mailer component
Email message builder
Twig integration for email templates
Email configuration file:
config/packages/mailer.yaml
Configuring Email Transport
Transport is defined in .env:
MAILER_DSN=smtp://username:password@smtp.example.com:587
Supported transports:
SMTP (most common)
Gmail
SendGrid
Mailgun
Postmark
Amazon SES
sendmail
Examples:
MAILER_DSN=gmail://username:password@default
MAILER_DSN=sendgrid://API_KEY@default
MAILER_DSN=ses+smtp://AWS_KEY:AWS_SECRET@default
Sending a Basic Email
Inject MailerInterface into a controller:
<?php
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
public function sendEmail(MailerInterface $mailer)
{
$email = (new Email())
->from('admin@example.com')
->to('user@example.com')
->subject('Welcome!')
->text('Hello!')
->html('<p>Welcome to our service!</p>');
$mailer->send($email);
}
?>
Emails can include both plain text and HTML.
Sending Templated Emails (Twig)
<?php
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
$email = (new TemplatedEmail())
->from('support@example.com')
->to('user@example.com')
->subject('Account Activation')
->htmlTemplate('emails/activation.html.twig')
->context([
'username' => 'Junzhe',
'activationLink' => 'https://example.com/activate',
]);
?>
Example Twig template:
<h1>Hello {{ username }}!</h1>
<p>Click the link to activate your account:</p>
<a href="{{ activationLink }}">Activate</a>
Attaching Files
$email = (new Email())
->from('admin@example.com')
->to('user@example.com')
->subject('Invoice')
->attachFromPath('invoices/jan.pdf');
You can attach binary data:
$email->attach($pdfContent, 'invoice.pdf', 'application/pdf');
Using Inline Images
You can embed images inside HTML emails:
$email->embedFromPath('images/logo.png', 'logo');
Twig template:
<img src="cid:logo" alt="Logo" />
Multiple Recipients
$email->to('a@example.com')
->cc('b@example.com')
->bcc('c@example.com')
->replyTo('support@example.com');
Supports:
to()
cc()
bcc()
replyTo()
Transport Failover and Load Balancing
Symfony can automatically retry emails using multiple transports.
Example failover DSN:
MAILER_DSN=failover(smtp://a.com, smtp://b.com)
Load balancing across transports:
MAILER_DSN=roundrobin(smtp://a.com, smtp://b.com)
Sending Emails Asynchronously (Messenger Integration)
Heavy email processing should be done asynchronously.
Enable async transport:
framework:
messenger:
transports:
async: 'doctrine://default'
routing:
'Symfony\Component\Mailer\Messenger\SendEmailMessage': async
Now emails are queued and sent in background:
symfony console messenger:consume async
Testing Emails in Development
Symfony provides a nice email tester:
symfony open:local:webmail
Emails are captured in:
Symfony Web Debug Toolbar
Symfony Profiler (Emails tab)
No real emails are sent in dev mode by default.
Logging and Debugging Email Sending
var/log/dev.log
Use Profiler & Toolbar to inspect:
email content
transport
attachments
status
You can enable mail delivery disable mode for safety:
framework:
mailer:
envelope:
recipients: ['test@example.com']
Symfony Unit Testing
What Is Unit Testing in Symfony?
Unit testing is the technique of testing the smallest pieces of application logic in isolation (functions, methods, or classes).
Symfony uses PHPUnit as its standard testing framework.
Goals of unit testing include:
ensuring correctness of business logic
preventing regressions
making refactoring safer
providing reliable CI pipelines
Unit tests avoid:
HTTP requests
database access
framework bootstrapping
These tests run extremely fast and validate small components in isolation.
Installing PHPUnit
Install using Symfony CLI (recommended):
symfony php bin/phpunit
If PHPUnit is missing:
composer require --dev symfony/phpunit-bridge
Configuration file is automatically added:
phpunit.xml.dist
Test Directory Structure
Symfony recommends the following structure:
tests/
Controller/
Entity/
Service/
Security/
Functional/
All test classes must end with Test.php and extend TestCase.
Writing Your First Unit Test
<?php
use PHPUnit\Framework\TestCase;
class MathTest extends TestCase
{
public function testAdd()
{
$this->assertEquals(4, 2 + 2);
}
}
?>
Run tests:
bin/phpunit
Testing Symfony Services
Unit tests cannot automatically use dependency injection.
You test services by instantiating them manually:
<?php
class PriceCalculatorTest extends TestCase
{
public function testPriceCalculation()
{
$calculator = new PriceCalculator();
$result = $calculator->calculate(100);
$this->assertEquals(120, $result);
}
}
?>
Unit tests should not interact with the Symfony kernel.
Using Mocks
Mocks simulate objects or dependencies so tests remain isolated.
Example using PHPUnit’s mocking tools:
$repo = $this->createMock(UserRepository::class);
$repo->method('find')->willReturn(new User('Junzhe'));
$service = new UserService($repo);
$this->assertEquals('Junzhe', $service->getUserNameById(1));
Mocks ensure:
no database communication
no external API calls
predictable input/output
Testing Exception Handling
$this->expectException(\InvalidArgumentException::class);
$calc->calculate(-1);
Useful for verifying business logic validations.
Data Providers
Use data providers to test multiple inputs:
/**
* @dataProvider provideNumbers
*/
public function testMultiply($a, $b, $expected)
{
$this->assertEquals($expected, $a * $b);
}
public function provideNumbers()
{
return [
[2, 3, 6],
[4, 5, 20],
[10, 10, 100],
];
}
Makes tests more compact and systematic.
Testing Private / Protected Methods
Best practice: never directly test private methods .
Test through the public API instead.
If absolutely needed, use PHP’s Reflection API (not recommended).
Testing Entities and Value Objects
Entity tests usually focus on:
getter/setter behavior
validation logic
relationships
Example:
$product = new Product();
$product->setName('Laptop');
$this->assertEquals('Laptop', $product->getName());
Functional Testing vs Unit Testing
Unit tests:
smallest pieces of logic
no kernel boot
very fast
no HTTP requests
Functional tests:
boot Symfony kernel
simulate HTTP requests
test routing, controllers, templates
This chapter focuses on unit tests .
Running Unit Tests with Coverage
To see which lines of code are tested:
bin/phpunit --coverage-html coverage/
Open coverage/index.html in a browser.
Organizing Tests
Follow the same structure as your source code:
src/Service/PriceCalculator.php
tests/Service/PriceCalculatorTest.php
Each file in src/ should have a corresponding test in tests/.
Keep unit tests small and specific.
Symfony Advanced Concepts
Overview of Advanced Symfony Concepts
Symfony provides a powerful and highly extensible architecture built from independent components.
Advanced concepts include:
Service decoration
Compiler passes
Custom configuration & extensions
Event subscribers
Security voters
Serializer customization
Messenger message buses
HTTP caching (reverse proxy)
Environment variable processing
Custom annotations/attributes
These advanced topics allow developers to:
customize framework behavior
extend bundle features
optimize performance
build reusable modules
Service Decoration
Service decoration allows you to wrap or override core services without modifying their original class.
Example: decorating a mailer service to log outgoing messages.
services:
app.mailer_decorator:
decorates: mailer
arguments:
- '@app.mailer_decorator.inner'
Use cases:
logging
profiling
pre/post processing
feature toggling
Compiler Passes
A compiler pass executes during container compilation.
Used for:
modifying service definitions
registering tagged services
building service locators
adding arguments dynamically
Example structure:
<?php
class RegisterHandlersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->findDefinition('app.handler_registry');
$tagged = $container->findTaggedServiceIds('app.handler');
foreach ($tagged as $id => $tags) {
$definition->addMethodCall('addHandler', [new Reference($id)]);
}
}
}
?>
Compiler passes are added inside bundle classes.
Custom Configuration & Bundle Extensions
Symfony bundles can provide custom configuration in config/packages.
Example configuration:
my_bundle:
option: true
cache_dir: '%kernel.cache_dir%/mybundle'
Bundle extension loads and validates config:
<?php
class MyBundleExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$config = $this->processConfiguration(new Configuration(), $configs);
$container->setParameter('my.option', $config['option']);
}
}
?>
Used for building reusable, configurable modules.
Advanced Event Subscribers
A subscriber listens to multiple events and centralizes event logic.
Example:
<?php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SecuritySubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => 'checkRequest',
KernelEvents::RESPONSE => 'addHeaders',
];
}
}
?>
Used for adding:
request filtering
response modification
logging
security checks
Security Voters
Voters determine whether a user is allowed to perform certain actions.
Example voter:
<?php
class DocumentVoter extends Voter
{
protected function supports($attribute, $subject)
{
return $attribute === 'EDIT' && $subject instanceof Document;
}
protected function voteOnAttribute($attribute, $document, TokenInterface $token)
{
return $document->getOwner() === $token->getUser();
}
}
?>
Voters are essential for fine-grained access control.
Serializer Component (Advanced Usage)
Serializer converts objects ↔ JSON/XML automatically.
Advanced features:
Groups for conditional serialization
Context processors
Custom normalizers & denormalizers
Example using groups:
#[Groups(['public'])]
public string $name;
#[Groups(['admin'])]
public float $salary;
Serializing with a group:
$serializer->serialize($obj, 'json', ['groups' => ['public']]);
Messenger Component (Advanced Message Bus)
Messenger provides:
async processing
background jobs
queue workers
command & event buses
Example message:
<?php
class SendNewsletter
{
public string $email;
}
?>
Handler:
class SendNewsletterHandler
{
public function __invoke(SendNewsletter $msg)
{
// send email
}
}
Dispatching asynchronously:
$bus->dispatch(new SendNewsletter('user@example.com'));
HTTP Caching & Reverse Proxy
Symfony can integrate with reverse proxies such as Varnish or Symfony HttpCache.
Set cache headers:
$response->setPublic();
$response->setMaxAge(3600);
$response->setSharedMaxAge(3600);
Supports:
ETags
Cache invalidation
Cache tagging
Custom Annotations / Attributes
You can create custom PHP attributes to add metadata:
#[RateLimit(10)]
public function index() {}
Processed via:
event subscribers
kernel request listeners
custom bundles
Environment Variable Processing
Symfony supports environment variable processors:
base64:
file:
json:
resolve:
Example:
DATABASE_URL="mysql://user:pass@localhost/db"
Used in services.yaml:
parameters:
secret_key: '%env(base64:APP_SECRET)%'