nucleos/matomo-bundle
Symfony bundle for integrating Matomo analytics into your application. It offers configuration for Matomo tracking, Twig helpers to embed the tracking code, and services that simplify adding privacy-friendly web analytics to Symfony projects without boilerplate.
Install the package and its HTTP transport requirements:
composer require nucleos/matomo-bundle
The bundle is not in Symfony Flex's recipe set (low adoption), so register it manually if it wasn't auto-registered:
// config/bundles.php
return [
// ...
Nucleos\MatomoBundle\NucleosMatomoBundle::class => ['all' => true],
];
In a Laravel app you usually skip the bundle registration and just consume the underlying nucleos/matomo-client service through the container — check whether the bundle's service definitions are worth pulling in over wiring the client yourself.
Configure the connection to your Matomo instance. The root key is nucleos_matomo; the keys below are the ones you'll be editing first — confirm exact names against the bundle's DependencyInjection/Configuration.php since the package moves fast:
# config/packages/nucleos_matomo.yaml
nucleos_matomo:
host: '%env(MATOMO_HOST)%' # e.g. https://matomo.example.com
site_id: '%env(int:MATOMO_SITE_ID)%'
token_auth: '%env(MATOMO_TOKEN)%' # only required for server-side / API calls
MATOMO_HOST=https://matomo.example.com
MATOMO_SITE_ID=1
MATOMO_TOKEN=your-api-token
Where to look first: read src/DependencyInjection/Configuration.php and src/Resources/config/*.xml. These two files tell you the real config keys and the actual service IDs you'll inject — far more reliable than the README.
First use case: render tracking on a page. The bundle's headline feature is a block service, so the fastest win is embedding the tracking block in your layout:
{# templates/base.html.twig, before </body> #}
{{ sonata_block_render({ type: 'nucleos_matomo.tracking' }) }}
If you're not using a block renderer, inject the underlying client service directly in a controller/listener and call its tracking method (trackPageView, trackEvent, etc.) — run bin/console debug:container matomo to find the exact service ID.
Version note (4.4.0, 2026-05-02): this release is dependency-only — Symfony 8 support, PHPUnit 13, and frontend dependency bumps. No API, config-key, or service-ID changes, so anything written against 4.3.x carries over untouched. What does change is the install surface: the bundle's Symfony component constraints now reach ^8.0, which is wider than the Symfony components a given Laravel release pins. If composer require fails on a Symfony constraint, run composer why-not symfony/http-client (or symfony/options-resolver) before assuming the bundle is incompatible — it's usually a transitive pinned component.
Inject the client, don't hand-roll HTTP. The bundle wraps nucleos/matomo-client; treat it as the single integration point. Constructor-inject the client interface and call typed methods:
final class OrderTracker
{
public function __construct(private MatomoClient $matomo) {}
public function trackPurchase(Order $order): void
{
$this->matomo
->setUserId($order->getCustomerId())
->trackEvent('checkout', 'purchase', $order->getNumber(), $order->getTotal());
}
}
Server-side tracking for things JS can't see. Use the client in domain-event listeners or Messenger handlers for refunds, subscription changes, and back-office actions. Wrap the call so a Matomo outage never breaks the domain flow:
try {
$this->matomo->trackEvent(...);
} catch (\Throwable $e) {
$this->logger->warning('Matomo tracking failed', ['exception' => $e]);
}
Front-end block for page views. Keep the block in your base layout and drive per-page data through block settings rather than duplicating the snippet:
{{ sonata_block_render({ type: 'nucleos_matomo.tracking' }, {
'site_id': app.request.attributes.get('matomo_site_id', 1),
'goal': 'newsletter_signup'
}) }}
Multi-site / multi-tenant. If the bundle exposes named clients or a factory, configure one client per Matomo site and select by request (host, locale, tenant). Inject a small resolver service rather than spreading if statements through controllers.
Test with a stub transport. Because the client speaks PSR-18/HTTPlug, you can bind a mock HTTP client in the test container and assert on the recorded requests. This keeps trackEvent calls out of your real Matomo instance in CI:
# config/packages/test/nucleos_matomo.yaml
services:
matomo.http_client:
class: Symfony\Component\HttpClient\MockHttpClient
Gate rendering, don't gate the service. Wrap the block and the Twig call in {{ matomo_enabled ? ... : '' }} style checks driven by consent state and kernel.debug, so enabling/disabling is a config concern, not a code change.
token_auth to the browser. It belongs to the server-side client only. Keep it in an env var and expose no route that reflects it back. If you see it in a rendered page, you're using the block where the client was meant.when@dev guard — add nucleos_matomo: { enabled: false } (or short-circuit in a decorator) for dev and test, or you'll pollute analytics with localhost page views.Content-Security-Policy, the Matomo tracker snippet needs a nonce or a script-src entry for your Matomo host. The block does not add nonces for you; wire it into your CSP listener.host should be the base URL with no trailing slash, and Matomo installed under a path (e.g. /analytics) must be included in host. Mismatches produce silent no-ops rather than errors.composer require symfony/http-client (or nyholm/psr7 + a client) resolves it.enabled: false vs. omitted config. Disabling a site can leave a null/placeholder service that throws \LogicException when injected. Prefer if ($this->matomo->isEnabled()) style guards, or bind a no-op decorator in dev.symfony/phpunit-bridge v8; those are require-dev constraints for the bundle's own CI and do not affect your app's test stack. Keep your project on whatever test runner your Laravel version ships with, and don't bump phpunit to satisfy the bundle.composer require --with-all-dependencies if you've verified the full graph.MatomoClientInterface) to add logging, buffering, or a NullClient for environments; decorate the block service to alter render array data. Both are cleaner than forking the bundle.composer.json — the 2026 release cadence suggests breaking changes between minors are possible, and there are zero dependents to surface regressions. 4.4.0 itself is safe to take: dependency-only, no runtime behaviour changed.How can I help you explore Laravel packages today?