Skip to content

Commit 4a3dbc4

Browse files
authored
Merge pull request #58 from fabpl/features/eloquent-builders
Features/eloquent builders
2 parents ab528ec + d83f416 commit 4a3dbc4

File tree

4 files changed

+261
-225
lines changed

4 files changed

+261
-225
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
namespace Zap\Models\Builders;
4+
5+
use Carbon\Carbon;
6+
use Illuminate\Database\Eloquent\Builder;
7+
use Zap\Enums\Frequency;
8+
use Zap\Enums\ScheduleTypes;
9+
use Zap\Helper\DateHelper;
10+
11+
class ScheduleBuilder extends Builder
12+
{
13+
public function active(bool $active = true): ScheduleBuilder
14+
{
15+
return $this->where('is_active', $active);
16+
}
17+
18+
public function recurring(bool $recurring = true): ScheduleBuilder
19+
{
20+
return $this->where('is_recurring', $recurring);
21+
}
22+
23+
/**
24+
* Scope a query to only include schedules of a specific type.
25+
*/
26+
public function ofType(ScheduleTypes|string $type): ScheduleBuilder
27+
{
28+
return $this->where('schedule_type', $type);
29+
}
30+
31+
/**
32+
* Scope a query to only include availability schedules.
33+
*/
34+
public function availability(): ScheduleBuilder
35+
{
36+
return $this->where('schedule_type', ScheduleTypes::AVAILABILITY->value);
37+
}
38+
39+
/**
40+
* Scope a query to only include appointment schedules.
41+
*/
42+
public function appointments(): ScheduleBuilder
43+
{
44+
return $this->where('schedule_type', ScheduleTypes::APPOINTMENT->value);
45+
}
46+
47+
/**
48+
* Scope a query to only include blocked schedules.
49+
*/
50+
public function blocked(): ScheduleBuilder
51+
{
52+
return $this->where('schedule_type', ScheduleTypes::BLOCKED->value);
53+
}
54+
55+
/**
56+
* Scope a query to only include schedules for a specific date.
57+
*/
58+
public function forDate(string $date): ScheduleBuilder
59+
{
60+
$checkDate = Carbon::parse($date);
61+
$weekday = strtolower($checkDate->format('l')); // monday, tuesday, ...
62+
$dayOfMonth = $checkDate->day;
63+
$isDateInEvenIsoWeek = DateHelper::isDateInEvenIsoWeek($date);
64+
65+
return $this
66+
// date range
67+
->where('start_date', '<=', $checkDate)
68+
->where(function ($q) use ($checkDate) {
69+
$q->whereNull('end_date')
70+
->orWhere('end_date', '>=', $checkDate);
71+
})
72+
73+
// recurrence logic
74+
->where(function ($q) use ($weekday, $dayOfMonth, $isDateInEvenIsoWeek) {
75+
76+
//
77+
// 1️⃣ NOT RECURRING — always match
78+
//
79+
$q->where('is_recurring', false)
80+
81+
//
82+
// 2️⃣ DAILY — match all days
83+
//
84+
->orWhere(function ($daily) {
85+
$daily->where('is_recurring', true)
86+
->where('frequency', Frequency::DAILY->value);
87+
})
88+
89+
//
90+
// 3️⃣ WEEKLY | BI-WEEKLY — match weekday inside config
91+
//
92+
->orWhere(function ($weekly) use ($weekday) {
93+
$weekly->where('is_recurring', true)
94+
->whereIn(
95+
'frequency',
96+
array_map(
97+
fn (Frequency $frequency) => $frequency->value,
98+
Frequency::filteredByWeekday()
99+
)
100+
)
101+
->whereJsonContains('frequency_config->days', $weekday);
102+
})
103+
//
104+
// 4 WEEKLY_EVEN | WEEKLY_ODD — match weekday inside config
105+
//
106+
->orWhere(function ($query) use ($weekday, $isDateInEvenIsoWeek) {
107+
$query->where('is_recurring', true)
108+
->where('frequency', $isDateInEvenIsoWeek ? Frequency::WEEKLY_EVEN->value : Frequency::WEEKLY_ODD->value)
109+
->whereJsonContains('frequency_config->days', $weekday);
110+
})
111+
112+
//
113+
// 5️⃣ MONTHLY — match day_of_month from config
114+
//
115+
->orWhere(function ($monthly) use ($dayOfMonth) {
116+
$monthly->where('is_recurring', true)
117+
->whereIn(
118+
'frequency',
119+
array_map(
120+
fn (Frequency $frequency) => $frequency->value,
121+
Frequency::filteredByDaysOfMonth()
122+
)
123+
)
124+
->where(function ($m) use ($dayOfMonth) {
125+
$m->whereJsonContains('frequency_config->days_of_month', $dayOfMonth)
126+
->orWhere('frequency_config->days_of_month', $dayOfMonth);
127+
});
128+
});
129+
});
130+
}
131+
132+
/**
133+
* Scope a query to only include schedules within a date range.
134+
*/
135+
public function forDateRange(string $startDate, string $endDate): ScheduleBuilder
136+
{
137+
return $this->where(function ($q) use ($startDate, $endDate) {
138+
$q->whereBetween('start_date', [$startDate, $endDate])
139+
->orWhereBetween('end_date', [$startDate, $endDate])
140+
->orWhere(function ($q2) use ($startDate, $endDate) {
141+
$q2->where('start_date', '<=', $startDate)
142+
->where(
143+
fn ($q3) => $q3->whereNull('end_date')->orWhere('end_date', '>=', $endDate),
144+
);
145+
});
146+
});
147+
}
148+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
namespace Zap\Models\Builders;
4+
5+
use Carbon\Carbon;
6+
use Carbon\CarbonInterface;
7+
use Illuminate\Database\Connection;
8+
use Illuminate\Database\Eloquent\Builder;
9+
use PDO;
10+
11+
class SchedulePeriodBuilder extends Builder
12+
{
13+
/**
14+
* Scope a query to only include available periods.
15+
*/
16+
public function available(): SchedulePeriodBuilder
17+
{
18+
return $this->where('is_available', true);
19+
}
20+
21+
/**
22+
* Scope a query to only include periods for a specific date.
23+
*/
24+
public function forDate(string $date): SchedulePeriodBuilder
25+
{
26+
return $this->where('date', Carbon::parse($date));
27+
}
28+
29+
/**
30+
* Scope a query to only include periods within a time range.
31+
*/
32+
public function forTimeRange(string $startTime, string $endTime): SchedulePeriodBuilder
33+
{
34+
return $this->where('start_time', '>=', $startTime)
35+
->where('end_time', '<=', $endTime);
36+
}
37+
38+
/**
39+
* Scope a query to find overlapping periods.
40+
*/
41+
public function overlapping(string $date, string $startTime, string $endTime, ?CarbonInterface $endDate = null): SchedulePeriodBuilder
42+
{
43+
// Normalize input times to HH:MM format
44+
$startTime = str_pad($startTime, 5, '0', STR_PAD_LEFT);
45+
$endTime = str_pad($endTime, 5, '0', STR_PAD_LEFT);
46+
47+
// Apply date filter
48+
$this->when(is_null($endDate), fn ($q) => $q->whereDate('date', $date));
49+
50+
// Apply time overlap logic based on database driver
51+
52+
/** @var Connection $connection */
53+
$connection = $this->getConnection();
54+
$driver = $connection->getPdo()->getAttribute(PDO::ATTR_DRIVER_NAME);
55+
56+
if ($driver === 'sqlite') {
57+
return $this->applySqliteTimeOverlap($this, $startTime, $endTime);
58+
}
59+
60+
if ($driver === 'pgsql') {
61+
return $this->applyPostgresTimeOverlap($this, $startTime, $endTime);
62+
}
63+
64+
return $this->applyStandardTimeOverlap($this, $startTime, $endTime);
65+
}
66+
67+
/**
68+
* Apply SQLite-specific time overlap conditions.
69+
*/
70+
private function applySqliteTimeOverlap(SchedulePeriodBuilder $query, string $startTime, string $endTime): SchedulePeriodBuilder
71+
{
72+
return $query
73+
->whereRaw('CASE WHEN LENGTH(start_time) = 4 THEN "0" || start_time ELSE start_time END < ?', [$endTime])
74+
->whereRaw('CASE WHEN LENGTH(end_time) = 4 THEN "0" || end_time ELSE end_time END > ?', [$startTime]);
75+
}
76+
77+
/**
78+
* Apply standard SQL time overlap conditions (MySQL).
79+
*/
80+
private function applyStandardTimeOverlap(SchedulePeriodBuilder $query, string $startTime, string $endTime): SchedulePeriodBuilder
81+
{
82+
return $query
83+
->whereRaw("LPAD(start_time, 5, '0') < ?", [$endTime])
84+
->whereRaw("LPAD(end_time, 5, '0') > ?", [$startTime]);
85+
}
86+
87+
/**
88+
* Apply PostgreSQL-specific time overlap conditions.
89+
*/
90+
private function applyPostgresTimeOverlap(SchedulePeriodBuilder $query, string $startTime, string $endTime): SchedulePeriodBuilder
91+
{
92+
return $query
93+
->whereRaw('LPAD(start_time::text, 5, \'0\') < ?', [$endTime])
94+
->whereRaw('LPAD(end_time::text, 5, \'0\') > ?', [$startTime]);
95+
}
96+
}

0 commit comments

Comments
 (0)