|
| 1 | +--- |
| 2 | +name: stats-development |
| 3 | +description: Create, register, and filter time-series stat repositories using javaabu/stats. |
| 4 | +--- |
| 5 | + |
| 6 | +# Stats Development |
| 7 | + |
| 8 | +## When to use this skill |
| 9 | + |
| 10 | +Use when creating stat repositories, registering metrics, adding filters, or setting up stats routes with `javaabu/stats`. |
| 11 | + |
| 12 | +## Core Principle: Use What Exists |
| 13 | + |
| 14 | +The package provides built-in controllers, routes, middleware, formatters, and export. Your job is to generate stat repository classes for each model — not to reimplement infrastructure. Specifically: |
| 15 | + |
| 16 | +- **Use the Artisan generator** to scaffold stat classes — it auto-registers them too |
| 17 | +- **Use `registerApiRoute()` / `registerRoutes()`** — never write custom route handlers for stats |
| 18 | +- **Use built-in formatters** (`default`, `chartjs`, `sparkline`, `flot`, `combined`) before creating custom ones |
| 19 | +- **Use the `stats.view-time-series` middleware** — it's auto-registered, don't recreate auth logic |
| 20 | +- **Use `ExportsTimeSeriesStats` trait** in existing controllers for CSV export — don't build export from scratch |
| 21 | + |
| 22 | +When refactoring existing stats code, check for: direct filter class instantiation (should use `StatsFilter` factory), manual route definitions (should use `registerApiRoute`/`registerRoutes`), and reimplemented formatting or export logic. |
| 23 | + |
| 24 | +## Creating a Stat |
| 25 | + |
| 26 | +Place stat classes in `app/Stats/TimeSeries/`. Extend `CountStatsRepository` for row counts or `SumStatsRepository` for numeric sums. One class per model/metric — each stat targets a single table. |
| 27 | + |
| 28 | +**Quick path — Artisan generator (auto-registers in AppServiceProvider):** |
| 29 | + |
| 30 | +```bash |
| 31 | +php artisan stats:time-series OrdersCount Order --type=count |
| 32 | +php artisan stats:time-series PaymentAmounts Payment --type=sum |
| 33 | +``` |
| 34 | + |
| 35 | +**Manual — Count stat with filters** (namespace `App\Stats\TimeSeries`, import `StatsFilter`, `CountStatsRepository`, `Builder`): |
| 36 | + |
| 37 | +```php |
| 38 | +class OrdersCount extends CountStatsRepository |
| 39 | +{ |
| 40 | + public function query(): Builder { return Order::query(); } |
| 41 | + public function getTable(): string { return 'orders'; } |
| 42 | + public function getAggregateFieldName(): string { return 'count'; } |
| 43 | + |
| 44 | + public function allowedFilters(): array |
| 45 | + { |
| 46 | + return [ |
| 47 | + StatsFilter::exact('customer', 'customer_id'), |
| 48 | + StatsFilter::exact('status', 'status'), |
| 49 | + ]; |
| 50 | + } |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +**For a sum stat**, extend `SumStatsRepository` instead and add one extra method: |
| 55 | + |
| 56 | +```php |
| 57 | +public function getFieldToSum(): string |
| 58 | +{ |
| 59 | + return 'amount'; |
| 60 | +} |
| 61 | +``` |
| 62 | + |
| 63 | +## Registering Stats |
| 64 | + |
| 65 | +Register in `AppServiceProvider::boot()`. Metric names must be snake_case. |
| 66 | + |
| 67 | +```php |
| 68 | +use Javaabu\Stats\TimeSeriesStats; |
| 69 | + |
| 70 | +public function boot(): void |
| 71 | +{ |
| 72 | + TimeSeriesStats::register([ |
| 73 | + 'orders_count' => OrdersCount::class, |
| 74 | + 'payment_amounts' => PaymentAmounts::class, |
| 75 | + ]); |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +To suppress built-in user_signups/user_logins stats: `TimeSeriesStats::excludeDefaultStats();` |
| 80 | + |
| 81 | +## Filters |
| 82 | + |
| 83 | +Always use the `StatsFilter` factory. Never instantiate filter classes directly. |
| 84 | + |
| 85 | +```php |
| 86 | +use Javaabu\Stats\Filters\StatsFilter; |
| 87 | + |
| 88 | +public function allowedFilters(): array |
| 89 | +{ |
| 90 | + return [ |
| 91 | + // Exact column match |
| 92 | + StatsFilter::exact('customer', 'customer_id'), |
| 93 | + |
| 94 | + // Eloquent query scope — calls $query->whereActive() |
| 95 | + StatsFilter::scope('active', 'whereActive'), |
| 96 | + |
| 97 | + // Custom closure — receives ($query, $value, $stat) |
| 98 | + StatsFilter::closure('min_amount', function (Builder $query, $value, $stat) { |
| 99 | + return $query->where('amount', '>=', $value); |
| 100 | + }), |
| 101 | + ]; |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +Pass filters when creating a stat instance: |
| 106 | + |
| 107 | +```php |
| 108 | +$stats = TimeSeriesStats::createFromMetric('orders_count', PresetDateRanges::THIS_YEAR, [ |
| 109 | + 'customer' => 5, |
| 110 | + 'status' => 'completed', |
| 111 | +]); |
| 112 | +``` |
| 113 | + |
| 114 | +## Routes |
| 115 | + |
| 116 | +**API route (JSON) — register in `routes/api.php`:** |
| 117 | + |
| 118 | +```php |
| 119 | +use Javaabu\Stats\TimeSeriesStats; |
| 120 | + |
| 121 | +// IMPORTANT: Do NOT include /api in the URL — routes/api.php adds it automatically. |
| 122 | +TimeSeriesStats::registerApiRoute('/stats/time-series', 'stats.time-series.index'); |
| 123 | +``` |
| 124 | + |
| 125 | +**Admin routes (web view + CSV export):** |
| 126 | + |
| 127 | +```php |
| 128 | +TimeSeriesStats::registerRoutes('/stats/time-series', 'stats.index', 'stats.export', ['auth', 'stats.view-time-series']); |
| 129 | +``` |
| 130 | + |
| 131 | +The `stats.view-time-series` middleware alias is auto-registered by the package. |
| 132 | + |
| 133 | +## Quick Reference |
| 134 | + |
| 135 | +| What | How | |
| 136 | +|------|-----| |
| 137 | +| Base classes | `CountStatsRepository`, `SumStatsRepository` | |
| 138 | +| Register metrics | `TimeSeriesStats::register(['name' => Class::class])` | |
| 139 | +| Create instance | `TimeSeriesStats::createFromMetric('name', $dateRange, $filters)` | |
| 140 | +| Time modes | `TimeSeriesModes::HOUR\|DAY\|WEEK\|MONTH\|YEAR` | |
| 141 | +| Date ranges | `PresetDateRanges::THIS_YEAR\|LAST_30_DAYS\|LAST_7_DAYS\|...` | |
| 142 | +| Custom range | `new ExactDateRange('2024-01-01', '2024-12-31')` | |
| 143 | +| Format output | `$stat->format('chartjs', TimeSeriesModes::DAY)` | |
| 144 | +| Built-in formats | `default`, `chartjs`, `sparkline`, `flot`, `combined` | |
| 145 | +| Get total | `$stat->total()` | |
| 146 | +| Custom date col | Override `getDateFieldName()` (default: `created_at`) | |
| 147 | +| Authorization | Override `canView(?Authorizable $user)` (default: `view_stats` permission) | |
| 148 | + |
| 149 | +See `references/` for formatters, export, authorization, and advanced features. |
| 150 | + |
| 151 | +## Verify Against Live State |
| 152 | + |
| 153 | +If the app has `laravel/mcp` installed, use the MCP tools to cross-check before writing code: |
| 154 | + |
| 155 | +- **ListMetrics** — confirm which metrics are registered, their filters, and aggregate fields |
| 156 | +- **ListFormatters** — confirm available formatters (including custom ones) |
| 157 | +- **QueryStat** — test a metric with real data before building on top of it |
| 158 | + |
| 159 | +This avoids guessing metric names or filter keys — the MCP tools reflect the actual running application. |
0 commit comments