syrian-open-source/laravel-relational-metrics
A Laravel package for calculating relational metrics from Eloquent models, letting you define and query counts, sums, averages, and other aggregates for dashboards, reports, and insights with clean, reusable code.
Install
composer require syrian-open-source/laravel-relational-metrics
Laravel's package auto-discovery should register the service provider. If the repo ships a config file, publish it before you write your first metric:
php artisan vendor:publish --provider="SyrianOpenSource\RelationalMetrics\RelationalMetricsServiceProvider"
First place to look: the README.md for the exact facade/class name and method signatures, then src/ (there is usually a single Metric/Metrics class plus a service provider). The package is small and last tagged in March 2022, so reading the source is faster than trusting blog posts.
First use case: replace a hand-written Model::where(...)->count() buried in a controller with a named, reusable metric. The canonical first metric is "count of a model matching a condition", then "count of a model through a relation", then "aggregate (sum/avg) over a relation".
// Confirm the exact entry point in the README; the shape is roughly:
use SyrianOpenSource\RelationalMetrics\Facades\RelationalMetrics;
RelationalMetrics::model(Order::class)->count();
RelationalMetrics::model(Order::class)->where('status', 'paid')->count();
RelationalMetrics::model(User::class)->relation('orders')->count();
If the package exposes a trait, put it on the models you metric most:
use SyrianOpenSource\RelationalMetrics\Concerns\HasMetrics;
class Order extends Model
{
use HasMetrics;
}
Sanity-check the install with a tinker session (php artisan tinker) against a seeded table before wiring it into a dashboard — that catches config/connection assumptions immediately.
1. Named metrics instead of scattered queries. Wrap each KPI in a small class or a dedicated method so the definition lives in one place and can be reused by HTTP controllers, console commands, and widgets.
class PaidOrdersThisMonth
{
public function __invoke(): int
{
return RelationalMetrics::model(Order::class)
->where('status', 'paid')
->whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()])
->count();
}
}
2. Relation-scoped metrics. The package's distinguishing feature is measuring through relations — e.g. "average order total per customer", "users with at least one failed payment". Model this by chaining the relation name onto the metric builder rather than pre-loading models in PHP.
// Count related rows without hydrating them
RelationalMetrics::model(User::class)->relation('posts')->count();
// Conditional metrics *inside* the relation
RelationalMetrics::model(User::class)
->relation('orders')
->where('orders.status', 'refunded')
->count();
3. Conditional / multi-condition metrics. Use this when a dashboard needs several numbers from the same base model in one request. Define them side by side so a reviewer can see the differences:
$metrics = [
'total' => RelationalMetrics::model(Order::class)->count(),
'paid' => RelationalMetrics::model(Order::class)->where('status', 'paid')->count(),
'refunded' => RelationalMetrics::model(Order::class)->where('status', 'refunded')->count(),
];
If the underlying implementation returns a builder you can reuse, prefer cloning one base query (clone $base) over rebuilding from scratch three times.
4. Aggregates, not just counts. Sums/averages over relations are the natural second tier: revenue per merchant, average rating per product. Keep the aggregate column name explicit and qualify it with the table when joining (orders.total, not total).
5. Cache the expensive ones. Metrics over large tables or nested relations should sit behind Cache::remember() with a key that encodes the metric name and its parameters, and be invalidated by model events:
Cache::remember("metrics:paid_orders:{$month}", 300, fn () => (new PaidOrdersThisMonth)());
6. Integrate at the edges. Call metrics from API resource collections, dashboard widgets (Filament/Nova cards), and scheduled report commands — not from model accessors and not from Blade. Keeping metrics out of serialization paths avoids accidental N+1s.
7. Test at the query level. Because a metric is just a query, assert against seeded data:
Order::factory()->count(3)->create(['status' => 'paid']);
Order::factory()->count(2)->create(['status' => 'pending']);
$this->assertSame(3, RelationalMetrics::model(Order::class)->where('status', 'paid')->count());
composer.json constraints for laravel/framework and php before adding it to a Laravel 10/11 app; you may need --ignore-platform-reqs (a red flag) or a fork. Pin the version and add a comment in your composer.json rationale.->relation(...)->count() executed per row in a loop is still N queries. For "counts per parent", prefer native withCount()/withSum() and reserve this package for cross-cutting, conditional, or dashboard-style metrics.SoftDeletes and any global scopes apply — which may or may not be what your KPI means. Verify by comparing the metric to a raw DB::table(...)->count() on a table with soft-deleted rows.where('status', ...) can become ambiguous and throw a SQL error. Always prefix with the table name in relational metrics.SUM(CASE WHEN ...) or FILTER (WHERE ...)) behave differently on SQLite vs MySQL vs Postgres. Run your metric test suite against the same driver you deploy on; SQLite green does not guarantee Postgres green.src/ directory as the source of truth. If a method you expect doesn't exist, wrap the builder in your own thin helper rather than forking — you keep upgrade freedom.Builder, use ->toSql()/->getBindings() (or DB::listen) on the metric to see the real query when numbers look wrong. Most "bug reports" against metric packages are wrong scopes, not wrong packages.created/updated/deleted events on the base and related models to bust metric cache keys, otherwise dashboards silently drift.Metric contract, extend it instead of calling the facade inline; that gives you one place to add caching, logging, and default date ranges across every KPI. If it only offers a facade, create your own App\Metrics\* classes that delegate to it — that insulation is what makes replacing the package later cheap.How can I help you explore Laravel packages today?