knplabs/knp-piwik-client
A PHP client for the Piwik (Matomo) analytics API, making it easy to query reports and pull metrics such as visits, page views, and conversions into your Laravel or PHP application.
This is a framework-agnostic, archived (last tag 2013) PHP client for the Piwik/Matomo HTTP API. There is no Laravel service provider, config file, facade, or published migrations — you wire it into the container yourself.
composer require knplabs/knp-piwik-client will likely fail on a modern Laravel app because it depends on Guzzle 3.x. Check the composer.json in the repo before trying; if it conflicts, composer require --dev it into a throwaway project to read the source, then copy the 2–3 real classes into app/Services/Analytics/.src/Knp/PiwikClient/Client.php (the API surface — essentially a call($method, array $params) wrapper) and src/Knp/PiwikClient/Connection/HttpConnection.php (how it talks to index.php?module=API). Everything else is exception plumbing.token_auth, and note the site IDs under Administration → Websites → Manage. The token needs at least view on the sites you'll query.https://matomo.example.com/ (the client appends index.php). Drop these into config/services.php under a matomo key and read from env().VisitsSummary.get with idSite, period=day, date=yesterday, and stash the result in cache.Wrap it, don't sprinkle it. Bind a single MatomoClient wrapper as a singleton in AppServiceProvider and expose typed methods (visitsYesterday(int $siteId): int, topPages(int $siteId, string $period): array). The upstream call() string-method API is easy to typo and hard to mock in tests; your wrapper gives you a seam for Http::fake() alternatives.
// AppServiceProvider::register()
$this->app->singleton(Analytics::class, fn () => new Analytics(
baseUrl: config('services.matomo.url'),
token: config('services.matomo.token'),
));
Use the connection as the extension point. ConnectionInterface is the one genuinely useful abstraction here: keep the Client logic and substitute a new HttpConnection built on Illuminate\Support\Facades\Http (or Guzzle 7). That gives you retries, timeouts, PSR-18, and Laravel test fakes without inheriting a 2013 Guzzle dependency.
Cache every report. Analytics data is not real-time-critical. Key the cache on md5($method . serialize($params)) with a 15–60 minute TTL:
return Cache::remember("matomo:{$site}:{$method}:".md5(serialize($params)), 1800, fn () => ...);
Batch by site, not by page. Loop over configured site IDs in a queued job or an artisan command (php artisan analytics:warm) and populate the cache on a schedule (->hourly()), so no user request ever waits on Matomo.
Know the common calls:
| Goal | Method | Key params |
|---|---|---|
| Visitors/summary | VisitsSummary.get |
idSite, period, date |
| Page rankings | Actions.getPageUrls |
+ filter_limit, filter_sort_column |
| Referrers | Referrers.getWebsites |
same |
| Site list | SitesManager.getAllSites |
— |
date accepts yesterday, today, last7, last30, YYYY-MM-DD, or YYYY-MM-DD,YYYY-MM-DD with period=range.
Segment support. Pass segment=... for filtered views; just make sure the segment string is part of your cache key or you'll cross-contaminate reports.
result: error / message field (e.g. "You can't access this resource") rather than a 4xx. Every wrapper method must inspect the payload, not just the status code.token_auth is a full-access secret and this client passes it as a query parameter. It will end up in access logs, proxies, and Storage::logs. Never log the request URL, and rotate the token if it leaks.var_dump on your first call and json_decode accordingly — don't assume an array.yesterday/today in the site's timezone, not config('app.timezone'). Set the site timezone in Matomo, or pass explicit dates calculated in that zone.today + period=day is the slowest query because the report archive is invalidated constantly. Use yesterday or last7 wherever you can, and cache harder for today.filter_limit=-1 returns "everything" and can produce multi-megabyte responses on busy sites. Use filter_limit=50 with filter_offset and page through.Http::withQueryParameters(...)->get("{$base}/index.php", ['module' => 'API', 'method' => ..., 'format' => 'json', ...]) is simpler, safer, and fully testable.How can I help you explore Laravel packages today?