-
Notifications
You must be signed in to change notification settings - Fork 0
Complex Scenarios
Real-world implementation patterns for sophisticated availability requirements.
Conference room booking with equipment, catering, and staff requirements.
namespace App\Services;
use RomegaSoftware\Availability\Support\AvailabilityEngine;
use RomegaSoftware\Availability\Support\Effect;
class ConferenceBookingService
{
private AvailabilityEngine $engine;
public function __construct(AvailabilityEngine $engine)
{
$this->engine = $engine;
}
public function checkAvailability($roomId, $startTime, $duration, array $requirements = [])
{
$room = ConferenceRoom::find($roomId);
$endTime = $startTime->copy()->addMinutes($duration);
// Check room availability
if (!$this->isTimeSlotAvailable($room, $startTime, $endTime)) {
return ['available' => false, 'reason' => 'Room not available'];
}
// Check equipment
if (!empty($requirements['equipment'])) {
foreach ($requirements['equipment'] as $equipmentId) {
$equipment = Equipment::find($equipmentId);
if (!$this->isTimeSlotAvailable($equipment, $startTime, $endTime)) {
return [
'available' => false,
'reason' => "Equipment {$equipment->name} not available"
];
}
}
}
// Check catering availability
if ($requirements['catering'] ?? false) {
$catering = CateringService::first();
if (!$this->engine->isAvailable($catering, $startTime)) {
return ['available' => false, 'reason' => 'Catering not available'];
}
}
// Check staff requirements
if ($requirements['staff_support'] ?? false) {
$availableStaff = Staff::whereHas('availabilityRules')
->get()
->filter(fn($staff) => $this->engine->isAvailable($staff, $startTime))
->count();
if ($availableStaff < 1) {
return ['available' => false, 'reason' => 'No staff available'];
}
}
return ['available' => true, 'resources' => $this->gatherResources($requirements)];
}
private function isTimeSlotAvailable($resource, $start, $end)
{
$current = $start->copy();
while ($current < $end) {
if (!$this->engine->isAvailable($resource, $current)) {
return false;
}
$current->addMinutes(15); // Check every 15 minutes
}
return true;
}
public function setupRoomRules(ConferenceRoom $room)
{
// Basic business hours
$room->availabilityRules()->createMany([
[
'type' => 'weekdays',
'config' => ['days' => [1, 2, 3, 4, 5]],
'effect' => Effect::Allow,
'priority' => 10,
],
[
'type' => 'time_of_day',
'config' => ['from' => '08:00', 'to' => '18:00'],
'effect' => Effect::Allow,
'priority' => 20,
],
]);
// Maintenance window
$room->availabilityRules()->create([
'type' => 'rrule',
'config' => [
'rrule' => 'FREQ=WEEKLY;BYDAY=FR;BYHOUR=16,17',
],
'effect' => Effect::Deny,
'priority' => 30,
]);
// Capacity check
$room->availabilityRules()->create([
'type' => 'inventory_gate',
'config' => ['min' => 1],
'effect' => Effect::Allow,
'priority' => 40,
]);
}
}Medical appointments requiring prep and cleanup time.
namespace App\Models;
use RomegaSoftware\Availability\Traits\HasAvailability;
class Practitioner extends Model
{
use HasAvailability;
public function createAppointmentSlot($startTime, $duration, $type)
{
$bufferBefore = $this->getBufferBefore($type);
$bufferAfter = $this->getBufferAfter($type);
$totalDuration = $bufferBefore + $duration + $bufferAfter;
$actualStart = $startTime->copy()->subMinutes($bufferBefore);
$actualEnd = $startTime->copy()->addMinutes($duration + $bufferAfter);
// Create blackout for the entire period including buffers
$this->availabilityRules()->create([
'type' => 'blackout_date',
'config' => [
'dates' => [$actualStart->format('Y-m-d')],
'_times' => [
'start' => $actualStart->format('H:i'),
'end' => $actualEnd->format('H:i'),
],
],
'effect' => Effect::Deny,
'priority' => 100, // High priority to override all other rules
]);
return Appointment::create([
'practitioner_id' => $this->id,
'start_time' => $startTime,
'duration' => $duration,
'type' => $type,
'buffer_before' => $bufferBefore,
'buffer_after' => $bufferAfter,
]);
}
private function getBufferBefore($type)
{
return match($type) {
'surgery' => 30,
'consultation' => 10,
'checkup' => 5,
default => 15,
};
}
private function getBufferAfter($type)
{
return match($type) {
'surgery' => 45,
'consultation' => 5,
'checkup' => 5,
default => 10,
};
}
}
// Custom evaluator for time-specific blackouts
class TimeSpecificBlackoutEvaluator implements RuleEvaluator
{
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
$dates = $config['dates'] ?? [];
$times = $config['_times'] ?? null;
if (!in_array($moment->format('Y-m-d'), $dates)) {
return false;
}
if ($times) {
$start = Carbon::parse($moment->format('Y-m-d') . ' ' . $times['start']);
$end = Carbon::parse($moment->format('Y-m-d') . ' ' . $times['end']);
return $moment->between($start, $end);
}
return true;
}
}Content availability based on subscription tiers and limits.
namespace App\Services;
class SubscriptionAvailabilityService
{
public function setupSubscriptionRules(User $user)
{
$subscription = $user->currentSubscription();
switch ($subscription->tier) {
case 'basic':
$this->setupBasicTier($user);
break;
case 'professional':
$this->setupProfessionalTier($user);
break;
case 'enterprise':
$this->setupEnterpriseTier($user);
break;
}
}
private function setupBasicTier($user)
{
$user->availabilityRules()->createMany([
// Business hours only
[
'type' => 'time_of_day',
'config' => ['from' => '09:00', 'to' => '17:00'],
'effect' => Effect::Allow,
'priority' => 10,
],
// Weekdays only
[
'type' => 'weekdays',
'config' => ['days' => [1, 2, 3, 4, 5]],
'effect' => Effect::Allow,
'priority' => 20,
],
// Usage quota
[
'type' => 'quota',
'config' => ['type' => 'daily', 'limit' => 100],
'effect' => Effect::Allow,
'priority' => 30,
],
]);
}
private function setupProfessionalTier($user)
{
$user->availabilityRules()->createMany([
// Extended hours
[
'type' => 'time_of_day',
'config' => ['from' => '06:00', 'to' => '22:00'],
'effect' => Effect::Allow,
'priority' => 10,
],
// All week
[
'type' => 'weekdays',
'config' => ['days' => [1, 2, 3, 4, 5, 6, 7]],
'effect' => Effect::Allow,
'priority' => 20,
],
// Higher quota
[
'type' => 'quota',
'config' => ['type' => 'daily', 'limit' => 1000],
'effect' => Effect::Allow,
'priority' => 30,
],
]);
}
private function setupEnterpriseTier($user)
{
// Enterprise has no restrictions
$user->availability_default = Effect::Allow;
$user->save();
// Only apply system maintenance windows
$user->availabilityRules()->create([
'type' => 'rrule',
'config' => [
'rrule' => 'FREQ=MONTHLY;BYMONTHDAY=1;BYHOUR=2,3',
],
'effect' => Effect::Deny,
'priority' => 100,
]);
}
}Adjust availability based on demand and pricing tiers.
namespace App\Models;
class PricingAwareResource extends Model
{
use HasAvailability;
public function checkAvailabilityWithPricing($moment, $maxPrice = null)
{
$engine = app(AvailabilityEngine::class);
// Base availability check
if (!$engine->isAvailable($this, $moment)) {
return ['available' => false, 'reason' => 'Not available at this time'];
}
// Calculate dynamic price
$price = $this->calculatePrice($moment);
// Check if price is within budget
if ($maxPrice && $price > $maxPrice) {
return [
'available' => false,
'reason' => 'Price exceeds budget',
'current_price' => $price,
];
}
return [
'available' => true,
'price' => $price,
'tier' => $this->getPriceTier($moment),
];
}
private function calculatePrice($moment)
{
$basePrice = $this->base_price;
$multiplier = 1.0;
// Peak hours (20% increase)
if ($moment->hour >= 17 && $moment->hour <= 20) {
$multiplier *= 1.2;
}
// Weekend (15% increase)
if ($moment->isWeekend()) {
$multiplier *= 1.15;
}
// High demand dates (30% increase)
if ($this->isHighDemand($moment)) {
$multiplier *= 1.3;
}
// Low demand (20% discount)
if ($this->isLowDemand($moment)) {
$multiplier *= 0.8;
}
return round($basePrice * $multiplier, 2);
}
private function isHighDemand($moment)
{
$bookingRate = $this->bookings()
->whereDate('date', $moment->format('Y-m-d'))
->count();
return $bookingRate > $this->capacity * 0.8;
}
private function isLowDemand($moment)
{
$daysAhead = now()->diffInDays($moment);
// Last minute and low bookings
return $daysAhead <= 2 &&
$this->bookings()->whereDate('date', $moment->format('Y-m-d'))->count() < 2;
}
}
// Custom evaluator for price-based availability
class PriceThresholdEvaluator implements RuleEvaluator
{
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
$maxPrice = $config['max_price'] ?? PHP_INT_MAX;
$minPrice = $config['min_price'] ?? 0;
if (method_exists($subject, 'calculatePrice')) {
$price = $subject->calculatePrice($moment);
return $price >= $minPrice && $price <= $maxPrice;
}
return true;
}
}Hotel with rooms, amenities, and services with interdependent availability.
namespace App\Services;
class HotelAvailabilityService
{
public function setupCascadingRules(Hotel $hotel)
{
// Hotel-level rules (affects all resources)
$hotel->availabilityRules()->createMany([
// Open year-round except Christmas
[
'type' => 'blackout_date',
'config' => ['dates' => ['2025-12-25']],
'effect' => Effect::Deny,
'priority' => 100,
],
]);
// Room-specific rules
foreach ($hotel->rooms as $room) {
$this->setupRoomRules($room, $hotel);
}
// Amenity rules dependent on hotel
foreach ($hotel->amenities as $amenity) {
$this->setupAmenityRules($amenity, $hotel);
}
}
private function setupRoomRules($room, $hotel)
{
// Inherit hotel availability
$room->availabilityRules()->create([
'type' => 'parent_availability',
'config' => ['parent_id' => $hotel->id, 'parent_type' => Hotel::class],
'effect' => Effect::Allow,
'priority' => 5,
]);
// Room-specific maintenance
if ($room->next_maintenance_date) {
$room->availabilityRules()->create([
'type' => 'date_range',
'config' => [
'from' => $room->next_maintenance_date,
'to' => $room->next_maintenance_date->addDays(3)->format('Y-m-d'),
'kind' => 'absolute',
],
'effect' => Effect::Deny,
'priority' => 50,
]);
}
}
private function setupAmenityRules($amenity, $hotel)
{
switch ($amenity->type) {
case 'pool':
$amenity->availabilityRules()->createMany([
// Seasonal
[
'type' => 'months_of_year',
'config' => ['months' => [5, 6, 7, 8, 9]],
'effect' => Effect::Allow,
'priority' => 10,
],
// Daily hours
[
'type' => 'time_of_day',
'config' => ['from' => '06:00', 'to' => '22:00'],
'effect' => Effect::Allow,
'priority' => 20,
],
]);
break;
case 'restaurant':
$amenity->availabilityRules()->createMany([
// Breakfast
[
'type' => 'time_of_day',
'config' => ['from' => '06:30', 'to' => '10:30'],
'effect' => Effect::Allow,
'priority' => 10,
],
// Dinner
[
'type' => 'time_of_day',
'config' => ['from' => '18:00', 'to' => '22:00'],
'effect' => Effect::Allow,
'priority' => 15,
],
]);
break;
}
}
}
// Custom evaluator for parent availability
class ParentAvailabilityEvaluator implements RuleEvaluator
{
private AvailabilityEngine $engine;
public function __construct(AvailabilityEngine $engine)
{
$this->engine = $engine;
}
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
$parentType = $config['parent_type'] ?? null;
$parentId = $config['parent_id'] ?? null;
if (!$parentType || !$parentId) {
return true;
}
$parent = $parentType::find($parentId);
if (!$parent) {
return true;
}
return $this->engine->isAvailable($parent, $moment);
}
}High-traffic availability checking with caching.
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class OptimizedAvailabilityService
{
private AvailabilityEngine $engine;
public function checkAvailabilityOptimized($resource, $moment)
{
// Level 1: In-memory cache
$memoryKey = "availability.{$resource->id}.{$moment->timestamp}";
if (isset($this->memoryCache[$memoryKey])) {
return $this->memoryCache[$memoryKey];
}
// Level 2: Redis cache
$cacheKey = $this->getCacheKey($resource, $moment);
$cached = Cache::get($cacheKey);
if ($cached !== null) {
$this->memoryCache[$memoryKey] = $cached;
return $cached;
}
// Level 3: Check if rules have changed
$rulesHash = $this->getRulesHash($resource);
$lastHash = Cache::get("rules_hash.{$resource->id}");
if ($rulesHash !== $lastHash) {
// Rules changed, invalidate cache
$this->invalidateResourceCache($resource);
Cache::put("rules_hash.{$resource->id}", $rulesHash, 3600);
}
// Actual availability check
$result = $this->engine->isAvailable($resource, $moment);
// Cache result
$this->cacheResult($resource, $moment, $result);
return $result;
}
private function getCacheKey($resource, $moment)
{
// Round to nearest 5 minutes for better cache hit rate
$rounded = $moment->copy()->minute(floor($moment->minute / 5) * 5)->second(0);
return "avail:{$resource->getMorphClass()}:{$resource->id}:{$rounded->timestamp}";
}
private function getRulesHash($resource)
{
$rules = $resource->availabilityRules()
->where('enabled', true)
->orderBy('priority')
->get(['type', 'config', 'effect', 'priority']);
return md5(json_encode($rules->toArray()));
}
private function cacheResult($resource, $moment, $result)
{
$key = $this->getCacheKey($resource, $moment);
// Different TTL based on how far in future
$daysAhead = now()->diffInDays($moment);
$ttl = match(true) {
$daysAhead <= 1 => 300, // 5 minutes for today/tomorrow
$daysAhead <= 7 => 1800, // 30 minutes for this week
$daysAhead <= 30 => 3600, // 1 hour for this month
default => 7200, // 2 hours for far future
};
Cache::put($key, $result, $ttl);
}
public function batchCheckAvailability(array $resources, $moment)
{
// Use pipeline for Redis operations
$results = [];
$uncached = [];
Redis::pipeline(function ($pipe) use ($resources, $moment, &$results, &$uncached) {
foreach ($resources as $resource) {
$key = $this->getCacheKey($resource, $moment);
$pipe->get($key);
}
});
// Process uncached resources in parallel
$chunks = array_chunk($uncached, 10);
$promises = [];
foreach ($chunks as $chunk) {
$promises[] = async(fn() => $this->processChunk($chunk, $moment));
}
$chunkResults = await($promises);
return array_merge($results, ...$chunkResults);
}
}- Performance Tips - More optimization strategies
- Testing - Testing complex scenarios
- Configuration - Fine-tuning for complex setups
Romega Software is software development agency specializing in helping customers integrate AI and custom software into their business, helping companies in growth mode better acquire, visualize, and utilize their data, and helping entrepreneurs bring their ideas to life.
Installation
Set up the package in your Laravel app
Quick Start
Get running in 5 minutes
Basic Usage
Common patterns and examples
How It Works
Understanding the evaluation engine
Rule Types
Available rule types and configurations
Priority System
How rule priority affects evaluation
Inventory Gates
Dynamic availability based on stock
Custom Evaluators
Build your own rule types
Complex Scenarios
Real-world implementation patterns
Performance Tips
Optimization strategies
Configuration
Package configuration options
Models & Traits
Available models and traits
Testing
Testing your availability rules