-
Notifications
You must be signed in to change notification settings - Fork 0
Custom Evaluators
Create your own rule types to handle specialized availability logic.
<?php
namespace App\Availability\Evaluators;
use RomegaSoftware\Availability\Contracts\RuleEvaluator;
use RomegaSoftware\Availability\Contracts\AvailabilitySubject;
use Carbon\CarbonInterface;
class MinimumNoticeEvaluator implements RuleEvaluator
{
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
$hoursNotice = $config['hours'] ?? 24;
$minimumTime = now()->addHours($hoursNotice);
// Available only if requested time is far enough in future
return $moment->isAfter($minimumTime);
}
}In config/availability.php:
'rule_types' => [
// ... existing types ...
'minimum_notice' => \App\Availability\Evaluators\MinimumNoticeEvaluator::class,
],Or register dynamically in a service provider:
use RomegaSoftware\Availability\Support\RuleEvaluatorRegistry;
public function boot()
{
$registry = app(RuleEvaluatorRegistry::class);
$registry->register('minimum_notice', new MinimumNoticeEvaluator());
}$service->availabilityRules()->create([
'type' => 'minimum_notice',
'config' => ['hours' => 48], // Require 48 hours notice
'effect' => Effect::Allow,
'priority' => 25,
]);namespace App\Availability\Evaluators;
class BusinessDaysEvaluator implements RuleEvaluator
{
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
$daysAhead = $config['days_ahead'] ?? 3;
$includeToday = $config['include_today'] ?? false;
$start = $includeToday ? now()->startOfDay() : now()->addDay()->startOfDay();
$businessDays = 0;
$current = $start->copy();
while ($businessDays < $daysAhead && $current->diffInDays(now()) < 365) {
if ($current->isWeekday()) {
$businessDays++;
if ($current->isSameDay($moment)) {
return true;
}
}
$current->addDay();
}
return false;
}
}
// Usage: Available only within next 3 business days
$delivery->availabilityRules()->create([
'type' => 'business_days',
'config' => [
'days_ahead' => 3,
'include_today' => false,
],
'effect' => Effect::Allow,
'priority' => 20,
]);namespace App\Availability\Evaluators;
class CapacityEvaluator implements RuleEvaluator
{
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
$requiredCapacity = $config['required'] ?? 1;
$bufferPercentage = $config['buffer'] ?? 0;
// Get current bookings for this time
$usedCapacity = $subject->bookings()
->where('start_time', '<=', $moment)
->where('end_time', '>', $moment)
->sum('capacity_used');
$totalCapacity = $subject->total_capacity;
$bufferAmount = $totalCapacity * ($bufferPercentage / 100);
$availableCapacity = $totalCapacity - $usedCapacity - $bufferAmount;
return $availableCapacity >= $requiredCapacity;
}
}
// Usage: Require at least 10 seats with 20% buffer
$venue->availabilityRules()->create([
'type' => 'capacity',
'config' => [
'required' => 10,
'buffer' => 20,
],
'effect' => Effect::Allow,
'priority' => 30,
]);namespace App\Availability\Evaluators;
use Illuminate\Support\Facades\Http;
class WeatherEvaluator implements RuleEvaluator
{
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
$conditions = $config['conditions'] ?? ['clear', 'partly_cloudy'];
$maxWindSpeed = $config['max_wind_speed'] ?? 25;
$minTemp = $config['min_temperature'] ?? 10;
// Check if moment is within forecast range (e.g., next 7 days)
if ($moment->isAfter(now()->addDays(7))) {
// Too far in future, assume available
return true;
}
try {
$forecast = Cache::remember(
"weather.{$subject->latitude}.{$subject->longitude}.{$moment->format('Y-m-d')}",
3600,
function () use ($subject, $moment) {
return Http::get('https://api.weather.com/forecast', [
'lat' => $subject->latitude,
'lon' => $subject->longitude,
'date' => $moment->format('Y-m-d'),
])->json();
}
);
$weather = $forecast['hourly'][$moment->hour] ?? null;
if (!$weather) {
return true; // No data, assume available
}
return in_array($weather['condition'], $conditions)
&& $weather['wind_speed'] <= $maxWindSpeed
&& $weather['temperature'] >= $minTemp;
} catch (\Exception $e) {
// Weather API failed, default to available
Log::warning('Weather API failed', ['error' => $e->getMessage()]);
return true;
}
}
}
// Usage: Outdoor venue only available in good weather
$outdoorVenue->availabilityRules()->create([
'type' => 'weather',
'config' => [
'conditions' => ['clear', 'partly_cloudy', 'cloudy'],
'max_wind_speed' => 20,
'min_temperature' => 15,
],
'effect' => Effect::Allow,
'priority' => 40,
]);namespace App\Availability\Evaluators;
class QuotaEvaluator implements RuleEvaluator
{
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
$quotaType = $config['type'] ?? 'daily';
$limit = $config['limit'] ?? 10;
$query = $subject->usages();
switch ($quotaType) {
case 'hourly':
$query->where('created_at', '>=', $moment->copy()->startOfHour())
->where('created_at', '<', $moment->copy()->endOfHour());
break;
case 'daily':
$query->whereDate('created_at', $moment->toDateString());
break;
case 'weekly':
$query->where('created_at', '>=', $moment->copy()->startOfWeek())
->where('created_at', '<', $moment->copy()->endOfWeek());
break;
case 'monthly':
$query->whereYear('created_at', $moment->year)
->whereMonth('created_at', $moment->month);
break;
}
$currentUsage = $query->count();
return $currentUsage < $limit;
}
}
// Usage: Limit to 100 daily API calls
$apiEndpoint->availabilityRules()->create([
'type' => 'quota',
'config' => [
'type' => 'daily',
'limit' => 100,
],
'effect' => Effect::Allow,
'priority' => 35,
]);namespace App\Availability\Evaluators;
class DependencyEvaluator implements RuleEvaluator
{
private $dependencyChecker;
public function __construct(DependencyChecker $checker)
{
$this->dependencyChecker = $checker;
}
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
$requiredResources = $config['requires'] ?? [];
foreach ($requiredResources as $resourceId) {
if (!$this->dependencyChecker->isAvailable($resourceId, $moment)) {
return false;
}
}
return true;
}
}
// Register with dependency injection
$registry->register('dependencies', function () {
return app(DependencyEvaluator::class);
});namespace App\Availability\Evaluators;
class CompositeEvaluator implements RuleEvaluator
{
private $registry;
public function __construct(RuleEvaluatorRegistry $registry)
{
$this->registry = $registry;
}
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
$operator = $config['operator'] ?? 'AND';
$rules = $config['rules'] ?? [];
$results = [];
foreach ($rules as $rule) {
$evaluator = $this->registry->get($rule['type']);
if ($evaluator) {
$results[] = $evaluator->matches(
$rule['config'] ?? [],
$moment,
$subject
);
}
}
if ($operator === 'AND') {
return !in_array(false, $results, true);
} elseif ($operator === 'OR') {
return in_array(true, $results, true);
} elseif ($operator === 'XOR') {
$trueCount = count(array_filter($results));
return $trueCount === 1;
}
return false;
}
}
// Usage: Complex rule composition
$resource->availabilityRules()->create([
'type' => 'composite',
'config' => [
'operator' => 'AND',
'rules' => [
['type' => 'weekdays', 'config' => ['days' => [1,2,3,4,5]]],
['type' => 'time_of_day', 'config' => ['from' => '09:00', 'to' => '17:00']],
['type' => 'minimum_notice', 'config' => ['hours' => 24]],
],
],
'effect' => Effect::Allow,
'priority' => 50,
]);namespace Tests\Unit\Availability;
use App\Availability\Evaluators\MinimumNoticeEvaluator;
use Carbon\Carbon;
class MinimumNoticeEvaluatorTest extends TestCase
{
private MinimumNoticeEvaluator $evaluator;
private $subject;
protected function setUp(): void
{
parent::setUp();
$this->evaluator = new MinimumNoticeEvaluator();
$this->subject = Resource::factory()->create();
}
public function test_requires_minimum_notice()
{
$config = ['hours' => 24];
// Test: 12 hours from now (not enough notice)
$tooSoon = now()->addHours(12);
$this->assertFalse(
$this->evaluator->matches($config, $tooSoon, $this->subject)
);
// Test: 25 hours from now (enough notice)
$farEnough = now()->addHours(25);
$this->assertTrue(
$this->evaluator->matches($config, $farEnough, $this->subject)
);
}
public function test_uses_default_notice_period()
{
$config = []; // No hours specified
// Should use default of 24 hours
$justEnough = now()->addHours(24)->addMinute();
$this->assertTrue(
$this->evaluator->matches($config, $justEnough, $this->subject)
);
}
}Each evaluator should handle one specific type of logic:
// Good: Single responsibility
class HolidayEvaluator { /* checks holidays */ }
class MaintenanceEvaluator { /* checks maintenance */ }
// Bad: Too many responsibilities
class ComplexEvaluator { /* checks holidays, maintenance, capacity, weather... */ }public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
// Validate config
if (!isset($config['required_field'])) {
return false; // Safe default
}
// Handle null values
$value = $config['value'] ?? $this->getDefaultValue();
// Prevent division by zero
if ($divisor == 0) {
return false;
}
// Your logic here...
}// Good: Can be tested in isolation
class PureEvaluator implements RuleEvaluator
{
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
return $moment->hour >= $config['start_hour']
&& $moment->hour < $config['end_hour'];
}
}
// Harder to test: Direct database/API calls
class CoupledEvaluator implements RuleEvaluator
{
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
return DB::table('some_table')->where(...)->exists(); // Hard to mock
}
}/**
* Evaluates availability based on minimum advance notice
*
* Configuration:
* - hours (int): Minimum hours of advance notice required (default: 24)
* - business_hours_only (bool): Count only business hours (default: false)
* - exclude_weekends (bool): Don't count weekend hours (default: false)
*
* Example:
* ['hours' => 48, 'business_hours_only' => true]
*/
class MinimumNoticeEvaluator implements RuleEvaluator
{
// ...
}- Complex Scenarios - See custom evaluators in action
- Performance Tips - Optimize evaluator performance
- Testing - Testing strategies for custom rules
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