andydefer/laravel-roster

Clean and flexible scheduling for Laravel applications.

Maintainers

Package info

github.com/andydefer/laravel-roster

pkg:composer/andydefer/laravel-roster

Statistics

Installs: 39

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 3

0.14.5 2026-04-20 22:31 UTC

README

PHP Version Laravel Version License Tests Coverage

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'
);

๐Ÿ›ก๏ธ Performance Protection: Absolute Minimum Duration

To prevent infinite loops and performance degradation, Roster enforces an absolute minimum duration of 10 minutes for ALL entity types (Availability, Schedule, Impediment).

Why 10 minutes?

When searching for available slots, the system generates time slots based on the duration. A duration that is too small would generate an enormous number of iterations:

Duration Generated Slots (1 year) Performance Impact
1 minute ~525,600 slots ๐Ÿ”ด Infinite loop risk - System overload, memory exhaustion
5 minutes ~105,120 slots ๐ŸŸ  Very slow - Timeout possible, poor user experience
10 minutes ~52,560 slots ๐ŸŸข Optimal - Fast and stable
15 minutes ~35,040 slots ๐ŸŸข Excellent - Best performance
30 minutes ~17,520 slots ๐ŸŸข Perfect - Maximum efficiency

Technical Protection Implementation

The protection is enforced at the lowest level of the validation system:

// In AbstractRule.php - Protected against configuration errors
private const ABSOLUTE_MIN_DURATION_MINUTES = 10;

protected function getMinimumDuration(EntityType $entityType): int
{
    $configuredMinutes = match ($entityType) {
        EntityType::AVAILABILITY => config('roster.durations.minimum_availability_minutes', 10),
        EntityType::SCHEDULE => config('roster.durations.minimum_schedule_minutes', 10),
        EntityType::IMPEDIMENT => config('roster.durations.minimum_impediment_minutes', 5),
    };

    // FORCE absolute minimum - Configuration cannot go below 10 minutes
    if ($configuredMinutes < self::ABSOLUTE_MIN_DURATION_MINUTES) {
        $actualMinutes = $configuredMinutes;
        $configuredMinutes = self::ABSOLUTE_MIN_DURATION_MINUTES;

        // Automatic warning when configuration is overridden
        logger()->warning('Minimum duration configuration overridden for performance reasons', [
            'entity_type' => $entityType->value,
            'configured_minutes' => $actualMinutes,
            'enforced_minutes' => self::ABSOLUTE_MIN_DURATION_MINUTES,
            'reason' => 'Durations below 10 minutes would generate too many iterations and slow down the system',
        ]);
    }

    return $configuredMinutes;
}

What happens if you try to configure less than 10 minutes?

// In config/roster.php
'durations' => [
    'minimum_availability_minutes' => 5, // โŒ Will be forced to 10
    'minimum_schedule_minutes' => 3,     // โŒ Will be forced to 10
    'minimum_impediment_minutes' => 1,   // โŒ Will be forced to 10
],

// The system automatically:
// 1. Detects the configuration below 10 minutes
// 2. Logs a warning for debugging
// 3. Enforces 10 minutes as the actual minimum
// 4. Prevents infinite loops and performance issues

Validation in Action

// Attempt to create an availability with 5 minutes duration
$context = $this->createMock(ValidationContextInterface::class);
$context->method('getEntityType')->willReturn(EntityType::AVAILABILITY);
$context->method('safeData')->willReturn([
    'start_time' => '09:00:00',
    'end_time' => '09:05:00', // 5 minutes - BELOW absolute minimum
]);

// This will FAIL with a clear error message:
// "Minimum duration of 10 minutes required for availability. Got 5 minutes"

// Attempt with 10 minutes
$context->method('safeData')->willReturn([
    'start_time' => '09:00:00',
    'end_time' => '09:10:00', // 10 minutes - MEETS absolute minimum
]);

// This will PASS validation

๐Ÿ”— 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

  1. Availability: Defines when a resource is available (days, times, period)
  2. Schedule: Represents a booked slot in an availability
  3. 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)
  • DurationRule (90) - Enforces minimum duration (with 10 minutes absolute minimum)

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)
    // IMPORTANT: The system enforces an absolute minimum of 10 minutes
    // for ALL entity types to prevent infinite loops and performance issues.
    // Any value below 10 will be automatically forced to 10.
    'durations' => [
        'minimum_availability_minutes' => 15,  // Will be enforced to >= 10
        'minimum_schedule_minutes' => 15,      // Will be enforced to >= 10
        'minimum_impediment_minutes' => 5,     // Will be enforced to >= 10
        '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
  • โœ… Minimum duration enforcement (10 minutes absolute minimum)
  • โœ… Protection against infinite loops in slot generation

๐Ÿšจ 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);
}

Duration validation error example

try {
    schedule_for($availability)->create([
        'start_datetime' => '2024-06-10 09:00:00',
        'end_datetime' => '2024-06-10 09:05:00', // 5 minutes
    ]);
} catch (ValidationFailedException $e) {
    // Error message:
    // "Minimum duration of 10 minutes required for Schedule. Got 5 minutes"
    
    // The system automatically prevents durations below 10 minutes
    // to protect against infinite loops in slot generation
}

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

  1. Fork the repository
  2. Create a branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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, automatic protection against infinite loops (10 minutes absolute minimum duration), and a comprehensive polymorphic link system, Roster ensures the integrity of your scheduling systems in the most demanding environments.