This feature is available in Contao 4.10 and later.
Starting with Contao 4.10 you can implement so called Page Controllers. These are special page types implemented as controllers in order to handle the request to the route of a specific page type within the Contao site structure. Page controllers combine the ability to define a page in Contao’s site structure while still having full control over the routing and route attributes like with regular controllers.
For example, imagine you need to provide an RSS feed or other structured feed for
entries of your own DCA. This RSS feed could be implemented as controller with its
own route. By implementing it as a page controller, you might allow the administrator
or editor of a site to freely define the route (i.e. alias) of the page, plus additional
configuration settings from within the back end of Contao. Even the suffix can be
freely defined, so you might have a list of your database records under https://example.com/foobar/records.html
,
while the RSS feed is defined to have a route like https://example.com/foobar/records.xml
.
For Page Controllers to work the Legacy Routing Mode must be disabled in your application configuration:
contao:
legacy_routing: false
As with content elements, front end modules, hooks and DCA callbacks, Page controllers can be registered via attributes, annotations or YAML. The following shows the most basic example:
// src/Controller/Page/ExamplePageController.php
namespace App\Controller\Page;
use Contao\CoreBundle\DependencyInjection\Attribute\AsPage;
use Symfony\Component\HttpFoundation\Response;
#[AsPage]
class ExamplePageController
{
public function __invoke(): Response
{
return new Response('Hello World!');
}
}
// src/Controller/Page/ExamplePageController.php
namespace App\Controller\Page;
use Contao\CoreBundle\ServiceAnnotation\Page;
use Symfony\Component\HttpFoundation\Response;
/**
* @Page
*/
class ExamplePageController
{
public function __invoke(): Response
{
return new Response('Hello World!');
}
}
# config/services.yaml
services:
App\Controller\Page\ExamplePageController:
tags: [contao.page]
// src/Controller/Page/ExamplePageController.php
namespace App\Controller\Page;
use Symfony\Component\HttpFoundation\Response;
class ExamplePageController
{
public function __invoke(): Response
{
return new Response('Hello World!');
}
}
Without any additional parameters, the type of the page is inferred from the class name. In
this case the type of the page will be example
, since suffixes like Page
and
Controller
(or both together) are automatically ignored.
Next a palette for the back end should be defined for this page type:
// contao/dca/tl_page.php
$GLOBALS['TL_DCA']['tl_page']['palettes']['example'] =
'{title_legend},title,alias,type;{publish_legend},published,start,stop';
A translation for the back end label should be defined to:
// contao/languages/en/default.php
$GLOBALS['TL_LANG']['PTY']['example'] = ['Example', 'Example page type.'];
Now we are all set and can add this new page in the site structure of the Contao back end:
The alias will be the “route” of this controller. When accessing
https://example.com/route/to/example/page/controller
in the front end, you should
see the Hello World!
response.
You might want to implement pages that should only exist once within a website (see
Contao’s 401, 403 and 404 error pages for example). Use the FilterPageTypeEvent
to dynamically limit which pages are available for selection in the back end.
In principle, the AsPage
attribute and @Page
annotation allows you to set parameters that you would
normally be able to define with regular controllers, like requirements
, options
,
methods
and defaults
for request attributes. See the Symfony routing documentation
for these possibilities.
There are however a few differences and additional options.
#[AsPage(
type: 'example',
path: '/foo/bar',
urlSuffix: '.html',
contentComposition: true
)]
/**
* @Page(
* type="example",
* path="/foo/bar",
* urlSuffix=".html",
* contentComposition=true
* )
*/
# config/services.yaml
services:
App\Controller\Page\ExamplePageController:
tags:
-
name: contao.page
type: example
path: /foo/bar
urlSuffix: .html
contentComposition: true
type
As mentioned previously, the type is automatically inferred from the page controller’s
class name, if not specified. If you want to specifically set the type string yourself,
you can pass it as the first parameter of the annotation (or use type="custom_type"
).
#[AsPage(type: 'custom_type')]
Note that this one of the differences between the @Page
and Symfony’s @Route
annotation where in the latter case, the first parameter is the path of the route.
path
For regular Symfony routes the URL of the route is only defined via the path
parameter.
In case of page controllers the URL of the page will either be defined via its alias,
which is defined in the back end, or its configured path
- or even a combination
of both!
For instance, with the following annotation and the default .html
URL suffix:
#[AsPage(path: '/foo/bar')]
the URL of the page will always be https://example.com/foo/bar.html
, no matter
what is defined in the back end. This means that you should not add the alias
field to the palette of this page in the tl_page
DCA.
However, if the defined path of the page configuration is a relative path rather than an absolute one, then the URL of the page will be a combination of both the configured path and the defined alias of the page, where the configured path of the page will be appended to the alias of the page.
So for example, with the following annotation:
#[AsPage(path: 'foo/bar')]
and an alias like example/alias
defined in the back end, the final front end URL
of the page will be https://example.com/example/alias/foo/bar.html
.
Also, just like with routes for regular controllers in Symfony, the path of your page controller can also contain
parameters. The following page route for example only consists of a single foobar
parameter:
#[AsPage(path: '{foobar}')]
Since it is defined as a relative path the final URL will consist of the page’s alias, plus any mandatory parameter.
This particular setup would be useful for reader pages for example. And, as with regular Symfony routes, parameters can
also be optional through its defaults
:
#[AsPage(path: '{lorem}/{ipsum}', defaults: ['ipsum' => ''])]
urlSuffix
Since Contao 4.10 you can define the URL suffix of a site in the settings of the respective website root. However, with page controllers you can also override that URL suffix in the page controller’s configuration:
#[AsPage(urlSuffix: '.csv')]
So if the page in the site structure has the alias foo/bar
then the final front
end URL will be https://example.com/foo/bar.csv
even though the root page’s url
suffix might be .html
.
contentComposition
This is a boolean property defining whether this page type is used for content composition. Pages with content composition manage their content and layout via the back end. For example an RSS feed page controller would not use content composition, since its content is not supposed to be editable via the back end. By default, content composition is enabled.
If you do not want to use content composition for your page controller, thus you do not want that articles can be assigned to those pages, disable the property:
#[AsPage(contentComposition: false)]
There is no abstraction yet in place for you to render such content
easily. You can use the FrontendIndex
class of the legacy framework of Contao
to render the page layout as defined in the page structure (in addition to processing
your own logic):
// src/Controller/Page/ExamplePageController.php
namespace App\Controller\Page;
use Contao\CoreBundle\DependencyInjection\Attribute\AsPage;
use Contao\FrontendIndex;
use Contao\PageModel;
use Symfony\Component\HttpFoundation\Response;
#[AsPage]
class ExamplePageController
{
public function __invoke(PageModel $pageModel): Response
{
// Render the page using the FrontendIndex handler from the legacy framework
return (new FrontendIndex())->renderPage($pageModel);
}
}
However, upcoming feature versions of Contao will likely provide better abstraction for this task.
In Symfony you can require the current Request
object to be passed into your invokable
controller or action method as an argument (see the Controller documentation).
Contao also extends Symfony’s argument value resolver
and thus allows you to automatically pass the PageModel
of the page controller’s
page as an argument as well:
// src/Controller/Page/ExamplePageController.php
namespace App\Controller\Page;
use Contao\CoreBundle\DependencyInjection\Attribute\AsPage;
use Contao\PageModel;
use Symfony\Component\HttpFoundation\Response;
#[AsPage]
class ExamplePageController
{
public function __invoke(Request $request, PageModel $pageModel): Response
{
return new Response('Hello page: '.$pageModel->title);
}
}
Within the database all pages are stored in the tl_page
table. An entry for a page will be created there when you
create a new page for any page type (including your page controllers). Instances of pages in Contao are generally
represented by the Contao\PageModel
. This class allows you to generate URLs to pages via its getFrontendUrl
and
getAbsoluteUrl
method. The former will generate URLs relative to the <base>
- unless the page is on a different
domain than the current one. The latter will always produce absolute URLs (including http://
or https://
).
since 5.0 getFrontendUrl
will now generate path absolute URLs, not relative to the <base>
.
Both methods allow you to specify optional parameters as one string. These are path parameters and are used when you
want to generate a URL with an auto_item
or other path parameters. For example
$page->getAbsoluteUrl();
might generate a URL like https://example.com/alias-of-the-page.html
while
$page->getAbsoluteUrl('/foobar');
might generate a URL like https://example.com/alias-of-the-page/foobar.html
(in these examples a .html
suffix would
be configured).
This works fine for any legacy page type. However, with modern page controllers there is a caveat: your Route
might
have specific, mandatory parameters in them that need to be known when generating the URL. So for example if you have a
page controller like this
#[AsPage(path: '{foo}/{bar}')]
and you then try to execute $page->getFrontendUrl()
for a PageModel
of such page it will result in an error, since
the parameters foo
and bar
are missing for URL generation.
But for modern page controllers you can generate the URL for such pages in your code via Symfony’s
UrlGeneratorInterface
services:
use Contao\PageModel;
use Contao\CoreBundle\Routing\Page\PageRoute;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class MyService
{
public function __construct(UrlGeneratorInterface $urlGenerator)
{
}
private function getUrlForPage(PageModel $page): string
{
return $this->urlGenerator->generate(
PageRoute::PAGE_BASED_ROUTE_NAME,
[
RouteObjectInterface::CONTENT_OBJECT => $page,
'foo' => 'lorem',
'bar' => 'ipsum',
]
);
}
}
The important thing to note here is that the name of the route we are generating is not the name or type of the
page controller, but a general page_routing_object
route - and then we pass the model instance of the page as a
_content
parameter, alongside our actual route parameters (foo
and bar
).
since 5.3 Starting with Contao 5.3 you are able to use getFrontendUrl
and getAbsoluteUrl
of
the PageModel
as well though. Instead of a string representing path parameters you can instead pass an array with the
parameters to the methods:
$page->getFrontendUrl([
'foo' => 'lorem',
'bar' => 'ipsum',
]);