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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
// 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:
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:
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:
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:
// 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:
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:
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:
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:
- Best Practices → - Professional development approaches
- Deep Dive Section - Theoretical foundations
- Examples Section - Real-world case studies
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.