Extending
Trend bucketing SQL is produced per database driver by a QueryExpression:
namespace RoundlyConsulting\Metrics\Types\Trend\QueryExpressions;
interface QueryExpression
{
// SQL that formats $column into the unit's bucket key
public function toSql(string $unit, string $column): string;
// SQL that moves the datetime $column by $minutes — re-reads it on the reporting clock
public function addMinutes(string $column, int $minutes): string;
}toSql() formats a column into the unit’s bucket key; addMinutes() shifts a column onto the reporting clock (see Timezones). To support another driver, implement both and map the class in config('metrics.trend_drivers') under the connection’s driver name — at runtime, config(['metrics.trend_drivers.oracle' => OracleExpression::class]). The config map is the only driver registry, used by class-based trends and Metrics::trend() alike:
namespace App\Metrics;
use RoundlyConsulting\Metrics\Exceptions\InvalidUnitException;
use RoundlyConsulting\Metrics\Types\Trend\QueryExpressions\QueryExpression;
final class SqlServerExpression implements QueryExpression
{
public function toSql(string $unit, string $column): string
{
return match ($unit) {
'DAY' => "CONVERT(char(10), {$column}, 23)", // Y-m-d
'MONTH' => "CONVERT(char(7), {$column}, 23)", // Y-m
'YEAR' => "CONVERT(char(4), {$column}, 23)", // Y
default => throw InvalidUnitException::for($unit),
};
}
public function addMinutes(string $column, int $minutes): string
{
return "DATEADD(minute, {$minutes}, {$column})";
}
}// config/metrics.php
'trend_drivers' => [
'mysql' => Mysql::class,
'mariadb' => Mysql::class,
'pgsql' => Postgres::class,
'sqlite' => Sqlite::class,
'sqlsrv' => App\Metrics\SqlServerExpression::class,
],$unit is the Unit value (MINUTE … YEAR) and $column the qualified date column. The keys your SQL emits must match the package’s PHP-side bucket keys character for character (Y-m-d for days, Y-m for months, o-W for ISO weeks — see Trends): gap filling looks each generated bucket up in the query result, and a mismatch silently reports 0. Throw InvalidUnitException for units you don’t support, as the built-in expressions do.
Custom windows
The built-in periods resolve through the Period enum. For a bespoke window, use a CUSTOM range with an explicit start and end: range(Period::Custom, $start, $end).
Recipe: an event → metric sink
Metrics read from your existing tables, so any other package’s domain events can feed a metric with no extra dependency — this is host wiring, not a package require. Point a metric at the model the event writes, or record a lightweight counter and count that:
use App\Models\Order;
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Metrics\Facades\Metrics;
// Register a metric over whatever table the events already populate.
Metrics::register('orders_today', fn () => Metrics::value()->count(Order::query())->range('TODAY'));
// Or fan a package's event into your own metrics table, then build a metric over it.
Event::listen(OrderPlaced::class, function (OrderPlaced $event): void {
MetricEvent::create(['name' => 'order_placed', 'occurred_at' => now()]);
});
Metrics::register('orders_placed', fn () => Metrics::trend()
->count(MetricEvent::query()->where('name', 'order_placed'), 'occurred_at')
->daily());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.