andydefer / laravel-roster
Clean and flexible scheduling for Laravel applications.
Installs: 34
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 1
Open Issues: 2
pkg:composer/andydefer/laravel-roster
Requires
- php: ^8.2
- illuminate/database: ^12.0
- illuminate/support: ^12.0
- laravel/framework: ^12.0
Requires (Dev)
- barryvdh/laravel-ide-helper: ^3.6
- larastan/larastan: ^3.8
- laravel/pint: ^1.26
- orchestra/testbench: ^10.8
- phpunit/phpunit: ^12.5
- rector/rector: *
- symfony/var-dumper: ^7.0
- vimeo/psalm: ^6.14
This package is auto-updated.
Last update: 2026-01-30 06:43:50 UTC
README
Roster is a comprehensive Laravel package for advanced scheduling, availability, and booking management. Built with a robust architecture, it handles recurring availability, booked slots, and impediments with exhaustive business validation.
๐ฆ Installation
composer require andydefer/laravel-roster
Publish package resources:
php artisan roster:install
Or manually:
# Configuration php artisan vendor:publish --tag=roster-config # Migrations php artisan vendor:publish --tag=roster-migrations # Run migrations php artisan migrate
๐ Quick Start
1. Add the trait to your models
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Roster\Traits\HasRoster; class Doctor extends Model { use HasRoster; }
2. Create recurring availabilities
// Create an availability for a doctor $availability = availability_for($doctor)->create([ 'type' => 'consultation', 'daily_start' => '09:00:00', 'daily_end' => '17:00:00', 'days' => ['monday', 'wednesday', 'friday'], 'validity_start' => '2038-01-01', 'validity_end' => '2038-12-31', ]);
3. Schedule appointments
// Book a slot in this availability $schedule = schedule_for($availability)->create([ 'title' => 'Annual Checkup - Patient A', 'start_datetime' => '2038-01-04 10:00:00', 'end_datetime' => '2038-01-04 11:00:00', 'status' => \Roster\Enums\ScheduleStatus::BOOKED, 'metadata' => ['patient_id' => 123], ]);
4. Manage temporary unavailability
// Block a slot for training $impediment = impediment_for($availability)->create([ 'reason' => 'Mandatory medical training', 'start_datetime' => '2038-01-04 09:00:00', 'end_datetime' => '2038-01-04 12:00:00', ]);
5. Search for available slots
// Find the next available slot $nextSlot = schedule_for($availability)->findNextSlot( durationMinutes: 45, type: 'consultation', startFrom: now()->addDay() ); // Check availability for a specific slot $isAvailable = schedule_for($availability)->isTimeSlotAvailable( start: '2038-01-06 14:00:00', end: '2038-01-06 15:00:00', type: 'consultation' );
๐ Polymorphic Scheduling Link System
Roster includes an advanced system that allows any Eloquent model to be associated with schedules with customizable metadata.
Attach resources to schedules
use Roster\Traits\AttachableToSchedules; // Add the trait to your models class Room extends Model { use AttachableToSchedules; } class Vehicle extends Model { use AttachableToSchedules; } class Equipment extends Model { use AttachableToSchedules; } // Usage: attach resources to a schedule $schedule = schedule_for($availability)->create([ 'title' => 'Scheduled Surgery', 'start_datetime' => '2038-01-04 08:00:00', 'end_datetime' => '2038-01-04 12:00:00', ]); // Attach resources with metadata $room = Room::find(1); $vehicle = Vehicle::find(1); $doctor = Doctor::find(1); $service = schedule_for($availability)->schedule($schedule); $service->attach($room, ['role' => 'operating_room', 'equipment' => 'surgical']); $service->attach($vehicle, ['role' => 'transport', 'urgent' => true]); $service->attach($doctor, ['role' => 'surgeon', 'specialty' => 'orthopedics']); // Attach multiple resources at once $service->attachMany([$room, $vehicle, $doctor], ['operation_id' => 'OP123']);
Manage attached resources
// Check if a resource is attached $service->hasAttached($room); // true // Retrieve all attached resources $attachedResources = $service->getAttached(); // Collection containing room, vehicle, doctor // Filter by model type $rooms = $service->getAttachedByType(Room::class); $doctors = $service->getAttachedByType(Doctor::class); // Detach resources $service->detach($vehicle); $service->detachMany([$room, $doctor]); // Synchronize resources completely $service->sync([$room, $doctor], ['session' => 'morning']); // Detach all resources $service->detachAll();
Direct usage from models
// From an attachable model $room->isAttachedToSchedule($schedule); // true/false $room->attachToSchedule($schedule, ['role' => 'consultation']); $room->detachFromSchedule($schedule); // Get all schedules with metadata $schedulesWithMetadata = $room->attachedSchedulesWithLinkMetadata(); // Filter by metadata $surgeries = $room->attachedSchedulesWithMetadata('role', 'operating_room'); // Synchronize schedules $room->syncSchedules([$schedule1, $schedule2], ['default_room' => true]);
Eloquent relationships
// The polymorphic relationship is automatically available $room->attachedSchedules; // Collection of schedules $schedule->linkables; // Collection of attached models (via pivot) // With link metadata $room->attachedSchedules()->withPivot('metadata')->get();
Advanced use cases
1. Operating room management
// Prepare surgery with all necessary resources $surgerySchedule = schedule_for($availability)->create([ 'title' => 'Knee Arthroscopy', 'start_datetime' => '2038-01-04 08:00:00', 'end_datetime' => '2038-01-04 10:00:00', ]); $service = schedule_for($availability)->schedule($surgerySchedule); $service->attach($operatingRoom, [ 'role' => 'operating_room', 'equipment' => ['arthroscope', 'monitor', 'instruments'], 'sterilization' => 'level_2' ]); $service->attach($surgeon, [ 'role' => 'primary_surgeon', 'specialty' => 'orthopedics', 'assistant_required' => true ]); $service->attach($anesthesiologist, [ 'role' => 'anesthesiologist', 'type_anesthesia' => 'general' ]); $service->attach($nurse, [ 'role' => 'instrument_nurse', 'experience' => 'senior' ]);
2. Shared resource booking
// Two different schedules sharing the same resources $schedule1 = schedule_for($availability)->create([...]); $schedule2 = schedule_for($availability)->create([...]); $sharedRoom = Room::find(1); $sharedEquipment = Equipment::find(1); $service1 = schedule_for($availability)->schedule($schedule1); $service2 = schedule_for($availability)->schedule($schedule2); $service1->attach($sharedRoom, ['usage' => 'consultation']); $service2->attach($sharedRoom, ['usage' => 'training']); $service1->attach($sharedEquipment, ['reserved' => true]); // The system tracks which resource is used where and when
3. Complex metadata for tracking
$service->attach($patient, [ 'medical_history' => ['hypertension', 'diabetes'], 'insurance' => 'ABC Insurance', 'priority' => 'high', 'contact' => [ 'phone' => '555-0123', 'email' => 'patient@example.com' ], 'notes' => ['allergic to penicillin', 'needs interpreter'] ]);
๐ Model Query Methods (HasRoster Trait)
The HasRoster trait includes methods to retrieve impediments and schedules of a model within a given period.
Added Methods
// 1. Get all items (impediments + schedules) in a period $items = $model->getRosterItemsInPeriod($start, $end); // Returns: ['impediments' => Collection, 'schedules' => Collection] // 2. Get only impediments in a period $impediments = $model->getImpedimentsInPeriod($start, $end); // 3. Get only schedules in a period $schedules = $model->getSchedulesInPeriod($start, $end); // 4. Check for conflicts $hasConflicts = $model->hasConflictsInPeriod($start, $end); // Returns true if at least one impediment or schedule exists
Simple Example
// A doctor with the HasRoster trait $doctor = Doctor::find(1); // Check availability for tomorrow 10am-11am $start = Carbon::parse('2024-06-10 10:00:00'); $end = Carbon::parse('2024-06-10 11:00:00'); // Check for conflicts if ($doctor->hasConflictsInPeriod($start, $end)) { // Get details $conflicts = $doctor->getRosterItemsInPeriod($start, $end); echo "Conflicting schedules: " . $conflicts['schedules']->count(); echo "Conflicting impediments: " . $conflicts['impediments']->count(); } else { echo "Time slot available"; }
Practical Use Case
// Before creating a new schedule public function createSchedule(Doctor $doctor, array $data) { $start = Carbon::parse($data['start_datetime']); $end = Carbon::parse($data['end_datetime']); // Check if the time slot is free if ($doctor->hasConflictsInPeriod($start, $end)) { return response()->json([ 'error' => 'Time slot not available', 'conflicts' => $doctor->getRosterItemsInPeriod($start, $end) ], 422); } // Create the schedule return schedule_for($doctor->availabilities()->first()) ->create($data); }
๐ Core Concepts
Immutability Principle
Roster prevents direct model mutations to ensure data integrity. All operations must go through appropriate services:
// โ FORBIDDEN: Direct modification $availability->update(['daily_end' => '18:00:00']); // Throws exception // โ ALLOWED: Via service availability_for($doctor)->update($availability->id, [ 'daily_end' => '18:00:00' ]);
Single-action context
Each service is designed for a single action with its own context:
// โ FORBIDDEN: Service reuse $service = availability_for($doctor); $service->create([...]); $service->update(1, [...]); // Corrupted context // โ ALLOWED: New context for each action availability_for($doctor)->create([...]); availability_for($doctor)->update(1, [...]);
The 3 main entities
- Availability: Defines when a resource is available (days, times, period)
- Schedule: Represents a booked slot in an availability
- Impediment: Temporarily blocks an availability
๐ก๏ธ Secure Architecture
Mutation access control
The system uses two contexts to control access:
// 1. Mutation context (internal) // Used by repositories to allow CRUD operations RosterMutationContext::allow(function () { return Availability::create([...]); // Allowed in this context }); // 2. Service context (public) // Used by helpers to allow service usage RosterServiceContext::allow(function () { return $service->create([...]); // Allowed via helper });
Secure helpers
The availability_for(), schedule_for(), and impediment_for() helpers automatically create the necessary context:
// These helpers automatically handle: // 1. Execution context creation // 2. Schedulable entity validation // 3. Reuse prevention
๐ Advanced Search and Data Consistency
first() method for targeted search
// Retrieve the first availability matching criteria $availability = availability_for($doctor) ->whereType('consultation') ->first(); // Retrieve the next upcoming appointment $nextAppointment = schedule_for($availability) ->setFilter('start_datetime', '>', now()) ->first(); // Retrieve the first scheduled impediment $firstImpediment = impediment_for($availability) ->setFilter('reason', 'like', '%training%') ->first();
Automatic days consistency
The system automatically ensures consistency between specified days and validity periods:
// During an update, days outside the period are automatically reconciled $availability = availability_for($doctor)->create([ 'validity_start' => '2024-01-01', 'validity_end' => '2024-01-07', // Week from January 1-7 'days' => ['monday', 'wednesday', 'friday'], ]); // If you extend the period, days are automatically adjusted availability_for($doctor)->update($availability->id, [ 'validity_end' => '2024-01-14', // Two weeks // Days remain consistent with the new period ]); // Reconciliation behavior configuration // In config/roster.php: 'reconciliation_warning' => env('ROSTER_RECONCILIATION_WARNING', false), // If true: PHP warning when days are outside the period // If false: silent reconciliation
Standardized days sorting
Utility functions always return days in standard week order (Monday โ Sunday):
$days = roster_days_in_period('2024-01-01', '2024-01-07'); // Returns: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] // Automatically sorted in standard order
๐ฏ Exhaustive Business Validation
Roster includes 17 validation rules that guarantee system consistency:
Main rules:
- SchedulableValidationRule (110) - Checks for schedulable context presence
- RequiredFieldsRule (100) - Validates required fields per operation
- AvailabilityTemporalCoherenceRule (100) - Ensures temporal coherence
- TemporalConflictRule (80) - Prevents scheduling overlaps
- AvailabilityOverlapRule (80) - Prevents availability overlaps
- TimeRangeRule (85) - Validates time ranges (no multi-day spans)
Rule visualization:
# List all available rules php artisan roster:debug-rules # See rules for a specific entity php artisan roster:debug-rules availability --operation=create
๐ Real-world Usage Examples
Medical clinic management
// Create availabilities for different specialists $cardiologist = Doctor::where('specialty', 'cardiology')->first(); $availability = availability_for($cardiologist)->create([ 'type' => 'consultation', 'daily_start' => '08:30:00', 'daily_end' => '12:30:00', 'days' => ['monday', 'wednesday', 'friday'], 'validity_start' => '2024-01-01', 'validity_end' => '2024-12-31', ]); // Patient booking $appointment = schedule_for($availability)->create([ 'title' => 'Cardiac Consultation', 'start_datetime' => '2024-06-10 10:00:00', 'end_datetime' => '2024-06-10 11:00:00', 'status' => ScheduleStatus::BOOKED, 'metadata' => [ 'patient_id' => 'CARD001', 'priority' => 'medium', 'tests_required' => ['echocardiogram', 'stress_test'] ], ]); // Quick search for next availability $nextAvailability = availability_for($cardiologist) ->setFilter('validity_start', '>', now()) ->first(); // Manage unavailability (training) impediment_for($availability)->create([ 'reason' => 'Continuing education', 'start_datetime' => '2024-06-15 09:00:00', 'end_datetime' => '2024-06-15 12:00:00', 'metadata' => ['mandatory' => true, 'location' => 'Auditorium'], ]);
Room booking system
// Two doctors sharing a room $room = Room::find(1); // First doctor uses the room on Monday $doctor1Availability = availability_for($doctor1)->create([ 'type' => 'room_a', 'daily_start' => '09:00:00', 'daily_end' => '17:00:00', 'days' => ['monday', 'wednesday', 'friday'], 'validity_start' => '2024-01-01', 'validity_end' => '2024-12-31', ]); // Second doctor uses the room on Tuesday $doctor2Availability = availability_for($doctor2)->create([ 'type' => 'room_a', 'daily_start' => '09:00:00', 'daily_end' => '17:00:00', 'days' => ['tuesday', 'thursday'], 'validity_start' => '2024-01-01', 'validity_end' => '2024-12-31', ]); // Search for first availability for urgent slot $urgentSlot = schedule_for($doctor1Availability) ->setFilter('status', ScheduleStatus::AVAILABLE) ->first(); // System automatically prevents conflicts schedule_for($doctor1Availability)->create([ 'title' => 'Room A usage - Dr. Smith', 'start_datetime' => '2024-06-10 10:00:00', // Monday 'end_datetime' => '2024-06-10 12:00:00', ]); // โ This booking will fail (inter-doctor conflict) schedule_for($doctor2Availability)->create([ 'title' => 'Room A usage - Dr. Jones', 'start_datetime' => '2024-06-10 11:00:00', // Same day as Dr. Smith 'end_datetime' => '2024-06-10 13:00:00', ]);
Recurrent impediment management
// Create weekly availability $weeklyAvailability = availability_for($doctor)->create([ 'type' => 'consultation', 'daily_start' => '08:00:00', 'daily_end' => '18:00:00', 'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], 'validity_start' => '2024-01-01', 'validity_end' => '2024-12-31', ]); // Recurrent impediments (lunch break) $weekdays = ['2024-01-08', '2024-01-09', '2024-01-10', '2024-01-11', '2024-01-12']; foreach ($weekdays as $weekday) { impediment_for($weeklyAvailability)->create([ 'reason' => 'Lunch break', 'start_datetime' => Carbon::parse($weekday)->setTime(12, 0, 0), 'end_datetime' => Carbon::parse($weekday)->setTime(13, 0, 0), 'metadata' => ['type' => 'lunch', 'recurring' => true], ]); } // Find first available slot after impediments $firstAvailableSlot = schedule_for($weeklyAvailability) ->setFilter('start_datetime', '>', now()) ->first(); // Find available slots despite impediments $availableSlots = schedule_for($weeklyAvailability)->findAvailableSlots( startDate: '2024-01-08', endDate: '2024-01-12', durationMinutes: 60, type: 'consultation' );
๐ง Complete API
Availability Service
// CRUD availability_for($schedulable)->create($data); availability_for($schedulable)->find($id); availability_for($schedulable)->update($id, $data); availability_for($schedulable)->delete($id); // Search availability_for($schedulable)->all(); availability_for($schedulable)->setFilter('type', 'consultation')->all(); availability_for($schedulable)->first(); // New method // Checks availability_for($schedulable)->isAvailableOnDate($date, $type); availability_for($schedulable)->getAvailabilityForTimeSlot($start, $end, $type);
Schedule Service
// Booking schedule_for($availability)->create($data); schedule_for($availability)->update($id, $data); schedule_for($availability)->delete($id); // Slot search schedule_for($availability)->findNextSlot($durationMinutes, $type, $startFrom); schedule_for($availability)->findAvailableSlots($startDate, $endDate, $durationMinutes, $type); schedule_for($availability)->first(); // New method // Checks schedule_for($availability)->isTimeSlotAvailable($start, $end, $type); schedule_for($availability)->isPeriodAvailable($start, $end, $type); // Polymorphic link management schedule_for($availability)->schedule($scheduleModel); // Set context schedule_for($availability)->schedule($scheduleModel)->attach($model, $metadata); schedule_for($availability)->schedule($scheduleModel)->detach($model); schedule_for($availability)->schedule($scheduleModel)->getAttached(); schedule_for($availability)->schedule($scheduleModel)->sync($models, $metadata);
Impediment Service
// Impediment management impediment_for($availability)->create($data); impediment_for($availability)->update($id, $data); impediment_for($availability)->delete($id); // Search impediment_for($availability)->first(); // New method // Checks impediment_for($availability)->isTimeSlotBlocked($start, $end); impediment_for($availability)->getAvailableTimeSlots($start, $end, $type);
โ๏ธ Configuration
Configuration file (config/roster.php)
return [ // Allowed activity types 'allowed_types' => [ 'consultation', 'surgery', 'emergency', 'training', 'room_a', 'echography', 'scan', ], // Minimum durations (in minutes) 'durations' => [ 'minimum_availability_minutes' => 15, 'minimum_schedule_minutes' => 15, 'minimum_impediment_minutes' => 5, 'max_search_period_days' => 365, 'max_availability_days' => 365, ], // Validation rule cache 'cache' => [ 'enabled' => env('ROSTER_CACHE_ENABLED', true), 'cache_file' => storage_path('framework/cache/roster_rules.php'), 'cache_max_age_hours' => 24, ], // Days reconciliation 'reconciliation_warning' => env('ROSTER_RECONCILIATION_WARNING', false), // Controls behavior during updates when days are // outside the validity period: // - true: triggers a PHP warning (E_USER_WARNING) // - false: silent reconciliation ];
Environment variables
ROSTER_TIMEZONE=Europe/Paris ROSTER_CACHE_ENABLED=true ROSTER_RECONCILIATION_WARNING=false
๐งช Comprehensive Tests
The package includes 2300 tests covering all scenarios:
# Run all tests php artisan test # Integration tests php artisan test --group=integration # Performance tests php artisan test --filter=test_performance_and_load_scenario # Complex scenario tests php artisan test --filter=test_real_world_complex_scenario
Tested scenarios:
- โ Full availability lifecycle
- โ Impediment management with conflicts
- โ Intelligent booking system
- โ Complex interactions (availabilities + impediments + schedules)
- โ Multi-user conflicts with shared resources
- โ Error handling and edge cases
- โ Performance testing with massive data
- โ Recovery after errors
- โ Realistic complex scenario (hospital with multiple specialists)
- โ Data consistency with automatic reconciliation
- โ
first()method for targeted search - โ Polymorphic link system with metadata
- โ Attached resource management (rooms, vehicles, equipment)
- โ Synchronization and detachment tests
๐จ Error Handling
use Roster\Validation\Exceptions\ValidationFailedException; try { $schedule = schedule_for($availability)->create($data); } catch (ValidationFailedException $e) { // Get detailed violations with rule information $violations = $e->getViolations(); // Array of ViolationData objects containing: // - field name // - error message // - rule that triggered the violation // - rule description for context $detailedReport = $e->toDetailedArray(); // Includes rule descriptions for better debugging return response()->json([ 'error' => 'validation_failed', 'message' => $e->getFormattedMessage(), 'violations' => $detailedReport['violations'], ], 422); }
Reconciliation warning handling
// Configuration to enable warnings config()->set('roster.reconciliation_warning', true); // Capture warnings set_error_handler(function ($errno, $errstr) { if ($errno === E_USER_WARNING && str_contains($errstr, 'outside the validity period')) { // Log or handle the warning Log::warning('Days reconciliation detected', ['message' => $errstr]); return true; // Prevents propagation } return false; }); // During an update with days outside the period: availability_for($doctor)->update($availability->id, [ 'validity_end' => '2024-01-10', 'days' => ['monday', 'saturday'], // 'saturday' will be filtered with warning ]); restore_error_handler();
๐ Development Tools
Validation rule debugging
# Display all rules php artisan roster:debug-rules # Filter by entity php artisan roster:debug-rules availability # Filter by operation php artisan roster:debug-rules availability --operation=create # Display methods php artisan roster:debug-rules availability --show-methods # Display sources php artisan roster:debug-rules availability --show-source
Cache management
# Generate rule cache php artisan roster:cache-rules # Display cache statistics php artisan roster:cache-rules --show # Clear cache php artisan roster:cache-rules --clear # Force regeneration php artisan roster:cache-rules --force
๐ค Contribution
- Fork the repository
- Create a branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Run tests
# All tests composer test # With code coverage composer test-coverage # Check code style composer lint
๐ License
This package is open-source and available under the MIT license.
๐ Useful Links
Roster - A professional solution for advanced scheduling management, designed for critical applications where every minute counts. โ๏ธโฐโจ
With advanced search features, data consistency, exhaustive business validation, and a comprehensive polymorphic link system, Roster ensures the integrity of your scheduling systems in the most demanding environments.