PHP Framework

Symfony

Symfony is both a set of reusable PHP components and a full framework built from them. It trades convenience for structure, stability and long-term maintainability at scale.

advanced15 min readUpdated Sep 16, 2026
src/Controller/BlogController.php
php
<?php
// src/Controller/BlogController.php

namespace App\Controller;

use App\Repository\PostRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class BlogController extends AbstractController
{
    #[Route('/blog/{slug}', name: 'blog_show')]
    public function show(string $slug, PostRepository $posts): Response
    {
        $post = $posts->findOneBy(['slug' => $slug]);

        if (!$post) {
            throw $this->createNotFoundException('Post not found');
        }

        return $this->render('blog/show.html.twig', [
            'post' => $post,
        ]);
    }
}
Released
2005
Creator
Fabien Potencier
Language
PHP
Architecture
Components plus full framework
ORM
Doctrine
CLI
bin/console
Config
YAML, XML or PHP

Why it matters

Why enterprises pick Symfony

Components you can use anywhere

HttpFoundation, Routing, Console, Mailer and Serializer are independent libraries. You can adopt one without the framework, and much of the PHP ecosystem already does.

Stability and long-term support

Symfony offers long-term support releases with a documented deprecation path, so upgrades are predictable instead of rewrites.

Flex and recipes

Symfony Flex configures bundles and generates the config files for you, so adding a feature is a single composer require.

The big picture

Components, bundles, container

Decoupled libraries you can use anywhere, bundles that package features, and a container that wires everything together.

Components

Reuse

Standalone libraries for HTTP, routing, console, mail and serialization that work in any PHP project.

Bundles

Package

Bundles group routes, services, configuration and templates into a distributable unit, wired up by Flex recipes.

Container

Wire

A compiled service container builds your object graph and autowires constructor dependencies automatically.

At a glance

The Symfony toolbox

Components

Standalone libraries you can use in any PHP project, with or without the framework.

Bundles

Packaged features that bring routes, services, configuration and templates together.

Flex recipes

Composer plugins that configure bundles and create the config files automatically.

Attribute routing

Declare routes next to the controller action with PHP 8 attributes.

Doctrine

A data-mapper ORM with entities, repositories and migrations.

Console

bin/console generates code, runs migrations, clears cache and consumes queues.

A short history

A framework and the components beneath it

  1. 2005

    Symfony 1

    Fabien Potencier releases a framework for his own projects, inspired by Rails and Mojavi.

    05
  2. 2011

    Symfony 2

    A full rewrite built from decoupled components that other projects can reuse.

    11
  3. 2015

    Symfony 3

    Deprecations and a cleaner API prepare the ecosystem for future majors.

    15
  4. 2019

    Symfony 4 and Flex

    Flex recipes automate bundle setup and make applications dramatically slimmer.

    19
  5. 2023

    Symfony 7

    PHP 8 attributes, typed properties and modern defaults across the framework.

    23
  6. Today

    The engine room of PHP

    Symfony components power Laravel, Drupal, phpBB and thousands of libraries.

    Today

The complete guide

Symfony: Everything you need to know

What is Symfony?

Symfony is two things at once. It is a set of reusable PHP components — HttpFoundation, Routing, Console, Mailer, Serializer and dozens more — and it is a full web framework assembled from those components. That dual nature explains almost everything about it: the framework is stable because its parts are stable, and its parts are useful on their own.

Created by Fabien Potencier in 2005 and maintained by SensioLabs, Symfony has become the engine room of modern PHP. Laravel is built on Symfony components, Drupal and phpBB depend on them, and thousands of libraries use them without ever touching the full framework.

Symfony asks more of you than a batteries-included framework. In return it gives you a system you can reason about, extend and upgrade for years.

Components: the real product

The components are the heart of the project. Each one solves a single problem and has no dependency on the framework.

  • HttpFoundation — object-oriented Request and Response classes.
  • Routing — map URLs to controllers with attributes, YAML or PHP.
  • Console — build command-line tools with arguments and options.
  • Mailer — send email through transports and templated messages.
  • Serializer — convert objects to JSON, XML and back.
  • EventDispatcher — decouple code with events and listeners.

You can install any of them directly:

composer require symfony/http-foundation
use Symfony\Component\HttpFoundation\Request;

$request = Request::createFromGlobals();
$page = $request->query->getInt('page', 1);

That portability is why Symfony components show up everywhere, even in projects that would never adopt the full framework.

The HTTP kernel and controllers

In a full application, every request flows through the HttpKernel. It dispatches an event, matches a route, calls a controller and turns the returned Response into output. Controllers extend AbstractController to get convenient helpers.

class BlogController extends AbstractController
{
    #[Route('/blog/{slug}', name: 'blog_show')]
    public function show(string $slug, PostRepository $posts): Response
    {
        $post = $posts->findOneBy(['slug' => $slug]);

        if (!$post) {
            throw $this->createNotFoundException('Post not found');
        }

        return $this->render('blog/show.html.twig', ['post' => $post]);
    }
}

A controller must return a Response. $this->render() builds one from a Twig template, $this->json() returns JSON, and $this->redirectToRoute() returns a redirect. Because the response object is explicit, testing and middleware are straightforward.

Twig templates extend a base layout and fill in blocks, and output is escaped unless you opt out:

{# templates/blog/show.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <article>
        <h1>{{ post.title }}</h1>
        <p>{{ post.publishedAt|date('F j, Y') }}</p>
        {{ post.body|raw }}
    </article>
{% endblock %}

Attribute routing

Routes are declared next to the action that handles them using PHP 8 attributes.

#[Route('/blog', name: 'blog_index', methods: ['GET'])]
#[Route('/blog/{slug}', name: 'blog_show', methods: ['GET'])]

Routing can also live in config/routes.yaml when you prefer to keep it separate. Attributes are the default in modern Symfony because the route and the method stay together. Inspect what is registered at any time with:

php bin/console debug:router

The service container and autowiring

Almost everything in Symfony is a service, and the container builds and connects them. With autowiring enabled, you only need to type-hint a constructor.

class PostPublisher
{
    public function __construct(
        private EntityManagerInterface $em,
        private MailerInterface $mailer,
    ) {}
}

config/services.yaml tells the container to autowire your classes:

services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\:
        resource: '../src/'
        exclude:
            - '../src/Entity/'
            - '../src/Kernel.php'

When two implementations of an interface exist, bind the right one explicitly. The container is compiled into plain PHP for production, which is why it is fast despite being so dynamic to write.

Bundles and Flex recipes

A bundle packages features into a distributable unit. Third-party functionality — Doctrine, security, mailer, admin panels — arrives as a bundle, and the framework itself is composed of core bundles.

Installing one is a single command:

composer require symfony/orm-pack

Symfony Flex is the Composer plugin that makes this pleasant. When a package has a recipe, Flex creates the config files, registers the bundle, and often adds environment variables or a docker-compose service. That is why modern Symfony apps have small, explicit configuration instead of pages of boilerplate.

Doctrine ORM

Doctrine is the default persistence layer. Entities are plain PHP classes annotated with mapping attributes, and repositories encapsulate queries.

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

    #[ORM\Column(length: 255)]
    private string $title;

    #[ORM\Column(type: 'text')]
    private string $body;

    #[ORM\Column(type: 'datetime_immutable', nullable: true)]
    private ?\DateTimeImmutable $publishedAt = null;
}

Queries are usually expressed with the repository API or DQL rather than SQL strings:

$latest = $posts->findBy([], ['publishedAt' => 'DESC'], limit: 10);

Schema changes are managed with Doctrine Migrations, which compare your mapping to the database and generate versioned migration files you can review before running.

Configuration and environments

Symfony separates configuration from code and applies it per environment. APP_ENV selects the environment, and files under config/ layer on top of each other.

  • .env holds local defaults and environment variables.
  • config/packages/ configures bundles for every environment.
  • config/packages/dev/ and config/packages/prod/ override per environment.
  • config/routes.yaml and config/services.yaml are the app’s own settings.

The dev environment enables the profiler and debug tools, test uses a separate database, and prod compiles everything for speed. Clearing cache after config changes is a normal part of the workflow:

php bin/console cache:clear

Messenger and async work

The Messenger component moves work out of the request. You dispatch a message to a bus and a handler processes it, either immediately or on a worker.

final class UserRegistered
{
    public function __construct(public readonly int $userId) {}
}

#[AsMessageHandler]
final class SendWelcomeEmail
{
    public function __invoke(UserRegistered $message): void
    {
        // send the email here
    }
}

Dispatch it from anywhere, and consume the queue in a separate process:

$bus->dispatch(new UserRegistered($user->getId()));
php bin/console messenger:consume async

Transports can be Doctrine, Redis, AMQP or a third-party service, and failed messages can be retried or stored, which makes background processing a first-class concern.

Testing with PHPUnit

Symfony ships with PHPUnit and a WebTestCase base class that boots the kernel and makes real HTTP requests.

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

        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Hello world');
    }
}

Because services are injected, unit tests can replace a dependency with a stub or a mock without touching the container. Keep integration tests for the HTTP layer and unit tests for the services that hold business rules.

Best practices

  • Prefer constructor injection over fetching services from the container.
  • Keep controllers thin; move logic into services.
  • Use repositories for queries and keep SQL out of controllers.
  • Declare routes as attributes next to the action they call.
  • Let Flex manage bundle configuration instead of writing boilerplate.
  • Separate configuration by environment and keep secrets in environment variables.
  • Put slow work on Messenger instead of in the request.
  • Read the profiler when something is slow or surprising.

Common mistakes

  • Calling $container->get() instead of injecting dependencies.
  • Putting business logic in controllers and entities.
  • Forgetting to register or bind an interface, then wondering what the container resolved.
  • Committing .env secrets instead of keeping them out of version control.
  • Editing generated migrations after they have run.
  • Mixing configuration styles randomly across YAML, XML and PHP.
  • Ignoring deprecation notices until the next major upgrade.

Where to go next

Symfony gives you the components and the discipline to build systems that last, and its ideas are visible in frameworks across the ecosystem. Read the Laravel guide to see those components used for rapid application development, or compare the philosophy with the minimal Express approach. To design and evolve the APIs you expose, continue with REST and API versioning.

Getting a service

Let the container inject what you need. Pulling services out of the container hides dependencies and makes the class harder to test.

Prefer
class NewsletterService
{
    public function __construct(
        private MailerInterface $mailer,
    ) {}
}
Avoid
class NewsletterService
{
    public function send(): void
    {
        $mailer = $this->container->get('mailer');
        $mailer->send(/* ... */);
    }
}

Finding an entity

Repositories express intent and return objects. Raw SQL in a controller couples the HTTP layer to the schema and bypasses Doctrine's mapping.

Prefer
$post = $posts->findOneBy(['slug' => $slug]);
if (!$post) {
    throw $this->createNotFoundException();
}
Avoid
$conn = $this->getDoctrine()->getConnection();
$post = $conn->fetchAssociative(
    'SELECT * FROM posts WHERE slug = ?',
    [$slug],
);

Trade-offs

Is Symfony the right PHP framework?

Symfony trades instant gratification for structure, stability and reuse. That trade pays off most on long-lived, complex systems.

Strengths

  • Components you can reuse

    Because the framework is assembled from libraries, you can take the parts you need into any project, and you already know them when you work on Drupal or Laravel.

  • Stability and predictable upgrades

    LTS releases, documented deprecations and a strict backwards-compatibility policy mean upgrades are planned work rather than a rewrite.

  • Built for complex domains

    The container, event dispatcher, Messenger, workflow and form components handle the kind of application that outgrows a simpler stack.

Trade-offs

  • More concepts up front

    Kernel, container, bundles, environments and recipes are a lot to learn before you write a feature. Budget time for the mental model.

  • Configuration heavy

    Even with Flex, Symfony asks you to configure services, routing and packages explicitly, which is powerful but verbose.

  • Slower to prototype

    For a small CRUD app, a batteries-included framework gets you to a working screen faster. Symfony's structure starts to win as the domain grows.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Symfony?

Our interactive tutorial walks you through Symfony step by step — with quizzes and real code you can run in the browser.