Introduction
Routing and controllers are two of the most important concepts in Symfony.
When a user visits a URL in your application, Symfony needs to decide which PHP code should run. This is the job of the routing system.
Once Symfony finds the correct route, it executes a controller method. The controller reads the request, runs the required logic, and returns a response.
In simple terms:
URL
↓
Route
↓
Controller
↓
ResponseIf you understand routing and controllers, you understand the entry point of most Symfony features.
This article explains Symfony routing and controllers in a beginner-friendly way for PHP developers.
What Is a Route in Symfony?
A route is a rule that tells Symfony which controller method should run for a specific URL.
For example:
#[Route('/hello', name: 'hello')]
public function hello(): Response
{
return new Response('Hello Symfony');
}This route tells Symfony:
When the user visits /hello,
execute the hello() method.The route has two important parts:
'/hello'This is the URL path.
name: 'hello'This is the internal route name.
The route name is useful when generating URLs or redirecting users.
What Is a Controller?
A controller is a PHP class that contains methods responsible for handling requests.
A simple Symfony controller looks like this:
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class HelloController
{
#[Route('/hello', name: 'hello')]
public function index(): Response
{
return new Response('Hello from Symfony');
}
}The controller method reads the incoming request, performs some logic, and returns a response.
A response can be:
HTML
JSON
Redirect
File download
404 error
XML
Plain textThe most important rule is simple:
A controller must return a Symfony Response object or something Symfony can convert into a response.
Symfony Request and Response
Symfony uses the HttpFoundation component to represent HTTP in an object-oriented way.
Instead of working directly with PHP globals like:
$_GET
$_POST
$_FILES
$_COOKIESymfony gives you objects like:
Request
Response
JsonResponse
RedirectResponseA basic response looks like this:
return new Response('Hello Symfony');A JSON response looks like this:
return new JsonResponse([
'status' => 'ok',
]);This makes HTTP handling cleaner and more predictable.
Defining Routes with Attributes
Modern Symfony applications commonly define routes using PHP attributes.
Example:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ArticleController
{
#[Route('/articles', name: 'article_index')]
public function index(): Response
{
return new Response('Article list');
}
}The route is placed directly above the controller method.
This keeps the URL definition close to the code that handles it.
Route Names
Every important route should have a name.
Example:
#[Route('/articles', name: 'article_index')]The route name allows you to generate URLs and redirects without hardcoding paths.
For example, instead of writing:
return new RedirectResponse('/articles');You can write:
return $this->redirectToRoute('article_index');This is better because the URL can change later while the route name stays the same.
For example, you can change the path:
#[Route('/blog', name: 'article_index')]Your redirect code will still work:
return $this->redirectToRoute('article_index');This makes your application easier to maintain.
Route Parameters
Many URLs contain dynamic values.
For example:
/articles/10
/articles/25
/articles/100In Symfony, you can define a dynamic route parameter like this:
#[Route('/articles/{id}', name: 'article_show')]
public function show(int $id): Response
{
return new Response('Article ID: ' . $id);
}The {id} part is read from the URL and passed to the controller method.
If the user visits:
/articles/15Symfony passes 15 to the $id argument.
Route Requirements
Sometimes a route parameter should follow a specific pattern.
For example, an article ID should usually be a number.
You can enforce that with route requirements:
#[Route('/articles/{id}', name: 'article_show', requirements: ['id' => '\d+'])]
public function show(int $id): Response
{
return new Response('Article ID: ' . $id);
}This route will match:
/articles/10But it will not match:
/articles/helloRoute requirements help prevent conflicts and invalid URLs.
HTTP Methods
A route can be limited to specific HTTP methods.
For example, a page that displays articles should use GET:
#[Route('/articles', name: 'article_index', methods: ['GET'])]
public function index(): Response
{
return new Response('Article list');
}A route that stores data should usually use POST:
#[Route('/articles', name: 'article_store', methods: ['POST'])]
public function store(): Response
{
return new Response('Article stored');
}This means two routes can have the same URL path but different HTTP methods:
#[Route('/articles', name: 'article_index', methods: ['GET'])]
public function index(): Response
{
return new Response('Article list');
}
#[Route('/articles', name: 'article_store', methods: ['POST'])]
public function store(): Response
{
return new Response('Article stored');
}This is very common in web applications and APIs.
Class-Level Route Prefixes
If multiple routes share the same prefix, you can put the prefix on the controller class.
Example:
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/articles')]
class ArticleController
{
#[Route('', name: 'article_index', methods: ['GET'])]
public function index(): Response
{
return new Response('Article list');
}
#[Route('/{id}', name: 'article_show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function show(int $id): Response
{
return new Response('Article ID: ' . $id);
}
}Symfony combines the class route prefix with the method routes.
The final URLs are:
/articles
/articles/{id}This keeps controllers cleaner when many routes belong to the same feature.
Reading Request Data
Symfony can inject the Request object into your controller method.
Example:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/search', name: 'search', methods: ['GET'])]
public function search(Request $request): Response
{
$query = $request->query->get('q');
return new Response('Search query: ' . $query);
}If the user visits:
/search?q=symfonyThe value of $query will be:
symfonyDifferent request data is stored in different places:
$request->query->get('q'); // Query string
$request->request->get('name'); // POST form data
$request->headers->get('User-Agent'); // Headers
$request->attributes->get('id'); // Route attributesIt is a good practice to be explicit about where the data comes from.
Returning JSON Responses
Symfony is often used to build APIs.
A simple JSON endpoint can look like this:
namespace App\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
class ApiHealthController
{
#[Route('/api/health', name: 'api_health', methods: ['GET'])]
public function __invoke(): JsonResponse
{
return new JsonResponse([
'status' => 'ok',
'framework' => 'Symfony',
]);
}
}If your controller extends AbstractController, you can also use the json() helper:
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
class ApiHealthController extends AbstractController
{
#[Route('/api/health', name: 'api_health', methods: ['GET'])]
public function __invoke(): JsonResponse
{
return $this->json([
'status' => 'ok',
'framework' => 'Symfony',
]);
}
}This is useful for APIs, AJAX endpoints, and health checks.
The AbstractController Class
Many Symfony controllers extend:
Symfony\Bundle\FrameworkBundle\Controller\AbstractControllerThis class provides helpful methods like:
$this->render()
$this->json()
$this->redirectToRoute()
$this->generateUrl()
$this->createNotFoundException()Example:
return $this->redirectToRoute('article_index');Or:
return $this->render('article/index.html.twig', [
'articles' => $articles,
]);Using AbstractController is common and beginner-friendly.
However, more advanced Symfony developers sometimes avoid it in order to keep controllers more explicit and framework-independent.
For learning Symfony, using AbstractController is completely fine.
Rendering Twig Templates
For traditional web pages, controllers often render Twig templates.
Example controller:
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ArticleController extends AbstractController
{
#[Route('/articles', name: 'article_index', methods: ['GET'])]
public function index(): Response
{
$articles = [
['title' => 'Learning Symfony Routing'],
['title' => 'Understanding Controllers'],
];
return $this->render('article/index.html.twig', [
'articles' => $articles,
]);
}
}Example Twig template:
<h1>Articles</h1>
<ul>
{% for article in articles %}
<li>{{ article.title }}</li>
{% endfor %}
</ul>The controller prepares the data.
The template displays the data.
This separation keeps the application cleaner.
Redirects in Symfony
Redirecting to another route is common.
Example:
return $this->redirectToRoute('article_index');Redirecting with route parameters:
return $this->redirectToRoute('article_show', [
'id' => 10,
]);If the route is:
#[Route('/articles/{id}', name: 'article_show')]Symfony generates:
/articles/10This is better than hardcoding URLs manually.
Single Action Controllers
A controller can contain many methods, but sometimes a controller only needs one action.
In that case, you can use the __invoke() method.
Example:
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class AboutController
{
#[Route('/about', name: 'about', methods: ['GET'])]
public function __invoke(): Response
{
return new Response('About us');
}
}Single action controllers are useful when you want very focused controllers.
Examples:
ShowDashboardController
CreateCheckoutSessionController
PublishArticleControllerEach controller has one clear responsibility.
Debugging Routes
Symfony provides a very useful command for inspecting routes:
php bin/console debug:routerThis command lists all registered routes in your application.
Example output:
Name Method Scheme Host Path
article_index GET ANY ANY /articles
article_show GET ANY ANY /articles/{id}
api_health GET ANY ANY /api/healthTo inspect one specific route:
php bin/console debug:router article_showThis is one of the most useful commands when working with Symfony routing.
Common Beginner Mistakes
Not Naming Routes
Avoid this:
#[Route('/articles')]Prefer this:
#[Route('/articles', name: 'article_index')]Named routes are easier to use for redirects and URL generation.
Making Controllers Too Large
A controller should not contain all business logic.
Bad example:
public function store(Request $request): Response
{
// validation
// database logic
// payment logic
// notification logic
// email logic
// redirect
}Better approach:
public function store(Request $request, ArticleCreator $articleCreator): Response
{
// handle request
// call service
// return response
}Keep controllers thin and move business logic into services.
Reading Request Data Too Generally
Avoid unclear request access:
$value = $request->get('q');Prefer explicit access:
$value = $request->query->get('q');For form data:
$value = $request->request->get('name');This makes your code easier to understand.
Forgetting HTTP Method Restrictions
Avoid this:
#[Route('/articles', name: 'article_store')]Prefer this:
#[Route('/articles', name: 'article_store', methods: ['POST'])]Specifying HTTP methods makes routes clearer and safer.
Practical Example
Here is a complete beginner-friendly controller example:
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/articles')]
class ArticleController extends AbstractController
{
#[Route('', name: 'article_index', methods: ['GET'])]
public function index(): Response
{
return new Response('Article list');
}
#[Route('/{id}', name: 'article_show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function show(int $id): Response
{
return new Response('Article ID: ' . $id);
}
#[Route('/api/latest', name: 'article_latest_api', methods: ['GET'])]
public function latest(): JsonResponse
{
return $this->json([
'title' => 'Learning Symfony Routing',
'status' => 'published',
]);
}
}This controller demonstrates:
Class-level route prefix
GET routes
Route parameters
Route requirements
JSON response
Route namesIt is a good starting point for learning Symfony controllers.
Conclusion
Routing and controllers are the foundation of Symfony applications.
A route connects a URL to a controller method.
A controller handles the request and returns a response.
The Request object gives you access to incoming data.
The Response object represents the HTTP response returned to the browser or API client.
The basic flow is:
URL
↓
Route
↓
Controller Method
↓
Request Data
↓
Application Logic
↓
ResponseOnce you understand this flow, Symfony becomes much easier to learn.
The next important topic is services and dependency injection, because that is where Symfony becomes especially powerful