Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Matomo Php Tracker Laravel Package

matomo/matomo-php-tracker

Official PHP client for Matomo Analytics that enables server-side tracking of page views, events, goals, ecommerce, and custom dimensions from PHP applications. Supports bulk requests, visitor attribution, and privacy-friendly analytics without relying on client-side JavaScript.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require matomo/matomo-php-tracker

There is no service provider, facade, or config file shipped with this package — it's a single, global-namespace MatomoTracker class. Your first stop should be vendor/matomo/matomo-php-tracker/MatomoTracker.php itself; it's one readable file and the doTrack* / getUrlTrack* method pairs are your whole API surface. Skim the README for the parameter list, then read the class.

Minimum viable setup — put your credentials in config/services.php:

// config/services.php
'matomo' => [
    'url'     => env('MATOMO_URL', 'https://analytics.example.com/matomo.php'),
    'site_id' => (int) env('MATOMO_SITE_ID'),
],

Point MATOMO_URL at the full matomo.php endpoint of your instance (self-hosted, Matomo Cloud, or a proxy). Then a first use case that JS cannot cover — a backend event:

use MatomoTracker;

$tracker = new MatomoTracker(config('services.matomo.site_id'), config('services.matomo.url'));
$tracker->setUrl('https://app.example.com/checkout/complete/1234');
$tracker->setIp(request()->ip());
$tracker->setUserAgent(request()->userAgent());
$tracker->setUserId((string) $user->id);
$tracker->doTrackEvent('Checkout', 'paid', 'order-1234', 149.00);

The canonical first use case is exactly this: server-side events the browser can't see (payment webhooks, queue jobs, refunds). Do not start by replicating page views the JS snippet already sends.

Implementation Patterns

Bind it scoped, not singleton. The tracker accumulates per-request state (IP, URL, custom dimensions, e-commerce items) and has no reset method. Under Laravel Octane a singleton will leak one request's visitor data into the next.

// AppServiceProvider::register()
$this->app->scoped(MatomoTracker::class, fn () => new MatomoTracker(
    (int) config('services.matomo.site_id'),
    config('services.matomo.url'),
));

For multi-tenant or multi-site apps, skip the container binding and use a small factory that takes a site ID — one MatomoTracker per site.

Wrap it. A thin Analytics service gives you a stable API, centralizes the setIp/setUserAgent/setUrl boilerplate, and — critically — gives you something mockable, because the package can't be faked by Http::fake() (it uses raw cURL).

class Analytics
{
    public function __construct(private MatomoTracker $tracker) {}

    public function event(string $category, string $action, ?string $name = null, ?float $value = null): bool
    {
        return $this->tracker->doTrackEvent($category, $action, $name, $value);
    }
}

Queue jobs for anything in a request cycle. Every doTrack* call is a blocking cURL round-trip (default 5s timeout). A queued job is the right home for analytics: it removes the latency from the user's request, and it isolates the failure. But remember to serialize the context you need, because $_SERVER won't be there (see Gotchas):

final class TrackOrderPaid implements ShouldQueue
{
    public function __construct(
        public int $orderId,
        public int $userId,
        public string $url,
        public ?string $ip,
        public ?string $userAgent,
    ) {}

    public function handle(MatomoTracker $tracker): void
    {
        $tracker->setUrl($this->url)
                ->setIp($this->ip)
                ->setUserAgent($this->userAgent)
                ->setUserId((string) $this->userId)
                ->doTrackEvent('Shop', 'order_paid', "order-{$this->orderId}");
    }
}

E-commerce orders: build items first, then the order. setEcommerceView() / addEcommerceItem() only queue data; the order is only sent when you call doTrackEcommerceOrder(). Use doBulkTrack() if you're emitting several events for one transaction.

$tracker->setUrl($orderUrl);
$tracker->addEcommerceItem($sku, $name, $category, $price, $qty);
$tracker->doTrackEcommerceOrder($orderId, $grandTotal, $subTotal, $tax, $shipping, $discount);

Custom dimensions for tenant/user attributes. Call these before the tracking call — they're read at send time, not at call time:

$tracker->setCustomDimension(1, (string) $tenant->id);
$tracker->setCustomDimension(2, $user->plan);
$tracker->doTrackPageView('Dashboard');

Persist the visitor ID yourself. Otherwise every request is a fresh visitor and your funnels are meaningless:

$visitorId = $request->session()->get('matomo_visitor_id')
    ?? tap(bin2hex(random_bytes(8)), fn ($id) => $request->session()->put('matomo_visitor_id', $id));

$tracker->setVisitorId($visitorId)->setUserId((string) $user->id);

Testing without HTTP. Don't hit the network in tests. Use the getUrlTrack* counterparts and assert against the query string — that's the cleanest seam the package offers:

$url = $tracker->getUrlTrackEvent('Shop', 'refund', 'order-42', 19.99);
parse_str(parse_url($url, PHP_URL_QUERY), $q);

$this->assertSame('Shop', $q['e_c']);
$this->assertSame('order-42', $q['e_n']);

Wrap those asserts in a single test helper so you only do the parsing once.

Gotchas and Tips

Every doTrack* fires immediately. There's no builder-and-flush. If you call doTrackPageView() and then set a custom dimension or user ID, it never reaches Matomo. Order matters: all setters first, tracker call last.

The tracker reads $_SERVER for everything you didn't set. IP comes from REMOTE_ADDR/X-Forwarded-For, user agent from HTTP_USER_AGENT, language from HTTP_ACCEPT_LANGUAGE, referrer from HTTP_REFERER, and the page URL from SERVER_NAME/REQUEST_URI. In a queue worker, an artisan command, or a webhook handler, all of that is empty — you'll silently send a page view with a blank url, which Matomo usually discards. In a queued context, always setUrl(), setIp(), setUserAgent() explicitly, and pass them through the job constructor.

Wrong scheme behind a proxy. When the tracker derives the URL itself it can guess http:// behind a TLS-terminating load balancer. Always call setUrl() with the canonical URL instead of relying on inference.

setVisitorId() throws on bad length. It requires exactly 16 hex characters (bin2hex(random_bytes(8))). Passing a raw UUID or a Str::random(16) will blow up. Also note setVisitorId() (tracking identity, _id) and setUserId() (uid) are different things — set both for cross-device stitching.

Http::fake() will not intercept this. The package uses curl_* directly, not Guzzle, so Laravel's HTTP fake is blind to it. Either point MATOMO_URL at a local stub server, or — better — mock your own Analytics wrapper and assert on calls.

Failures are silent. doTrack* returns true/false; it does not throw on a 4xx/5xx or a timeout. Never let tracking gate a business transaction, and log the boolean if you care.

Cookies and setcookie(). enableCookies() writes via PHP's setcookie(), which requires headers not yet sent. In a Laravel controller after a response has started streaming, it silently fails. For pure server-side tracking you generally want cookies off and the visitor ID persisted in your own session/cache instead.

sendImageResponse() calls header() and echoes a GIF. Fine for a beacon proxy route, but it will conflict with middleware that has already sent headers. Keep that route isolated and outside your normal middleware stack.

Custom variable scope. setCustomVariable($i, $n, $v, 'page') is consumed by the next page view and reset; 'visit' persists for the visit. Don't mix them up when debugging "why did this disappear after one hit". Prefer setCustomDimension() for anything new.

IP override may need token_auth. Some Matomo configurations only honour cip when the tracking request carries a token_auth with write access. If every visit shows your server's IP despite setIp(), call setTokenAuth() or check the instance's trust settings.

Do Not Track is read at construction. It inspects HTTP_DNT on instantiation; in a queue worker that's absent, so DNT is effectively bypassed. If you need to honour consent, gate the call yourself before building the tracker.

GDPR/consent is your responsibility. The package has no consent API. Server-side tracking easily sidesteps your cookie banner — wrap your Analytics service in a consent check rather than letting controllers call the tracker directly.

Timeout under load. setRequestTimeout() defaults to 5 seconds and this is a synchronous cURL call. If you must track inline, drop it to 1s so a slow Matomo never stalls your app.

Don't double-track page views. If the frontend already runs the Matomo JS snippet, mirroring every page view server-side inflates sessions and skews bounce rate. Reserve server-side for events the browser can't observe.

Escape hatch. setCustomTrackingParameter($key, $value) lets you send any raw Tracking API parameter the class doesn't wrap — useful for new Matomo params before the client catches up, and for experimentation.

PHP 8.x deprecations. This class is old, global-namespace, and not strictly typed. Pin a recent version, and if you see dynamic-property deprecations, check whether you're on a stale lock — the maintainers ship compatibility fixes, but Composer may be holding you back.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package