diff --git a/database/migrations/2021_09_01_010000_create_cargo_projections_table.php b/database/migrations/2021_09_01_010000_create_cargo_projections_table.php index b0a5e3a..15e70a5 100644 --- a/database/migrations/2021_09_01_010000_create_cargo_projections_table.php +++ b/database/migrations/2021_09_01_010000_create_cargo_projections_table.php @@ -11,7 +11,7 @@ public function up() Schema::create('cargo_projections', function (Blueprint $table) { $table->id(); - $table->string('name'); + $table->string('projector_name'); $table->string('key')->nullable(); $table->string('period'); $table->timestamp('start_date'); diff --git a/src/Exceptions/MissingProjectionNameException.php b/src/Exceptions/MissingProjectorNameException.php similarity index 73% rename from src/Exceptions/MissingProjectionNameException.php rename to src/Exceptions/MissingProjectorNameException.php index a937a22..21818e3 100644 --- a/src/Exceptions/MissingProjectionNameException.php +++ b/src/Exceptions/MissingProjectorNameException.php @@ -4,7 +4,7 @@ use Exception; -class MissingProjectionNameException extends Exception +class MissingProjectorNameException extends Exception { protected $message = "The projection's name is missing from you query."; } diff --git a/src/Models/Projection.php b/src/Models/Projection.php index fb4accb..443418a 100644 --- a/src/Models/Projection.php +++ b/src/Models/Projection.php @@ -9,8 +9,8 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Support\Carbon; use Illuminate\Support\Str; -use Laravelcargo\LaravelCargo\Exceptions\MissingProjectionNameException; use Laravelcargo\LaravelCargo\Exceptions\MissingProjectionPeriodException; +use Laravelcargo\LaravelCargo\Exceptions\MissingProjectorNameException; use Laravelcargo\LaravelCargo\ProjectionCollection; class Projection extends Model @@ -31,9 +31,9 @@ class Projection extends Model ]; /** - * The projection's name used in query. + * The projector's name used in query. */ - protected string | null $queryName = null; + protected string | null $projectorName = null; /** * The projection's period used in query. @@ -59,11 +59,11 @@ public function from(string $modelName): MorphToMany /** * Scope a query to filter by name. */ - public function scopeName(Builder $query, string $name): Builder + public function scopeFromProjector(Builder $query, string $projectorName): Builder { - $this->queryName = $name; + $this->projectorName = $projectorName; - return $query->where('name', $name); + return $query->where('projector_name', $projectorName); } /** @@ -77,14 +77,32 @@ public function scopePeriod(Builder $query, string $period): Builder } /** - * Scope a query to filter between dates + * Scope a query to filter by key. + */ + public function scopeKey(Builder $query, array | string | int $keys): Builder + { + if (gettype($keys) === 'array') { + return $query->where(function ($query) use (&$keys) { + collect($keys)->each(function ($key, $index) use (&$query) { + return $index === 0 ? + $query->where('key', (string) $key) : + $query->orWhere('key', (string) $key); + }); + }); + } + + return $query->where('key', (string) $keys); + } + + /** + * Scope a query to filter by the given dates + * @throws MissingProjectorNameException * @throws MissingProjectionPeriodException - * @throws MissingProjectionNameException */ public function scopeBetween(Builder $query, Carbon $startDate, Carbon $endDate): Builder { - if (is_null($this->queryName)) { - throw new MissingProjectionNameException(); + if (is_null($this->projectorName)) { + throw new MissingProjectorNameException(); } if (is_null($this->queryPeriod)) { @@ -100,20 +118,17 @@ public function scopeBetween(Builder $query, Carbon $startDate, Carbon $endDate) } /** - * Scope a query to filter by key. + * Scope a query to filter by the given dates and fill with empty period if necessary. */ - public function scopeKey(Builder $query, array | string | int $keys): Builder + public function scopeFillBetween(Builder $query, Carbon $startDate, Carbon $endDate): ProjectionCollection { - if (gettype($keys) === 'array') { - return $query->where(function ($query) use (&$keys) { - collect($keys)->each(function ($key, $index) use (&$query) { - return $index === 0 ? - $query->where('key', (string) $key) : - $query->orWhere('key', (string) $key); - }); - }); - } - - return $query->where('key', (string) $keys); + $projections = $query->between($startDate, $endDate)->get(); + + return $projections->fillBetween( + $this->projectorName, + $this->queryPeriod, + $startDate, + $endDate, + ); } } diff --git a/src/ProjectionCollection.php b/src/ProjectionCollection.php index 4266923..760d9b9 100644 --- a/src/ProjectionCollection.php +++ b/src/ProjectionCollection.php @@ -3,10 +3,64 @@ namespace Laravelcargo\LaravelCargo; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Carbon; +use Illuminate\Support\Str; +use Laravelcargo\LaravelCargo\Models\Projection; class ProjectionCollection extends Collection { /** - * Fills the collection with empty + * Fills the collection with empty projection between the given dates. */ + public function fillBetween(string $projectionName, string $period, Carbon $startDate, Carbon $endDate) + { + [$periodQuantity, $periodType] = Str::of($period)->split('/[\s]+/'); + + $startDate->floorUnit($periodType, $periodQuantity); + $endDate->floorUnit($periodType, $periodQuantity); + + $allPeriods = $this->getAllPeriods($startDate, $endDate, $period); + $allProjections = new self([]); + + $allPeriods->each(function (string $projectionPeriod) use (&$projectionName, &$period, &$allProjections) { + $projection = $this->firstWhere('start_date', $projectionPeriod); + + is_null($projection) ? + $allProjections->push($this->makeEmptyProjection($projectionName, $period, $projectionPeriod)) : + $allProjections->push($projection); + }); + + return $allProjections; + } + + /** + * Get the projections dates. + */ + private function getAllPeriods(Carbon $startDate, Carbon $endDate, string $period): \Illuminate\Support\Collection + { + $cursorDate = clone $startDate; + $allProjectionsDates = collect([$startDate]); + [$periodQuantity, $periodType] = Str::of($period)->split('/[\s]+/'); + + while ($cursorDate->notEqualTo($endDate)): + $cursorDate->add($periodQuantity, $periodType); + $allProjectionsDates->push(clone $cursorDate); + endwhile; + + return $allProjectionsDates; + } + + /** + * Makes an empty projection from the given projector name. + */ + private function makeEmptyProjection(string $projectorName, string $period, string $startDate): Projection + { + return Projection::make([ + 'projector_name' => $projectorName, + 'key' => null, + 'period' => $period, + 'start_date' => $startDate, + 'content' => $projectorName::defaultContent(), + ]); + } } diff --git a/src/Projector.php b/src/Projector.php index e3720a0..100f130 100644 --- a/src/Projector.php +++ b/src/Projector.php @@ -44,7 +44,7 @@ private function parsePeriod(string $period): void private function findProjection(string $period, int $quantity, string $periodType): Projection | null { return Projection::firstWhere([ - ['name', $this::class], + ['projector_name', $this::class], ['key', $this->hasKey() ? $this->key($this->model) : null], ['period', $period], ['start_date', Carbon::now()->floorUnit($periodType, $quantity)], @@ -57,11 +57,11 @@ private function findProjection(string $period, int $quantity, string $periodTyp private function createProjection(string $period, int $quantity, string $periodType): void { $this->model->projections()->create([ - 'name' => $this::class, + 'projector_name' => $this::class, 'key' => $this->hasKey() ? $this->key($this->model) : null, 'period' => $period, 'start_date' => Carbon::now()->floorUnit($periodType, $quantity), - 'content' => $this->handle($this->defaultContent(), $this->model), + 'content' => $this->handle($this::defaultContent(), $this->model), ]); } diff --git a/src/WithProjections.php b/src/WithProjections.php index 3611d4f..fa0b8e9 100644 --- a/src/WithProjections.php +++ b/src/WithProjections.php @@ -36,13 +36,13 @@ public function bootProjectors(): void * Get all the projections of the model. */ public function projections( - string | null $projectionName = null, + string | null $projectorName = null, string | array | null $periods = null, ): MorphToMany { $query = $this->morphToMany(Projection::class, 'projectable', 'cargo_projectables'); - if (isset($projectionName)) { - $query->where('name', $projectionName); + if (isset($projectorName)) { + $query->where('projector_name', $projectorName); } if (isset($periods) && gettype($periods) === 'string') { diff --git a/tests/ProjectionCollectionTest.php b/tests/ProjectionCollectionTest.php index 30a403f..82c1190 100644 --- a/tests/ProjectionCollectionTest.php +++ b/tests/ProjectionCollectionTest.php @@ -21,63 +21,78 @@ public function setUp(): void /** @test */ public function it_makes_the_missing_prior_period_when_filled() { - $startDate = Carbon::now()->subMinute(); + $startDate = Carbon::now()->subMinutes(5); $endDate = now(); Log::factory()->create(); - $unfilledProjections = Projection::name(SingleIntervalProjector::class) + $unfilledProjections = Projection::fromProjector(SingleIntervalProjector::class) ->period('5 minutes') ->between($startDate, $endDate)->get(); $this->assertCount(1, $unfilledProjections); - $filledProjections = Projection::name(SingleIntervalProjector::class) + $filledProjections = Projection::fromProjector(SingleIntervalProjector::class) ->period('5 minutes') - ->fillBetween($startDate, $endDate)->get(); + ->fillBetween($startDate, $endDate); $this->assertCount(2, $filledProjections); - $this->assertEquals($unfilledProjections->first()->id, $filledProjections()->last()->id); + $this->assertEquals($unfilledProjections->first()->id, $filledProjections->last()->id); + } + + /** @test */ + public function it_makes_the_missing_subsequent_period_when_filled() + { + $startDate = now(); + $endDate = Carbon::now()->addMinutes(5); + Log::factory()->create(); + + $unfilledProjections = Projection::fromProjector(SingleIntervalProjector::class) + ->period('5 minutes') + ->between($startDate, $endDate)->get(); + $this->assertCount(1, $unfilledProjections); + + $filledProjections = Projection::fromProjector(SingleIntervalProjector::class) + ->period('5 minutes') + ->fillBetween($startDate, $endDate); + $this->assertCount(2, $filledProjections); + + $this->assertEquals($unfilledProjections->first()->id, $filledProjections->first()->id); + } + + /** @test */ + public function it_makes_the_missing_between_period_when_filled() + { + $startDate = now(); + $endDate = Carbon::now()->addMinutes(10); + Log::factory()->create(); + $this->travel(10)->minutes(); + Log::factory()->create(); + + $unfilledProjections = Projection::fromProjector(SingleIntervalProjector::class) + ->period('5 minutes') + ->between($startDate, $endDate)->get(); + $this->assertCount(2, $unfilledProjections); + + $filledProjections = Projection::fromProjector(SingleIntervalProjector::class) + ->period('5 minutes') + ->fillBetween($startDate, $endDate); + $this->assertCount(3, $filledProjections); + + $this->assertEquals($unfilledProjections->first()->id, $filledProjections->first()->id); + $this->assertEquals($unfilledProjections->last()->id, $filledProjections->last()->id); + } + + /** @test */ + public function missing_periods_are_filled_with_default_content() + { + $filledProjections = Projection::fromProjector(SingleIntervalProjector::class) + ->period('5 minutes') + ->fillBetween(now(), Carbon::now()->addMinutes(10)); + + $filledProjections->each(function (Projection $filledProjection) { + $this->assertEquals($filledProjection->content, SingleIntervalProjector::defaultContent()); + }); } -// -// /** @test */ -// public function it_makes_the_missing_subsequent_period_when_filled() -// { -// Log::factory()->create(); -// -// $projections = Projection::period('5 minutes') -// ->between(Carbon::now()->addMinutes(6), Carbon::now()) -// ->filled(); -// -// $this->assertCount(1, $projectionsDB = Projection::all()); -// $this->assertCount(2, $projections); -// $this->assertEquals($projectionsDB->first()->id, $projections()->first()->id); -// } -// -// /** @test */ -// public function it_makes_the_missing_between_period_when_filled() -// { -// Log::factory()->create(); -// -// $projections = Projection::period('5 minutes') -// ->between(Carbon::now()->addMinutes(11), Carbon::now()) -// ->filled(); -// -// $this->assertCount(1, $projectionsDB = Projection::all()); -// $this->assertCount(2, $projections); -// $this->assertEquals($projectionsDB->first()->id, $projections()->first()->id); -// } -// -// /** @test */ -// public function missing_periods_are_filled_with_default_content() -// { -// $projections = Projection::period('5 minutes') -// ->between(Carbon::now()->subMinute(), Carbon::now()) -// ->filled(); -// -// $this->assertCount(1, $projectionsDB = Projection::all()); -// $this->assertCount(2, $projections); -// } -// // /** @test */ // public function it_raises_an_exception_when_a_multiple_periods_collection_is_filled() // { diff --git a/tests/ProjectionTest.php b/tests/ProjectionTest.php index a3cf526..e0cdb2a 100644 --- a/tests/ProjectionTest.php +++ b/tests/ProjectionTest.php @@ -3,8 +3,8 @@ namespace Laravelcargo\LaravelCargo\Tests; use Illuminate\Support\Carbon; -use Laravelcargo\LaravelCargo\Exceptions\MissingProjectionNameException; use Laravelcargo\LaravelCargo\Exceptions\MissingProjectionPeriodException; +use Laravelcargo\LaravelCargo\Exceptions\MissingProjectorNameException; use Laravelcargo\LaravelCargo\Models\Projection; use Laravelcargo\LaravelCargo\ProjectionCollection; use Laravelcargo\LaravelCargo\Tests\Models\Log; @@ -43,12 +43,12 @@ public function it_has_a_relationship_with_the_model() } /** @test */ - public function it_get_the_projections_from_name() + public function it_get_the_projections_from_projector_name() { $this->createModelWithProjectors(Log::class, [SingleIntervalProjector::class]); $this->createModelWithProjectors(Log::class, [MultipleIntervalsProjector::class]); - $numberOfProjections = Projection::name(SingleIntervalProjector::class)->count(); + $numberOfProjections = Projection::fromProjector(SingleIntervalProjector::class)->count(); $this->assertEquals(1, $numberOfProjections); } @@ -69,17 +69,17 @@ public function it_get_the_projections_from_a_single_period() /** @test */ public function it_raises_an_exception_when_using_the_between_scope_without_a_period() { - $this->expectException(MissingProjectionNameException::class); + $this->expectException(MissingProjectorNameException::class); Projection::between(now()->subMinute(), now()); } /** @test */ - public function it_raises_an_exception_when_using_the_between_scope_without_a_name() + public function it_raises_an_exception_when_using_the_between_scope_without_the_projector_name() { $this->expectException(MissingProjectionPeriodException::class); - Projection::name(SingleIntervalProjector::class)->between(now()->subMinute(), now()); + Projection::fromProjector(SingleIntervalProjector::class)->between(now()->subMinute(), now()); } /** @test */ @@ -93,7 +93,7 @@ public function it_get_the_projections_between_the_given_dates() $this->travel(5)->minutes(); Log::factory()->create(); // Should be excluded - $betweenProjections = Projection::name(SingleIntervalProjector::class) + $betweenProjections = Projection::fromProjector(SingleIntervalProjector::class) ->period('5 minutes') ->between( Carbon::today()->addMinutes(6), // date will be rounded to the floor to 5 minutes diff --git a/tests/Projectors/SingleIntervalProjector.php b/tests/Projectors/SingleIntervalProjector.php index 73609b0..9b48189 100644 --- a/tests/Projectors/SingleIntervalProjector.php +++ b/tests/Projectors/SingleIntervalProjector.php @@ -17,7 +17,7 @@ class SingleIntervalProjector extends Projector /** * The default projection content. */ - public function defaultContent(): array + public static function defaultContent(): array { return [ 'number of logs' => 0, diff --git a/tests/WithProjectionTest.php b/tests/WithProjectionTest.php index ead4485..1e83cf6 100644 --- a/tests/WithProjectionTest.php +++ b/tests/WithProjectionTest.php @@ -106,7 +106,7 @@ public function it_get_the_projections_from_a_single_type() $this->assertCount(8, $projections); $projections->each(function (Projection $projection) { - $this->assertEquals(MultipleIntervalsProjector::class, $projection->name); + $this->assertEquals(MultipleIntervalsProjector::class, $projection->projector_name); }); }