spatie/laravel-stats
Lightweight Laravel package to track stat changes in your app over time. Create a stats class, call increase/decrease on events (e.g., subscriptions), then query totals, increments, decrements, and differences grouped by day/week/month for any date range.
Installation:
composer require spatie/laravel-stats
Publish the migration (if using database storage):
php artisan vendor:publish --provider="Spatie\Stats\StatsServiceProvider" --tag="migrations"
php artisan migrate
Define a Stats Class:
Create a class extending BaseStats in app/Models/Stats (or your preferred namespace):
namespace App\Models\Stats;
use Spatie\Stats\BaseStats;
class UserStats extends BaseStats {}
First Usage: Track an event (e.g., user signups):
UserStats::increase(); // Increment count on signup
Query historical data:
$stats = UserStats::query()
->start(now()->subDays(7))
->end(now())
->get();
Track daily active users (DAU):
// Increment on user activity
UserActivityStats::increase();
// Query DAU for last 30 days
$dau = UserActivityStats::query()
->start(now()->subDays(30))
->end(now())
->groupByDay()
->get();
Event-Driven Tracking:
Use increase()/decrease() in:
UserController@store).UserObserver@created).ProcessSubscriptionJob).
Example:// In UserObserver
public function created(User $user) {
UserStats::increase();
}
Querying Patterns:
// Last 7 days
Stats::query()->start(now()->subDays(7))->end(now())->get();
// Monthly trends
Stats::query()->groupByMonth()->get();
// Total sum over time
Stats::query()->sum();
Custom Storage:
Extend BaseStats to use Redis or custom logic:
class RedisUserStats extends BaseStats {
protected $storage = 'redis';
}
event(new UserRegistered($user));
// In listener:
UserStats::increase();
Route::get('/stats/dau', function () {
return UserStats::query()->groupByDay()->get();
});
// In App\Console\Commands\ResetDailyStats
UserStats::resetDaily();
Database Locking:
increase()/decrease() calls may cause race conditions.DB::transaction(function () {
UserStats::increase();
});
Time Zone Handling:
Stats::query()->start(now('America/New_York')->subDays(7))->get();
Grouping Edge Cases:
groupByWeek()/groupByMonth() may include empty days if no data exists.groupByWeek()->whereNotNull('value') or handle gaps in frontend.Migration Conflicts:
stats table exists or skip migrations:// In StatsServiceProvider
public function boot() {
if (!config('stats.use_database')) {
Schema::hasTable('stats') || $this->createStatsTable();
}
}
increase()/decrease() is called in the correct context (e.g., middleware vs. controller).
php artisan tinker
UserStats::query()->get(); // Check raw data
stats table:
Schema::table('stats', function (Blueprint $table) {
$table->index(['stat_key', 'date']);
});
BaseStats to support multi-dimensional stats:
class UserRegionStats extends BaseStats {
public function increase(string $region) {
$this->increment($region);
}
}
Spatie\Stats\StatsRepository interface for custom storage (e.g., DynamoDB).// In BaseStats
protected static function afterIncrement() {
Http::post('https://api.example.com/webhook', ['stats' => self::get()]);
}
user_signups, user_cancellations) for clarity.$stats = Cache::remember('dau_last_7_days', now()->addDays(1), function () {
return UserStats::query()->groupByDay()->get();
});
// Example with Laravel Charts
Charts::dataset('line', [0, 10, 5, 20])
->title('User Growth')
->colors(['#4CAF50']);
How can I help you explore Laravel packages today?