This section covers “routing” in Contao, e.g. how to implement your on routes in the Managed Edition, Contao-specific route attributes and Page controllers.
Routing and controllers are a core concept of any Symfony application. Regardless of whether you are using Contao within your own Symfony application or the Managed Edition, the same principles apply. This subsection provides a short introduction on how to implement your own routes and controllers in the Contao Managed Edition. Have a look at the Symfony routing documentation for the full range of possibilities.
Routes can be defined either in XML/PHP/YAML files or via annotations. For simplicity this guide will only show the latter. To start off, we first need to tell Symfony that our routes will be defined via annotations:
This will tell Symfony that any controller defined under src/Controller
within
your application (i.e. the App\Controller\
namespace) will use PHP annotations
for defining routes.
Now we can go right ahead and create a simple controller and define its route via
the Symfony\Component\Routing\Annotation\Route
annotation:
// src/Controller/ExampleController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/example', name: ExampleController::class)]
class ExampleController
{
public function __invoke(Request $request): Response
{
return new Response('Hello World!');
}
}
This is the most bare bones controller you can build. In this case it is implemented as an invokable controller. The route itself is registered with two parameters:
/example
. Our controller will be reachable under this path
in the front end.UrlGenerator
for example.We can use the debug:router
command to confirm its successful registration:
$ vendor/bin/contao-console debug:router "App\Controller\ExampleController"
+--------------+---------------------------------------------------------+
| Property | Value |
+--------------+---------------------------------------------------------+
| Route Name | App\Controller\ExampleController |
| Path | /example |
| Path Regex | #^/example$#sD |
| Host | ANY |
| Host Regex | |
| Scheme | ANY |
| Method | ANY |
| Requirements | NO CUSTOM |
| Class | Symfony\Component\Routing\Route |
| Defaults | _controller: App\Controller\ExampleController |
| Options | compiler_class: Symfony\Component\Routing\RouteCompiler |
+--------------+---------------------------------------------------------+
Accessing https://example.com/example
in the front end should show the following:
Hello World!
Within your controller you can access any information about the request via the
Request
parameter that is automatically passed to the function of your action
(in this case the __invoke
function of our invokable controller). See also the
Symfony routing documentation.
When using controllers as services and taking advantage of dependency injection,
the controller’s service needs to be set to public
or be tagged with the
controller.service_arguments
tag.
// src/Controller/ExampleController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Terminal42\ServiceAnnotationBundle\Annotation\ServiceTag;
#[Route('/example', name: ExampleController::class)]
#[AsController]
class ExampleController
{
public function __construct(
private readonly AuthorizationCheckerInterface $authorizationChecker
) {
}
public function __invoke(): Response
{
if ($this->authorizationChecker->isGranted('ROLE_MEMBER')) {
return new Response('Member is logged in.');
}
return new Response ('Member is not logged in.');
}
}
When defining a route Symfony allows you to set some default parameters for the request handled by this route. There are two different special request attributes that Contao will listen to during the handling of a request, which will be outlined here. Contao will also set additional request attributes which you can then access within your controller.
The scope of a request can be set via the _scope
request attribute. If the value
of this attribute is either frontend
or backend
, the request will be identified
as a “Contao request” and thus handled accordingly with the following effects:
_locale
request attribute will be automatically set by Contao, according
to which language the current request belongs to (depending on your site structure,
if the request can be matched there) or the Accept-Language
request header.backend
, the route is automatically protected by the contao_backend
firewall. Contao will also automatically generate
a “referer ID token” and store it as another request attribute under _contao_referer_id
. Plus the current and last URL will be stored in
the session. This is used in the back end for the “go back” links for example.In your own services you can query the current scope using the ScopeMatcher
service.
The following example will execute any request to the defined controller in the
Contao frontend
scope:
// src/Controller/ExampleController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/example', name: ExampleController::class, defaults: ['_scope' => 'frontend'])]
class ExampleController
{
public function __invoke(): Response
{
return new Response('I am a Contao request!');
}
}
See the Back End Routes Guide for a full example and explanation on how to create your own controller for the Contao back end.
Contao comes with its own protection against CSRF attacks. This protection can be
enabled for your own controller by using the _token_check
request attribute. The
protection is enabled by default for any Contao request (and thus can be disabled
using the request attribute), but needs to be manually enabled for your custom controller
that does not use either of Contao’s scopes.
// src/Controller/ExampleController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/example', name: ExampleController::class, defaults: ['_token_check' => true])]
class ExampleController
{
public function __invoke(): Response
{
return new Response('I am a CSRF protected controller.');
}
}
See the article on Request Tokens for more details.
The Contao back end allows you to enable a maintenance mode in the front end. The
maintenance mode is applied globally, but you can exempt routes by using the _bypass_maintenance
request attribute for your own routes.
// src/Controller/ExampleController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/example', name: ExampleController::class, defaults: ['_bypass_maintenance' => true])]
class ExampleController
{
public function __invoke(): Response
{
return new Response('This route is exempt from the maintenance mode.');
}
}
This feature is available in Contao 5.0 and later.
The Contao\PageModel
instance for a regular Contao page request is available under the pageModel
request attribute.
This allows you to access any current and inherited attributes of the current page.
// src/ExampleService.php
namespace App;
use Contao\PageModel;
use Symfony\Component\HttpFoundation\RequestStack;
class ExampleService
{
public function __construct(private readonly RequestStack $requestStack)
{
}
public function __invoke(): void
{
$request = $this->requestStack->getCurrentRequest();
/** @var PageModel $page */
if ($page = $request->attributes->get('pageModel')) {
$title = $page->title;
// …
}
}
}
This is also available in Contao 4.9 and later. However, prior to Contao 5 the pageModel
attribute might also
just be an integer ID rather than a PageModel
instance (specifically in fragment subrequests). The attribute might
also not be available at all, depending on the request’s circumstances. As explained in the respective articles you can
use the getPageModel()
method in fragment controller for content elements and fron end modules to retrieve the current
PageModel
. If you need this in a different service in Contao 4 then you will need to copy the
implementation
of that method.
This feature is available in Contao 5.4 and later.
Starting with Contao 5.4 you can also use the PageFinder
to retrieve the PageModel
of the current request, if
available:
// src/ExampleService.php
namespace App;
use Contao\CoreBundle\Routing\PageFinder;
class ExampleService
{
public function __construct(private readonly PageFinder $pageFinder)
{
}
public function __invoke(): void
{
$page = $this->pageFinder->getCurrentPage();
// …
}
}