laravel-chronos maintained by andydefer
Description
Clean and flexible scheduling for Laravel applications.
Author
Last update
2026/07/19 17:52
(dev-main)
Downloads
0
Tags
php - calendar - laravel - traits - eloquent - scheduling - scheduler - availability - open-source - bookings - roster - laravel-package - appointments - shifts - time-slots
Laravel Chronos
Un moteur de planification avancé pour Laravel. Gestion des disponibilités, rendez-vous et empêchements avec validation métier exhaustive.
📦 Installation
composer require andydefer/laravel-chronos
php artisan vendor:publish --tag=chronos-config
php artisan vendor:publish --tag=chronos-migrations
php artisan migrate
🎯 Pourquoi Laravel Chronos ?
Le problème : Vous gérez des disponibilités, des rendez-vous et des empêchements dans votre application. Vous devez :
- Vérifier qu'un créneau est disponible avant de le réserver
- Empêcher les doubles réservations
- Gérer les indisponibilités temporaires (congés, formations)
- Valider les règles métier (durée minimale, buffer entre rendez-vous)
- Trouver le prochain créneau disponible automatiquement
La solution : Laravel Chronos vous offre un moteur de planification complet avec :
- ✅ Disponibilités récurrentes (jours, plages horaires, périodes)
- ✅ Rendez-vous avec états (réservé, annulé, complété)
- ✅ Empêchements pour bloquer des créneaux
- ✅ Recherche automatique de créneaux
- ✅ 17 règles de validation métier exhaustives
- ✅ Protection contre les boucles infinies (durée minimale absolue)
- ✅ Collections typées (
SlotVOCollection,BlockedPeriodCollection)
🚀 Démarrage rapide
use AndyDefer\LaravelChronos\Contracts\Services\AvailabilityServiceInterface;
use AndyDefer\LaravelChronos\Contracts\Services\ScheduleServiceInterface;
use AndyDefer\LaravelChronos\Contracts\Services\SlotServiceInterface;
use AndyDefer\LaravelChronos\Records\AvailabilityRecord;
use AndyDefer\LaravelChronos\Records\ScheduleRecord;
use AndyDefer\LaravelChronos\Enums\ScheduleStatus;
$availabilityService = app(AvailabilityServiceInterface::class);
$scheduleService = app(ScheduleServiceInterface::class);
$slotService = app(SlotServiceInterface::class);
// 1. Créer une disponibilité avec scoping
$availability = $availabilityService
->for($doctor)
->create(AvailabilityRecord::from([
'name' => 'Consultations',
'days' => ['monday', 'wednesday', 'friday'],
'daily_start' => '09:00:00',
'daily_end' => '17:00:00',
'validity_start' => '2024-01-01T00:00:00Z',
'validity_end' => '2024-12-31T23:59:59Z',
]));
// 2. Créer un rendez-vous avec scoping
$schedule = $scheduleService
->for($doctor)
->create(ScheduleRecord::from([
'availability_id' => $availability->id,
'title' => 'Consultation patient',
'start_datetime' => '2024-01-15T10:00:00Z',
'end_datetime' => '2024-01-15T10:30:00Z',
'status' => ScheduleStatus::BOOKED,
]));
// 3. Trouver le prochain créneau disponible
$nextSlot = $slotService->findNextSlot($doctor, DateTimeZuluVO::now(), 30);
📅 Cas d'usage complets
Scénario 1 : Gestion d'un cabinet médical
1.1 Créer le planning d'un médecin avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\AvailabilityServiceInterface;
use AndyDefer\LaravelChronos\Records\AvailabilityRecord;
class DoctorScheduleSetup
{
public function __construct(
private AvailabilityServiceInterface $availabilityService
) {}
public function setup(Doctor $doctor): void
{
// Matin : Consultations classiques
$this->availabilityService
->for($doctor)
->create(AvailabilityRecord::from([
'name' => 'Consultations matin',
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
'daily_start' => '09:00:00',
'daily_end' => '12:00:00',
'validity_start' => '2024-01-01T00:00:00Z',
'validity_end' => '2024-12-31T23:59:59Z',
]));
// Après-midi : Consultations uniquement certains jours
$this->availabilityService
->for($doctor)
->create(AvailabilityRecord::from([
'name' => 'Consultations après-midi',
'days' => ['monday', 'tuesday', 'thursday'],
'daily_start' => '14:00:00',
'daily_end' => '18:00:00',
'validity_start' => '2024-01-01T00:00:00Z',
'validity_end' => '2024-12-31T23:59:59Z',
]));
// Permanence téléphonique
$this->availabilityService
->for($doctor)
->create(AvailabilityRecord::from([
'name' => 'Permanence téléphonique',
'days' => ['monday', 'wednesday'],
'daily_start' => '19:00:00',
'daily_end' => '21:00:00',
'validity_start' => '2024-01-01T00:00:00Z',
'validity_end' => '2024-06-30T23:59:59Z',
]));
}
}
1.2 Prendre un rendez-vous avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\SlotServiceInterface;
use AndyDefer\LaravelChronos\Contracts\Services\ScheduleServiceInterface;
use AndyDefer\LaravelChronos\Contracts\Services\AvailabilityServiceInterface;
use AndyDefer\LaravelChronos\Records\ScheduleRecord;
use AndyDefer\LaravelChronos\Enums\ScheduleStatus;
use AndyDefer\LaravelChronos\ValueObjects\DateTimeZuluVO;
class BookingService
{
public function __construct(
private SlotServiceInterface $slotService,
private ScheduleServiceInterface $scheduleService,
private AvailabilityServiceInterface $availabilityService
) {}
public function book(Doctor $doctor, Patient $patient, int $duration): Schedule
{
// Trouver le prochain créneau disponible
$slot = $this->slotService->findNextSlot($doctor, DateTimeZuluVO::now(), $duration);
if (!$slot) {
throw new \RuntimeException('Aucun créneau disponible');
}
// Vérifier que le créneau est toujours disponible
if (!$this->slotService->isSlotAvailable($doctor, $slot->getStart(), $slot->getEnd())) {
throw new \RuntimeException('Le créneau n\'est plus disponible');
}
// Récupérer la disponibilité pour ce créneau
$availability = $this->availabilityService
->for($doctor)
->findActiveAtDate($doctor, $slot->getStart())
->first();
// Créer le rendez-vous avec scoping
return $this->scheduleService
->for($doctor)
->create(ScheduleRecord::from([
'availability_id' => $availability->id,
'title' => 'Consultation - ' . $patient->name,
'start_datetime' => $slot->getStart()->getValue(),
'end_datetime' => $slot->getEnd()->getValue(),
'status' => ScheduleStatus::BOOKED,
]));
}
}
1.3 Annuler un rendez-vous avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\ScheduleServiceInterface;
use AndyDefer\LaravelChronos\Models\Schedule;
class AppointmentManager
{
public function __construct(
private ScheduleServiceInterface $scheduleService
) {}
public function cancel(Doctor $doctor, Schedule $schedule): void
{
// Vérifier si annulable
if (!$this->scheduleService->canBeCancelled($schedule)) {
throw new \RuntimeException(
'Impossible d\'annuler (statut: ' . $schedule->status->value . ')'
);
}
// Annuler avec scoping (vérifie l'appartenance)
$this->scheduleService
->for($doctor)
->cancel($schedule->id);
}
public function complete(Doctor $doctor, Schedule $schedule): void
{
// Vérifier si complétable
if (!$this->scheduleService->canBeCompleted($schedule)) {
throw new \RuntimeException(
'Impossible de compléter (statut: ' . $schedule->status->value . ')'
);
}
// Compléter avec scoping
$this->scheduleService
->for($doctor)
->complete($schedule->id);
}
}
1.4 Rechercher des rendez-vous avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\ScheduleServiceInterface;
use AndyDefer\LaravelChronos\Enums\ScheduleStatus;
use AndyDefer\LaravelChronos\ValueObjects\DateTimeZuluVO;
use Illuminate\Support\Collection;
class ScheduleSearchService
{
public function __construct(
private ScheduleServiceInterface $scheduleService
) {}
// Rendez-vous du jour avec scoping
public function today(Doctor $doctor): Collection
{
return $this->scheduleService
->for($doctor)
->findByDate(DateTimeZuluVO::today());
}
// Rendez-vous par statut avec scoping
public function byStatus(Doctor $doctor, ScheduleStatus $status, int $limit = null): Collection
{
return $this->scheduleService
->for($doctor)
->findByStatus($status, null, $limit);
}
// Rendez-vous par titre avec scoping
public function search(Doctor $doctor, string $query, int $limit = null): Collection
{
return $this->scheduleService
->for($doctor)
->searchByTitle($query, null, $limit);
}
// Rendez-vous sur une période avec scoping
public function inRange(Doctor $doctor, string $start, string $end, int $limit = null): Collection
{
return $this->scheduleService
->for($doctor)
->findInDateRange(
DateTimeZuluVO::from($start),
DateTimeZuluVO::from($end),
null,
$limit
);
}
}
Scénario 2 : Gestion des empêchements avec scoping
2.1 Bloquer une période pour formation
use AndyDefer\LaravelChronos\Contracts\Services\ImpedimentServiceInterface;
use AndyDefer\LaravelChronos\Contracts\Services\SlotServiceInterface;
use AndyDefer\LaravelChronos\Contracts\Services\AvailabilityServiceInterface;
use AndyDefer\LaravelChronos\Records\ImpedimentRecord;
use AndyDefer\LaravelChronos\ValueObjects\DateTimeZuluVO;
class ImpedimentManager
{
public function __construct(
private ImpedimentServiceInterface $impedimentService,
private SlotServiceInterface $slotService,
private AvailabilityServiceInterface $availabilityService
) {}
public function blockForTraining(Doctor $doctor, string $start, string $end): void
{
$startDate = DateTimeZuluVO::from($start);
$endDate = DateTimeZuluVO::from($end);
// Vérifier les conflits
$conflicts = $this->slotService->getBlockedPeriods(
$doctor,
$startDate,
$endDate
);
if ($conflicts->isNotEmpty()) {
throw new \RuntimeException('Des rendez-vous existent déjà sur cette période');
}
// Récupérer la disponibilité
$availability = $this->availabilityService
->for($doctor)
->findActiveAtDate($doctor, $startDate)
->first();
// Créer l'empêchement avec scoping
$this->impedimentService
->for($doctor)
->create(ImpedimentRecord::from([
'availability_id' => $availability->id,
'reason' => 'Formation médicale obligatoire',
'start_datetime' => $start,
'end_datetime' => $end,
]));
}
}
2.2 Analyser l'impact d'un empêchement
use AndyDefer\LaravelChronos\Contracts\Services\ImpedimentServiceInterface;
use AndyDefer\LaravelChronos\Models\Impediment;
class ImpactAnalyzer
{
public function __construct(
private ImpedimentServiceInterface $impedimentService
) {}
public function analyze(Impediment $impediment): array
{
$blocked = $this->impedimentService->getBlockedSchedules($impediment);
$fullyBlocked = $this->impedimentService->getFullyBlockedSchedules($impediment);
$partiallyBlocked = $this->impedimentService->getPartiallyBlockedSchedules($impediment);
return [
'total_impact' => $blocked->count() . ' rendez-vous impactés',
'fully_blocked' => $fullyBlocked->count() . ' rendez-vous totalement bloqués',
'partially_blocked' => $partiallyBlocked->count() . ' rendez-vous partiellement bloqués',
'details' => [
'fully' => $fullyBlocked->pluck('title')->toArray(),
'partially' => $partiallyBlocked->pluck('title')->toArray(),
]
];
}
public function isActive(Impediment $impediment): bool
{
return $this->impedimentService->isActive($impediment);
}
}
2.3 Rechercher des empêchements avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\ImpedimentServiceInterface;
use AndyDefer\LaravelChronos\ValueObjects\DateTimeZuluVO;
use Illuminate\Support\Collection;
class ImpedimentSearchService
{
public function __construct(
private ImpedimentServiceInterface $impedimentService
) {}
// Empêchements actifs avec scoping
public function active(Doctor $doctor, int $limit = null): Collection
{
return $this->impedimentService
->for($doctor)
->findActive(null, $limit);
}
// Par motif avec scoping
public function byReason(Doctor $doctor, string $reason, int $limit = null): Collection
{
return $this->impedimentService
->for($doctor)
->searchByReason($reason, null, $limit);
}
// Par date avec scoping
public function byDate(Doctor $doctor, string $date, int $limit = null): Collection
{
return $this->impedimentService
->for($doctor)
->findByDate(DateTimeZuluVO::from($date), null, $limit);
}
// Par plage avec scoping
public function inRange(Doctor $doctor, string $start, string $end, int $limit = null): Collection
{
return $this->impedimentService
->for($doctor)
->findInDateRange(
DateTimeZuluVO::from($start),
DateTimeZuluVO::from($end),
null,
$limit
);
}
}
Scénario 3 : Recherche avancée de créneaux avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\SlotServiceInterface;
use AndyDefer\LaravelChronos\ValueObjects\DateTimeZuluVO;
use AndyDefer\LaravelChronos\ValueObjects\SlotVO;
use Illuminate\Support\Collection;
class WeekScheduleService
{
public function __construct(
private SlotServiceInterface $slotService
) {}
public function findWeekSlots(Doctor $doctor, string $startDate): array
{
$weekSlots = [];
$start = DateTimeZuluVO::from($startDate);
for ($i = 0; $i < 7; $i++) {
$date = $start->addDays($i);
$slots = $this->slotService->findSlotsForDay($doctor, $date, 30);
if ($slots->isNotEmpty()) {
$weekSlots[$date->toDateString()] = $slots;
}
}
return $weekSlots;
}
public function getFirstAvailable(Doctor $doctor, int $duration): ?SlotVO
{
return $this->slotService->findNextSlot($doctor, DateTimeZuluVO::now(), $duration);
}
}
class BlockedPeriodsService
{
public function __construct(
private SlotServiceInterface $slotService
) {}
public function getBusyPeriods(Doctor $doctor, string $start, string $end): array
{
$blocked = $this->slotService->getBlockedPeriods(
$doctor,
DateTimeZuluVO::from($start),
DateTimeZuluVO::from($end)
);
$busyPeriods = [];
foreach ($blocked as $period) {
$busyPeriods[] = [
'start' => $period->getStart()->toDateTimeString(),
'end' => $period->getEnd()->toDateTimeString(),
'type' => $period->getType(),
'id' => $period->getId(),
'duration' => $period->getDurationInMinutes() . ' minutes',
];
}
return $busyPeriods;
}
public function getTotalBlockedDuration(Doctor $doctor, string $start, string $end): int
{
return $this->slotService->getBlockedPeriods(
$doctor,
DateTimeZuluVO::from($start),
DateTimeZuluVO::from($end)
)->getTotalDuration();
}
}
class SlotSplitService
{
public function __construct(
private SlotServiceInterface $slotService
) {}
public function splitIntoChunks(SlotVO $slot, int $chunkDuration, int $limit = null): SlotVOCollection
{
return $this->slotService->generateSlotsFromSlot($slot, $chunkDuration, $limit);
}
public function isSlotFree(Doctor $doctor, string $start, string $end): bool
{
return $this->slotService->isSlotAvailable(
$doctor,
DateTimeZuluVO::from($start),
DateTimeZuluVO::from($end)
);
}
}
Scénario 4 : Gestion des disponibilités avec scoping
4.1 Mettre à jour une disponibilité
use AndyDefer\LaravelChronos\Contracts\Services\AvailabilityServiceInterface;
use AndyDefer\LaravelChronos\Records\AvailabilityRecord;
use AndyDefer\LaravelChronos\Models\Availability;
class AvailabilityUpdater
{
public function __construct(
private AvailabilityServiceInterface $availabilityService
) {}
public function extendHours(Doctor $doctor, Availability $availability, string $newEnd): Availability
{
return $this->availabilityService
->for($doctor)
->update($availability->id, AvailabilityRecord::from([
'daily_end' => $newEnd,
]));
}
public function addDays(Doctor $doctor, Availability $availability, array $newDays): Availability
{
return $this->availabilityService
->for($doctor)
->update($availability->id, AvailabilityRecord::from([
'days' => $newDays,
]));
}
public function extendValidity(Doctor $doctor, Availability $availability, string $newEnd): Availability
{
return $this->availabilityService
->for($doctor)
->update($availability->id, AvailabilityRecord::from([
'validity_end' => $newEnd,
]));
}
}
4.2 Supprimer une disponibilité avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\AvailabilityServiceInterface;
use AndyDefer\LaravelChronos\Models\Availability;
class AvailabilityDeleter
{
public function __construct(
private AvailabilityServiceInterface $availabilityService
) {}
public function softDelete(Doctor $doctor, Availability $availability): void
{
// Suppression avec validation (empêche si rendez-vous futurs)
$this->availabilityService
->for($doctor)
->delete($availability->id);
}
public function forceDelete(Doctor $doctor, Availability $availability): void
{
// Suppression forcée (ignore la validation)
$this->availabilityService
->for($doctor)
->delete($availability->id, true);
}
}
4.3 Rechercher des disponibilités avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\AvailabilityServiceInterface;
use AndyDefer\LaravelChronos\ValueObjects\DateTimeZuluVO;
use Illuminate\Support\Collection;
class AvailabilitySearchService
{
public function __construct(
private AvailabilityServiceInterface $availabilityService
) {}
// Toutes les disponibilités d'un médecin avec scoping
public function forDoctor(Doctor $doctor, int $limit = null): Collection
{
return $this->availabilityService
->for($doctor)
->findBySchedulable(null, $limit);
}
// Disponibilités actives aujourd'hui avec scoping
public function activeToday(Doctor $doctor, int $limit = null): Collection
{
return $this->availabilityService
->for($doctor)
->findActiveAtDate($doctor, DateTimeZuluVO::today(), $limit);
}
// Par type avec scoping
public function byType(Doctor $doctor, string $type, int $limit = null): Collection
{
return $this->availabilityService
->for($doctor)
->findByType($type, $limit);
}
// Vérifier si le médecin existe
public function doctorExists(Doctor $doctor): bool
{
return $this->availabilityService->schedulableExists($doctor);
}
}
Scénario 5 : Gestion des cross-day (permanence de nuit) avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\AvailabilityServiceInterface;
use AndyDefer\LaravelChronos\Contracts\Services\SlotServiceInterface;
use AndyDefer\LaravelChronos\Records\AvailabilityRecord;
use AndyDefer\LaravelChronos\ValueObjects\DateTimeZuluVO;
class NightShiftService
{
public function __construct(
private AvailabilityServiceInterface $availabilityService,
private SlotServiceInterface $slotService
) {}
// Créer une permanence de nuit (22h-2h) avec scoping
public function createNightShift(Doctor $doctor): Availability
{
return $this->availabilityService
->for($doctor)
->create(AvailabilityRecord::from([
'name' => 'Permanence de nuit',
'days' => ['monday', 'tuesday'], // Jours consécutifs pour cross-day
'daily_start' => '22:00:00',
'daily_end' => '02:00:00', // Cross-day automatiquement détecté
'validity_start' => '2024-01-01T00:00:00Z',
'validity_end' => '2024-12-31T23:59:59Z',
]));
}
// Rechercher les créneaux de nuit
public function findNightSlots(Doctor $doctor, string $date, int $duration = 30): Collection
{
return $this->slotService->findSlotsForDay(
$doctor,
DateTimeZuluVO::from($date),
$duration
);
}
// Vérifier si un créneau de nuit est disponible
public function isNightSlotAvailable(Doctor $doctor, string $start, string $end): bool
{
return $this->slotService->isSlotAvailable(
$doctor,
DateTimeZuluVO::from($start),
DateTimeZuluVO::from($end)
);
}
}
Scénario 6 : Utilisation des limites pour les grandes collections
use AndyDefer\LaravelChronos\Contracts\Services\ScheduleServiceInterface;
use AndyDefer\LaravelChronos\Contracts\Services\SlotServiceInterface;
use AndyDefer\LaravelChronos\Enums\ScheduleStatus;
class PaginatedScheduleService
{
public function __construct(
private ScheduleServiceInterface $scheduleService,
private SlotServiceInterface $slotService
) {}
// Récupérer les 10 premiers rendez-vous d'un médecin
public function getRecentSchedules(Doctor $doctor, int $limit = 10): Collection
{
return $this->scheduleService
->for($doctor)
->findBySchedulable(null, $limit);
}
// Récupérer les 5 premiers créneaux d'une journée
public function getFirstSlotsOfDay(Doctor $doctor, string $date, int $limit = 5): SlotVOCollection
{
return $this->slotService->findSlotsForDay(
$doctor,
DateTimeZuluVO::from($date),
30,
null,
$limit
);
}
// Récupérer les 20 derniers rendez-vous complétés
public function getCompletedSchedules(Doctor $doctor, int $limit = 20): Collection
{
return $this->scheduleService
->for($doctor)
->findByStatus(ScheduleStatus::COMPLETED, null, $limit);
}
}
✅ Validation métier en action
Exemples d'erreurs de validation
// 1. Durée minimale non respectée
$record = AvailabilityRecord::from([
'daily_start' => '09:00:00',
'daily_end' => '09:05:00', // 5 minutes
]);
// ❌ "Availability duration must be at least 15 minutes. Current duration: 5 minutes."
// 2. Chevauchement de disponibilités
$record = AvailabilityRecord::from([
'days' => ['monday'],
'daily_start' => '10:00:00',
'daily_end' => '12:00:00',
]);
// ❌ "Availability overlaps with existing availability #42"
// 3. Cross-day sans jours consécutifs
$record = AvailabilityRecord::from([
'days' => ['monday', 'wednesday'],
'daily_start' => '22:00:00',
'daily_end' => '02:00:00',
]);
// ❌ "Availability crosses midnight but days array is not consecutive"
// 4. Buffer non respecté
$record = ScheduleRecord::from([
'start_datetime' => '2024-01-15T10:05:00Z',
'end_datetime' => '2024-01-15T10:30:00Z',
]);
// ❌ "Buffer time of 15 minutes not respected between previous schedule #123"
// 5. Durée maximale dépassée
$record = ScheduleRecord::from([
'start_datetime' => '2024-01-15T09:00:00Z',
'end_datetime' => '2024-01-15T17:00:00Z', // 8 heures
]);
// ❌ "Duration (8 hours) exceeds maximum allowed duration (4 hours)"
📚 API complète avec exemples de code
AvailabilityService - Toutes les méthodes avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\AvailabilityServiceInterface;
use AndyDefer\LaravelChronos\Records\AvailabilityRecord;
use AndyDefer\LaravelChronos\ValueObjects\DateTimeZuluVO;
$availabilityService = app(AvailabilityServiceInterface::class);
$doctor = Doctor::find(42);
// 1. create() - Créer une disponibilité avec scoping
$availability = $availabilityService
->for($doctor)
->create(AvailabilityRecord::from([
'name' => 'Consultations',
'days' => ['monday', 'wednesday', 'friday'],
'daily_start' => '09:00:00',
'daily_end' => '17:00:00',
'validity_start' => '2024-01-01T00:00:00Z',
'validity_end' => '2024-12-31T23:59:59Z',
]));
// 2. update() - Mettre à jour une disponibilité avec scoping
$updated = $availabilityService
->for($doctor)
->update($availability->id, AvailabilityRecord::from([
'daily_end' => '18:00:00',
]));
// 3. delete() - Supprimer une disponibilité avec scoping
$deleted = $availabilityService
->for($doctor)
->delete($availability->id); // Avec validation
$forceDeleted = $availabilityService
->for($doctor)
->delete($availability->id, true); // Forcé
// 4. find() - Trouver par ID avec scoping
$availability = $availabilityService
->for($doctor)
->find(42);
// 5. findBySchedulable() - Toutes les disponibilités d'une entité avec scoping
$availabilities = $availabilityService
->for($doctor)
->findBySchedulable(null, 10); // Limit à 10
// 6. findByType() - Par type avec scoping
$consultations = $availabilityService
->for($doctor)
->findByType('consultation', 5);
// 7. findActiveAtDate() - Disponibilités actives à une date
$today = DateTimeZuluVO::today();
$active = $availabilityService
->for($doctor)
->findActiveAtDate($doctor, $today, 3);
// 8. findActiveInDateRange() - Disponibilités actives sur une période
$start = DateTimeZuluVO::from('2024-01-01T00:00:00Z');
$end = DateTimeZuluVO::from('2024-01-31T23:59:59Z');
$activeInRange = $availabilityService
->for($doctor)
->findActiveInDateRange($doctor, $start, $end, 5);
// 9. schedulableExists() - Vérifier si l'entité existe
if ($availabilityService->schedulableExists($doctor)) {
echo "Le médecin existe";
}
// 10. getSchedulableModel() - Récupérer le modèle
$model = $availabilityService->getSchedulableModel($doctor);
ScheduleService - Toutes les méthodes avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\ScheduleServiceInterface;
use AndyDefer\LaravelChronos\Records\ScheduleRecord;
use AndyDefer\LaravelChronos\Enums\ScheduleStatus;
use AndyDefer\LaravelChronos\ValueObjects\DateTimeZuluVO;
$scheduleService = app(ScheduleServiceInterface::class);
$doctor = Doctor::find(42);
// 1. create() - Créer un rendez-vous avec scoping
$schedule = $scheduleService
->for($doctor)
->create(ScheduleRecord::from([
'availability_id' => $availability->id,
'title' => 'Consultation patient',
'start_datetime' => '2024-01-15T10:00:00Z',
'end_datetime' => '2024-01-15T10:30:00Z',
'status' => ScheduleStatus::BOOKED,
]));
// 2. update() - Mettre à jour un rendez-vous avec scoping
$updated = $scheduleService
->for($doctor)
->update($schedule->id, ScheduleRecord::from([
'title' => 'Consultation urgente',
'status' => ScheduleStatus::BOOKED,
]));
// 3. delete() - Supprimer un rendez-vous avec scoping
$deleted = $scheduleService
->for($doctor)
->delete($schedule->id);
// 4. find() - Trouver par ID avec scoping
$schedule = $scheduleService
->for($doctor)
->find(42);
// 5. findByAvailability() - Par disponibilité
$schedules = $scheduleService
->for($doctor)
->findByAvailability($availability->id, 10);
// 6. findBySchedulable() - Par entité avec scoping
$schedules = $scheduleService
->for($doctor)
->findBySchedulable(null, 10);
// 7. findByStatus() - Par statut avec scoping
$booked = $scheduleService
->for($doctor)
->findByStatus(ScheduleStatus::BOOKED, null, 5);
$completed = $scheduleService
->for($doctor)
->findByStatus(ScheduleStatus::COMPLETED, null, 5);
// 8. findByDate() - Par date avec scoping
$today = DateTimeZuluVO::today();
$todaySchedules = $scheduleService
->for($doctor)
->findByDate($today, null, 10);
// 9. findInDateRange() - Sur une période avec scoping
$start = DateTimeZuluVO::from('2024-01-15T00:00:00Z');
$end = DateTimeZuluVO::from('2024-01-20T23:59:59Z');
$schedules = $scheduleService
->for($doctor)
->findInDateRange($start, $end, null, 20);
// 10. searchByTitle() - Recherche par titre avec scoping
$results = $scheduleService
->for($doctor)
->searchByTitle('Consultation', null, 5);
// 11. cancel() - Annuler un rendez-vous avec scoping
$cancelled = $scheduleService
->for($doctor)
->cancel($schedule->id);
// 12. complete() - Compléter un rendez-vous avec scoping
$completed = $scheduleService
->for($doctor)
->complete($schedule->id);
// 13. canBeCancelled() - Vérifier si annulable
if ($scheduleService->canBeCancelled($schedule)) {
$scheduleService
->for($doctor)
->cancel($schedule->id);
}
// 14. canBeCompleted() - Vérifier si complétable
if ($scheduleService->canBeCompleted($schedule)) {
$scheduleService
->for($doctor)
->complete($schedule->id);
}
ImpedimentService - Toutes les méthodes avec scoping
use AndyDefer\LaravelChronos\Contracts\Services\ImpedimentServiceInterface;
use AndyDefer\LaravelChronos\Records\ImpedimentRecord;
use AndyDefer\LaravelChronos\ValueObjects\DateTimeZuluVO;
$impedimentService = app(ImpedimentServiceInterface::class);
$doctor = Doctor::find(42);
// 1. create() - Créer un empêchement avec scoping
$impediment = $impedimentService
->for($doctor)
->create(ImpedimentRecord::from([
'availability_id' => $availability->id,
'reason' => 'Formation obligatoire',
'start_datetime' => '2024-01-15T14:00:00Z',
'end_datetime' => '2024-01-15T16:00:00Z',
]));
// 2. update() - Mettre à jour un empêchement avec scoping
$updated = $impedimentService
->for($doctor)
->update($impediment->id, ImpedimentRecord::from([
'reason' => 'Formation reportée',
'start_datetime' => '2024-01-16T14:00:00Z',
'end_datetime' => '2024-01-16T16:00:00Z',
]));
// 3. delete() - Supprimer un empêchement avec scoping
$deleted = $impedimentService
->for($doctor)
->delete($impediment->id);
// 4. find() - Trouver par ID avec scoping
$impediment = $impedimentService
->for($doctor)
->find(42);
// 5. findByAvailability() - Par disponibilité
$impediments = $impedimentService
->for($doctor)
->findByAvailability($availability->id, 10);
// 6. findBySchedulable() - Par entité avec scoping
$impediments = $impedimentService
->for($doctor)
->findBySchedulable(null, 10);
// 7. findByDate() - Par date avec scoping
$today = DateTimeZuluVO::today();
$impediments = $impedimentService
->for($doctor)
->findByDate($today, null, 5);
// 8. findInDateRange() - Sur une période avec scoping
$start = DateTimeZuluVO::from('2024-01-15T00:00:00Z');
$end = DateTimeZuluVO::from('2024-01-20T23:59:59Z');
$impediments = $impedimentService
->for($doctor)
->findInDateRange($start, $end, null, 10);
// 9. findActive() - Empêchements actifs avec scoping
$active = $impedimentService
->for($doctor)
->findActive(null, 5);
// 10. searchByReason() - Par motif avec scoping
$results = $impedimentService
->for($doctor)
->searchByReason('formation', null, 3);
// 11. isActive() - Vérifier si actif
if ($impedimentService->isActive($impediment)) {
echo "L'empêchement est en cours";
}
// 12. overlapsWith() - Vérifier chevauchement
$start = DateTimeZuluVO::from('2024-01-15T14:30:00Z');
$end = DateTimeZuluVO::from('2024-01-15T15:30:00Z');
if ($impedimentService->overlapsWith($impediment, $start, $end)) {
echo "L'empêchement chevauche la période";
}
// 13. getBlockedSchedules() - Plannings bloqués
$blocked = $impedimentService
->for($doctor)
->getBlockedSchedules($impediment, 10);
// 14. getFullyBlockedSchedules() - Totalement bloqués
$fullyBlocked = $impedimentService
->for($doctor)
->getFullyBlockedSchedules($impediment, 10);
// 15. getPartiallyBlockedSchedules() - Partiellement bloqués
$partiallyBlocked = $impedimentService
->for($doctor)
->getPartiallyBlockedSchedules($impediment, 10);
SlotService - Toutes les méthodes
use AndyDefer\LaravelChronos\Contracts\Services\SlotServiceInterface;
use AndyDefer\LaravelChronos\ValueObjects\DateTimeZuluVO;
use AndyDefer\LaravelChronos\ValueObjects\SlotVO;
$slotService = app(SlotServiceInterface::class);
$doctor = Doctor::find(42);
// 1. findNextSlot() - Prochain créneau disponible
$slot = $slotService->findNextSlot($doctor, DateTimeZuluVO::now(), 30);
if ($slot) {
echo "Prochain créneau: " . $slot->getStart() . " - " . $slot->getEnd();
}
// 2. findPreviousSlot() - Dernier créneau avant une date
$slot = $slotService->findPreviousSlot($doctor, DateTimeZuluVO::now(), 30);
// 3. findSlotsInRange() - Créneaux sur une période avec limite
$start = DateTimeZuluVO::from('2024-01-15T00:00:00Z');
$end = DateTimeZuluVO::from('2024-01-15T23:59:59Z');
$slots = $slotService->findSlotsInRange($doctor, $start, $end, 30, null, 10);
foreach ($slots as $slot) {
echo $slot->getStart() . " - " . $slot->getEnd() . "\n";
}
// 4. findSlotsForDay() - Créneaux du jour avec limite
$today = DateTimeZuluVO::today();
$slots = $slotService->findSlotsForDay($doctor, $today, 30, null, 5);
echo "Total minutes disponibles: " . $slots->getTotalAvailableMinutes();
// 5. isSlotAvailable() - Vérifier disponibilité d'un créneau
$start = DateTimeZuluVO::from('2024-01-15T10:00:00Z');
$end = DateTimeZuluVO::from('2024-01-15T10:30:00Z');
if ($slotService->isSlotAvailable($doctor, $start, $end)) {
echo "Le créneau est disponible";
}
// 6. getNextAvailableStart() - Prochaine heure de début
$startTime = $slotService->getNextAvailableStart($doctor, DateTimeZuluVO::now(), 30);
// 7. hasAvailabilityOnDate() - Vérifier si disponibilité ce jour
if ($slotService->hasAvailabilityOnDate($doctor, $today)) {
echo "Le médecin est disponible aujourd'hui";
}
// 8. getBlockedPeriods() - Périodes bloquées avec limite
$blocked = $slotService->getBlockedPeriods($doctor, $start, $end, null, 10);
foreach ($blocked as $period) {
echo "Bloqué par " . $period->getType() . " #" . $period->getId() . "\n";
echo "De " . $period->getStart() . " à " . $period->getEnd() . "\n";
}
// 9. generateSlotsFromSlot() - Découper un créneau avec limite
$slot = SlotVO::fromDuration(DateTimeZuluVO::from('2024-01-15T10:00:00Z'), 60);
$chunks = $slotService->generateSlotsFromSlot($slot, 15, 3);
foreach ($chunks as $chunk) {
echo $chunk->getStart() . " - " . $chunk->getEnd() . "\n";
}
🔧 Bonnes pratiques
✅ Injection des interfaces
// BON - Injection des interfaces
class BookingController
{
public function __construct(
private readonly AvailabilityServiceInterface $availabilityService,
private readonly ScheduleServiceInterface $scheduleService,
private readonly SlotServiceInterface $slotService
) {}
}
// ÉVITER - Résolution directe
class BookingController
{
public function book(Request $request)
{
$service = app(ScheduleServiceInterface::class); // ❌
}
}
✅ Utilisation du scoping
// BON - Utilisation du scoping
$availability = $availabilityService
->for($doctor)
->create($record);
// schedulable_type et schedulable_id sont automatiquement injectés
// ÉVITER - Spécification manuelle
$availability = $availabilityService->create(AvailabilityRecord::from([
...$record->toArray(),
'schedulable_type' => Doctor::class,
'schedulable_id' => $doctor->id,
]));
✅ Utilisation des limites
// BON - Utilisation des limites pour les grandes collections
$schedules = $scheduleService
->for($doctor)
->findBySchedulable(null, 10);
$slots = $slotService->findSlotsForDay($doctor, $today, 30, null, 5);
// ÉVITER - Récupération sans limite
$schedules = $scheduleService
->for($doctor)
->findBySchedulable(); // Peut être lourd
✅ Gestion des erreurs
try {
$schedule = $scheduleService
->for($doctor)
->create($record);
} catch (ValidationException $e) {
$errors = $e->getErrors();
foreach ($errors as $error) {
Log::error('Validation échouée: ' . $error);
}
return response()->json(['errors' => $errors], 422);
} catch (ModelNotFoundException $e) {
return response()->json(['error' => $e->getMessage()], 404);
}
✅ Utilisation des collections typées
// Les collections typées offrent des helpers
$slots = $slotService->findSlotsForDay($doctor, $today, 30);
$totalMinutes = $slots->getTotalAvailableMinutes();
$firstSlot = $slots->firstSlot();
$earliest = $slots->getEarliestStart();
$latest = $slots->getLatestEnd();
// L'IDE vous donne l'autocomplétion sur SlotVOCollection
⚙️ Configuration
// config/chronos.php
return [
'min_durations' => [
'availability' => 15,
'schedule' => 15,
'impediment' => 15,
'slot_search' => 5,
],
'max_duration' => 240,
'buffer_time' => 0,
];
📄 Licence
MIT © Andy Defer