alfrasc/laravel-matomo-tracker
Server-side Matomo analytics for Laravel. Track page views, events, goals, and e-commerce without client-side JavaScript, with a fluent API, configurable sites, and user attribution.
This is a thin Laravel service-provider/facade wrapper around matomo/matomo-php-tracker — the official server-side PHP tracker. There is no Eloquent model, migration, route, or JS; it is essentially a configured singleton plus a facade. Treat it as "a way to call Matomo's PHP SDK with Laravel config and DI," not as a full analytics integration.
Install and configure
composer require alfrasc/laravel-matomo-tracker
php artisan vendor:publish --provider="Alfrasc\MatomoTracker\MatomoTrackerServiceProvider"
Auto-discovery registers the provider and MatomoTracker facade alias on Laravel 5.5+. The published config/matomo-tracker.php holds the Matomo base URL, numeric site ID, and token auth. Set them via env:
MATOMO_URL=https://analytics.example.com/
MATOMO_SITE_ID=1
MATOMO_TOKEN_AUTH=xxxxxxxxxxxxxxxx
MATOMO_URL must be the Matomo installation root, including scheme and any subdirectory (https://example.com/matomo/), and MATOMO_SITE_ID must be the integer idsite for the site you configured in Matomo — not the site name.
First use case: track a page view
use Alfrasc\MatomoTracker\Facades\MatomoTracker;
MatomoTracker::doTrackPageView('Dashboard');
Where to look first
vendor/alfrasc/laravel-matomo-tracker/src/ — confirm the facade accessor, config keys actually read by the provider, and how the singleton is constructed.vendor/matomo/matomo-php-tracker/MatomoTracker.php — this is your real API reference. Every doTrack* and set* method lives here, and the package adds little on top.composer.json — check the illuminate/* and matomo/matomo-php-tracker constraints against your Laravel version before investing (see Gotchas).Middleware + terminate() for page views. Don't track inline in controllers. Register a middleware that records the request and sends it after the response:
public function handle($request, Closure $next) { return $next($request); }
public function terminate($request, $response): void
{
if (app()->isProduction() && ! $request->is('admin/*', 'horizon/*', 'health')) {
MatomoTracker::setUrl($request->fullUrl())
->setUrlReferrer($request->headers->get('referer') ?? '')
->setIp($request->ip())
->setUserAgent($request->userAgent() ?? '');
MatomoTracker::doTrackPageView($this->titleFor($request));
}
}
Listeners for conversions and ecommerce. Track goals and orders from domain events, not from controllers — that keeps analytics out of HTTP code and makes it replayable/queueable:
// OrderPlaced listener
MatomoTracker::doTrackEcommerceOrder(
$event->order->number,
$event->order->total,
$event->order->subtotal,
$event->order->tax,
$event->order->shipping,
$event->order->discount
);
MatomoTracker::doTrackGoal(config('analytics.goal_order'), $event->order->total);
Wrap it in your own service. The package facade is global and stateful. For anything larger than a hobby app, inject a small Analytics class that builds a fresh tracker (or resets the singleton), applies user/custom-dimension context, and can be swapped for a null implementation in tests and local:
final class Analytics
{
public function __construct(private MatomoTracker $tracker) {}
public function event(string $category, string $action, ?string $name = null, ?float $value = null): void
{
try {
$this->tracker->doTrackEvent($category, $action, $name, $value);
} catch (\Throwable $e) {
report($e); // never let analytics break a request
}
}
}
Set identity once per request/job. For logged-in users, setUserId($user->id) gives cross-device attribution in Matomo. Pair it with custom dimensions (setCustomDimension($id, $value)) for plan, tenant, locale — but remember state persists on the singleton within the request (see Gotchas).
Defer or queue the network call. Every doTrack* sends an HTTP request immediately. Either call from a terminate middleware, or dispatch a job/listener that resolves the tracker inside handle():
dispatch(fn () => app(MatomoTracker::class)->doTrackEvent('billing', 'upgrade'))->afterResponse();
For queue workers, resolve a fresh instance per job rather than reusing the singleton, or you'll carry custom variables and visitor IDs between jobs.
Multi-tenancy / multiple Matomo sites. The service provider binds one singleton from one config. For per-tenant site IDs, don't fight the config — construct trackers yourself:
$tracker = new MatomoTracker($tenant->matomo_site_id, config('matomo-tracker.url'));
$tracker->doTrackPageView($title);
Consent, local, and GDPR gating. Server-side tracking bypasses the browser, so your own consent check is the only gate. A simple analytics.enabled (false in local/testing, false until consent recorded) checked inside your wrapper is the cleanest lever. Also call disableCookie()/avoiding visitor-ID persistence and rely on Matomo's IP anonymization settings; if you setIp(), document it in your privacy policy.
Site search and content. doTrackSiteSearch($keyword, $category, $count) from your search endpoint; doTrackContentImpression/doTrackContentInteraction for banners. These are just calls to the SDK — no package-specific plumbing.
Testing. Because it is a facade, MatomoTracker::shouldReceive('doTrackPageView')->once()->with('Dashboard') works. In feature tests, also bind a fake into the container so your wrapper resolves it.
illuminate/support and matomo/matomo-php-tracker constraints support your Laravel version before adopting it; for Laravel 10/11 you may need to require matomo/matomo-php-tracker directly and write a 20-line service provider yourself. That is a very reasonable alternative.doTrack* performs an outbound HTTP request from the PHP process. Tracking inside a controller will add Matomo's latency to your response time. Use terminate(), afterResponse(), or a queued job, and set a sane timeout.setUserId, setCustomDimension, setCustomVariable, setIp, setUrl, etc. mutate the instance and leak into subsequent calls in the same request or queue worker. Reset or re-resolve between logical units of work.php artisan config:cache freezes the URL/site ID/token. If you read env() at runtime in your own code after caching, it returns null. Keep all Matomo config in the package config file.MATOMO_URL (missing scheme, missing /matomo/ path, missing trailing slash) or a wrong idsite typically produces a 200 with an empty body from Matomo while no data appears in reports. Check the actual request URL first when dashboards are empty.@json/front-end config, and rotate it if it leaks into logs.setUserId/setVisitorId, server-side hits may be treated as new visitors. Decide explicitly how you correlate them.try/catch and degrade quietly. Conversely, don't swallow errors silently in staging — log them so misconfiguration is caught before launch.admin/*, bots, and /livewire/*; otherwise your reports will be dominated by infrastructure traffic.$request->ip() respects TrustProxies; if you're behind a proxy without it configured you'll record the load balancer's IP. Test in staging.MatomoTracker::class in the container, and instantiating the SDK directly for multi-site. Plan around that rather than expecting package-level hooks.doTrackEvent, doTrackGoal, doTrackEcommerceOrder, setCustomVariable vs setCustomDimension are easy to mix up, and the wrapper's docs are thin. vendor/matomo/matomo-php-tracker/MatomoTracker.php is the source of truth.How can I help you explore Laravel packages today?