This guide assumes a Contao version of at least 4.13. Back end routes can be created in previous Contao versions as well, but might require additional steps.
You can use the Contao back end to display content generated in your own custom Controllers. This way you can develop custom extensions without the need to use DCA configuration. The following example can be changed according to your own setup. For example you’re not obliged to use the annotation configuration for your routes you could use XML or YAML interchangeably.
The first step is to create your own Controller. A more detailed explanation
on how Symfony Controller work can be found in the Symfony documentation.
The Controller class is placed inside the Controller
directory
and is configured through annotations.
// src/Controller/BackendController.php
namespace App\Controller;
use Contao\CoreBundle\Controller\AbstractBackendController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
#[Route('%contao.backend.route_prefix%/my-backend-route', name: self::class, defaults: ['_scope' => 'backend'])]
class BackendController extends AbstractBackendController
{
public function __invoke(): Response
{
return $this->render(
'my_backend_route.html.twig',
[
'error' => 'Oh no, an error!',
'title' => 'My title',
'headline' => 'My headline',
'version' => 'I can overwrite what I want',
'foo' => 'bar',
]
);
}
}
In order to have a correct Contao back end route, we need an additional request parameter called _scope
with the value backend
. This way
you are telling Contao that this route belongs to the back end and should be handled accordingly. See this article for more
information about the backend
scope.
Be sure to have imported your bundle’s Controllers in your routes.yaml
before
the ContaoCoreBundle
routes. If you are developing a bundle for the Contao Managed Edition, be sure to load your routes in your Plugin
class.
# config/routes.yaml
app.controller:
resource: ../src/Controller
type: attribute
Our route will render the template my_backend_route.html.twig
which must be placed
into /templates
.
{% extends "@Contao/be_main" %}
{% block main_content %}
<div class="tl_listing_container">
Main Content: {{ foo }}
</div>
{% endblock %}
This example renders like this:
Most of the time you probably want to add a menu entry for your back end module.
Since the back end menu can be extended with an EventListener
we can easily
create one that listens for the menu event to be dispatched.
// src/EventListener/BackendMenuListener.php
namespace App\EventListener;
use App\Controller\BackendController;
use Contao\CoreBundle\Event\ContaoCoreEvents;
use Contao\CoreBundle\Event\MenuEvent;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Routing\RouterInterface;
#[AsEventListener(ContaoCoreEvents::BACKEND_MENU_BUILD, priority: -255)]
class BackendMenuListener
{
protected $router;
protected $requestStack;
public function __construct(RouterInterface $router, RequestStack $requestStack)
{
$this->router = $router;
$this->requestStack = $requestStack;
}
public function __invoke(MenuEvent $event): void
{
$factory = $event->getFactory();
$tree = $event->getTree();
if ('mainMenu' !== $tree->getName()) {
return;
}
$contentNode = $tree->getChild('content');
$node = $factory
->createItem('my-module')
->setUri($this->router->generate(BackendController::class))
->setLabel('My Modules')
->setLinkAttribute('title', 'Title')
->setLinkAttribute('class', 'my-module')
->setCurrent($this->requestStack->getCurrentRequest()->get('_controller') === BackendController::class)
;
$contentNode->addChild($node);
}
}
This EventListener creates a new menu node and handles its own current state by
reading and matching the controller class, which Symfony provides under the _controller
request attribute by default.
The EventListener registers itself to the contao.backend_menu_build
event by using
the AsEventListener
PHP attribute directly in the PHP file.
This allows us to skip defining a service tag in the service configuration. We
purposely assign it a low priority, so that we can be sure to be loaded after the
Contao Core EventListeners. Otherwise, the content
node we assign ourself to will
not be available yet.
And that’s it. Your controller should now be callable from the main back end menu in the sidebar.