Home PHP Symfony Framework Introduction: Build Robust PHP Apps
Intermediate 3 min · July 13, 2026

Symfony Framework Introduction: Build Robust PHP Apps

Learn Symfony framework from scratch: routing, controllers, Doctrine ORM, Twig templates, forms, security, and testing.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of PHP (variables, functions, OOP)
  • Familiarity with MVC pattern
  • Composer installed
  • PHP 8.1 or higher
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Symfony is a PHP framework for building web applications and APIs.
  • It follows the MVC pattern and uses reusable components.
  • Key features: routing, Doctrine ORM, Twig templating, form handling, security.
  • Symfony promotes best practices like dependency injection and testing.
  • Ideal for large-scale, maintainable projects.
✦ Definition~90s read
What is Introduction to Symfony Framework?

Symfony is a PHP framework for building web applications and APIs, following the MVC pattern and providing reusable components.

Think of Symfony as a pre-built toolkit for building a house.
Plain-English First

Think of Symfony as a pre-built toolkit for building a house. Instead of making each brick and nail from scratch, Symfony gives you ready-made walls, doors, and windows (components) that you can assemble quickly and safely. It also provides a blueprint (MVC pattern) to keep everything organized.

Building a web application from scratch can be overwhelming. You need to handle routing, database interactions, form validation, security, and more. Symfony, one of the most popular PHP frameworks, provides a structured and reusable set of components that streamline development. It follows the Model-View-Controller (MVC) pattern, separating business logic from presentation. Symfony is used by thousands of projects, from small websites to enterprise applications like Drupal and phpBB. In this tutorial, you'll learn the core concepts of Symfony: installation, routing, controllers, Doctrine ORM, Twig templates, forms, security, and testing. By the end, you'll be able to build a fully functional web application with best practices. Let's dive in!

Installing Symfony and Setting Up a Project

Symfony provides a flexible installer. First, ensure you have PHP 8.1+ and Composer installed. Then create a new project using the Symfony CLI or Composer. The Symfony CLI offers a built-in web server and tools. Alternatively, use Composer: composer create-project symfony/skeleton my_project. This creates a minimal skeleton. For a full-featured web application, use symfony/website-skeleton. After installation, start the server with symfony server:start or use PHP's built-in server. Verify by visiting http://localhost:8000. You'll see the Symfony welcome page. The project structure includes config/, src/, templates/, and var/ directories. The config/ folder holds YAML files for routing, services, and Doctrine. The src/ folder contains your PHP code (controllers, entities, etc.). The templates/ folder stores Twig templates. The var/ folder is for cache and logs. Symfony uses environment variables for configuration; check the .env file for database settings.

install.shPHP
1
2
3
composer create-project symfony/website-skeleton my_project
cd my_project
symfony server:start
Output
[OK] Your application is now ready. You can access it at http://localhost:8000
💡Use Symfony CLI for Development
📊 Production Insight
In production, use environment variables for sensitive data like database passwords. Never commit .env files to version control.
🎯 Key Takeaway
Symfony projects are created via Composer. The skeleton provides a minimal setup, while the website-skeleton includes common bundles for web development.

Understanding Routing and Controllers

Routing maps URLs to controller methods. In Symfony, routes can be defined via annotations, YAML, XML, or PHP attributes. The most common is using annotations (or PHP 8 attributes) directly in the controller. A controller is a PHP class that extends AbstractController. Each public method can be a route. For example, a route for the homepage: #[Route('/', name: 'home')]. The method returns a Response object. Symfony's AbstractController provides shortcuts like render() for Twig templates and json() for APIs. You can also pass parameters in the URL, like #[Route('/blog/{slug}', name: 'blog_show')]. The {slug} is automatically injected into the method. Controllers can access request data via the Request object. Symfony's routing is flexible and supports requirements, defaults, and HTTP methods. Use methods: ['GET'] to restrict. You can also generate URLs from route names using $this->generateUrl(). This decouples views from hardcoded paths.

src/Controller/BlogController.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    #[Route('/blog/{slug}', name: 'blog_show')]
    public function show(string $slug): Response
    {
        // Fetch blog post by slug
        return $this->render('blog/show.html.twig', [
            'slug' => $slug,
        ]);
    }
}
🔥Route Naming Conventions
📊 Production Insight
Avoid using app.php or app_dev.php in URLs. Symfony uses a front controller (index.php) and routing handles everything.
🎯 Key Takeaway
Routes map URLs to controller methods. Use attributes for concise definition. Controllers return Response objects, often rendered via Twig.

Working with Doctrine ORM and Database

Doctrine is Symfony's default ORM. It maps PHP objects to database tables. First, configure the database connection in .env (e.g., DATABASE_URL="mysql://user:pass@127.0.0.1:3306/db_name"). Then create an entity: a PHP class that represents a table. Use annotations or attributes to define mappings. For example, a Product entity with id, name, and price. Run php bin/console make:entity to generate the class. Then create the database schema: php bin/console doctrine:database:create and php bin/console doctrine:schema:update --force. To interact with the database, use the EntityManager. In a controller, inject EntityManagerInterface and use $entityManager->persist($product) and $entityManager->flush(). Doctrine supports relationships: OneToMany, ManyToOne, ManyToMany. For example, a Category can have many Products. Use the #[OneToMany] attribute. Doctrine also provides a query builder and DQL for complex queries. Repositories are classes that encapsulate query logic. Generate them with make:entity or manually. For production, use migrations instead of schema:update to track changes.

src/Entity/Product.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
namespace App\Entity;

use App\Repository\ProductRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $name = null;

    #[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
    private ?float $price = null;

    // Getters and setters...
}
⚠ Avoid schema:update in Production
📊 Production Insight
Always validate the schema before deploying: php bin/console doctrine:schema:validate. This catches mapping errors early.
🎯 Key Takeaway
Doctrine ORM maps PHP objects to database tables. Use EntityManager to persist and flush. Use migrations for schema changes.

Templating with Twig

Twig is Symfony's templating engine. It's fast, secure, and extensible. Templates are stored in templates/. Use Twig syntax: {{ variable }} for output, {% if %} for logic, and {# comment #} for comments. Twig auto-escapes output to prevent XSS. You can extend layouts using {% extends 'base.html.twig' %} and define blocks with {% block content %}. Twig includes functions like path() to generate URLs from route names, and asset() for static files. Filters modify output: {{ name|upper }}, {{ date|date('Y-m-d') }}. You can also create custom filters and functions. Twig supports macros for reusable HTML snippets. In controllers, use $this->render('template.html.twig', ['key' => 'value']) to pass variables. Twig also handles forms rendering with {{ form(form) }} or individual fields. For performance, enable Twig cache in production.

templates/blog/show.html.twigPHP
1
2
3
4
5
6
7
8
9
10
{% extends 'base.html.twig' %}

{% block title %}Blog Post: {{ slug }}{% endblock %}

{% block body %}
    <h1>Blog Post: {{ slug }}</h1>
    <p>Published on {{ post.publishedAt|date('F j, Y') }}</p>
    <div>{{ post.content|raw }}</div>
    <a href="{{ path('blog_index') }}">Back to list</a>
{% endblock %}
💡Use `|raw` Carefully
📊 Production Insight
Enable Twig's strict_variables option in production to catch undefined variables early. Set it in config/packages/twig.yaml.
🎯 Key Takeaway
Twig provides a secure and flexible templating system. Use blocks for layout inheritance, filters for formatting, and functions for dynamic content.

Handling Forms and Validation

Symfony's Form component simplifies form creation and validation. Create a form class that extends AbstractType. Define fields using $builder->add('field', TextType::class). Use options like required, label, and constraints. For validation, add constraints via annotations or attributes in the entity. For example, #[Assert\NotBlank] on a property. In the controller, create the form with $form = $this->createForm(ProductType::class, $product). Handle the request: $form->handleRequest($request). If the form is submitted and valid, persist the entity. Symfony automatically validates constraints. You can also create custom constraints. Forms can be rendered in Twig with {{ form_start(form) }}, {{ form_widget(form) }}, and {{ form_end(form) }}. For AJAX submissions, return JSON responses. Symfony also provides CSRF protection by default. Ensure your forms include the CSRF token field.

src/Form/ProductType.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
namespace App\Form;

use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, [
                'constraints' => [new NotBlank()],
            ])
            ->add('price', MoneyType::class, [
                'currency' => 'USD',
            ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Product::class,
        ]);
    }
}
🔥Form Theming
📊 Production Insight
Always validate on the server side even if client-side validation exists. Symfony's validation ensures data integrity.
🎯 Key Takeaway
Forms are defined as classes with fields and validation constraints. Handle submission in the controller and render with Twig.

Security: Authentication and Authorization

Symfony Security provides authentication (who you are) and authorization (what you can do). Configure security in config/packages/security.yaml. Define firewalls for different parts of your app. For example, a main firewall for the site. Use form login, HTTP basic, or API tokens. For user management, create a User entity implementing UserInterface. Use make:user to generate it. Store passwords hashed with UserPasswordEncoderInterface. In controllers, use $this->denyAccessUnlessGranted('ROLE_ADMIN') to check roles. Twig has is_granted() function. Symfony also supports voters for custom authorization logic. For API authentication, use JWT or API tokens. Symfony's security component is highly configurable. Always use HTTPS in production. Remember to logout: $this->redirectToRoute('app_logout').

config/packages/security.yamlPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
security:
    encoders:
        App\Entity\User:
            algorithm: auto

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
            logout:
                path: app_logout
                target: home

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
⚠ Password Hashing
📊 Production Insight
Use security:false for the dev firewall to avoid login prompts during development. But ensure it's not exposed in production.
🎯 Key Takeaway
Symfony Security handles authentication and authorization. Configure firewalls, providers, and access controls in YAML.

Testing with PHPUnit

Symfony integrates with PHPUnit for testing. Tests are stored in tests/. Use make:test to generate test classes. For unit tests, test individual classes. For functional tests, simulate HTTP requests using Symfony's WebTestCase. Create a client and make requests: $client->request('GET', '/'). Assert status codes, content, and selectors. Use assertSelectorTextContains() to check HTML. For database testing, use a separate test database and load fixtures. Symfony's Panther extends PHPUnit for browser testing with JavaScript. Always run tests before deployment: php bin/phpunit. Use data providers for multiple scenarios. Mock external services with MockObject. Testing ensures code quality and prevents regressions.

tests/Controller/BlogControllerTest.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class BlogControllerTest extends WebTestCase
{
    public function testShowPost(): void
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/blog/hello-world');

        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Blog Post: hello-world');
    }
}
💡Use Test Fixtures
📊 Production Insight
Include tests in your CI/CD pipeline. Use environment-specific configuration for test databases to avoid data loss.
🎯 Key Takeaway
Write unit and functional tests with PHPUnit. Use WebTestCase for HTTP testing. Run tests regularly to catch bugs.
● Production incidentPOST-MORTEMseverity: high

The Case of the Missing CSRF Token

Symptom
Users report that after submitting a form, they see a 400 Bad Request error. The form data is not saved.
Assumption
The developer assumed the CSRF token was automatically included in all forms.
Root cause
A custom JavaScript was dynamically adding form fields without including the CSRF token. Symfony's form component requires the token for validation.
Fix
Updated the JavaScript to fetch the CSRF token from a meta tag and append it to the form data before submission.
Key lesson
  • Always include CSRF tokens in forms, especially when using AJAX.
  • Test form submissions with JavaScript disabled to catch token issues.
  • Use Symfony's built-in form theming to ensure tokens are rendered.
  • Monitor error logs for 400 errors to detect CSRF failures early.
  • Educate frontend developers about Symfony's security requirements.
Production debug guideSymptom to Action4 entries
Symptom · 01
Form submission returns 400 Bad Request
Fix
Check if CSRF token is missing or invalid. Verify the token field name and value in the request.
Symptom · 02
Page loads slowly
Fix
Enable Symfony profiler in dev mode to identify bottlenecks. Check database queries and cache usage.
Symptom · 03
404 error for a route
Fix
Run php bin/console debug:router to see registered routes. Verify route name and parameters.
Symptom · 04
Doctrine entity not persisted
Fix
Check if flush() is called after persist(). Verify transaction boundaries and error logs.
★ Quick Debug Cheat SheetCommon Symfony issues and immediate actions.
CSRF token invalid
Immediate action
Regenerate token or disable CSRF protection temporarily for debugging
Commands
php bin/console debug:container --parameters | grep csrf
Check form theme templates
Fix now
Add {{ form_row(form._token) }} in Twig
Route not found+
Immediate action
Clear cache and list routes
Commands
php bin/console cache:clear
php bin/console debug:router
Fix now
Correct route annotation or YAML configuration
Entity not saved+
Immediate action
Check if EntityManager is flushed
Commands
php bin/console doctrine:schema:validate
Check logs for SQL errors
Fix now
Call $entityManager->flush() after persist
FeatureSymfonyLaravel
ArchitectureModular, reusable componentsOpinionated, monolithic
Learning CurveSteeperGentler
TemplatingTwigBlade
ORMDoctrineEloquent
ConfigurationYAML, PHP, annotationsPHP, .env
CommunityEnterprise-focusedLarge, beginner-friendly
PerformanceHigh (caching, compilation)Good (optimized for speed)
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
install.shcomposer create-project symfony/website-skeleton my_projectInstalling Symfony and Setting Up a Project
srcControllerBlogController.phpnamespace App\Controller;Understanding Routing and Controllers
srcEntityProduct.phpnamespace App\Entity;Working with Doctrine ORM and Database
templatesblogshow.html.twig{% extends 'base.html.twig' %}Templating with Twig
srcFormProductType.phpnamespace App\Form;Handling Forms and Validation
configpackagessecurity.yamlsecurity:Security
testsControllerBlogControllerTest.phpnamespace App\Tests\Controller;Testing with PHPUnit

Key takeaways

1
Symfony follows MVC pattern and uses reusable components.
2
Routing maps URLs to controllers using attributes or YAML.
3
Doctrine ORM simplifies database interactions with entities and repositories.
4
Twig provides secure and flexible templating with auto-escaping.
5
Forms and validation are handled via form classes and constraints.
6
Security is managed through firewalls, providers, and access controls.
7
Testing with PHPUnit ensures code quality and prevents regressions.

Common mistakes to avoid

5 patterns
×

Forgetting to call `flush()` after `persist()`

×

Using `schema:update` in production

×

Not escaping output in Twig

×

Hardcoding URLs instead of using `path()`

×

Ignoring environment variables for configuration

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is dependency injection in Symfony?
Q02SENIOR
Explain the Symfony event dispatcher.
Q03SENIOR
How do you secure a Symfony application?
Q04JUNIOR
What is a Symfony bundle?
Q05SENIOR
How do you create a custom Twig extension?
Q01 of 05SENIOR

What is dependency injection in Symfony?

ANSWER
Dependency injection is a design pattern where dependencies are passed to a class rather than created inside it. Symfony uses a service container to manage dependencies. Services are defined in services.yaml and can be autowired.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between Symfony and Laravel?
02
How do I install Symfony without the CLI?
03
Can I use Symfony for REST APIs?
04
How do I handle file uploads in Symfony?
05
What is the Symfony Profiler?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's Advanced PHP. Mark it forged?

3 min read · try the examples if you haven't

Previous
PHP Typed Properties, Readonly Classes, and Property Hooks
20 / 29 · Advanced PHP
Next
Symfony vs Laravel: Choosing Your PHP Framework