Skip to content

Laravel Integration

Seamlessly integrate specifications throughout your Laravel application. Learn how to use specifications with middleware, jobs, events, policies, and other Laravel features for a cohesive, specification-driven architecture.

Framework Integration Points

Specifications can be integrated at every layer of your Laravel application:

  • HTTP Layer: Middleware, controllers, form requests
  • Service Layer: Business logic, domain services
  • Data Layer: Eloquent models, repositories
  • Background Layer: Jobs, queues, scheduling
  • Event Layer: Listeners, observers, broadcasting
  • Authorization Layer: Policies, gates, middleware

Controller Integration

Clean Controller Actions

Transform messy controller logic into clean, specification-driven code:

php
class ProductController extends Controller
{
    public function __construct(
        private readonly ProductSpecificationFactory $specFactory
    ) {}
    
    public function index(ProductFilterRequest $request)
    {
        $specification = $this->specFactory->fromRequest($request);
        
        return ProductResource::collection(
            Product::whereSpecification($specification)
                ->with(['category', 'brand'])
                ->paginate($request->get('per_page', 15))
        );
    }
    
    public function show(Product $product, ViewProductSpecification $viewSpec)
    {
        // Check if user can view this product
        if (!$viewSpec->isSatisfiedBy(['user' => auth()->user(), 'product' => $product])) {
            abort(403, 'You cannot view this product');
        }
        
        return new ProductResource($product->load(['reviews', 'variants']));
    }
    
    public function update(
        Product $product, 
        UpdateProductRequest $request,
        CanEditProductSpecification $editSpec
    ) {
        if (!$editSpec->isSatisfiedBy(['user' => auth()->user(), 'product' => $product])) {
            abort(403, 'You cannot edit this product');
        }
        
        $product->update($request->validated());
        
        return new ProductResource($product);
    }
}

Specification-Based Form Requests

Use specifications in form request validation:

php
class ProductFilterRequest extends FormRequest
{
    public function authorize(): bool
    {
        $canViewProducts = new CanViewProductsSpecification();
        return $canViewProducts->isSatisfiedBy(auth()->user());
    }
    
    public function rules(): array
    {
        return [
            'category' => 'array',
            'category.*' => 'integer|exists:categories,id',
            'price_min' => 'numeric|min:0',
            'price_max' => 'numeric|min:0|gte:price_min',
            'in_stock' => 'boolean',
            'featured' => 'boolean',
            'brand' => 'array',
            'brand.*' => 'integer|exists:brands,id',
        ];
    }
    
    public function toSpecification(): SpecificationInterface
    {
        $spec = new AlwaysTrueSpecification();
        
        if ($this->filled('category')) {
            $spec = $spec->and(new ProductCategorySpecification($this->input('category')));
        }
        
        if ($this->filled('price_min') || $this->filled('price_max')) {
            $spec = $spec->and(new ProductPriceRangeSpecification(
                $this->input('price_min'),
                $this->input('price_max')
            ));
        }
        
        if ($this->boolean('in_stock')) {
            $spec = $spec->and(new InStockProductSpecification());
        }
        
        if ($this->boolean('featured')) {
            $spec = $spec->and(new FeaturedProductSpecification());
        }
        
        if ($this->filled('brand')) {
            $spec = $spec->and(new ProductBrandSpecification($this->input('brand')));
        }
        
        return $spec;
    }
}

Middleware Integration

Specification-Based Middleware

Create middleware that uses specifications for access control:

php
class SpecificationMiddleware
{
    public function __construct(
        private readonly SpecificationInterface $specification
    ) {}
    
    public function handle(Request $request, Closure $next): Response
    {
        $context = [
            'user' => $request->user(),
            'request' => $request,
            'route' => $request->route(),
        ];
        
        if (!$this->specification->isSatisfiedBy($context)) {
            abort(403, 'Access denied by specification');
        }
        
        return $next($request);
    }
}

// Usage in routes
Route::middleware(new SpecificationMiddleware(new AdminUserSpecification()))
    ->group(function () {
        Route::get('/admin/dashboard', [AdminController::class, 'dashboard']);
        Route::resource('/admin/users', AdminUserController::class);
    });

// Or with dependency injection
class RequiresPremiumMiddleware
{
    public function __construct(
        private readonly PremiumUserSpecification $premiumSpec
    ) {}
    
    public function handle(Request $request, Closure $next): Response
    {
        if (!$this->premiumSpec->isSatisfiedBy($request->user())) {
            return redirect()->route('upgrade')
                ->with('message', 'Premium subscription required');
        }
        
        return $next($request);
    }
}

Dynamic Middleware Selection

Choose middleware based on specifications:

php
class ConditionalMiddleware
{
    private array $middlewareSpecs = [
        'premium' => PremiumUserSpecification::class,
        'admin' => AdminUserSpecification::class,
        'verified' => VerifiedUserSpecification::class,
    ];
    
    public function handle(Request $request, Closure $next, string ...$requirements): Response
    {
        $user = $request->user();
        
        foreach ($requirements as $requirement) {
            if (!isset($this->middlewareSpecs[$requirement])) {
                abort(500, "Unknown middleware specification: {$requirement}");
            }
            
            $specClass = $this->middlewareSpecs[$requirement];
            $specification = app($specClass);
            
            if (!$specification->isSatisfiedBy($user)) {
                abort(403, "Requirement not met: {$requirement}");
            }
        }
        
        return $next($request);
    }
}

// Usage in routes
Route::middleware(['conditional:premium,verified'])
    ->get('/premium-feature', [PremiumController::class, 'feature']);

Job Integration

Specification-Filtered Jobs

Filter job processing based on specifications:

php
class ProcessEligibleUsersJob implements ShouldQueue
{
    public function __construct(
        private readonly SpecificationInterface $eligibilitySpec,
        private readonly ?int $batchSize = 1000
    ) {}
    
    public function handle(): void
    {
        User::whereSpecification($this->eligibilitySpec)
            ->chunk($this->batchSize, function ($users) {
                foreach ($users as $user) {
                    // Double-check eligibility in case data changed
                    if ($this->eligibilitySpec->isSatisfiedBy($user)) {
                        $this->processUser($user);
                    }
                }
            });
    }
    
    private function processUser(User $user): void
    {
        // Process individual user
        Log::info('Processing eligible user', ['user_id' => $user->id]);
    }
}

// Dispatch with specific specifications
ProcessEligibleUsersJob::dispatch(
    (new ActiveUserSpecification())
        ->and(new VerifiedUserSpecification())
        ->and(new MinimumAgeSpecification(18))
);

Conditional Job Execution

Execute jobs only when specifications are met:

php
class ConditionalNewsletterJob implements ShouldQueue
{
    public function __construct(
        private readonly Newsletter $newsletter,
        private readonly SpecificationInterface $audienceSpec
    ) {}
    
    public function handle(MailManager $mail): void
    {
        $recipients = User::whereSpecification($this->audienceSpec)->get();
        
        if ($recipients->isEmpty()) {
            Log::info('No recipients found for newsletter', [
                'newsletter_id' => $this->newsletter->id,
                'specification' => get_class($this->audienceSpec)
            ]);
            return;
        }
        
        foreach ($recipients as $user) {
            // Final check before sending
            if ($this->audienceSpec->isSatisfiedBy($user)) {
                $mail->to($user->email)->send(new NewsletterMail($this->newsletter));
            }
        }
    }
}

Event Integration

Specification-Based Event Listeners

Create event listeners that respond to specific conditions:

php
class UserEventListener
{
    public function __construct(
        private readonly PremiumEligibilitySpecification $premiumSpec,
        private readonly VipEligibilitySpecification $vipSpec
    ) {}
    
    public function handleUserUpdated(UserUpdated $event): void
    {
        $user = $event->user;
        
        // Check for premium eligibility
        if ($this->premiumSpec->isSatisfiedBy($user)) {
            PremiumUpgradeOfferJob::dispatch($user);
        }
        
        // Check for VIP status
        if ($this->vipSpec->isSatisfiedBy($user)) {
            VipWelcomeJob::dispatch($user);
        }
    }
    
    public function handleOrderPlaced(OrderPlaced $event): void
    {
        $order = $event->order;
        $user = $order->user;
        
        // Trigger loyalty program checks
        $loyaltySpecs = [
            new FrequentBuyerSpecification(),
            new HighValueCustomerSpecification(),
            new LongTermCustomerSpecification(),
        ];
        
        foreach ($loyaltySpecs as $spec) {
            if ($spec->isSatisfiedBy($user)) {
                LoyaltyRewardJob::dispatch($user, $spec::class);
            }
        }
    }
}

// Register in EventServiceProvider
class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        UserUpdated::class => [
            UserEventListener::class . '@handleUserUpdated',
        ],
        OrderPlaced::class => [
            UserEventListener::class . '@handleOrderPlaced',
        ],
    ];
}

Dynamic Event Subscriptions

Subscribe to events based on specifications:

php
class SpecificationEventSubscriber
{
    private array $eventSpecifications = [];
    
    public function subscribe(Dispatcher $events): void
    {
        // Register specification-based event handlers
        $events->listen(UserUpdated::class, [$this, 'handleUserEvent']);
        $events->listen(OrderPlaced::class, [$this, 'handleOrderEvent']);
        $events->listen(ProductUpdated::class, [$this, 'handleProductEvent']);
    }
    
    public function handleUserEvent(UserUpdated $event): void
    {
        $this->processEventWithSpecifications($event, [
            'premium_eligible' => new PremiumEligibleSpecification(),
            'trial_expired' => new TrialExpiredSpecification(),
            'inactive_user' => new InactiveUserSpecification(),
        ]);
    }
    
    private function processEventWithSpecifications($event, array $specifications): void
    {
        foreach ($specifications as $name => $specification) {
            if ($specification->isSatisfiedBy($event->getSubject())) {
                $this->triggerSpecificationAction($event, $name, $specification);
            }
        }
    }
    
    private function triggerSpecificationAction($event, string $specName, SpecificationInterface $spec): void
    {
        Log::info('Specification triggered', [
            'event' => get_class($event),
            'specification' => $specName,
            'subject_id' => $event->getSubject()->id ?? null,
        ]);
        
        // Dispatch appropriate jobs or notifications
        match($specName) {
            'premium_eligible' => PremiumUpgradeOfferJob::dispatch($event->getSubject()),
            'trial_expired' => TrialExpirationNotificationJob::dispatch($event->getSubject()),
            'inactive_user' => WinBackCampaignJob::dispatch($event->getSubject()),
        };
    }
}

Policy Integration

Specification-Based Policies

Integrate specifications with Laravel's authorization system:

php
class ProductPolicy
{
    public function __construct(
        private readonly CanViewProductSpecification $viewSpec,
        private readonly CanEditProductSpecification $editSpec,
        private readonly CanDeleteProductSpecification $deleteSpec
    ) {}
    
    public function view(User $user, Product $product): bool
    {
        return $this->viewSpec->isSatisfiedBy([
            'user' => $user,
            'product' => $product
        ]);
    }
    
    public function update(User $user, Product $product): bool
    {
        return $this->editSpec->isSatisfiedBy([
            'user' => $user,
            'product' => $product
        ]);
    }
    
    public function delete(User $user, Product $product): bool
    {
        return $this->deleteSpec->isSatisfiedBy([
            'user' => $user,
            'product' => $product
        ]);
    }
    
    public function create(User $user): bool
    {
        $createSpec = new CanCreateProductSpecification();
        return $createSpec->isSatisfiedBy($user);
    }
}

// Advanced policy with dynamic specifications
class DynamicResourcePolicy
{
    private array $specifications = [];
    
    public function addSpecification(string $action, SpecificationInterface $spec): void
    {
        $this->specifications[$action] = $spec;
    }
    
    public function can(User $user, string $action, $resource = null): bool
    {
        if (!isset($this->specifications[$action])) {
            return false;
        }
        
        $context = [
            'user' => $user,
            'resource' => $resource,
            'action' => $action,
        ];
        
        return $this->specifications[$action]->isSatisfiedBy($context);
    }
}

Model Integration

Eloquent Model Scopes

Add specification-based scopes to your models:

php
class User extends Model
{
    public function scopeWhereSpecification(Builder $query, SpecificationInterface $spec): Builder
    {
        return $spec->toQuery($query);
    }
    
    public function scopeActive(Builder $query): Builder
    {
        return $query->whereSpecification(new ActiveUserSpecification());
    }
    
    public function scopeEligibleForDiscount(Builder $query): Builder
    {
        return $query->whereSpecification(
            (new ActiveUserSpecification())
                ->and(new VerifiedUserSpecification())
                ->and(new MinimumPurchaseSpecification(100))
        );
    }
    
    public function scopeBySpecification(Builder $query, string $specificationName, ...$args): Builder
    {
        $specClass = "App\\Specifications\\User\\{$specificationName}Specification";
        
        if (!class_exists($specClass)) {
            throw new InvalidArgumentException("Specification not found: {$specClass}");
        }
        
        $specification = new $specClass(...$args);
        return $query->whereSpecification($specification);
    }
}

// Usage
$activeUsers = User::active()->get();
$eligibleUsers = User::eligibleForDiscount()->get();
$premiumUsers = User::bySpecification('Premium')->get();
$recentUsers = User::bySpecification('RegisteredAfter', now()->subMonth())->get();

Model Observers with Specifications

Use specifications in model observers:

php
class UserObserver
{
    public function __construct(
        private readonly PremiumEligibilitySpecification $premiumSpec,
        private readonly SuspiciousActivitySpecification $suspiciousSpec
    ) {}
    
    public function updated(User $user): void
    {
        // Check for premium eligibility when user is updated
        if ($this->premiumSpec->isSatisfiedBy($user) && !$user->isPremium()) {
            event(new UserBecameEligibleForPremium($user));
        }
        
        // Check for suspicious activity
        if ($this->suspiciousSpec->isSatisfiedBy($user)) {
            event(new SuspiciousUserActivityDetected($user));
        }
    }
    
    public function creating(User $user): void
    {
        $validationSpec = new ValidNewUserSpecification();
        
        if (!$validationSpec->isSatisfiedBy($user)) {
            throw new ModelCreationException('User does not meet creation requirements');
        }
    }
}

Service Container Integration

Automatic Specification Resolution

Configure the service container to automatically resolve specifications:

php
// In AppServiceProvider
public function register(): void
{
    // Bind specifications as singletons
    $this->app->singleton(ActiveUserSpecification::class);
    $this->app->singleton(PremiumUserSpecification::class);
    
    // Or use a factory for complex specifications
    $this->app->bind(UserEligibilitySpecification::class, function ($app) {
        return new UserEligibilitySpecification(
            $app->make(ActiveUserSpecification::class),
            $app->make(VerifiedUserSpecification::class)
        );
    });
}

// Automatic resolution in controllers
class UserController extends Controller
{
    public function __construct(
        private readonly ActiveUserSpecification $activeSpec,
        private readonly PremiumUserSpecification $premiumSpec
    ) {}
    
    public function index(Request $request)
    {
        $spec = $request->boolean('premium_only') 
            ? $this->premiumSpec 
            : $this->activeSpec;
            
        return User::whereSpecification($spec)->paginate();
    }
}

Specification Factory Service

Create a centralized specification factory:

php
class SpecificationFactory
{
    public function __construct(
        private readonly Application $app
    ) {}
    
    public function make(string $specification, ...$args): SpecificationInterface
    {
        $class = $this->resolveSpecificationClass($specification);
        return $this->app->make($class, $args);
    }
    
    public function makeFromRequest(Request $request, array $mapping): SpecificationInterface
    {
        $spec = new AlwaysTrueSpecification();
        
        foreach ($mapping as $param => $specClass) {
            if ($request->has($param)) {
                $value = $request->input($param);
                $specification = $this->app->make($specClass, compact('value'));
                $spec = $spec->and($specification);
            }
        }
        
        return $spec;
    }
    
    private function resolveSpecificationClass(string $name): string
    {
        // Support multiple naming conventions
        $possibilities = [
            "App\\Specifications\\{$name}",
            "App\\Specifications\\{$name}Specification",
            "App\\Domain\\Specifications\\{$name}Specification",
        ];
        
        foreach ($possibilities as $class) {
            if (class_exists($class)) {
                return $class;
            }
        }
        
        throw new InvalidArgumentException("Specification not found: {$name}");
    }
}

// Usage
$factory = app(SpecificationFactory::class);
$activeSpec = $factory->make('ActiveUser');
$premiumSpec = $factory->make('PremiumUser');

Artisan Commands

Specification-Based Commands

Create Artisan commands that use specifications:

php
class ProcessUsersCommand extends Command
{
    protected $signature = 'users:process 
                           {--specification= : Specification class name}
                           {--batch-size=1000 : Batch size for processing}
                           {--dry-run : Only show what would be processed}';
    
    protected $description = 'Process users matching a specification';
    
    public function __construct(
        private readonly SpecificationFactory $specFactory
    ) {
        parent::__construct();
    }
    
    public function handle(): int
    {
        $specName = $this->option('specification') ?? 'ActiveUser';
        $batchSize = (int) $this->option('batch-size');
        $dryRun = $this->option('dry-run');
        
        try {
            $specification = $this->specFactory->make($specName);
        } catch (InvalidArgumentException $e) {
            $this->error("Invalid specification: {$specName}");
            return Command::FAILURE;
        }
        
        $query = User::whereSpecification($specification);
        $totalUsers = $query->count();
        
        $this->info("Found {$totalUsers} users matching specification: {$specName}");
        
        if ($dryRun) {
            $this->warn('Dry run mode - no users will be processed');
            return Command::SUCCESS;
        }
        
        if (!$this->confirm('Do you want to continue?')) {
            $this->info('Operation cancelled');
            return Command::SUCCESS;
        }
        
        $processed = 0;
        $bar = $this->output->createProgressBar($totalUsers);
        
        $query->chunk($batchSize, function ($users) use (&$processed, $bar, $specification) {
            foreach ($users as $user) {
                // Double-check specification in case data changed
                if ($specification->isSatisfiedBy($user)) {
                    $this->processUser($user);
                    $processed++;
                }
                $bar->advance();
            }
        });
        
        $bar->finish();
        $this->newLine();
        $this->info("Successfully processed {$processed} users");
        
        return Command::SUCCESS;
    }
    
    private function processUser(User $user): void
    {
        // Your processing logic here
        Log::info('Processing user via command', ['user_id' => $user->id]);
    }
}

Testing Integration

Laravel-Specific Tests

Test specifications within Laravel's testing framework:

php
use Illuminate\Foundation\Testing\RefreshDatabase;

class LaravelSpecificationIntegrationTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_specification_works_with_middleware()
    {
        $user = User::factory()->create(['role' => 'admin']);
        
        Route::middleware(new SpecificationMiddleware(new AdminUserSpecification()))
            ->get('/test-route', fn() => response('success'));
        
        $response = $this->actingAs($user)->get('/test-route');
        
        $response->assertStatus(200);
        $response->assertSeeText('success');
    }
    
    public function test_specification_works_with_policies()
    {
        $user = User::factory()->create();
        $product = Product::factory()->create(['user_id' => $user->id]);
        
        $this->assertTrue($user->can('view', $product));
        $this->assertTrue($user->can('update', $product));
    }
    
    public function test_specification_works_with_jobs()
    {
        Queue::fake();
        
        $users = User::factory()->count(5)->create(['status' => 'active']);
        User::factory()->count(3)->create(['status' => 'inactive']);
        
        ProcessEligibleUsersJob::dispatch(new ActiveUserSpecification());
        
        Queue::assertPushed(ProcessEligibleUsersJob::class, 1);
    }
    
    public function test_specification_works_with_events()
    {
        Event::fake();
        
        $user = User::factory()->create([
            'total_purchases' => 1000,
            'registration_date' => now()->subYear(),
        ]);
        
        $user->update(['total_purchases' => 10000]);
        
        Event::assertDispatched(UserBecameEligibleForPremium::class);
    }
}

Configuration

Specification Configuration

Create configuration for your specifications:

php
// config/specifications.php
return [
    'cache' => [
        'default_ttl' => 3600,
        'enabled' => env('SPECIFICATIONS_CACHE_ENABLED', true),
    ],
    
    'performance' => [
        'profiling_enabled' => env('SPECIFICATIONS_PROFILING_ENABLED', false),
        'slow_threshold' => 0.1, // 100ms
    ],
    
    'user_specifications' => [
        'premium_minimum_spent' => 1000,
        'loyalty_minimum_orders' => 10,
        'vip_minimum_total' => 50000,
    ],
    
    'auto_register' => [
        // Automatically register these specifications in the container
        'specifications' => [
            'ActiveUser' => App\Specifications\User\ActiveUserSpecification::class,
            'PremiumUser' => App\Specifications\User\PremiumUserSpecification::class,
        ],
    ],
];

// Use configuration in specifications
class PremiumUserSpecification extends AbstractSpecification
{
    private readonly float $minimumSpent;
    
    public function __construct()
    {
        $this->minimumSpent = config('specifications.user_specifications.premium_minimum_spent');
    }
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->total_spent >= $this->minimumSpent;
    }
}

Best Practices for Laravel Integration

1. Service Layer Integration

Keep specifications in your service layer:

php
class UserService
{
    public function __construct(
        private readonly ActiveUserSpecification $activeSpec,
        private readonly PremiumUserSpecification $premiumSpec
    ) {}
    
    public function getEligibleUsers(string $type = 'active'): Collection
    {
        $spec = match($type) {
            'active' => $this->activeSpec,
            'premium' => $this->premiumSpec,
            'both' => $this->activeSpec->and($this->premiumSpec),
            default => throw new InvalidArgumentException("Unknown type: {$type}")
        };
        
        return User::whereSpecification($spec)->get();
    }
}

2. Resource Layer Integration

Use specifications in API resources:

php
class UserResource extends JsonResource
{
    public function toArray($request): array
    {
        $premiumSpec = new PremiumUserSpecification();
        $activeSpec = new ActiveUserSpecification();
        
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'is_premium' => $premiumSpec->isSatisfiedBy($this->resource),
            'is_active' => $activeSpec->isSatisfiedBy($this->resource),
            'subscription_eligible' => $this->when(
                $activeSpec->and($premiumSpec->not())->isSatisfiedBy($this->resource),
                true
            ),
        ];
    }
}

3. Notification Integration

Filter notifications with specifications:

php
class PremiumUpgradeNotification extends Notification
{
    public function via($notifiable): array
    {
        $emailSpec = new HasVerifiedEmailSpecification();
        $smsSpec = new HasPhoneNumberSpecification();
        
        $channels = [];
        
        if ($emailSpec->isSatisfiedBy($notifiable)) {
            $channels[] = 'mail';
        }
        
        if ($smsSpec->isSatisfiedBy($notifiable)) {
            $channels[] = 'sms';
        }
        
        return $channels;
    }
}

Next Steps

Complete your specification mastery with professional best practices:


Integration Excellence

The power of specifications shines when they're integrated throughout your entire Laravel application. Think of them as the connective tissue that brings consistency and clarity to your business logic across all layers.

Next: Best Practices →

Released under the MIT License.