knplabs/knp-piwik-client-bundle
Symfony bundle that integrates the KNP Piwik client, letting you configure Piwik/Matomo analytics through bundle config and inject a ready-to-use tracker service. Adds Twig helpers and straightforward page-view and goal tracking to Symfony apps with minimal setup.
Read the "no longer maintained" banner before anything else. This bundle is archived, explicitly unmaintained by KnpLabs, targets Symfony 2.x, and has 0 dependents. For a Laravel developer the honest starting position is: it is not a package you install into a Laravel app, and on a legacy Symfony 2.x app it is a liability you inherit, not a tool you adopt. Everything below is written for the realistic cases — maintaining a legacy app that already uses it, or reading it as a reference before writing your own thin Matomo client.
Minimal wiring on a legacy Symfony 2.x app:
composer require knplabs/knp-piwik-client-bundle
// app/AppKernel.php
$bundles = [ /* ... */ new Knp\Bundle\PiwikBundle\KnpPiwikBundle() ];
# app/config/config.yml
knp_piwik:
host: "%piwik_host%" # e.g. https://analytics.example.com
site_id: "%piwik_site_id%"
token_auth: "%piwik_token%" # only needed for write/admin calls
disabled: false # true in dev/test
Where to look first, in order:
DependencyInjection/Configuration.php — the authoritative list of config keys and defaults. The README lags behind it; trust the config tree.Resources/config/services.xml — the service IDs you inject (knp_piwik.tracker and friends).doTrackPageView, doTrackEvent, doTrackGoal, setUserId, setCustomVariable) belongs to the wrapped class, not the bundle.Twig/ — the extension that renders the client-side _paq JavaScript snippet.First use case: server-side tracking the browser cannot see — a newsletter signup, an API endpoint hit, a webhook-driven conversion, a cron import. Inject the tracker into a controller and fire a goal or event:
public function signupAction(Request $request, Tracker $tracker)
{
// ... persist ...
$tracker->doTrackEvent('Newsletter', 'subscribe', $request->get('source', 'unknown'));
return $this->redirectToRoute('home');
}
If you are starting fresh on Laravel, skip this entirely: use Http::post("{$host}/matomo.php", [...]) behind a small service class, or a maintained Matomo tracker. That is roughly the same amount of code as wiring this bundle in, minus the Symfony baggage.
Inject the service, never the container. Alias knp_piwik.tracker to a small interface of your own so call sites are insulated. This is the highest-value refactor you can do for a package you already know is dead — when you migrate to a direct HTTP call, you change one binding.
Make it environment-aware and default-off. Set disabled: true in config_dev.yml and config_test.yml. In tests, bind your interface to a fake — never let the suite open outbound HTTP connections to an analytics host. Because each tracker call is a synchronous request, a "real" tracker in tests slows suites and makes them flaky.
Treat tracking as a fire-and-forget side channel. The tracker issues one HTTP request per call, synchronously. That means:
kernel.terminate listener so the visitor gets their response before the tracking call runs.setRequestTimeout() before batch loops so a slow or unreachable Matomo instance can't stall a cron job.public function onTerminate(TerminateEvent $event)
{
$this->tracker->doTrackPageView($event->getRequest()->getPathInfo());
}
Use setCustomVariable() / setCustomDimension() as the extension point that outlives the bundle. These map cleanly to Matomo's current API. Store site-specific values (plan tier, tenant, A/B bucket) as custom variables now so that when you replace the wrapper, the payload shape — not the call sites — is what changes.
$tracker->setCustomVariable(1, 'plan', $user->plan, 'visit');
$tracker->doTrackEvent('Billing', 'invoice_paid', $invoice->currency);
Pair server-side goals with the Twig snippet deliberately. The Twig extension emits _paq for page views and JS events. Use the PHP tracker only for the server-authoritative events. If you call setUserId() server-side, make sure the JS snippet calls setUserId() with the same value, or Matomo splits the visit into two identities and attribution breaks.
Centralize payload construction in one place even if you keep the bundle. Wrap the tracker behind one service that owns event/category/action naming. Analytics taxonomies drift within weeks when each controller invents its own strings, and that inconsistency is what makes migration painful — you'll have to reconcile names anyway.
Batch deliberately. If you need to track an import of thousands of rows, either use the bulk/queued API the bundle exposes or accept that you are doing thousands of synchronous requests and schedule accordingly. There is no built-in retry or offline queue — if Matomo is down, those events are gone.
ServiceProvider, no config/knp_piwik.php, no facade. Symfony 2.x DI, YAML config, bundle registration. In a Laravel app you would be installing Symfony DI alongside Laravel — don't. Use Http client and a tiny service class.--ignore-platform-reqs, stop — that flag is a red flag, not a workaround. You are now pinned to an abandoned package along your entire upgrade path.matomo.php/index.php paths and parameter names against the Matomo version you actually run. "It installed" is the beginning of validation, not the end.token_auth leaks easily. Don't commit it, don't put it in a config.yml default, and check logs and error reporters — a tracker exception can dump the whole request including the token. Scope tokens to the minimum privilege Matomo offers, and rotate them if they've ever been in a stack trace.disabled is not the same as "safe." Confirm that setting disabled: true actually short-circuits before any HTTP call in the version of the wrapped client you have. Verify it once by running a dev request against a host you can observe.warning with the event name, not the token.setUserId mismatch is the classic source of "why are half my conversions unattributed." Keep a single source of truth for the user ID and set it on both sides.'visit' vs 'page' scope changes how Matomo counts things — 'visit' is set once per session, 'page' per hit. The wrong scope silently corrupts segmentation reports, and the bundle gives you no guard rail.Http replacement you get ->timeout(), ->retry(), and pooling for free.Http call, only the wrapper changes. Don't fork the bundle — you'd be inheriting an entire Symfony2 bundle to keep a few lines of URL construction alive.How can I help you explore Laravel packages today?