Skip to main content

Contracts

Every service is bound to an interface in MatomoAnalytics\Contracts, so any of them can be swapped in your own service provider. Most applications never need to; the ones worth knowing about are marked below.

// in a service provider's register() method
use MatomoAnalytics\Contracts\VisitorIdResolver;

$this->app->singleton(VisitorIdResolver::class, MyOwnVisitorId::class);

Bindings that hold request state are registered scoped, which matters under Octane — see Laravel Octane. Rebind with the same lifetime the package used.

Tracker

The write side. Matomo is its facade, and the fake implements the same interface.

public function track(Hit $hit): static;
public function aiChatbot(?Request $request = null): static;
public function pageView(string $title, ?string $url = null): static;
public function event(string $category, string $action, ?string $name = null, int|float|null $value = null): static;
public function contentImpression(string $name, ?string $piece = null, ?string $target = null): static;
public function contentInteraction(string $interaction, string $name, ?string $piece = null, ?string $target = null): static;
public function siteSearch(string $keyword, ?string $category = null, ?int $count = null): static;
public function searchFromRequest(?Request $request = null, string $keywordKey = 'q', ?string $categoryKey = null, ?int $count = null): static;
public function goal(int $id, ?float $revenue = null): static;
public function ecommerceView(?string $sku = null, ?string $name = null, ?string $category = null, ?float $price = null, ?string $title = null, ?string $url = null): static;
public function ecommerceCartUpdate(float $grandTotal, array $items = []): static;
public function ecommerceOrder(string $orderId, float $grandTotal, array $items = [], ?float $subTotal = null, ?float $tax = null, ?float $shipping = null, ?float $discount = null): static;
public function download(string $url): static;
public function outlink(string $url): static;
public function ping(): static;
public function flush(): void;

Every tracking method returns the tracker, so calls chain. flush() is the exception — it returns void, and it is called for you when the framework terminates the request, so you do not normally call it at all.

Type against this interface rather than the facade when you inject the tracker — but see the Octane note about holding onto the instance.

Hit

A single trackable action — MatomoAnalytics\Tracking\Hit, the one interface on this page that does not live in the Contracts namespace, because it is a value type rather than a service. A hit carries only its action-specific parameters; request context and visitor identity are layered on afterwards.

interface Hit
{
/** @return array<string, scalar> */
public function toParams(): array;
}

Worth implementing. This is how you add a hit type the package does not model, without touching the package: implement Hit, return the Tracking-API parameters, and pass it to Matomo::track(). The gate, redaction, batching and fail-safe delivery all apply to it automatically.

For a one-off parameter, CustomParameters is the lighter option — it decorates an existing hit rather than defining a new one.

TrackingGate

The single decision point for "is this request tracked?".

public function decide(Request $request, Hit $hit): GateDecision;

GateDecision::allow() or GateDecision::deny($reason); the reason string is what the VisitorExcluded event carries.

Rarely worth replacing. Replacing the gate discards every shipped rule — bot detection, Do-Not-Track, the opt-out cookie, the exclusion lists — and you have to reimplement them. The tracking.gate config hook composes with the shipped gate instead, and that is almost always what you want. See the tracking gate.

BotDetector

public function isBot(string $userAgent): bool;
public function isAiCrawler(string $userAgent): bool;
public function isAiChatbot(string $userAgent): bool;

The three questions are separate because the answers are used differently: isBot() gates visits, isAiCrawler() classifies training crawlers, and isAiChatbot() decides what counts as an on-demand assistant fetch.

Rarely worth replacing. The bots.detector config hook adds a layer without discarding the shipped ones, and DeviceDetectorBotDetector already wraps Matomo's own catalog. See bots and AI crawlers.

VisitorIdResolver

/** Resolve a 16-character lowercase hex Matomo visitor id for the request. */
public function resolve(Request $request): string;

Worth replacing if you have a better identifier than the cookieless hash — a stable pseudonymous id you already assign, for instance. The return value must be exactly 16 lowercase hex characters, which is what Matomo accepts.

Replacing this changes what a "visitor" means in every report you have, so do it before you accumulate history rather than after.

Sender

/** @param list<array<string, scalar>> $payloads */
public function send(array $payloads): SendResult;

One payload goes as a single form POST; several go as one Bulk request. SendResult carries the HTTP status and whether it failed.

The package ships HttpSender and NullSender — the latter is what matomo:load-sim uses to measure the client without sending anything.

HitBuffer

The cross-request buffer behind batch mode. Delivery is at-least-once: claim a batch, send it, then acknowledge on a confirmed 200 or release it back on failure.

/** @param array<string, scalar> $payload */
public function push(array $payload): void;
public function size(): int;
public function claim(int $limit): BufferBatch;
public function ack(BufferBatch $batch): void;
public function release(BufferBatch $batch): void;

Worth implementing for a store the package does not ship — a message queue, a cloud-native log. The contract's guarantee is the claim: an implementation must not hand the same payload to two concurrent claims, and must make an unacknowledged claim recoverable. See transmission modes.

ReportClient

The read side, behind the MatomoReports facade. Two halves: a five-method protocol, and the curated shortcuts.

/** @param array<string, scalar> $params */
public function get(string $method, array $params = []): ?array;
public function query(string $method): ReportQuery;
/** @param list<string|array<string, scalar>> $requests */
public function bulk(array $requests): array;
public function flushCache(): void;
public function lastError(): ?string;

Below those the contract declares the 22 shortcuts the facade advertises — visitsSummary(), liveCounters(), topPageUrls(), countries(), funnelFlow(), cohorts() and the rest. They are declared so that injecting the contract and calling the facade give the same answer:

public function __construct(private readonly ReportClient $reports) {}

// …and this resolves, autocompletes and type-checks:
$this->reports->visitsSummary();

Implementing it is still a five-method job. Every shortcut is one call to get() with a fixed Matomo method name, and ResolvesCommonReports supplies all 22 from get() alone:

final class MyReportClient implements ReportClient
{
use ResolvesCommonReports;

// implement get(), query(), bulk(), flushCache(), lastError() — the shortcuts come free
}

The ?array return is the whole error convention: null means the call failed and lastError() says why. A failed result is never cached. See reporting.

GdprClient

public function findDataSubjects(string $segment, int|string|null $site = null): ?array;
public function forget(string $segment, int|string|null $site = null): ?array;
public function export(string $segment, int|string|null $site = null): ?array;
/** @param list<array{idsite: int, idvisit: int}> $visits */
public function deleteVisits(array $visits): ?array;
public function exportVisits(array $visits): ?array;
public function lastError(): ?string;

Admin, destructive, never cached. null is failure; [] is success with nothing matched. See GDPR requests.

AnnotationsClient

public function add(string $note, ?string $date = null, bool $starred = false, int|string|null $site = null): ?array;
public function annotateRelease(?string $version = null, ?string $date = null): ?array;
public function lastError(): ?string;

Uses the same token-safe POST transport as the reporting client, and is never cached. See release annotations.

What is not a contract

PayloadBuilder, Connection, UrlRedactor, Snippet, ReportCache and the buffer support classes are concrete. They are internal composition rather than extension seams, so their signatures can change in a way the interfaces above will not. If you find yourself needing to replace one, that is worth raising as an issue — a missing seam is a gap in the package, not something to work around locally.