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:
| Method | Effect |
|---|---|
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.