spatie/laravel-stats
Lightweight Laravel package to track and summarize stat changes over time. Define a stats class, call increase/decrease on events, then query totals and increments/decrements across date ranges grouped by day/week/month.
Installation:
composer require spatie/laravel-stats
php artisan vendor:publish --provider="Spatie\Stats\StatsServiceProvider" --tag="stats-migrations"
php artisan migrate
Create a Stats Class:
// app/Stats/SubscriptionStats.php
namespace App\Stats;
use Spatie\Stats\BaseStats;
class SubscriptionStats extends BaseStats {}
First Use Case: Track subscriptions in a controller:
use App\Stats\SubscriptionStats;
// When a user subscribes
SubscriptionStats::increase();
// When a user cancels
SubscriptionStats::decrease();
Query Stats:
$stats = SubscriptionStats::query()
->start(now()->subMonths(2))
->end(now())
->groupByWeek()
->get();
Define Stats Classes:
Create dedicated classes for each metric (e.g., UserStats, OrderStats).
class UserStats extends BaseStats {
public function getName(): string {
return 'user_activity';
}
}
Integrate with Business Logic:
created, deleted, or custom events.
// In UserObserver.php
public function created(User $user) {
UserStats::increase();
}
// Handle webhook for order completion
OrderCompleted::increase();
Querying Patterns:
$dailyStats = OrderStats::query()
->start(now()->subDays(7))
->end(now())
->groupByDay()
->get();
$stats = UserStats::query()
->start(request('start_date'))
->end(request('end_date'))
->groupByMonth()
->get();
Custom Attributes:
Track stats with additional context (e.g., by payment_method):
// Write
StatsWriter::for(OrderStats::class, ['payment_method' => 'credit_card'])->increase();
// Query
$stats = StatsQuery::for(OrderStats::class, ['payment_method' => 'credit_card'])
->groupByWeek()
->get();
Relationship-Based Stats:
Track stats for polymorphic relationships (e.g., Tenant → Order):
// Write
StatsWriter::for($tenant->orders)->increase();
// Query
$stats = StatsQuery::for($tenant->orders)
->groupByMonth()
->get();
Bulk Updates:
Use set() for initial loads or external syncs:
$totalUsers = User::count();
UserStats::set($totalUsers);
Time-Shifted Updates: Record historical data:
StatsWriter::for(OrderStats::class)->increase(1, now()->subDays(2));
Caching Queries: Cache frequent queries (e.g., dashboard metrics):
$stats = Cache::remember('user_stats_weekly', now()->addWeek(), function() {
return UserStats::query()->groupByWeek()->get();
});
Real-Time Dashboards: Use Laravel Echo/Pusher to push stat updates:
// Broadcast stat changes
Echo.channel('stats')
.listen('StatUpdated', (data) => {
updateDashboard(data);
});
Migration Conflicts:
php artisan stats:migrate-fresh
stats_events table conflicts with the new schema.Time Zone Handling:
StatsQuery::for(OrderStats::class)
->start(now('America/New_York')->subMonth())
->groupByDay();
Performance with Large Datasets:
start(now()->subYears(10))).groupByMonth() or groupByYear() for long-term trends.Concurrent Writes:
increase()/decrease() are atomic, but bulk operations may race. Use transactions for critical paths:
DB::transaction(function() {
StatsWriter::for(OrderStats::class)->increase(100);
// Other DB operations...
});
Custom Attributes Quirks:
StatsWriter::for(OrderStats::class, ['tags' => ['premium', 'recurring']])->increase();
Query Inspection: Enable query logging to debug slow queries:
DB::enableQueryLog();
$stats = UserStats::query()->groupByWeek()->get();
dd(DB::getQueryLog());
DataPoint Validation:
Check for malformed data in the stats_events table:
SELECT * FROM stats_events WHERE type NOT IN ('change', 'set') LIMIT 10;
Migration Rollback: If migrations fail, manually drop the table:
DROP TABLE stats_events;
Custom Storage:
Override the default StatsRepository to use Redis or another store:
// app/Providers/AppServiceProvider.php
public function register() {
$this->app->bind(
\Spatie\Stats\StatsRepository::class,
\App\Stats\RedisStatsRepository::class
);
}
Event Listeners:
Extend BaseStats to trigger events:
class SubscriptionStats extends BaseStats {
protected static function afterIncrease() {
event(new StatIncreased('subscriptions'));
}
}
API Resources: Format stats for APIs:
// app/Http/Resources/StatResource.php
public function toArray($request) {
return [
'period' => $this->start,
'value' => $this->value,
'trend' => $this->difference,
];
}
Testing:
Use StatsTestCase for isolated tests:
use Spatie\Stats\Testing\StatsTestCase;
class SubscriptionStatsTest extends StatsTestCase {
public function test_subscription_trend() {
SubscriptionStats::increase();
$stats = SubscriptionStats::query()->groupByDay()->get();
$this->assertEquals(1, $stats[0]->value);
}
}
Table Prefix:
Ensure the stats_events table uses the correct prefix if your app uses one:
DB_TABLE_PREFIX=myapp_
Then update the migration or use:
StatsQuery::for(OrderStats::class)->table('myapp_stats_events');
Laravel 10+:
The package supports Laravel 10+ out of the box. For older versions, pin to v1.x:
composer require spatie/laravel-stats:^1.0
SQLite Support:
Ensure your config/database.php uses the correct SQLite driver and path. The package includes SQLite-specific optimizations.
How can I help you explore Laravel packages today?