diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e1b877..4d10065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ All notable changes to `laravel-stats` will be documented in this file +## 2.0.0 - 2022-02-20 + +### Added + +- Added `StatsWriter` with classname support (`StatsWriter::for(MyModel::class)`) +- Added `StatsWriter` with eloquent-model support (`StatsWriter::for($eloquent)`) +- Added `StatsWriter` with "has-many"-relationship support (`StatsWriter::for($model->relationship())`) - other relationships are untested yet +- Added `StatsWriter` with custom-attribute support (`StatsWriter::for(MyModel::class, ['custom_column' => 'orders])`) +- Extended `StatsQuery` with relationship-support (`StatsQuery::for($model->relationship())`) +- Extended `StatsQuery` with additional attributes (`StatsQuery::for(StatsEvent::class, ['name' => 'OrderStats'])`) +- Extended `BaseStats` with direct writer access (`OrderStats::writer()` as addition to `OrderStats::query()`) + +### Changed (breaks BC) + +- Changed visibility of `StatsQuery::for($model)->generatePeriods()` from `public` to `protected` +- Replaced `StatsQuery::for($model)->getStatistic()` with `StatsQuery::for($model)->getAttributes()` +- Removed `BaseStats->createEvent()` + +### Migrations + +- Replace `StatsQuery::for(OrderStats::class)` with `OrderStats::query()` +- Replace `StatsEvent::TYPE_SET` use `DataPoint::TYPE_SET` instead +- Replace `StatsEvent::TYPE_CHANGE` use `DataPoint::TYPE_CHANGE` instead + ## 1.0.1 - 2022-02-02 - Add support for Laravel 9 diff --git a/README.md b/README.md index 04cf1bf..12ababa 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ grouped by week. ```php use Spatie\Stats\StatsQuery; -$stats = StatsQuery::for(SubscriptionStats::class) +$stats = SubscriptionStats::query() ->start(now()->subMonths(2)) ->end(now()->subSecond()) ->groupByWeek() @@ -171,6 +171,47 @@ This will return an array containing arrayable `Spatie\Stats\DataPoint` objects. ] ``` +## Extended Use-Cases + +### Read and Write from a custom Model + +* Create a new table with `type (string)`, `value (bigInt)`, `created_at`, `updated_at` fields +* Create a model and add `HasStats`-trait + +```php +StatsWriter::for(MyCustomModel::class)->set(123) +StatsWriter::for(MyCustomModel::class, ['custom_column' => '123'])->increment(1) +StatsWriter::for(MyCustomModel::class, ['another_column' => '234'])->decrement(1, now()->subDay()) + +$stats = StatsQuery::for(MyCustomModel::class) + ->start(now()->subMonths(2)) + ->end(now()->subSecond()) + ->groupByWeek() + ->get(); + +// OR + +$stats = StatsQuery::for(MyCustomModel::class, ['additional_column' => '123']) + ->start(now()->subMonths(2)) + ->end(now()->subSecond()) + ->groupByWeek() + ->get(); +``` + +### Read and Write from a HasMany-Relationship + +```php +$tenant = Tenant::find(1) + +StatsWriter::for($tenant->orderStats(), ['payment_type_column' => 'recurring'])->increment(1) + +$stats = StatsQuery::for($tenant->orderStats(), , ['payment_type_column' => 'recurring']) + ->start(now()->subMonths(2)) + ->end(now()->subSecond()) + ->groupByWeek() + ->get(); +``` + ## Testing ``` bash diff --git a/composer.json b/composer.json index 4945eac..16e94ff 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "spatie/laravel-package-tools": "^1.9.2" }, "require-dev": { + "doctrine/dbal": "^3.3", "orchestra/testbench": "^6.23|^7.0", "phpunit/phpunit": "^9.4", "vimeo/psalm": "^4.12" diff --git a/src/BaseStats.php b/src/BaseStats.php index 82b1c1d..bf5bcbc 100644 --- a/src/BaseStats.php +++ b/src/BaseStats.php @@ -14,41 +14,30 @@ public function getName(): string public static function query(): StatsQuery { - return new StatsQuery(static::class); + return StatsQuery::for(StatsEvent::class, [ + 'name' => (new static)->getName(), + ]); } - public static function increase(mixed $number = 1, ?DateTimeInterface $timestamp = null) + public static function writer(): StatsWriter { - $number = is_int($number) ? $number : 1; - - $stats = new static; - - $stats->createEvent(StatsEvent::TYPE_CHANGE, $number, $timestamp); + return StatsWriter::for(StatsEvent::class, [ + 'name' => (new static)->getName(), + ]); } - public static function decrease(mixed $number = 1, ?DateTimeInterface $timestamp = null) + public static function increase(mixed $number = 1, ?DateTimeInterface $timestamp = null) { - $number = is_int($number) ? $number : 1; - - $stats = new static; - - $stats->createEvent(StatsEvent::TYPE_CHANGE, -$number, $timestamp); + static::writer()->increase($number, $timestamp); } - public static function set(int $value, ?DateTimeInterface $timestamp = null) + public static function decrease(mixed $number = 1, ?DateTimeInterface $timestamp = null) { - $stats = new static; - - $stats->createEvent(StatsEvent::TYPE_SET, $value, $timestamp); + static::writer()->decrease($number, $timestamp); } - protected function createEvent($type, $value, ?DateTimeInterface $timestamp = null): StatsEvent + public static function set(int $value, ?DateTimeInterface $timestamp = null) { - return StatsEvent::create([ - 'name' => $this->getName(), - 'type' => $type, - 'value' => $value, - 'created_at' => $timestamp ?? now(), - ]); + static::writer()->set($value, $timestamp); } } diff --git a/src/DataPoint.php b/src/DataPoint.php index d816c0e..99953f9 100644 --- a/src/DataPoint.php +++ b/src/DataPoint.php @@ -7,6 +7,10 @@ class DataPoint implements Arrayable { + + const TYPE_SET = 'set'; + const TYPE_CHANGE = 'change'; + public function __construct( public Carbon $start, public Carbon $end, diff --git a/src/Models/StatsEvent.php b/src/Models/StatsEvent.php index 0a880aa..2853d33 100644 --- a/src/Models/StatsEvent.php +++ b/src/Models/StatsEvent.php @@ -2,46 +2,27 @@ namespace Spatie\Stats\Models; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Spatie\Stats\DataPoint; +use Spatie\Stats\Traits\HasStats; class StatsEvent extends Model { - const TYPE_SET = 'set'; - const TYPE_CHANGE = 'change'; + use HasStats; + + /** + * @deprecated use DataPoint::TYPE_SET + */ + const TYPE_SET = DataPoint::TYPE_SET; + + /** + * @deprecated use DataPoint::TYPE_CHANGE + */ + const TYPE_CHANGE = DataPoint::TYPE_CHANGE; protected $casts = [ 'value' => 'integer', ]; protected $guarded = []; - - public function scopeGroupByPeriod(Builder $query, string $period): void - { - $periodGroupBy = static::getPeriodDateFormat($period); - - $query->groupByRaw($periodGroupBy)->selectRaw("{$periodGroupBy} as period"); - } - - public static function getPeriodDateFormat(string $period): string - { - return match ($period) { - 'year' => "date_format(created_at,'%Y')", - 'month' => "date_format(created_at,'%Y-%m')", - 'week' => "yearweek(created_at, 3)", // see https://stackoverflow.com/questions/15562270/php-datew-vs-mysql-yearweeknow - 'day' => "date_format(created_at,'%Y-%m-%d')", - 'hour' => "date_format(created_at,'%Y-%m-%d %H')", - 'minute' => "date_format(created_at,'%Y-%m-%d %H:%i')", - }; - } - - public function scopeIncrements(Builder $query): void - { - $query->where('value', '>', 0); - } - - public function scopeDecrements(Builder $query): void - { - $query->where('value', '<', 0); - } } diff --git a/src/StatsQuery.php b/src/StatsQuery.php index b9cbf2c..ca7b135 100644 --- a/src/StatsQuery.php +++ b/src/StatsQuery.php @@ -5,13 +5,16 @@ use DateTimeInterface; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection as EloquentCollection; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; -use Spatie\Stats\Models\StatsEvent; class StatsQuery { - protected BaseStats $statistic; + private Model|Relation|string $subject; + + private array $attributes = []; protected string $period; @@ -19,9 +22,11 @@ class StatsQuery protected DateTimeInterface $end; - public function __construct(string $statistic) + public function __construct(Model|Relation|string $subject, array $attributes = []) { - $this->statistic = new $statistic(); + $this->subject = $subject; + + $this->attributes = $attributes; $this->period = 'week'; @@ -30,11 +35,24 @@ public function __construct(string $statistic) $this->end = now(); } - public static function for(string $statistic): self + public static function for(Model|Relation|string $subject, array $attributes = []): self + { + return new self($subject, $attributes); + } + + public static function getPeriodDateFormat(string $period): string { - return new self($statistic); + return match ($period) { + 'year' => "date_format(created_at,'%Y')", + 'month' => "date_format(created_at,'%Y-%m')", + 'week' => "yearweek(created_at, 3)", // see https://stackoverflow.com/questions/15562270/php-datew-vs-mysql-yearweeknow + 'day' => "date_format(created_at,'%Y-%m-%d')", + 'hour' => "date_format(created_at,'%Y-%m-%d %H')", + 'minute' => "date_format(created_at,'%Y-%m-%d %H:%i')", + }; } + public function groupByYear(): self { $this->period = 'year'; @@ -84,13 +102,13 @@ public function end(DateTimeInterface $end): self return $this; } - /** @return \Illuminate\Support\Collection|\Spatie\Stats\DataPoint[] */ + /** @return Collection|DataPoint[] */ public function get(): Collection { $periods = $this->generatePeriods(); $changes = $this->queryStats() - ->whereType(StatsEvent::TYPE_CHANGE) + ->where('type', DataPoint::TYPE_CHANGE) ->where('created_at', '>=', $this->start) ->where('created_at', '<', $this->end) ->get(); @@ -133,14 +151,14 @@ public function get(): Collection * Gets the value at a point in time by using the previous * snapshot and the changes since that snapshot. * - * @param \DateTimeInterface $dateTime + * @param DateTimeInterface $dateTime * * @return int */ public function getValue(DateTimeInterface $dateTime): int { $nearestSet = $this->queryStats() - ->where('type', StatsEvent::TYPE_SET) + ->where('type', DataPoint::TYPE_SET) ->where('created_at', '<', $dateTime) ->orderByDesc('created_at') ->first(); @@ -149,15 +167,15 @@ public function getValue(DateTimeInterface $dateTime): int $startValue = optional($nearestSet)->value ?? 0; $differenceSinceSet = $this->queryStats() - ->where('type', StatsEvent::TYPE_CHANGE) - ->where('id', '>', $startId) + ->where('type', DataPoint::TYPE_CHANGE) + ->where($this->getStatsKey(), '>', $startId) ->where('created_at', '<', $dateTime) ->sum('value'); return $startValue + $differenceSinceSet; } - public function generatePeriods(): Collection + protected function generatePeriods(): Collection { $data = collect(); $currentDateTime = (new Carbon($this->start))->startOf($this->period); @@ -187,21 +205,30 @@ public function getPeriodTimestampFormat(): string }; } - public function getStatistic(): BaseStats + public function getAttributes(): array { - return $this->statistic; + return $this->attributes; } protected function queryStats(): Builder { - return StatsEvent::query() - ->where('name', $this->statistic->getName()); + if ($this->subject instanceof Relation) { + return $this->subject->getQuery()->clone()->where($this->attributes); + } + + /** @var Model $subject */ + $subject = $this->subject; + if (is_string($subject) && class_exists($subject)) { + $subject = new $subject; + } + + return $subject->newQuery()->where($this->attributes); } protected function getDifferencesPerPeriod(): EloquentCollection { return $this->queryStats() - ->whereType(StatsEvent::TYPE_CHANGE) + ->where('type', DataPoint::TYPE_CHANGE) ->where('created_at', '>=', $this->start) ->where('created_at', '<', $this->end) ->selectRaw('sum(case when value > 0 then value else 0 end) as increments') @@ -214,11 +241,14 @@ protected function getDifferencesPerPeriod(): EloquentCollection protected function getLatestSetPerPeriod(): EloquentCollection { - $periodDateFormat = StatsEvent::getPeriodDateFormat($this->period); + $periodDateFormat = static::getPeriodDateFormat($this->period); + + $statsTable = $this->getStatsTableName(); + $statsKey = $this->getStatsKey(); $rankedSets = $this->queryStats() - ->selectRaw("ROW_NUMBER() OVER (PARTITION BY {$periodDateFormat} ORDER BY `id` DESC) AS rn, `stats_events`.*, {$periodDateFormat} as period") - ->whereType(StatsEvent::TYPE_SET) + ->selectRaw("ROW_NUMBER() OVER (PARTITION BY {$periodDateFormat} ORDER BY `{$statsKey}` DESC) AS rn, `{$statsTable}`.*, {$periodDateFormat} as period") + ->where('type', DataPoint::TYPE_SET) ->where('created_at', '>=', $this->start) ->where('created_at', '<', $this->end) ->get(); @@ -227,4 +257,34 @@ protected function getLatestSetPerPeriod(): EloquentCollection return $latestSetPerPeriod; } + + protected function getStatsKey(): string + { + if ($this->subject instanceof Relation) { + return $this->subject->getRelated()->getKeyName(); + } + + /** @var Model $subject */ + $subject = $this->subject; + if (is_string($subject) && class_exists($subject)) { + $subject = new $subject; + } + + return $subject->getKeyName(); + } + + protected function getStatsTableName(): string + { + if ($this->subject instanceof Relation) { + return $this->subject->getRelated()->getTable(); + } + + /** @var Model $subject */ + $subject = $this->subject; + if (is_string($subject) && class_exists($subject)) { + $subject = new $subject; + } + + return $subject->getTable(); + } } diff --git a/src/StatsWriter.php b/src/StatsWriter.php new file mode 100644 index 0000000..4cbab6b --- /dev/null +++ b/src/StatsWriter.php @@ -0,0 +1,70 @@ +subject = $subject; + $this->attributes = $attributes; + } + + public static function for(Model|Relation|string $subject, array $attributes = []) + { + return new static($subject, $attributes); + } + + public function increase(mixed $number = 1, ?DateTimeInterface $timestamp = null) + { + $number = is_int($number) ? $number : 1; + + $this->createEvent(DataPoint::TYPE_CHANGE, $number, $timestamp); + } + + public function decrease(mixed $number = 1, ?DateTimeInterface $timestamp = null) + { + $number = is_int($number) ? $number : 1; + + $this->createEvent(DataPoint::TYPE_CHANGE, -$number, $timestamp); + } + + public function set(int $value, ?DateTimeInterface $timestamp = null) + { + $this->createEvent(DataPoint::TYPE_SET, $value, $timestamp); + } + + public function getAttributes(): array + { + return $this->attributes; + } + + protected function createEvent($type, $value, ?DateTimeInterface $timestamp = null): Model + { + if ($this->subject instanceof Relation) { + return $this->subject->create(array_merge($this->attributes, [ + 'type' => $type, + 'value' => $value, + 'created_at' => $timestamp ?? now(), + ])); + } + + $subject = $this->subject; + if ($subject instanceof Model) { + $subject = get_class($subject); + } + + return $subject::create(array_merge($this->attributes, [ + 'type' => $type, + 'value' => $value, + 'created_at' => $timestamp ?? now(), + ])); + } +} diff --git a/src/Traits/HasStats.php b/src/Traits/HasStats.php new file mode 100644 index 0000000..f308fe5 --- /dev/null +++ b/src/Traits/HasStats.php @@ -0,0 +1,26 @@ +groupByRaw($periodGroupBy)->selectRaw("{$periodGroupBy} as period"); + } + + public function scopeIncrements(Builder $query): void + { + $query->where('value', '>', 0); + } + + public function scopeDecrements(Builder $query): void + { + $query->where('value', '<', 0); + } +} diff --git a/tests/BaseStatsTest.php b/tests/BaseStatsTest.php index 66c066e..2460842 100644 --- a/tests/BaseStatsTest.php +++ b/tests/BaseStatsTest.php @@ -4,6 +4,7 @@ use Carbon\Carbon; use Spatie\Stats\StatsQuery; +use Spatie\Stats\StatsWriter; use Spatie\Stats\Tests\Stats\OrderStats; class BaseStatsTest extends TestCase @@ -102,15 +103,6 @@ public function it_can_create_events_for_setting_fixed_values_at_a_given_timesta ]); } - /** @test */ - public function it_can_get_a_stats_query_object() - { - $query = OrderStats::query(); - - $this->assertInstanceOf(StatsQuery::class, $query); - $this->assertInstanceOf(OrderStats::class, $query->getStatistic()); - } - /** @test */ public function it_can_get_the_value_at_a_given_time() { @@ -118,28 +110,8 @@ public function it_can_get_the_value_at_a_given_time() OrderStats::decrease(1, now()->subDays(4)); OrderStats::increase(3, now()->subDays(2)); - $this->assertEquals(0, StatsQuery::for(OrderStats::class)->getValue(now()->subDays(30))); - $this->assertEquals(3, StatsQuery::for(OrderStats::class)->getValue(now()->subDays(18))); - $this->assertEquals(5, StatsQuery::for(OrderStats::class)->getValue(now())); - } - - /** @test */ - public function it_can_generate_and_array_of_periods() - { - $periods = StatsQuery::for(OrderStats::class)->start(now()->subYear())->end(now())->generatePeriods(); - - $this->assertCount(53, $periods); - - $this->assertEquals([ - Carbon::parse('2018-12-31'), - Carbon::parse('2019-01-07'), - '201901', - ], $periods[0]); - - $this->assertEquals([ - Carbon::parse('2019-12-30'), - Carbon::parse('2020-01-06'), - '202001', - ], $periods[52]); + $this->assertEquals(0, OrderStats::query()->getValue(now()->subDays(30))); + $this->assertEquals(3, OrderStats::query()->getValue(now()->subDays(18))); + $this->assertEquals(5, OrderStats::query()->getValue(now())); } } diff --git a/tests/Stats/CustomerStats.php b/tests/Stats/CustomerStats.php new file mode 100644 index 0000000..de9553a --- /dev/null +++ b/tests/Stats/CustomerStats.php @@ -0,0 +1,9 @@ + 'custom_value']); + + $this->assertInstanceOf(StatsQuery::class, $query); + $this->assertSame(['custom_attribute' => 'custom_value'], $query->getAttributes()); + } + + /** @test */ + public function it_can_get_stats_for_base_stats_class() + { + // adding customer stats, to proof name is correctly set + CustomerStats::set(3, now()->subMonth()); + CustomerStats::decrease(1, now()->subDays(13)); + CustomerStats::increase(3, now()->subDays(12)); + CustomerStats::set(3, now()->subDays(6)); + CustomerStats::decrease(1, now()->subDays(5)); + CustomerStats::increase(3, now()->subDays(4)); + OrderStats::set(3, now()->subMonth()); OrderStats::decrease(1, now()->subDays(13)); OrderStats::increase(3, now()->subDays(12)); @@ -25,7 +47,125 @@ public function it_can_get_stats() OrderStats::decrease(1, now()->subDays(5)); OrderStats::increase(3, now()->subDays(4)); - $stats = StatsQuery::for(OrderStats::class) + $stats = OrderStats::query() + ->start(now()->subWeeks(2)) + ->end(now()->startOfWeek()) + ->groupByWeek() + ->get(); + + $expected = [ + [ + 'value' => 5, + 'increments' => +3, + 'decrements' => 1, + 'difference' => 2, + 'start' => now()->subWeeks(2)->startOfWeek(), + 'end' => now()->subWeeks(1)->startOfWeek(), + ], + [ + 'value' => 5, + 'increments' => +3, + 'decrements' => 1, + 'difference' => 2, + 'start' => now()->subWeeks(1)->startOfWeek(), + 'end' => now()->startOfWeek(), + ], + ]; + + $this->assertEquals($expected, $stats->toArray()); + } + + /** @test */ + public function it_can_get_stats_for_classname() + { + StatsWriter::for(StatsEvent::class)->set(3, now()->subMonth()); + StatsWriter::for(StatsEvent::class)->decrease(1, now()->subDays(13)); + StatsWriter::for(StatsEvent::class)->increase(3, now()->subDays(12)); + StatsWriter::for(StatsEvent::class)->set(3, now()->subDays(6)); + StatsWriter::for(StatsEvent::class)->decrease(1, now()->subDays(5)); + StatsWriter::for(StatsEvent::class)->increase(3, now()->subDays(4)); + + $stats = StatsQuery::for(StatsEvent::class) + ->start(now()->subWeeks(2)) + ->end(now()->startOfWeek()) + ->groupByWeek() + ->get(); + + $expected = [ + [ + 'value' => 5, + 'increments' => +3, + 'decrements' => 1, + 'difference' => 2, + 'start' => now()->subWeeks(2)->startOfWeek(), + 'end' => now()->subWeeks(1)->startOfWeek(), + ], + [ + 'value' => 5, + 'increments' => +3, + 'decrements' => 1, + 'difference' => 2, + 'start' => now()->subWeeks(1)->startOfWeek(), + 'end' => now()->startOfWeek(), + ], + ]; + + $this->assertEquals($expected, $stats->toArray()); + } + + + /** @test */ + public function it_can_get_stats_for_object_instance() + { + StatsWriter::for(StatsEvent::class)->set(3, now()->subMonth()); + StatsWriter::for(StatsEvent::class)->decrease(1, now()->subDays(13)); + StatsWriter::for(StatsEvent::class)->increase(3, now()->subDays(12)); + StatsWriter::for(StatsEvent::class)->set(3, now()->subDays(6)); + StatsWriter::for(StatsEvent::class)->decrease(1, now()->subDays(5)); + StatsWriter::for(StatsEvent::class)->increase(3, now()->subDays(4)); + + $stats = StatsQuery::for(new StatsEvent()) + ->start(now()->subWeeks(2)) + ->end(now()->startOfWeek()) + ->groupByWeek() + ->get(); + + $expected = [ + [ + 'value' => 5, + 'increments' => +3, + 'decrements' => 1, + 'difference' => 2, + 'start' => now()->subWeeks(2)->startOfWeek(), + 'end' => now()->subWeeks(1)->startOfWeek(), + ], + [ + 'value' => 5, + 'increments' => +3, + 'decrements' => 1, + 'difference' => 2, + 'start' => now()->subWeeks(1)->startOfWeek(), + 'end' => now()->startOfWeek(), + ], + ]; + + $this->assertEquals($expected, $stats->toArray()); + } + + /** @test */ + public function it_can_get_stats_for_has_many_relationship() + { + /** @var Stat $stat */ + $stat = Stat::create(); + + StatsWriter::for($stat->events())->set(3, now()->subMonth()); + StatsWriter::for($stat->events())->decrease(1, now()->subDays(13)); + StatsWriter::for($stat->events())->increase(3, now()->subDays(12)); + StatsWriter::for($stat->events())->set(3, now()->subDays(6)); + StatsWriter::for($stat->events())->decrease(1, now()->subDays(5)); + StatsWriter::for($stat->events())->increase(3, now()->subDays(4)); + + $stats = StatsQuery::for($stat->events()) ->start(now()->subWeeks(2)) ->end(now()->startOfWeek()) ->groupByWeek() @@ -56,13 +196,13 @@ public function it_can_get_stats() /** @test */ public function it_can_get_stats_2() { - OrderStats::increase(100, now()->subMonth()); - OrderStats::decrease(1, now()->subDays(13)); - OrderStats::increase(3, now()->subDays(12)); - OrderStats::decrease(1, now()->subDays(5)); - OrderStats::increase(3, now()->subDays(4)); + StatsWriter::for(StatsEvent::class)->increase(100, now()->subMonth()); + StatsWriter::for(StatsEvent::class)->decrease(1, now()->subDays(13)); + StatsWriter::for(StatsEvent::class)->increase(3, now()->subDays(12)); + StatsWriter::for(StatsEvent::class)->decrease(1, now()->subDays(5)); + StatsWriter::for(StatsEvent::class)->increase(3, now()->subDays(4)); - $stats = StatsQuery::for(OrderStats::class) + $stats = StatsQuery::for(StatsEvent::class) ->start(now()->subWeeks(2)) ->end(now()->startOfWeek()) ->groupByWeek() @@ -93,9 +233,9 @@ public function it_can_get_stats_2() /** @test */ public function it_can_get_stats_3() { - OrderStats::increase(3, now()->subDays(12)); + StatsWriter::for(StatsEvent::class)->increase(3, now()->subDays(12)); - $stats = StatsQuery::for(OrderStats::class) + $stats = StatsQuery::for(StatsEvent::class) ->start(now()->subWeeks(2)) ->end(now()->startOfWeek()) ->groupByWeek() @@ -126,7 +266,7 @@ public function it_can_get_stats_3() /** @test */ public function it_can_get_stats_4() { - $stats = StatsQuery::for(OrderStats::class) + $stats = StatsQuery::for(StatsEvent::class) ->start(now()->subWeeks(2)) ->end(now()->startOfWeek()) ->groupByWeek() @@ -154,14 +294,69 @@ public function it_can_get_stats_4() $this->assertEquals($expected, $stats->toArray()); } + /** @test */ + public function it_can_get_stats_by_attributes() + { + StatsWriter::for(StatsEvent::class, ['name' => 'one-off'])->increase(1, now()->hour(12)); + StatsWriter::for(StatsEvent::class, ['name' => 'recurring'])->increase(1, now()->hour(12)); + + $stats = StatsQuery::for(StatsEvent::class, ['name' => 'recurring']) + ->start(now()->startOfDay()) + ->end(now()->endOfDay()) + ->groupByDay() + ->get(); + + $expected = [ + [ + 'value' => 1, + 'increments' => 1, + 'decrements' => 0, + 'difference' => 1, + 'start' => now()->startOfDay(), + 'end' => now()->endOfDay()->addMicro(), + ], + ]; + + $this->assertEquals($expected, $stats->toArray()); + } + + /** @test */ + public function it_can_get_stats_by_attributes_for_has_many_relationship() + { + /** @var Stat $stat */ + $stat = Stat::create(); + + StatsWriter::for($stat->events(), ['name' => 'one-off'])->increase(1, now()->hour(12)); + StatsWriter::for($stat->events(), ['name' => 'recurring'])->increase(1, now()->hour(12)); + + $stats = StatsQuery::for($stat->events(), ['name' => 'recurring']) + ->start(now()->startOfDay()) + ->end(now()->endOfDay()) + ->groupByDay() + ->get(); + + $expected = [ + [ + 'value' => 1, + 'increments' => 1, + 'decrements' => 0, + 'difference' => 1, + 'start' => now()->startOfDay(), + 'end' => now()->endOfDay()->addMicro(), + ], + ]; + + $this->assertEquals($expected, $stats->toArray()); + } + /** @test */ public function it_can_get_stats_grouped_by_day() { - OrderStats::set(3, now()->subDays(6)); - OrderStats::decrease(1, now()->subDays(2)); - OrderStats::increase(3, now()->subDays(1)); + StatsWriter::for(StatsEvent::class)->set(3, now()->subDays(6)); + StatsWriter::for(StatsEvent::class)->decrease(1, now()->subDays(2)); + StatsWriter::for(StatsEvent::class)->increase(3, now()->subDays(1)); - $stats = StatsQuery::for(OrderStats::class) + $stats = StatsQuery::for(StatsEvent::class) ->start(now()->subDays(3)) ->end(now()) ->groupByDay() @@ -200,11 +395,11 @@ public function it_can_get_stats_grouped_by_day() /** @test */ public function it_can_get_stats_grouped_by_hour() { - OrderStats::set(3, now()->subHours(6)); - OrderStats::decrease(1, now()->subHours(2)); - OrderStats::increase(3, now()->subHours(1)); + StatsWriter::for(StatsEvent::class)->set(3, now()->subHours(6)); + StatsWriter::for(StatsEvent::class)->decrease(1, now()->subHours(2)); + StatsWriter::for(StatsEvent::class)->increase(3, now()->subHours(1)); - $stats = StatsQuery::for(OrderStats::class) + $stats = StatsQuery::for(StatsEvent::class) ->start(now()->subHours(3)) ->end(now()) ->groupByHour() @@ -243,15 +438,15 @@ public function it_can_get_stats_grouped_by_hour() /** @test */ public function it_can_get_stats_based_on_youngest_sets_in_periods() { - OrderStats::set(1, now()->subHours(49)); - OrderStats::set(2, now()->subHours(37)); - OrderStats::set(3, now()->subHours(25)); // This set will be used for day 1 - OrderStats::decrease(2, now()->subHours(16)); // These decrements and increments will still show up in day 2 - OrderStats::set(4, now()->subHours(13)); - OrderStats::increase(4, now()->subHours(8)); - OrderStats::set(5, now()->subHours(1)); // This set will be used for day 2 - - $stats = StatsQuery::for(OrderStats::class) + StatsWriter::for(StatsEvent::class)->set(1, now()->subHours(49)); + StatsWriter::for(StatsEvent::class)->set(2, now()->subHours(37)); + StatsWriter::for(StatsEvent::class)->set(3, now()->subHours(25)); // This set will be used for day 1 + StatsWriter::for(StatsEvent::class)->decrease(2, now()->subHours(16)); // These decrements and increments will still show up in day 2 + StatsWriter::for(StatsEvent::class)->set(4, now()->subHours(13)); + StatsWriter::for(StatsEvent::class)->increase(4, now()->subHours(8)); + StatsWriter::for(StatsEvent::class)->set(5, now()->subHours(1)); // This set will be used for day 2 + + $stats = StatsQuery::for(StatsEvent::class) ->start(now()->subDays(2)) ->end(now()) ->groupByDay() @@ -278,4 +473,36 @@ public function it_can_get_stats_based_on_youngest_sets_in_periods() $this->assertEquals($expected, $stats->toArray()); } + + /** @test */ + public function it_can_get_the_value_at_a_given_time() + { + StatsWriter::for(StatsEvent::class)->set(3, now()->subDays(19)); + StatsWriter::for(StatsEvent::class)->decrease(1, now()->subDays(4)); + StatsWriter::for(StatsEvent::class)->increase(3, now()->subDays(2)); + + $this->assertEquals(0, StatsQuery::for(StatsEvent::class)->getValue(now()->subDays(30))); + $this->assertEquals(3, StatsQuery::for(StatsEvent::class)->getValue(now()->subDays(18))); + $this->assertEquals(5, StatsQuery::for(StatsEvent::class)->getValue(now())); + } + + /** @test */ + public function it_will_generate_stats_grouped_by_year() + { + $stats = StatsQuery::for(StatsEvent::class) + ->start(now()->subYear()) + ->end(now()) + ->groupByWeek() + ->get(); + + $this->assertCount(53, $stats); + + $this->assertInstanceOf(DataPoint::class, $stats[0]); + $this->assertSame('2018-12-31 00:00:00', (string)$stats[0]->start); + $this->assertSame('2019-01-07 00:00:00', (string)$stats[0]->end); + + $this->assertInstanceOf(DataPoint::class, $stats[52]); + $this->assertSame('2019-12-30 00:00:00', (string)$stats[52]->start); + $this->assertSame('2020-01-06 00:00:00', (string)$stats[52]->end); + } } diff --git a/tests/StatsWriterTest.php b/tests/StatsWriterTest.php new file mode 100644 index 0000000..24647bc --- /dev/null +++ b/tests/StatsWriterTest.php @@ -0,0 +1,182 @@ +assertDatabaseHas('stats_events', [ + 'name' => 'CustomerStats', + 'value' => 1, + 'type' => 'change', + ]); + + $this->assertDatabaseHas('stats_events', [ + 'name' => 'OrderStats', + 'value' => 1, + 'type' => 'change', + ]); + } + + /** @test */ + public function it_can_create_events_for_class_names() + { + StatsWriter::for(StatsEvent::class)->increase(); + + $this->assertDatabaseHas('stats_events', [ + 'value' => 1, + 'type' => 'change', + ]); + } + + /** @test */ + public function it_can_create_events_for_model_instances() + { + StatsWriter::for(new StatsEvent())->increase(1); + + $this->assertDatabaseHas('stats_events', [ + 'value' => 1, + 'type' => 'change', + ]); + } + + /** @test */ + public function it_can_create_events_for_has_many_relationships() + { + /** @var Stat $stats */ + $stats = Stat::create(); + + StatsWriter::for($stats->events())->increase(); + + $this->assertDatabaseHas('stats_events', [ + 'stat_id' => $stats->getKey(), + 'value' => 1, + 'type' => 'change', + ]); + } + + /** @test */ + public function it_can_create_events_for_has_many_relationships_with_custom_attributes() + { + /** @var Stat $stats */ + $stats = Stat::create(); + + StatsWriter::for($stats->events(), ['name' => 'recurring'])->increase(1); + + $this->assertDatabaseHas('stats_events', [ + 'stat_id' => $stats->getKey(), + 'name' => 'recurring', + 'value' => 1, + 'type' => 'change', + ]); + } + + /** @test */ + public function it_can_create_events_with_custom_attributes() + { + StatsWriter::for(new StatsEvent(), ['name' => 'OrderStats'])->increase(1); + + $this->assertDatabaseHas('stats_events', [ + 'name' => 'OrderStats', + 'value' => 1, + 'type' => 'change', + ]); + } + + /** @test */ + public function it_can_create_events_for_increments_at_a_given_timestamp() + { + StatsWriter::for(StatsEvent::class)->increase(1, now()->subWeek()); + + $this->assertDatabaseHas('stats_events', [ + 'value' => 1, + 'type' => 'change', + 'created_at' => now()->subWeek(), + ]); + } + + /** @test */ + public function it_can_create_events_for_increments() + { + StatsWriter::for(StatsEvent::class)->increase(); + + $this->assertDatabaseHas('stats_events', [ + 'value' => 1, + 'type' => 'change', + ]); + } + + /** @test */ + public function it_can_create_events_for_decrements() + { + StatsWriter::for(StatsEvent::class)->decrease(); + + $this->assertDatabaseHas('stats_events', [ + 'value' => -1, + 'type' => 'change', + ]); + } + + /** @test */ + public function it_can_create_events_for_decrements_at_a_given_timestamp() + { + StatsWriter::for(StatsEvent::class)->decrease(1, now()->subWeek()); + + $this->assertDatabaseHas('stats_events', [ + 'value' => -1, + 'type' => 'change', + 'created_at' => now()->subWeek(), + ]); + } + + /** @test */ + public function it_can_create_events_for_setting_fixed_values() + { + StatsWriter::for(StatsEvent::class)->set(1337); + + $this->assertDatabaseHas('stats_events', [ + 'value' => 1337, + 'type' => 'set', + ]); + } + + /** @test */ + public function it_can_create_events_for_setting_fixed_values_at_a_given_timestamp() + { + StatsWriter::for(StatsEvent::class)->set(1337, now()->subWeek()); + + $this->assertDatabaseHas('stats_events', [ + 'value' => 1337, + 'type' => 'set', + 'created_at' => now()->subWeek(), + ]); + } + + /** @test */ + public function it_can_pass_and_receive_attributes() + { + $writer = StatsWriter::for(StatsEvent::class, ['customer_attrib' => 'custom_val']); + + $this->assertInstanceOf(StatsWriter::class, $writer); + $this->assertSame(['customer_attrib' => 'custom_val'], $writer->getAttributes()); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 7ba6e84..61e8e7b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,7 @@ namespace Spatie\Stats\Tests; use CreateStatsTables; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Testing\DatabaseMigrations; use Orchestra\Testbench\TestCase as Orchestra; use Spatie\Stats\StatsServiceProvider; @@ -29,6 +30,16 @@ public function setupDatabase() { include_once __DIR__.'/../database/migrations/create_stats_tables.php.stub'; + $this->app['db']->connection()->getSchemaBuilder()->create('stats', function (Blueprint $table) { + $table->id(); + $table->timestamps(); + }); + (new CreateStatsTables())->up(); + + $this->app['db']->connection()->getSchemaBuilder()->table('stats_events', function (Blueprint $table) { + $table->string('stat_id')->nullable()->after('id'); + $table->string('name')->nullable()->change(); + }); } } diff --git a/tests/TestClasses/Models/Stat.php b/tests/TestClasses/Models/Stat.php new file mode 100644 index 0000000..3b54523 --- /dev/null +++ b/tests/TestClasses/Models/Stat.php @@ -0,0 +1,15 @@ +hasMany(StatsEvent::class); + } +}