Skip to main content

Laravel Octane

The package is Octane-safe, and there is nothing to configure on your side.

Under Octane a worker process lives across many requests, so a service that holds request state in a property is a state leak: request two sees request one's data. For an analytics package that would mean hits attributed to the wrong visitor — a bug that produces plausible-looking wrong data rather than an error.

Every request-stateful service here is bound scoped rather than singleton:

  • the tracker, which holds the pending hits of the current request
  • the in-memory buffer driver
  • the reporting client, the GDPR client and the annotations client, which hold the last error

Octane resets scoped instances between requests, so each request starts with a clean one. Within a classic request lifecycle, scoped behaves exactly like singleton, so nothing about non-Octane deployments changes.

Services that hold no request state — the payload builder, the sender, the bot detector, the gate, the report cache — stay shared, because sharing them is free.

What shared does not mean

Shared is a statement about request state, not about configuration. The connection details — host, site id, token, timeouts — are read once, when something first resolves them, and every service that needs them holds that same object afterwards. Where the package's configuration is the same for every request, which is the ordinary case, this is invisible and costs nothing.

It becomes visible in one deployment shape: an application that changes this package's configuration per request or per queued job, which is what a multi-tenant application does when each tenant tracks into its own Matomo site. A long-lived worker keeps the first values it resolved, because a shared binding is not rebuilt when the configuration underneath it changes. Restarting the worker picks the new values up; nothing else does.

If that describes your application, do not rely on forgetInstance() for the connection alone — the payload builder, the sender and the gate each hold a reference to the object that was current when they were built, so forgetting only the connection leaves them on the old one. Track per tenant through a tracking gate and custom dimensions, which read live configuration on every hit, rather than by swapping the connection underneath a running worker.

It is tested, not asserted

The claim above is covered by a test that drives Octane's between-request reset (forgetScopedInstances()) directly and asserts that nothing carries over. That is the difference between "we bound these scoped" and "no state leaks": the binding is the mechanism, the test is the property.

What to watch in your own code

The one way to reintroduce the problem is to resolve the tracker once and hold onto it:

// Wrong under Octane: $tracker outlives the request.
public function __construct(private Tracker $tracker) {}

If that constructor belongs to a singleton of your own, you have captured a request-scoped service in a long-lived object. Use the facade, or resolve it when you need it:

Matomo::pageView('Pricing');

The facade resolves through the container on each call, so it always gets the current request's instance.

Buffer drivers

The array buffer driver is scoped for the same reason, but it is still the wrong choice in production under Octane — or anywhere else. It holds hits in memory, and a worker restart loses them. Use database, redis or file; see transmission modes.