PHP code example of andydefer / laravel-roster

1. Go to this page and download the library: Download andydefer/laravel-roster library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

andydefer / laravel-roster example snippets




namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Roster\Traits\HasRoster;

class Doctor extends Model
{
    use HasRoster;
}

// 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',
]);

// 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],
]);

// 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',
]);

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

// 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;
}

// 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

// 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 

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

// 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();

// 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]);

// 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();

// 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_

// 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

$service->attach($patient, [
    'medical_history' => ['hypertension', 'diabetes'],
    'insurance' => 'ABC Insurance',
    'priority' => 'high',
    'contact' => [
        'phone' => '555-0123',
        'email' => '[email protected]'
    ],
    'notes' => ['allergic to penicillin', 'needs interpreter']
]);

// 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

// 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";
}

// 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);
}

// ❌ 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'
]);

// ❌ 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, [...]);

// 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
});

// These helpers automatically handle:
// 1. Execution context creation
// 2. Schedulable entity validation
// 3. Reuse prevention

// 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();

// 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

$days = roster_days_in_period('2024-01-01', '2024-01-07');
// Returns: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
// Automatically sorted in standard order

// 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_

// 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',
]);

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

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

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

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
];

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

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 

// 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();
bash
php artisan roster:install
bash
# Configuration
php artisan vendor:publish --tag=roster-config

# Migrations
php artisan vendor:publish --tag=roster-migrations

# Run migrations
php artisan migrate
bash
# 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