This guide assumes a Contao version of at least 4.9. Back end routes can be
created in previous Contao versions as well, but might require additional steps.
For example, in Contao 4.4 instead of using the contao.backend_menu_build
event, the back end menu needs to be altered using the getUserNavigation
hook.
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 Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment as TwigEnvironment;
#[Route('/contao/my-backend-route', name: BackendController::class, defaults: ['_scope' => 'backend'])]
class BackendController
{
private $twig;
public function __construct(TwigEnvironment $twig)
{
$this->twig = $twig;
}
public function __invoke(): Response
{
return new Response($this->twig->render(
'my_backend_route.html.twig',
[]
));
}
}
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.
Contao 4.13 allows configuring a custom backend path. Use "%contao.backend.route_prefix%/my-backend-route"
instead of "/contao/my-backend-route"
in the example above to support this feature.
Be sure to have imported your bundle’s Controllers in your routes.yaml
before
the ContaoCoreBundle
routes.
# config/routes.yaml
app.controller:
resource: ../src/Controller
type: annotation
Our route will render the template my_backend_route.html.twig
which must be placed
into /templates
.
{% extends "@ContaoCore/Backend/be_page.html.twig" %}
{% block headline %}
Not only the content of the `title`-tag but also the title of the content section.
{% endblock %}
{% block error %}
Will be placed within the error block.
{% endblock %}
{% block main %}
<div class="tl_listing_container">
Main Content.
</div>
{% endblock %}
As we extend from @ContaoCore/Backend/be_page.html.twig
it is worth noting
that there are three different blocks you can currently use:
headline
: This block renders the headline of the document.error
: In case of an error, place your message here, it will be placed prominently
on the top of the pagemain
: This is the content area for output.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
a @ServiceTag
annotation 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. You controller should now be callable from the main back end menu in the sidebar.