Extend Contao Without Editing Core Files Using Hooks
Register custom PHP classes at Contao extension points so core files stay untouched during updates.
21 Aug 2025, 16:30 UTC

The problem: core edits do not survive updates
If you change Contao’s core files to alter how a front-end module renders, those changes are lost the next time you update Contao. The useful takeaway is to register a hook instead. Contao calls your custom PHP class at a defined extension point, so the core files stay untouched and updates remain safe.
How Contao hooks work
A hook is a predefined point in Contao’s execution where the core hands control to registered custom code. Contao uses the Symfony service container to instantiate and wire services. A tag such as contao.hook tells the container that a service should be invoked when a specific hook fires.
Worked example: add text to every front-end module
This example appends a paragraph to the rendered output of any front-end module by hooking into frontend_module. It assumes Contao 4.x and a standard Symfony directory layout.
1. Create the hook class
<?php
namespace App\EventHook;
use Contao\FrontendModule;
class FrontendGreetingHook
{
public function __invoke(FrontendModule $module, string $buffer): string
{
return $buffer . '<p class="hook-greeting">Added by a Contao hook.</p>';
}
}
The method signature must match the hook. For frontend_module, Contao expects the module instance and the current HTML buffer, and it expects a string back.
2. Register the class in services.yml
services:
App\EventHook\FrontendGreetingHook:
tags:
- { name: 'contao.hook', hook: 'frontend_module' }
Place this in /app/config/services.yml or your project’s equivalent service configuration file. The hook name is case-sensitive and must match Contao’s core exactly.
3. Clear the Contao cache
Run from the project root:
php vendor/bin/contao-console contao:cache:warmup --env=prod
Run as a user with write access to var/cache/, typically the web server user or your deployment user. Running as the wrong user can leave cache files owned by another account and make them unreadable by the web server.
Expected check: the command finishes without errors and rebuilds the compiled container. Afterward, load a page that displays a front-end module and inspect the HTML for <p class="hook-greeting">.
Limits and common mistakes
- Only predefined extension points. Hooks exist only where Contao’s core defines them. If no hook covers the feature you need, use a custom module, template override, or another extension mechanism instead.
- Performance on every-request hooks.
frontend_moduleruns on many page requests. Avoid heavy logic such as remote API calls or uncached database queries inside the hook unless you cache the result. - Wrong return type. Returning
nullor an unexpected type can break the core rendering flow. Always return the type the hook signature requires. - Cache not cleared. If you register or change a hook but do not warm up the cache, the code never executes.
- Typo in hook name. Hook names are case-sensitive.
frontend_moduleis not the same asFrontend_Module.
Verify the result
- Clear the cache with
contao:cache:warmup --env=prod. - Reload a page containing a front-end module.
- Check the page source for the hook’s output.
- If the output is missing, check the Contao log for errors, confirm the class is autoloaded by Composer, and verify the service tag and hook name match the core exactly.
Reverting the change
Because this only adds configuration, remove the App\EventHook\FrontendGreetingHook block from services.yml and run contao:cache:warmup --env=prod again. Contao returns to its previous behavior; there is no database change to roll back.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.