-
Notifications
You must be signed in to change notification settings - Fork 0
Performance Tips
Optimization strategies for high-performance availability checking.
// Bad: N+1 problem
$resources = Resource::all();
foreach ($resources as $resource) {
$isAvailable = $engine->isAvailable($resource, now());
// Each call loads rules separately
}
// Good: Eager load rules
$resources = Resource::with('availabilityRules')->get();
foreach ($resources as $resource) {
$isAvailable = $engine->isAvailable($resource, now());
}// Load only what you need
$rules = $resource->availabilityRules()
->where('enabled', true)
->where(function ($query) {
$query->whereNull('expires_at')
->orWhere('expires_at', '>', now());
})
->select(['type', 'config', 'effect', 'priority'])
->orderBy('priority')
->get();// Migration with optimized indexes
Schema::table('availability_rules', function (Blueprint $table) {
// Composite index for common queries
$table->index(['subject_type', 'subject_id', 'enabled', 'priority']);
// Index for rule type filtering
$table->index('type');
// Index for expiration checks
$table->index('expires_at');
});namespace App\Services;
use Illuminate\Support\Facades\Cache;
class CachedAvailabilityService
{
private $engine;
public function isAvailable($resource, $moment)
{
$cacheKey = $this->getCacheKey($resource, $moment);
return Cache::remember($cacheKey, 300, function () use ($resource, $moment) {
return $this->engine->isAvailable($resource, $moment);
});
}
private function getCacheKey($resource, $moment)
{
// Round to 5-minute intervals for better cache hits
$rounded = $moment->copy()
->minute(floor($moment->minute / 5) * 5)
->second(0);
return sprintf(
'availability:%s:%d:%s',
$resource->getMorphClass(),
$resource->id,
$rounded->timestamp
);
}
}class TaggedCacheService
{
public function cacheAvailability($resource, $moment, $result)
{
$tags = [
'availability',
"resource-{$resource->id}",
"type-{$resource->getMorphClass()}",
];
Cache::tags($tags)->put(
$this->getCacheKey($resource, $moment),
$result,
$this->getCacheTtl($moment)
);
}
public function invalidateResource($resource)
{
Cache::tags(["resource-{$resource->id}"])->flush();
}
private function getCacheTtl($moment)
{
$hoursAhead = now()->diffInHours($moment);
return match(true) {
$hoursAhead <= 1 => 60, // 1 minute
$hoursAhead <= 24 => 300, // 5 minutes
$hoursAhead <= 168 => 1800, // 30 minutes
default => 3600, // 1 hour
};
}
}namespace App\Jobs;
class PreWarmAvailabilityCache extends Job
{
public function handle()
{
$resources = Resource::active()->get();
$moments = $this->getCheckPoints();
foreach ($resources as $resource) {
foreach ($moments as $moment) {
dispatch(new CacheAvailability($resource, $moment))
->onQueue('low');
}
}
}
private function getCheckPoints()
{
$points = [];
$current = now()->startOfHour();
// Next 24 hours, every hour
for ($i = 0; $i < 24; $i++) {
$points[] = $current->copy()->addHours($i);
}
// Next 7 days, every 6 hours
for ($i = 1; $i <= 7; $i++) {
$points[] = $current->copy()->addDays($i)->setTime(0, 0);
$points[] = $current->copy()->addDays($i)->setTime(6, 0);
$points[] = $current->copy()->addDays($i)->setTime(12, 0);
$points[] = $current->copy()->addDays($i)->setTime(18, 0);
}
return $points;
}
}class BulkAvailabilityService
{
public function checkMultiple(array $resources, $moment)
{
$results = [];
$uncached = [];
// Check cache first
foreach ($resources as $resource) {
$cacheKey = $this->getCacheKey($resource, $moment);
$cached = Cache::get($cacheKey);
if ($cached !== null) {
$results[$resource->id] = $cached;
} else {
$uncached[] = $resource;
}
}
// Process uncached in batches
$chunks = array_chunk($uncached, 10);
foreach ($chunks as $chunk) {
$this->processChunk($chunk, $moment, $results);
}
return $results;
}
private function processChunk($resources, $moment, &$results)
{
// Load all rules at once
$resourceIds = array_map(fn($r) => $r->id, $resources);
$resourceClass = get_class($resources[0]);
$allRules = AvailabilityRule::where('subject_type', $resourceClass)
->whereIn('subject_id', $resourceIds)
->where('enabled', true)
->orderBy('priority')
->get()
->groupBy('subject_id');
foreach ($resources as $resource) {
$rules = $allRules[$resource->id] ?? collect();
$resource->setRelation('availabilityRules', $rules);
$result = $this->engine->isAvailable($resource, $moment);
$results[$resource->id] = $result;
// Cache the result
Cache::put(
$this->getCacheKey($resource, $moment),
$result,
300
);
}
}
}use Illuminate\Support\Facades\Parallel;
class ParallelAvailabilityService
{
public function checkAvailabilityParallel(array $resources, $moment)
{
$chunks = array_chunk($resources, 50);
$results = Parallel::map($chunks, function ($chunk) use ($moment) {
$engine = app(AvailabilityEngine::class);
$chunkResults = [];
foreach ($chunk as $resource) {
$chunkResults[$resource->id] = $engine->isAvailable($resource, $moment);
}
return $chunkResults;
});
return array_merge(...$results);
}
}class CompiledRuleService
{
private array $compiledRules = [];
public function compileRules($resource)
{
$key = $resource->getMorphClass() . ':' . $resource->id;
if (isset($this->compiledRules[$key])) {
return $this->compiledRules[$key];
}
$rules = $resource->availabilityRules()
->where('enabled', true)
->orderBy('priority')
->get(['type', 'config', 'effect', 'priority'])
->map(function ($rule) {
return [
'type' => $rule->type,
'config' => $rule->config,
'effect' => $rule->effect === Effect::Allow,
'priority' => $rule->priority,
];
})
->toArray();
$this->compiledRules[$key] = $rules;
return $rules;
}
}class LazyConfigEvaluator implements RuleEvaluator
{
public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
{
// Don't load large data unless needed
if (!$this->quickCheck($config, $moment)) {
return false;
}
// Now load the full data
$fullData = $this->loadFullData($config['data_key']);
return $this->detailedCheck($fullData, $moment, $subject);
}
private function quickCheck($config, $moment)
{
// Quick elimination checks
return $moment->hour >= 8 && $moment->hour <= 20;
}
}// For very large rule sets, partition by subject type
Schema::create('availability_rules_products', function (Blueprint $table) {
// Same structure as main table
});
Schema::create('availability_rules_rooms', function (Blueprint $table) {
// Same structure as main table
});
// Custom model to use partitioned tables
class PartitionedAvailabilityRule extends Model
{
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
if (isset($attributes['subject_type'])) {
$this->setTable($this->getTableForType($attributes['subject_type']));
}
}
private function getTableForType($type)
{
$map = [
'App\\Models\\Product' => 'availability_rules_products',
'App\\Models\\Room' => 'availability_rules_rooms',
];
return $map[$type] ?? 'availability_rules';
}
}class ReadOptimizedAvailability
{
public function loadRules($resource)
{
return $resource->availabilityRules()
->on('read-replica') // Use read replica
->where('enabled', true)
->orderBy('priority')
->get();
}
}class MonitoredAvailabilityEngine
{
private $engine;
private $metrics;
public function isAvailable($resource, $moment)
{
$start = microtime(true);
try {
$result = $this->engine->isAvailable($resource, $moment);
$this->recordMetrics($resource, microtime(true) - $start, 'success');
return $result;
} catch (\Exception $e) {
$this->recordMetrics($resource, microtime(true) - $start, 'error');
throw $e;
}
}
private function recordMetrics($resource, $duration, $status)
{
$this->metrics->record([
'type' => 'availability_check',
'resource_type' => $resource->getMorphClass(),
'duration' => $duration,
'status' => $status,
'rule_count' => $resource->availabilityRules()->count(),
]);
// Alert if slow
if ($duration > 0.5) {
Log::warning('Slow availability check', [
'resource' => $resource->id,
'duration' => $duration,
]);
}
}
}class DebugAvailabilityService
{
public function debugCheck($resource, $moment)
{
DB::enableQueryLog();
$result = $this->engine->isAvailable($resource, $moment);
$queries = DB::getQueryLog();
Log::debug('Availability check queries', [
'count' => count($queries),
'total_time' => array_sum(array_column($queries, 'time')),
'queries' => $queries,
]);
return $result;
}
}- ✅ Cache frequently checked availability
- ✅ Use eager loading for multiple resources
- ✅ Index database columns used in queries
- ✅ Round timestamps for better cache hits
- ✅ Batch process bulk checks
- ✅ Use read replicas for high traffic
- ✅ Monitor performance metrics
- ❌ Load all rules when only checking one type
- ❌ Cache results for too long (data becomes stale)
- ❌ Make synchronous API calls in evaluators
- ❌ Store large configs in rule table
- ❌ Process rules one by one in loops
- ❌ Ignore slow query warnings
- Testing - Performance testing strategies
- Complex Scenarios - Real-world performance examples
- Configuration - Performance configuration options
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