Ehsan Bagherzadegan
Back to articles
15 min read

Symfony Routing and Controllers: A Beginner-Friendly Guide for PHP Developers

Routing and controllers are the foundation of every Symfony application. This beginner-friendly guide explains how Symfony maps URLs to controller methods, how to use route attributes, route parameters, HTTP methods, requests, responses, redirects, and JSON endpoints.

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
    ↓
Response

If 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 text

The 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
$_COOKIE

Symfony gives you objects like:

Request
Response
JsonResponse
RedirectResponse

A 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/100

In 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/15

Symfony 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/10

But it will not match:

/articles/hello

Route 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=symfony

The value of $query will be:

symfony

Different 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 attributes

It 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\AbstractController

This 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/10

This 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
PublishArticleController

Each controller has one clear responsibility.

Debugging Routes

Symfony provides a very useful command for inspecting routes:

php bin/console debug:router

This 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/health

To inspect one specific route:

php bin/console debug:router article_show

This 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 names

It 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
    ↓
Response

Once 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