In Symfony, a page is a method of a plain PHP class called a controller. No mandatory base class, no code generation, no XML. This is the entire code of a working page.
The #[Route] attribute defines the URL of the page, right next to the code that handles it. When you visit /lucky/number, Symfony runs this method.
The method's job is to return a Response object with the contents of the page. HTML, JSON, a file download: everything is a response.
src/Controller/LuckyController.php
Routes live next to the code they run, defined as PHP attributes. No separate routing files to keep in sync (though YAML config is also available if you prefer it).
Wrap any part of the URL in curly braces to make it dynamic. The matched value is passed to the method as an argument with the same name.
Add requirements to placeholders with regular expressions. Symfony also converts the value to the type of the argument: this $year is already an integer.
Routes have names, so you never hardcode URLs anywhere else: templates and controllers generate them with path('blog_show', {slug: 'symfony-8'}). Change the URL once, and it changes everywhere.
src/Controller/BlogController.php
Controllers usually extend AbstractController, an optional base class that adds shortcuts for the most common operations: rendering templates, returning JSON, redirecting, etc.
Need the incoming request? Type-hint a Request argument and Symfony passes it to you, with a clean object-oriented API for headers, cookies, query parameters and more.
Building an API? The json() helper serializes your data and sets the right response headers.
Or skip the boilerplate entirely: #[MapQueryParameter] maps a query string parameter (/api/search?query=symfony) straight into a typed method argument, with an optional default value.
src/Controller/ApiController.php
The render() helper renders a template and wraps the result in a Response object. The second argument is the list of variables the template can use.
Templates are written in Twig, a template language designed to be concise, safe (output is escaped by default) and friendly for designers. {{ ... }} prints a value, and filters like |title transform it.
{% ... %} runs logic, like looping over the speakers array passed by the controller.
The true Twig superpower: inheritance. Write the shared elements (e.g. the header and footer) once in a base.html.twig template, and let other templates extend it and fill in only the content blocks that change on each page.
src/Controller/ConferenceController.php
templates/conference/show.html.twig
Your new Symfony project is tiny on purpose: no mailer, no ORM, no templates until you decide you need them. When you do, install the feature with Composer.
Here is the magic: Symfony Flex, a Composer plugin, applies the package's recipe automatically. It creates the configuration files, registers the bundle and adds the environment variables the package needs. Zero manual setup.
Secrets and machine-specific values live in environment variables, following the twelve-factor app philosophy. The recipe already added a sensible default to your .env file. And don't worry: this file only stores harmless development defaults. In production, you define real environment variables or use Symfony's encrypted secrets vault.
This works for hundreds of packages: try composer require orm, composer require security or composer require debug. Un-installing is just as clean: composer remove mailer reverts all the changes made by the recipe.
Your app's logic lives in services: plain PHP classes like this one. There's nothing to register or configure: create the class and it's ready to use anywhere.
To use a service, type-hint it in the constructor. This is autowiring: Symfony reads the type and injects the right object. This design (dependency injection) keeps your classes decoupled and easy to test.
It works the same for the hundreds of services provided by Symfony and its ecosystem: here, a PSR-3 logger, ready to use by adding one constructor argument.
src/Service/GreetingGenerator.php
src/Controller/GreetingController.php
Symfony's debugging experience is one of its superpowers. Install it with one command.
Every page now shows a debug toolbar at the bottom: request time, memory, database queries, logs, deprecations... Click any value to open the full profiler, which keeps a detailed trace of every request, even redirects and API calls.
And forget var_dump() + die(): the dump() function sends any variable, beautifully formatted, to the toolbar without breaking the page.
src/Controller/CheckoutController.php
Symfony integrates Doctrine, the most powerful PHP ORM. Each database table maps to an entity: a plain PHP class where attributes describe the columns.
You don't even write these classes by hand: the interactive make:entity command generates and updates them, and the migration commands create the SQL to evolve your database schema safely.
To query the database, autowire the entity's repository. The built-in methods cover the common cases, and you add your own methods for complex queries.
src/Controller/ArticleController.php
Every Symfony app includes a console. Writing your own commands takes one class: the #[AsCommand] attribute registers it and gives it a name.
Arguments and options are declared as method parameters with attributes, and SymfonyStyle gives you beautiful output, tables, progress bars and interactive questions for free.
The console also comes packed with built-in commands: inspect routes and services, clear caches, run migrations... and the make:* commands, which generate controllers, entities, forms, tests and more, so you never start from a blank file.
src/Command/SendNewsletterCommand.php
That's it! You've seen the essentials: pages, routes, controllers, templates, services, the database, validation and the console. This is how Symfony feels every day: explicit, typed code with the framework doing the repetitive work for you.
Your next steps: the Advanced Tour below shows some of the most impressive Symfony features in small doses. When you want depth, Symfony: The Fast Track (the free official book) builds a complete project step by step, and the documentation covers everything else.