From f89dbbaccdb49c18df2f00aeaf29f80cc6d6ce67 Mon Sep 17 00:00:00 2001 From: Salh Date: Mon, 2 Sep 2024 07:42:25 +0100 Subject: [PATCH 1/7] Added configurable date column Change the date field for the time-series --- README.md | 21 +++++++++++++++++++++ composer.json | 1 + src/Models/Traits/Projectable.php | 2 ++ src/Projector.php | 6 +++--- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 416fa99..90f8013 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,27 @@ class MyProjectableModel extends Model ]; } ``` +If you want to use a different date field from your Model instead of created_at then do the following : + +```php +use App\Models\Projections\MyProjection; +use TimothePearce\TimeSeries\Projectable; + +class MyProjectableModel extends Model +{ + use Projectable; + + public string $dateColumn = 'other_date_time'; + + protected $casts = [ + 'other_date_time' => 'datetime:Y-m-d H:00', + ]; + + protected array $projections = [ + MyProjection::class, + ]; +} +``` ### Implement a Projection diff --git a/composer.json b/composer.json index b39c9ec..80cbd15 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "php": "^8.0" }, "require-dev": { + "brianium/paratest": "^6.11", "nunomaduro/collision": "^6.0", "nunomaduro/larastan": "^2.0.1", "orchestra/testbench": "^7.0", diff --git a/src/Models/Traits/Projectable.php b/src/Models/Traits/Projectable.php index 7bf1e3e..49266e9 100644 --- a/src/Models/Traits/Projectable.php +++ b/src/Models/Traits/Projectable.php @@ -10,6 +10,8 @@ trait Projectable { + public string $dateColumn = 'created_at'; + /** * Boots the trait. */ diff --git a/src/Projector.php b/src/Projector.php index 51bfc5d..1e63126 100644 --- a/src/Projector.php +++ b/src/Projector.php @@ -104,7 +104,7 @@ private function findProjection(string $period): Projection|null ['projection_name', $this->projectionName], ['key', $this->hasKey() ? $this->key() : null], ['period', $period], - ['start_date', app(TimeSeries::class)->resolveFloorDate($this->projectedModel->created_at, $period)], + ['start_date', app(TimeSeries::class)->resolveFloorDate($this->projectedModel->{$this->projectedModel->dateColumn}, $period)], ]); } @@ -117,7 +117,7 @@ private function createProjection(string $period): void 'projection_name' => $this->projectionName, 'key' => $this->hasKey() ? $this->key() : null, 'period' => $period, - 'start_date' => app(TimeSeries::class)->resolveFloorDate($this->projectedModel->created_at, $period), + 'start_date' => app(TimeSeries::class)->resolveFloorDate($this->projectedModel->{$this->projectedModel->dateColumn}, $period), 'content' => $this->mergeProjectedContent((new $this->projectionName())->defaultContent(), $period), ]); } @@ -193,7 +193,7 @@ private function resolveCallableMethod(array $content, string $period): array */ private function resolveStartDate(string $periodType, int $quantity): Carbon { - $startDate = $this->projectedModel->created_at->floorUnit($periodType, $quantity); + $startDate = $this->projectedModel->{$this->projectedModel->dateColumn}->floorUnit($periodType, $quantity); if (in_array($periodType, ['week', 'weeks'])) { $startDate->startOfWeek(config('time-series.beginning_of_the_week')); From ab4c8212567e863ff73be9c87d5eaa14ee4e9027 Mon Sep 17 00:00:00 2001 From: Salh Date: Mon, 2 Sep 2024 19:22:24 +0100 Subject: [PATCH 2/7] Removed datecolum from trait and placed it only in the Model --- README.md | 2 ++ src/Models/Traits/Projectable.php | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 90f8013..db32ffc 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ class MyProjectableModel extends Model { use Projectable; + public string $dateColumn = 'created_at'; + protected array $projections = [ MyProjection::class, ]; diff --git a/src/Models/Traits/Projectable.php b/src/Models/Traits/Projectable.php index 49266e9..7bf1e3e 100644 --- a/src/Models/Traits/Projectable.php +++ b/src/Models/Traits/Projectable.php @@ -10,8 +10,6 @@ trait Projectable { - public string $dateColumn = 'created_at'; - /** * Boots the trait. */ From c9d49853e035204fd4fb43cd28162efff9d564fc Mon Sep 17 00:00:00 2001 From: Salh Date: Tue, 3 Sep 2024 09:35:06 +0100 Subject: [PATCH 3/7] Updated the Test Models since the dateColumn was removed from the trait --- tests/Models/Log.php | 1 + tests/Models/Message.php | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/Models/Log.php b/tests/Models/Log.php index a4a68fc..46637c3 100644 --- a/tests/Models/Log.php +++ b/tests/Models/Log.php @@ -18,4 +18,5 @@ class Log extends Model * The projections list. */ protected array $projections = [SinglePeriodProjection::class]; + public string $dateColumn = 'created_at'; } diff --git a/tests/Models/Message.php b/tests/Models/Message.php index 6eda02c..ec828c0 100644 --- a/tests/Models/Message.php +++ b/tests/Models/Message.php @@ -18,4 +18,5 @@ class Message extends Model * The projections list. */ protected array $projections = [SinglePeriodProjection::class]; + public string $dateColumn = 'created_at'; } From 5d69e067549045a4a1eeb2c75dbd0e0485029669 Mon Sep 17 00:00:00 2001 From: Salh Date: Tue, 3 Sep 2024 12:28:05 +0100 Subject: [PATCH 4/7] Made the changes to allow changes of the date field on the Projection level. --- README.md | 25 +++++++++++++++++++++---- src/Projector.php | 12 +++++++++--- tests/Models/Log.php | 1 - tests/Models/Message.php | 1 - 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index db32ffc..08c2585 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,13 @@ class MyProjectableModel extends Model { use Projectable; - public string $dateColumn = 'created_at'; - protected array $projections = [ MyProjection::class, ]; } ``` If you want to use a different date field from your Model instead of created_at then do the following : +1) Make sure the field is casted to Carbon ```php use App\Models\Projections\MyProjection; @@ -73,8 +72,6 @@ class MyProjectableModel extends Model { use Projectable; - public string $dateColumn = 'other_date_time'; - protected $casts = [ 'other_date_time' => 'datetime:Y-m-d H:00', ]; @@ -84,6 +81,26 @@ class MyProjectableModel extends Model ]; } ``` +2) Add the dateColumn field in your Projection +```php +namespace App\Models\Projections; + +use Illuminate\Database\Eloquent\Model; +use TimothePearce\TimeSeries\Contracts\ProjectionContract; +use TimothePearce\TimeSeries\Models\Projection; + +class MyProjection extends Projection implements ProjectionContract +{ + /** + * The projected periods. + */ + public array $periods = []; + + public string $dateColumn = 'other_date_time'; +.... + +``` + ### Implement a Projection diff --git a/src/Projector.php b/src/Projector.php index 1e63126..362be9c 100644 --- a/src/Projector.php +++ b/src/Projector.php @@ -9,11 +9,17 @@ class Projector { + public string $dateColumn = 'created_at'; + public function __construct( protected Model $projectedModel, protected string $projectionName, protected string $eventName, ) { + $projection = (new $this->projectionName()); + if (isset($projection->dateColumn)) { + $this->dateColumn = $projection->dateColumn; + } } /** @@ -104,7 +110,7 @@ private function findProjection(string $period): Projection|null ['projection_name', $this->projectionName], ['key', $this->hasKey() ? $this->key() : null], ['period', $period], - ['start_date', app(TimeSeries::class)->resolveFloorDate($this->projectedModel->{$this->projectedModel->dateColumn}, $period)], + ['start_date', app(TimeSeries::class)->resolveFloorDate($this->projectedModel->{$this->dateColumn}, $period)], ]); } @@ -117,7 +123,7 @@ private function createProjection(string $period): void 'projection_name' => $this->projectionName, 'key' => $this->hasKey() ? $this->key() : null, 'period' => $period, - 'start_date' => app(TimeSeries::class)->resolveFloorDate($this->projectedModel->{$this->projectedModel->dateColumn}, $period), + 'start_date' => app(TimeSeries::class)->resolveFloorDate($this->projectedModel->{$this->dateColumn}, $period), 'content' => $this->mergeProjectedContent((new $this->projectionName())->defaultContent(), $period), ]); } @@ -193,7 +199,7 @@ private function resolveCallableMethod(array $content, string $period): array */ private function resolveStartDate(string $periodType, int $quantity): Carbon { - $startDate = $this->projectedModel->{$this->projectedModel->dateColumn}->floorUnit($periodType, $quantity); + $startDate = $this->projectedModel->{$this->dateColumn}->floorUnit($periodType, $quantity); if (in_array($periodType, ['week', 'weeks'])) { $startDate->startOfWeek(config('time-series.beginning_of_the_week')); diff --git a/tests/Models/Log.php b/tests/Models/Log.php index 46637c3..a4a68fc 100644 --- a/tests/Models/Log.php +++ b/tests/Models/Log.php @@ -18,5 +18,4 @@ class Log extends Model * The projections list. */ protected array $projections = [SinglePeriodProjection::class]; - public string $dateColumn = 'created_at'; } diff --git a/tests/Models/Message.php b/tests/Models/Message.php index ec828c0..6eda02c 100644 --- a/tests/Models/Message.php +++ b/tests/Models/Message.php @@ -18,5 +18,4 @@ class Message extends Model * The projections list. */ protected array $projections = [SinglePeriodProjection::class]; - public string $dateColumn = 'created_at'; } From 81bc14db77052db171a3c06863c28e1d58d668e6 Mon Sep 17 00:00:00 2001 From: Salh Date: Thu, 5 Sep 2024 15:21:07 +0100 Subject: [PATCH 5/7] Added New Functionality Tests --- ...TableReservationPerDiningDayProjection.php | 50 +++++ ...servationPerDiningDayProjectionWithKey.php | 18 ++ .../TableReservationPerMadeDayProjection.php | 14 ++ tests/Models/TableReservation.php | 29 +++ tests/ProjectionWithConfigurableDateTest.php | 187 ++++++++++++++++++ .../factories/TableReservationFactory.php | 24 +++ .../create_tablereservations_table.php | 26 +++ 7 files changed, 348 insertions(+) create mode 100644 tests/Models/Projections/TableReservationPerDiningDayProjection.php create mode 100644 tests/Models/Projections/TableReservationPerDiningDayProjectionWithKey.php create mode 100644 tests/Models/Projections/TableReservationPerMadeDayProjection.php create mode 100644 tests/Models/TableReservation.php create mode 100644 tests/ProjectionWithConfigurableDateTest.php create mode 100644 tests/database/factories/TableReservationFactory.php create mode 100644 tests/database/migrations/create_tablereservations_table.php diff --git a/tests/Models/Projections/TableReservationPerDiningDayProjection.php b/tests/Models/Projections/TableReservationPerDiningDayProjection.php new file mode 100644 index 0000000..b0c93d4 --- /dev/null +++ b/tests/Models/Projections/TableReservationPerDiningDayProjection.php @@ -0,0 +1,50 @@ + 0, + 'number_reservations' => 0, + ]; + } + + /** + * Computes the content when a projectable model is created. + */ + public function projectableCreated(array $content, Model $model): array + { + return [ + 'total_people' => $content['total_people'] += $model->number_people, + 'number_reservations' => $content['number_reservations'] + 1, + ]; + } + + /** + * Computes the content when a projectable model is deleted. + */ + public function projectableDeleted(array $content, Model $model): array + { + return [ + 'total_people' => $content['total_people'] -= $model->number_people, + 'number_reservations' => $content['number_reservations'] - 1, + ]; + } +} diff --git a/tests/Models/Projections/TableReservationPerDiningDayProjectionWithKey.php b/tests/Models/Projections/TableReservationPerDiningDayProjectionWithKey.php new file mode 100644 index 0000000..3720d75 --- /dev/null +++ b/tests/Models/Projections/TableReservationPerDiningDayProjectionWithKey.php @@ -0,0 +1,18 @@ +table_id; + } +} diff --git a/tests/Models/Projections/TableReservationPerMadeDayProjection.php b/tests/Models/Projections/TableReservationPerMadeDayProjection.php new file mode 100644 index 0000000..5cf54c1 --- /dev/null +++ b/tests/Models/Projections/TableReservationPerMadeDayProjection.php @@ -0,0 +1,14 @@ +dateColumn = 'reservation_made_date'; + } +} diff --git a/tests/Models/TableReservation.php b/tests/Models/TableReservation.php new file mode 100644 index 0000000..9f1190e --- /dev/null +++ b/tests/Models/TableReservation.php @@ -0,0 +1,29 @@ + 'datetime:Y-m-d', + 'reservation_made_date' => 'datetime:Y-m-d H:00', + ]; + /** + * The projections list. + */ + protected array $projections = [ + TableReservationPerMadeDayProjection::class, + TableReservationPerDiningDayProjection::class, + ]; +} diff --git a/tests/ProjectionWithConfigurableDateTest.php b/tests/ProjectionWithConfigurableDateTest.php new file mode 100644 index 0000000..9fd8ada --- /dev/null +++ b/tests/ProjectionWithConfigurableDateTest.php @@ -0,0 +1,187 @@ +travelTo(Carbon::today()); + } + + /** @test */ + public function it_gets_a_custom_collection() + { + TableReservation::factory()->count(2)->create(); + + $collection = Projection::all(); + + $this->assertInstanceOf(ProjectionCollection::class, $collection); + } + + /** @test */ + public function it_has_a_relationship_with_the_model() + { + TableReservation::factory()->create(); + $projection = Projection::first(); + + $this->assertNotEmpty($projection->from(TableReservation::class)->get()); + } + + /** @test */ + public function it_gets_the_projections_from_projection_name() + { + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); + $this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]); + + $numberOfProjections = Projection::name(TableReservationPerDiningDayProjection::class)->count(); + + $this->assertEquals(1, $numberOfProjections); + } + + /** @test */ + public function it_gets_the_projections_from_a_single_period() + { + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 + $this->travel(5)->days(); + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 2 + + $numberOfProjections = Projection::period('1 day')->count(); + + $this->assertEquals(2, $numberOfProjections); + } + + /** @test */ + public function it_raises_an_exception_when_using_the_between_scope_without_a_period() + { + $this->expectException(MissingProjectionNameException::class); + + Projection::between(now()->subMinute(), now()); + } + + /** @test */ + public function it_raises_an_exception_when_using_the_between_scope_without_the_projection_name() + { + $this->expectException(MissingProjectionPeriodException::class); + + Projection::name(TableReservationPerDiningDayProjection::class)->between(now()->subMinute(), now()); + } + + /** @test */ + public function it_gets_the_projections_between_the_given_dates_for_made_date() + { + $this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]); // 1 // Should be excluded + $this->travel(5)->days(); + $tablereservation = $this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]); // 1 // Should be included + $this->travel(5)->days(); + $this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]); // 1 // Should be excluded + + $this->travelBack(); + + $betweenProjections = Projection::name(TableReservationPerMadeDayProjection::class) + ->period('1 day') + ->between( + Carbon::today()->addDays(5), + Carbon::today()->addDays(10) + )->get(); + $this->assertCount(1, $betweenProjections); + $this->assertEquals($betweenProjections->first()->id, $tablereservation->firstProjection(TableReservationPerMadeDayProjection::class)->id); + $this->assertEquals($betweenProjections->first()->start_date, Carbon::today()->addDays(5)); + + } + + /** @test */ + public function it_gets_the_projections_between_the_given_dates_for_dining_date() + { + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 // Should be excluded + $this->travel(5)->days(); + $tablereservation = $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 // Should be included + $this->travel(5)->days(); + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 // Should be excluded + + $this->travelBack(); + + /* Here we test based on the dining date of the reservation and it should be made_date + 10 days */ + $betweenProjections = Projection::name(TableReservationPerDiningDayProjection::class) + ->period('1 day') + ->between( + Carbon::today()->addDays(15), + Carbon::today()->addDays(20) + )->get(); + + $this->assertCount(1, $betweenProjections); + $this->assertEquals($betweenProjections->first()->id, $tablereservation->firstProjection(TableReservationPerDiningDayProjection::class)->id); + $this->assertEquals($betweenProjections->first()->start_date, Carbon::today()->addDays(15)); // dining_date is set 10 day from creation date + } + + /** @test */ + public function it_gets_the_projections_between_the_given_dates_for_all_projections() + { + $tablereservation1 = TableReservation::factory()->create(); + // 1 made_date = created_at = now(), reservation_date = now()+10 days, total_people = 2, number_reservation = 1 + $this->travel(15)->minutes(); + $tablereservation2 = TableReservation::factory()->create(); + // 2 made_date = created_at = now(), reservation_date = now()+10, total_people = 2+2, number_reservation = 1+1 + $this->travel(2)->days(); + $tablereservation3 = TableReservation::factory()->create(); + // 3 made_date = created_at = now()+2, reservation_date = now()+2+10, total_people = 2, number_reservation = 1 + + $this->travelBack(); // reset the Carbon:date back to today + + /* Here we test based on the dining date of the reservation and it should be made_date + 10 days */ + $betweenProjections = Projection::name(TableReservationPerDiningDayProjection::class) + ->period('1 day') + ->between( + Carbon::today()->addDays(10), + Carbon::today()->addDays(11) + )->get(); + $this->assertCount(1, $betweenProjections); + $this->assertEquals(4, $betweenProjections->first()->content['total_people']); + $this->assertEquals(2, $betweenProjections->first()->content['number_reservations']); + + $betweenProjections = Projection::name(TableReservationPerMadeDayProjection::class) + ->period('1 day') + ->between( + Carbon::today(), + Carbon::today()->addDays(2) + )->get(); + $this->assertCount(1, $betweenProjections); + $this->assertEquals(4, $betweenProjections->first()->content['total_people']); + $this->assertEquals(2, $betweenProjections->first()->content['number_reservations']); + + $betweenProjections = Projection::name(TableReservationPerDiningDayProjection::class) + ->period('1 day') + ->between( + Carbon::today()->addDays(12), + Carbon::today()->addDays(13) + )->get(); + $this->assertCount(1, $betweenProjections); + $this->assertEquals(2, $betweenProjections->first()->content['total_people']); + $this->assertEquals(1, $betweenProjections->first()->content['number_reservations']); + + $betweenProjections = Projection::name(TableReservationPerMadeDayProjection::class) + ->period('1 day') + ->between( + Carbon::today()->addDays(2), + Carbon::today()->addDays(3) + )->get(); + $this->assertCount(1, $betweenProjections); + $this->assertEquals(2, $betweenProjections->first()->content['total_people']); + $this->assertEquals(1, $betweenProjections->first()->content['number_reservations']); + } +} diff --git a/tests/database/factories/TableReservationFactory.php b/tests/database/factories/TableReservationFactory.php new file mode 100644 index 0000000..4d80290 --- /dev/null +++ b/tests/database/factories/TableReservationFactory.php @@ -0,0 +1,24 @@ + 1, //$this->random_int(1, 10), + 'customer_name' => $this->faker->name, + 'reservation_date' => today()->addDays(10)->format('Y-m-d'), + 'reservation_made_date' => today()->format('Y-m-d H:00'), + 'number_people' => 2, //$this->random_int(1, 10), + ]; + } +} + + diff --git a/tests/database/migrations/create_tablereservations_table.php b/tests/database/migrations/create_tablereservations_table.php new file mode 100644 index 0000000..3db582b --- /dev/null +++ b/tests/database/migrations/create_tablereservations_table.php @@ -0,0 +1,26 @@ +id(); + $table->integer('table_id'); + $table->string('customer_name'); + $table->dateTime('reservation_date'); + $table->dateTime('reservation_made_date'); + $table->integer('number_people'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down() + { + Schema::dropIfExists('table_reservations'); + } +}; From 4e029eaee58aaa5947c42a6336b1fa4ce7bf7991 Mon Sep 17 00:00:00 2001 From: Salh Date: Thu, 5 Sep 2024 15:22:53 +0100 Subject: [PATCH 6/7] Removed SerializesModels as tests would fail if queue is enabled --- src/Jobs/ComputeProjection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jobs/ComputeProjection.php b/src/Jobs/ComputeProjection.php index 7249594..579e1f4 100644 --- a/src/Jobs/ComputeProjection.php +++ b/src/Jobs/ComputeProjection.php @@ -14,7 +14,7 @@ class ComputeProjection implements ShouldQueue use Dispatchable; use InteractsWithQueue; use Queueable; - use SerializesModels; + //use SerializesModels; /** * Create a new job instance. From c461efddc5765b8d8b8bd208fd634d3dd3e26abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9=20Pearce?= Date: Wed, 26 Mar 2025 21:15:16 +0100 Subject: [PATCH 7/7] Update ComputeProjection.php Keep model serialization. --- src/Jobs/ComputeProjection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jobs/ComputeProjection.php b/src/Jobs/ComputeProjection.php index 579e1f4..7249594 100644 --- a/src/Jobs/ComputeProjection.php +++ b/src/Jobs/ComputeProjection.php @@ -14,7 +14,7 @@ class ComputeProjection implements ShouldQueue use Dispatchable; use InteractsWithQueue; use Queueable; - //use SerializesModels; + use SerializesModels; /** * Create a new job instance.