eliseekn/laravel-metrics
Generate metrics and trends from your Laravel models for dashboards. Group counts and sums by day, month, or year, set custom date/label columns and tables, use date ranges, and query via Eloquent or the query builder. Supports MySQL, PostgreSQL, and SQLite.
Architecture fit
Eloquent\Builder or QueryBuilder instance — nothing is persisted, no migrations, no service container bindings to manage, no events or queues. It sits as a read-only projection layer between your models and whatever renders the dashboard.GROUP BY aggregates, and the caller shapes the result for Chart.js/ApexCharts/Filament widgets.selectRaw('COUNT(*) ... group by ...'), and scope it accordingly.HasMetrics trait is convenient but couples models to the package; prefer the static LaravelMetrics::query() entry point so the dependency stays in the presentation/service layer.Integration feasibility
composer require and go. Pure PHP, PHP ^8.2, Laravel ^11/^12 (Laravel 13 only on the separate 4.x branch). MySQL, PostgreSQL, and SQLite supported. No config publishing, no asset pipeline, no queue worker changes. Integration effort is measured in hours, not sprints.->count()->byMonth(6)->trends()) is discoverable and self-documenting; the combined shortcuts (countByMonth, sumBetween) reduce boilerplate further. Low cognitive load for mid-level Laravel developers.Technical risk
2.x, 3.x, and 4.x, with Laravel 13 support living only on 4.x. The mainline constraint targets Laravel 11/12. Choosing a version is a support-matrix decision, not a routine composer update.groupData() requires argument order to match data order — the README itself warns "follow same order to avoid false data." Missing buckets yield gaps unless fillMissingData() is called. labelColumn() with high cardinality can explode result sets. trends() vs metrics() vs metricsWithVariations() have different return shapes. These are not crash-on-deploy risks; they are wrong-chart-in-a-board-meeting risks.table(), dateColumn(), and labelColumn() accept string identifiers that get interpolated into raw SQL. Since values are developer-supplied rather than user-supplied, this is manageable — but any path that lets a user influence those strings must be treated as injection surface.DATE_FORMAT, EXTRACT) generally defeat plain B-tree indexes, and there is no result caching. On multi-million-row tables, a dashboard with eight widgets becomes eight full-range aggregates per page load.Key questions for the maintainer / spike
4.x intended as the long-term line, and will ^11|^12 constraints on the default branch be reconciled with Laravel 13?config('app.timezone') or the DB session timezone? Off-by-one-day bugs at month and year edges are the most common dashboard defect and the hardest to spot.Stack fit
app/Models, existing migrations, and existing auth scoping — you pass an already-constrained builder (->where('user_id', auth()->id())) and the package aggregates within that scope.Migration path
App\Support\Metrics\DashboardMetrics) so the package never appears in controllers or views. This makes a future swap to hand-written SQL, a different package, or a fork a one-file change, and it gives you a single place to add caching and tenant scoping.composer remove plus deleting one wrapper class. This is the strongest argument for adopting it for low-stakes internal dashboards.Compatibility
composer.json. Do not float on ^ across a package that ships breaking structural changes between major lines and whose minors carry bug fixes to core data shaping.4.x, which is a different code line from the one most README examples were written against.Sequencing
DashboardMetrics service, enforce tenant scoping there, add an explicit fillMissingData() convention, and forbid raw user input into table/labelColumn/dateColumn at the wrapper boundary.Cache::remember on the wrapper keyed by (widget, scope, period, timezone), with TTLs tuned per widget. This is not optional at any meaningful traffic level.Maintenance
fillMissingData, groupData, labelColumn, and each DB driver before bumping.Support
Scaling
(scope_column, date_column) for every metric source; (3) bound every query with the narrowest possible period — never let a default "current year" silently become "all time"; (4) route dashboard reads to a replica; (5) only if still insufficient, introduce nightly rollup tables and have the wrapper read those instead of source tables.labelColumn() cardinality. Grouping by a high-cardinality column (user_id, uuid) will produce large result sets that get serialized into a chart payload. Use groupData() with an explicit bucket list for status-like columns instead.Failure modes
fillMissingData() is applied. Charts render "correctly" and are simply wrong.groupData() with mismatched argument order maps values to the wrong labels, with no error. Add an assertion comparing the returned label set to the requested set.23:59 on the last day of a month.fillMissingData label-discovery defect has shipped in at least four releases. Assume any minor bump can regress it and gate upgrades on tests.byYear() over an unindexed column on a large table is a straightforward path to a dashboard-driven outage.Ramp-up
groupData, the difference between trends/metrics/metricsWithVariations return shapes, and the interaction between between() and groupBy*(). Capture these in a short internal "gotchas" note alongside the wrapper so the knowledge is institutional rather than personal.How can I help you explore Laravel packages today?