spatie/laravel-stats
Lightweight Laravel package that summarizes database changes over time. Define a stats class, call increase() or decrease() as events happen, then query totals grouped by day, week, or month, returning increments, decrements, and net differences for each period.
increase/decrease/set plus a grouped query gets you a defensible trend line for the cost of one Composer dependency and one migration.SubscriptionStats class is a deliberate product statement: this metric is a first-class KPI, owned by the app, versioned with the codebase, and queryable by any future feature (dashboards, admin panels, customer-facing reporting, internal alerting). It makes "we should track X" a one-hour ticket instead of a vendor-integration project.DataPoint carries value, increments, decrements, and difference per period. That distinction drives real decisions: a flat subscription count with high increments and high decrements is churn-masked-by-acquisition, and the data model surfaces it without extra work.StatsWriter::for($tenant->orderStats(), ['payment_type' => 'recurring']) and the matching StatsQuery::for(...) let you scope a metric per customer, per plan, or per channel. This is the difference between "revenue is up" and "revenue is up only in the self-serve segment, and declining in enterprise" — an actual roadmap input.increase(1, $subscription->created_at) writes an event as if it happened in the past. You can seed history from existing tables on launch day and ship a chart with twelve months of trend instead of an empty graph and a three-month waiting period.set($count) handles metrics your app doesn't own (Stripe subscription counts, warehouse job totals, third-party API quotas) — useful when deciding whether to trust a vendor's billing dashboard or your own numbers, and for detecting drift between them.Adopt when:
increase/decrease in the same transaction or job as the domain event), or you can poll an external number into set().type/value/timestamps table; migrating off means writing one export query.Look elsewhere when:
set() from the authoritative source, or don't use this at all for that number.decrease called without a matching increase (missed webhook, retried job, refund) silently corrupts the running value. You need idempotency keys or reconciliation jobs around it — budget for that.Operational caveats to plan for: define a retention/pruning policy before the table grows unbounded; index type and the timestamp column; decide whether stats writes belong in the same transaction as the domain change (consistency) or in an async job (latency); and document, per metric, exactly which domain events are allowed to call increase and decrease, because that invariant is the entire correctness model.
For executives: We need trend data on a handful of core numbers — subscriptions, active seats, order volume — and today we either don't have it or pay a vendor for it. This is a free, MIT-licensed Laravel component that stores those counters in our own database and returns them grouped by day, week, or month. It replaces, for our narrow use case, a five- or six-figure annual analytics contract, keeps the data in-house (no new data-processing agreement, no PII leaving our systems), and takes roughly a day to integrate per metric. Crucially, it is a building block, not a lock-in: the underlying table is plain and exportable, and the whole thing can be removed or replaced without touching product features. The trade-off is scope — it gives us counts over time, not user-level behavioural analytics or funnels. If we later need those, we still buy the bigger tool; this doesn't foreclose that decision, it just stops us from buying it prematurely.
For engineering: Small, well-maintained Spatie package, MIT, actively released and tracking current Laravel and PHP versions, with MySQL, PostgreSQL, and SQLite support. Integration is a published migration, a BaseStats subclass, and increase/decrease/set calls at the domain events — plus a fluent query builder for period grouping with increments, decrements, and net difference. It supports scoping to Eloquent models and HasMany relationships, which covers per-tenant reporting, and accepts explicit timestamps so we can backfill history from existing tables. No UI, no queues, no external services, no new infrastructure. Main engineering obligations: define the write-path invariants per metric so counters can't drift, add indexes and a retention policy, and treat high-volume counters as a separate problem. Low blast radius — one table, one trait, and the code is small enough to fork if we ever need to.
How can I help you explore Laravel packages today?