Metric classes
For metrics you reuse across a dashboard, extend a base type and implement calculate(), which returns the matching result object:
| Type | Extend | calculate() returns | Good for |
|---|---|---|---|
| Value | RoundlyConsulting\Metrics\Types\Value\Value | ValueResult | A single number, optionally vs. the previous period |
| Trend | RoundlyConsulting\Metrics\Types\Trend\Trend | TrendResult | A time series (line/area chart) |
| Progress | RoundlyConsulting\Metrics\Types\Progress\Progress | ProgressResult | Current value vs. a target (progress bar) |
| Partition | RoundlyConsulting\Metrics\Types\Partition\Partition | PartitionResult | A grouped breakdown (pie/bar chart) |
Value
use App\Models\User;
use RoundlyConsulting\Metrics\Types\Result;
use RoundlyConsulting\Metrics\Types\Value\Value;
final class RegisteredUsers extends Value
{
protected function calculate(): Result
{
return $this->count(User::query());
}
}
RegisteredUsers::make()->range('TODAY')->toArray();Trend
use App\Models\User;
use RoundlyConsulting\Metrics\Types\Result;
use RoundlyConsulting\Metrics\Types\Trend\Trend;
final class RegisteredUsersTrend extends Trend
{
protected function calculate(): Result
{
return $this->count(User::query(), 'created_at');
}
}
RegisteredUsersTrend::make()->hourly()->range('TODAY')->toArray();Progress
A Progress metric compares the current value to a target (default 100). Configure its defaults once in setup():
use App\Models\Subscription;
use RoundlyConsulting\Metrics\Types\Progress\Progress;
use RoundlyConsulting\Metrics\Types\Result;
final class MonthlyRevenueGoal extends Progress
{
protected function setup(): void
{
$this->target(10000)->range('MTD');
}
protected function calculate(): Result
{
return $this->sum(Subscription::query(), 'amount');
}
}Use shouldBeAvoided() when the target is a ceiling to stay under — an error budget or a spend cap — rather than a goal to reach. It surfaces as avoid in the result so the UI can colour it accordingly:
use App\Models\Incident;
use RoundlyConsulting\Metrics\Types\Progress\Progress;
use RoundlyConsulting\Metrics\Types\Result;
final class ErrorBudget extends Progress
{
protected function setup(): void
{
$this->target(50)->shouldBeAvoided()->range('MTD'); // stay under 50 this month
}
protected function calculate(): Result
{
return $this->count(Incident::query());
}
}
// result => [..., 'target' => 50.0, 'avoid' => true, ...]Partition
use App\Models\User;
use RoundlyConsulting\Metrics\Types\Partition\Partition;
use RoundlyConsulting\Metrics\Types\Result;
final class UsersByPlan extends Partition
{
protected function calculate(): Result
{
// count of users grouped by the `plan` column
return $this->count(User::query(), 'plan');
}
}Defaults in setup()
setup() runs in the constructor, after the config defaults. Configure presentation and behaviour there once instead of at every call site — fluent calls on the instance still win:
use App\Models\Order;
use RoundlyConsulting\Metrics\Enums\Period;
use RoundlyConsulting\Metrics\Types\Result;
use RoundlyConsulting\Metrics\Types\Value\Value;
final class Revenue extends Value
{
protected function setup(): void
{
$this->withChangeAgainstPreviousPeriod();
$this->name('Revenue')
->description('Paid orders in the selected period')
->prefix('$')
->precision(2)
->range(Period::MonthToDate);
}
protected function calculate(): Result
{
return $this->sum(Order::query()->where('status', 'paid'), 'total');
}
}
// setup() runs in the constructor, so call-site fluent calls still win:
Revenue::make()->range(Period::LastMonth)->toArray();Shared fluent API
Every metric — class-based or inline — extends the abstract RoundlyConsulting\Metrics\Metric class and exposes:
| Method | Effect |
|---|---|
name(string $name) | Display name; defaults to the humanized class name. |
description(string $description) | Free-text description in the envelope. |
prefix(string $prefix) / suffix(string $suffix) | Display prefix and suffix, e.g. a currency sign or a unit. |
precision(int $precision = 0, RoundingMode $mode = RoundingMode::HalfAwayFromZero) | Rounding precision and mode for every value. |
range(Period|string $range, ?string $customRangeStart = null, ?string $customRangeEnd = null) | Date range — see Date ranges. |
timezone(?string $timezone) | Resolve this metric’s ranges and label its trend buckets in an explicit timezone — see Date ranges. |
ranges(): array | The selectable range keys and labels. |
formatUsing(Closure $formatter) | Add a formatted value to the result — see Number formatting. |
cache(), cacheFor(), cacheKey(), dontCache() | Per-metric caching — see Caching. |
key(): ?string | The registry key it was resolved under, or null for ad-hoc metrics. |
make(mixed ...$arguments): static | Static constructor. |
result(): Result | The typed result object — calculated, or restored from the result cache. |
toArray(): array | Resolve and return the envelope. |
toResponse($request): JsonResponse | The envelope as a JSON response (Responsable). |
precision() takes PHP’s native RoundingMode enum, e.g. ->precision(2, RoundingMode::HalfEven). By default a metric’s name is the humanized class name (RegisteredUsers → “Registered Users”).
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.