Skip to content

Commit c7b4321

Browse files
Merge branch 'main' into drsdre-patch-fqdn-backslash
2 parents 7d48abd + 067d9b0 commit c7b4321

10 files changed

+391
-3
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,46 @@ class MyProjectableModel extends Model
6161
];
6262
}
6363
```
64+
If you want to use a different date field from your Model instead of created_at then do the following :
65+
1) Make sure the field is casted to Carbon
66+
67+
```php
68+
use App\Models\Projections\MyProjection;
69+
use TimothePearce\TimeSeries\Projectable;
70+
71+
class MyProjectableModel extends Model
72+
{
73+
use Projectable;
74+
75+
protected $casts = [
76+
'other_date_time' => 'datetime:Y-m-d H:00',
77+
];
78+
79+
protected array $projections = [
80+
MyProjection::class,
81+
];
82+
}
83+
```
84+
2) Add the dateColumn field in your Projection
85+
```php
86+
namespace App\Models\Projections;
87+
88+
use Illuminate\Database\Eloquent\Model;
89+
use TimothePearce\TimeSeries\Contracts\ProjectionContract;
90+
use TimothePearce\TimeSeries\Models\Projection;
91+
92+
class MyProjection extends Projection implements ProjectionContract
93+
{
94+
/**
95+
* The projected periods.
96+
*/
97+
public array $periods = [];
98+
99+
public string $dateColumn = 'other_date_time';
100+
....
101+
102+
```
103+
64104

65105
### Implement a Projection
66106

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"php": "^8.0"
2727
},
2828
"require-dev": {
29+
"brianium/paratest": "^6.11",
2930
"nunomaduro/collision": "^6.0",
3031
"nunomaduro/larastan": "^2.0.1",
3132
"orchestra/testbench": "^7.0",

src/Projector.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@
99

1010
class Projector
1111
{
12+
public string $dateColumn = 'created_at';
13+
1214
public function __construct(
1315
protected Model $projectedModel,
1416
protected string $projectionName,
1517
protected string $eventName,
1618
) {
19+
$projection = (new $this->projectionName());
20+
if (isset($projection->dateColumn)) {
21+
$this->dateColumn = $projection->dateColumn;
22+
}
1723
}
1824

1925
/**
@@ -105,7 +111,7 @@ private function findProjection(string $period): Projection|null
105111
->where([
106112
['key', $this->hasKey() ? $this->key() : null],
107113
['period', $period],
108-
['start_date', app(TimeSeries::class)->resolveFloorDate($this->projectedModel->updated_at, $period),
114+
['start_date', app(TimeSeries::class)->resolveFloorDate($this->projectedModel->{$this->dateColumn}, $period),
109115
],
110116
])
111117
->first();
@@ -120,7 +126,7 @@ private function createProjection(string $period): void
120126
'projection_name' => $this->projectionName,
121127
'key' => $this->hasKey() ? $this->key() : null,
122128
'period' => $period,
123-
'start_date' => app(TimeSeries::class)->resolveFloorDate($this->projectedModel->updated_at, $period),
129+
'start_date' => app(TimeSeries::class)->resolveFloorDate($this->projectedModel->{$this->dateColumn}, $period),
124130
'content' => $this->mergeProjectedContent((new $this->projectionName())->defaultContent(), $period),
125131
]);
126132
}
@@ -196,7 +202,7 @@ private function resolveCallableMethod(array $content, string $period): array
196202
*/
197203
private function resolveStartDate(string $periodType, int $quantity): Carbon
198204
{
199-
$startDate = $this->projectedModel->created_at->floorUnit($periodType, $quantity);
205+
$startDate = $this->projectedModel->{$this->dateColumn}->floorUnit($periodType, $quantity);
200206

201207
if (in_array($periodType, ['week', 'weeks'])) {
202208
$startDate->startOfWeek(config('time-series.beginning_of_the_week'));
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace TimothePearce\TimeSeries\Tests\Models\Projections;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use TimothePearce\TimeSeries\Contracts\ProjectionContract;
7+
use TimothePearce\TimeSeries\Models\Projection;
8+
9+
class TableReservationPerDiningDayProjection extends Projection implements ProjectionContract
10+
{
11+
/**
12+
* Lists the time intervals used to compute the projections.
13+
*/
14+
public array $periods = ['1 day'];
15+
16+
public string $dateColumn = 'reservation_date';
17+
18+
/**
19+
* The default projection content.
20+
*/
21+
public function defaultContent(): array
22+
{
23+
return [
24+
'total_people' => 0,
25+
'number_reservations' => 0,
26+
];
27+
}
28+
29+
/**
30+
* Computes the content when a projectable model is created.
31+
*/
32+
public function projectableCreated(array $content, Model $model): array
33+
{
34+
return [
35+
'total_people' => $content['total_people'] += $model->number_people,
36+
'number_reservations' => $content['number_reservations'] + 1,
37+
];
38+
}
39+
40+
/**
41+
* Computes the content when a projectable model is deleted.
42+
*/
43+
public function projectableDeleted(array $content, Model $model): array
44+
{
45+
return [
46+
'total_people' => $content['total_people'] -= $model->number_people,
47+
'number_reservations' => $content['number_reservations'] - 1,
48+
];
49+
}
50+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace TimothePearce\TimeSeries\Tests\Models\Projections;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use TimothePearce\TimeSeries\Models\Projection;
7+
8+
class TableReservationPerDiningDayProjectionWithKey extends TableReservationPerDiningDayProjection
9+
{
10+
/**
11+
* The key used to query the projection.
12+
*/
13+
public static function key(Model $model): string
14+
{
15+
return (string)$model->table_id;
16+
}
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace TimothePearce\TimeSeries\Tests\Models\Projections;
4+
5+
class TableReservationPerMadeDayProjection extends TableReservationPerDiningDayProjection
6+
{
7+
public function __construct()
8+
{
9+
$this->dateColumn = 'reservation_made_date';
10+
}
11+
}

tests/Models/TableReservation.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace TimothePearce\TimeSeries\Tests\Models;
4+
5+
use Illuminate\Database\Eloquent\Factories\HasFactory;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\SoftDeletes;
8+
use TimothePearce\TimeSeries\Models\Traits\Projectable;
9+
use TimothePearce\TimeSeries\Tests\Models\Projections\TableReservationPerDiningDayProjection;
10+
use TimothePearce\TimeSeries\Tests\Models\Projections\TableReservationPerMadeDayProjection;
11+
12+
class TableReservation extends Model
13+
{
14+
use HasFactory;
15+
use Projectable;
16+
use SoftDeletes;
17+
18+
protected $casts = [
19+
'reservation_date' => 'datetime:Y-m-d',
20+
'reservation_made_date' => 'datetime:Y-m-d H:00',
21+
];
22+
/**
23+
* The projections list.
24+
*/
25+
protected array $projections = [
26+
TableReservationPerMadeDayProjection::class,
27+
TableReservationPerDiningDayProjection::class,
28+
];
29+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<?php
2+
3+
namespace TimothePearce\TimeSeries\Tests;
4+
5+
use Illuminate\Support\Carbon;
6+
use TimothePearce\TimeSeries\Collections\ProjectionCollection;
7+
use TimothePearce\TimeSeries\Exceptions\MissingProjectionNameException;
8+
use TimothePearce\TimeSeries\Exceptions\MissingProjectionPeriodException;
9+
use TimothePearce\TimeSeries\Models\Projection;
10+
use TimothePearce\TimeSeries\Tests\Models\Projections\TableReservationPerDiningDayProjection;
11+
use TimothePearce\TimeSeries\Tests\Models\Projections\TableReservationPerMadeDayProjection;
12+
use TimothePearce\TimeSeries\Tests\Models\TableReservation;
13+
14+
class ProjectionWithConfigurableDateTest extends TestCase
15+
{
16+
use ProjectableFactory;
17+
18+
public function setUp(): void
19+
{
20+
parent::setUp();
21+
22+
$this->travelTo(Carbon::today());
23+
}
24+
25+
/** @test */
26+
public function it_gets_a_custom_collection()
27+
{
28+
TableReservation::factory()->count(2)->create();
29+
30+
$collection = Projection::all();
31+
32+
$this->assertInstanceOf(ProjectionCollection::class, $collection);
33+
}
34+
35+
/** @test */
36+
public function it_has_a_relationship_with_the_model()
37+
{
38+
TableReservation::factory()->create();
39+
$projection = Projection::first();
40+
41+
$this->assertNotEmpty($projection->from(TableReservation::class)->get());
42+
}
43+
44+
/** @test */
45+
public function it_gets_the_projections_from_projection_name()
46+
{
47+
$this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]);
48+
$this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]);
49+
50+
$numberOfProjections = Projection::name(TableReservationPerDiningDayProjection::class)->count();
51+
52+
$this->assertEquals(1, $numberOfProjections);
53+
}
54+
55+
/** @test */
56+
public function it_gets_the_projections_from_a_single_period()
57+
{
58+
$this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1
59+
$this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1
60+
$this->travel(5)->days();
61+
$this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 2
62+
63+
$numberOfProjections = Projection::period('1 day')->count();
64+
65+
$this->assertEquals(2, $numberOfProjections);
66+
}
67+
68+
/** @test */
69+
public function it_raises_an_exception_when_using_the_between_scope_without_a_period()
70+
{
71+
$this->expectException(MissingProjectionNameException::class);
72+
73+
Projection::between(now()->subMinute(), now());
74+
}
75+
76+
/** @test */
77+
public function it_raises_an_exception_when_using_the_between_scope_without_the_projection_name()
78+
{
79+
$this->expectException(MissingProjectionPeriodException::class);
80+
81+
Projection::name(TableReservationPerDiningDayProjection::class)->between(now()->subMinute(), now());
82+
}
83+
84+
/** @test */
85+
public function it_gets_the_projections_between_the_given_dates_for_made_date()
86+
{
87+
$this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]); // 1 // Should be excluded
88+
$this->travel(5)->days();
89+
$tablereservation = $this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]); // 1 // Should be included
90+
$this->travel(5)->days();
91+
$this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]); // 1 // Should be excluded
92+
93+
$this->travelBack();
94+
95+
$betweenProjections = Projection::name(TableReservationPerMadeDayProjection::class)
96+
->period('1 day')
97+
->between(
98+
Carbon::today()->addDays(5),
99+
Carbon::today()->addDays(10)
100+
)->get();
101+
$this->assertCount(1, $betweenProjections);
102+
$this->assertEquals($betweenProjections->first()->id, $tablereservation->firstProjection(TableReservationPerMadeDayProjection::class)->id);
103+
$this->assertEquals($betweenProjections->first()->start_date, Carbon::today()->addDays(5));
104+
105+
}
106+
107+
/** @test */
108+
public function it_gets_the_projections_between_the_given_dates_for_dining_date()
109+
{
110+
$this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 // Should be excluded
111+
$this->travel(5)->days();
112+
$tablereservation = $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 // Should be included
113+
$this->travel(5)->days();
114+
$this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 // Should be excluded
115+
116+
$this->travelBack();
117+
118+
/* Here we test based on the dining date of the reservation and it should be made_date + 10 days */
119+
$betweenProjections = Projection::name(TableReservationPerDiningDayProjection::class)
120+
->period('1 day')
121+
->between(
122+
Carbon::today()->addDays(15),
123+
Carbon::today()->addDays(20)
124+
)->get();
125+
126+
$this->assertCount(1, $betweenProjections);
127+
$this->assertEquals($betweenProjections->first()->id, $tablereservation->firstProjection(TableReservationPerDiningDayProjection::class)->id);
128+
$this->assertEquals($betweenProjections->first()->start_date, Carbon::today()->addDays(15)); // dining_date is set 10 day from creation date
129+
}
130+
131+
/** @test */
132+
public function it_gets_the_projections_between_the_given_dates_for_all_projections()
133+
{
134+
$tablereservation1 = TableReservation::factory()->create();
135+
// 1 made_date = created_at = now(), reservation_date = now()+10 days, total_people = 2, number_reservation = 1
136+
$this->travel(15)->minutes();
137+
$tablereservation2 = TableReservation::factory()->create();
138+
// 2 made_date = created_at = now(), reservation_date = now()+10, total_people = 2+2, number_reservation = 1+1
139+
$this->travel(2)->days();
140+
$tablereservation3 = TableReservation::factory()->create();
141+
// 3 made_date = created_at = now()+2, reservation_date = now()+2+10, total_people = 2, number_reservation = 1
142+
143+
$this->travelBack(); // reset the Carbon:date back to today
144+
145+
/* Here we test based on the dining date of the reservation and it should be made_date + 10 days */
146+
$betweenProjections = Projection::name(TableReservationPerDiningDayProjection::class)
147+
->period('1 day')
148+
->between(
149+
Carbon::today()->addDays(10),
150+
Carbon::today()->addDays(11)
151+
)->get();
152+
$this->assertCount(1, $betweenProjections);
153+
$this->assertEquals(4, $betweenProjections->first()->content['total_people']);
154+
$this->assertEquals(2, $betweenProjections->first()->content['number_reservations']);
155+
156+
$betweenProjections = Projection::name(TableReservationPerMadeDayProjection::class)
157+
->period('1 day')
158+
->between(
159+
Carbon::today(),
160+
Carbon::today()->addDays(2)
161+
)->get();
162+
$this->assertCount(1, $betweenProjections);
163+
$this->assertEquals(4, $betweenProjections->first()->content['total_people']);
164+
$this->assertEquals(2, $betweenProjections->first()->content['number_reservations']);
165+
166+
$betweenProjections = Projection::name(TableReservationPerDiningDayProjection::class)
167+
->period('1 day')
168+
->between(
169+
Carbon::today()->addDays(12),
170+
Carbon::today()->addDays(13)
171+
)->get();
172+
$this->assertCount(1, $betweenProjections);
173+
$this->assertEquals(2, $betweenProjections->first()->content['total_people']);
174+
$this->assertEquals(1, $betweenProjections->first()->content['number_reservations']);
175+
176+
$betweenProjections = Projection::name(TableReservationPerMadeDayProjection::class)
177+
->period('1 day')
178+
->between(
179+
Carbon::today()->addDays(2),
180+
Carbon::today()->addDays(3)
181+
)->get();
182+
$this->assertCount(1, $betweenProjections);
183+
$this->assertEquals(2, $betweenProjections->first()->content['total_people']);
184+
$this->assertEquals(1, $betweenProjections->first()->content['number_reservations']);
185+
}
186+
}

0 commit comments

Comments
 (0)