Skip to main content

Queries and segments

get() takes a parameter array, which is fine for one report and awkward for a dashboard. query() builds the same call fluently, and runs through the same cache and resilience path:

use MatomoAnalytics\Facades\MatomoReports;

$pages = MatomoReports::query('Actions.getPageUrls')
->period('month')->date('2026-01')
->segment('deviceType==smartphone')
->sortBy('nb_visits')->limit(10)
->flat()
->get();

Nothing happens until get(). The builder is a description of a request, so you can pass a partly-built query around and finish it elsewhere. toArray() gives you the parameter array it would send, which is what you want when a report comes back unexpected — but it is the request, not the response, so it says nothing about what Matomo did with it.

The filters​

They map one-to-one onto Matomo's own report parameters:

MethodEffect
period($period) · date($date)The date range — day, week, month, year, range
segment($definition)A raw definition, a named segment, or a Segment builder
limit($n) · offset($n)Paginate the rows
sortBy($column, $order)Sort; $order defaults to desc
search($pattern, $column)Keep the rows whose label, or $column, matches $pattern. Matomo reads it as a regular expression and ignores case; preg_quote($text) matches text as it is
truncate($rows)Collapse everything past $rows into an "Others" row
flat()Flatten a hierarchical report into one level
expanded()Return the full subtable hierarchy
showColumns([...]) · hideColumns([...])Restrict the columns returned
params([...])Anything else Matomo accepts

flat() is the one worth knowing about: page-URL reports are hierarchical by path segment, so without it Actions.getPageUrls gives you top-level folders rather than pages. truncate() is how you keep a "top 10 plus Others" widget honest — the Others row carries the remainder rather than dropping it.

params() is the escape hatch for a report parameter the builder does not model, the same way CustomParameters is on the write side.

Segments​

A segment restricts a report to a subset of visits. There are three ways to supply one, and they exist for three different situations.

A raw definition​

MatomoReports::query('VisitsSummary.get')->segment('visitCount>1;actions>=3')->get();

Matomo's own syntax: ; is AND, , is OR. Use this when you copied a segment out of the Matomo UI.

The builder​

use MatomoAnalytics\Reporting\Segment;

$segment = Segment::where('deviceType', '==', 'smartphone')->andWhere('visitCount', '>', 1);

MatomoReports::query('VisitsSummary.get')->segment($segment)->get();

where() starts a segment, andWhere() and orWhere() extend it. Use this when the segment is composed from application state — a filter a user picked, a tenant identifier — because it encodes the values rather than concatenating strings.

Matomo reads a segment as a list of AND conditions, each of which may hold OR alternatives: it splits the definition at ; first and each part at , after that. So orWhere() binds tighter than andWhere(), the reverse of SQL and of Eloquent's where() and orWhere(). orWhere() adds an alternative to the condition just before it, and andWhere() starts a new condition:

// deviceType==smartphone;countryCode==de,countryCode==at
// Matomo reads it as: smartphone AND (de OR at)
Segment::where('deviceType', '==', 'smartphone')
->andWhere('countryCode', '==', 'de')
->orWhere('countryCode', '==', 'at');

A segment has no parentheses, so (smartphone AND de) OR at has no direct form. Write it as (smartphone OR at) AND (de OR at), which selects the same visits, or run two queries:

Segment::where('deviceType', '==', 'smartphone')
->orWhere('countryCode', '==', 'at')
->andWhere('countryCode', '==', 'de')
->orWhere('countryCode', '==', 'at');

Each value is matched exactly as given. Matomo decodes a value up to three times and splits the definition at , and ; after the first decoding, so the builder encodes every value three times: a , or ; in it cannot add a condition, and a +, a % or a line break reaches Matomo unchanged. Pass values as they are. A value you URL-encoded yourself is matched literally, %40 and all.

Operators are validated against Matomo's set: equality and inequality (==, !=), the comparisons (<, <=, >, >=), contains and does-not-contain (=@, !@), and starts-with and ends-with (=^, =$). An unsupported operator throws InvalidArgumentException rather than being sent as a segment Matomo will silently misread. So does a dimension that holds a character Matomo reads as syntax: ;, ,, \, =, !, <, >, @, ^, $, %, + or a line break.

That is the right default for an operator you wrote, and the wrong one for an operator a visitor picked: an unvalidated value from a request turns into a 500. Validate the operator against the list above before it reaches the builder — Rule::in([...]) in a form request is enough — or catch the exception where you build the segment.

A named segment​

Register the segments you use repeatedly in config and reference them by key:

// config/matomo-analytics.php
'reporting' => [
'segments' => [
'mobile' => 'deviceType==smartphone',
'engaged' => 'visitCount>1;actions>=3',
],
],
MatomoReports::query('VisitsSummary.get')->segment('engaged')->get();

segment() resolves a string against the registry first and falls back to treating it as a raw definition, so both forms work through the same method. This is the right home for the segments your dashboards depend on: the definition lives in one place, and a call site reads as the question it is asking rather than as Matomo syntax.

A segment Matomo has never seen may need to be processed before it returns data, depending on your Matomo's archiving configuration. If a brand-new segment returns empty where the UI shows rows, that is a Matomo-side archiving question rather than a package one.