NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

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 crypto

By 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.