Basic Usage
Master the fundamentals of Laravel Specifications. This tutorial covers creating specifications, using them for validation and queries, and understanding the core concepts that make specifications powerful.
Your First Specification
Let's start with a simple example that demonstrates the power of specifications:
The Problem: Messy Conditional Logic
Imagine you have this scattered throughout your application:
// In controller
if ($user->status === 'active' && $user->email_verified_at !== null) {
// Allow access
}
// In service
if ($user->status === 'active' && $user->email_verified_at !== null) {
// Send notification
}
// In job
if ($user->status === 'active' && $user->email_verified_at !== null) {
// Process user
}
The Solution: A Simple Specification
<?php
namespace App\Specifications\User;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
class ActiveVerifiedUserSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->status === 'active'
&& $candidate->email_verified_at !== null;
}
public function toQuery($query)
{
return $query->where('status', 'active')
->whereNotNull('email_verified_at');
}
}
Now your code becomes:
$spec = new ActiveVerifiedUserSpecification();
// In controller
if ($spec->isSatisfiedBy($user)) {
// Allow access
}
// In service
if ($spec->isSatisfiedBy($user)) {
// Send notification
}
// In query
$activeUsers = User::whereSpecification($spec)->get();
Core Concepts
1. The Specification Interface
Every specification implements these key methods:
interface SpecificationInterface
{
// Check if a candidate satisfies the specification
public function isSatisfiedBy(mixed $candidate): bool;
// Convert to database query
public function toQuery($query);
// Combine with other specifications
public function and(SpecificationInterface $spec): SpecificationInterface;
public function or(SpecificationInterface $spec): SpecificationInterface;
public function not(): SpecificationInterface;
}
2. Two Modes of Operation
Specifications work in two distinct modes:
Memory Mode: Object Validation
$user = User::find(1);
$spec = new ActiveVerifiedUserSpecification();
if ($spec->isSatisfiedBy($user)) {
// User object satisfies the specification
}
Query Mode: Database Filtering
$activeUsers = User::whereSpecification(
new ActiveVerifiedUserSpecification()
)->get();
// Generates: SELECT * FROM users WHERE status = 'active' AND email_verified_at IS NOT NULL
Creating Specifications
Method 1: Using Artisan Generator
The fastest way to create specifications:
# Basic specification
php artisan make:specification User/ActiveUserSpecification
# With model binding
php artisan make:specification User/ActiveUserSpecification --model=User
# With caching support
php artisan make:specification User/ActiveUserSpecification --cacheable
Method 2: Manual Creation
Create the file manually:
<?php
namespace App\Specifications\User;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;
class PremiumUserSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->subscription_type === 'premium'
&& $candidate->subscription_expires_at > now();
}
public function toQuery($query): Builder
{
return $query->where('subscription_type', 'premium')
->where('subscription_expires_at', '>', now());
}
}
Practical Examples
Example 1: E-commerce Product Filtering
class InStockProductSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->stock_quantity > 0
&& $candidate->status === 'published';
}
public function toQuery($query): Builder
{
return $query->where('stock_quantity', '>', 0)
->where('status', 'published');
}
}
// Usage
$inStockProducts = Product::whereSpecification(
new InStockProductSpecification()
)->get();
// Or with a single product
$product = Product::find(1);
if ((new InStockProductSpecification())->isSatisfiedBy($product)) {
// Product is available
}
Example 2: User Permissions
class CanAccessAdminSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->role === 'admin'
&& $candidate->status === 'active'
&& $candidate->two_factor_enabled;
}
public function toQuery($query): Builder
{
return $query->where('role', 'admin')
->where('status', 'active')
->where('two_factor_enabled', true);
}
}
// Usage in controller
public function dashboard(Request $request)
{
$canAccess = new CanAccessAdminSpecification();
if (!$canAccess->isSatisfiedBy($request->user())) {
abort(403);
}
// Show admin dashboard
}
Example 3: Parameterized Specifications
Sometimes you need specifications that accept parameters:
class UserRegisteredAfterSpecification extends AbstractSpecification
{
public function __construct(
private readonly Carbon $date
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->created_at > $this->date;
}
public function toQuery($query): Builder
{
return $query->where('created_at', '>', $this->date);
}
}
// Usage
$newUsers = User::whereSpecification(
new UserRegisteredAfterSpecification(now()->subMonth())
)->get();
Working with Collections
Specifications work seamlessly with Laravel collections:
$users = User::all();
$spec = new ActiveVerifiedUserSpecification();
// Filter collection
$activeUsers = $users->filter(
fn($user) => $spec->isSatisfiedBy($user)
);
// Check if any user satisfies spec
$hasActiveUsers = $users->contains(
fn($user) => $spec->isSatisfiedBy($user)
);
// Get first user matching spec
$firstActive = $users->first(
fn($user) => $spec->isSatisfiedBy($user)
);
Error Handling
Invalid Candidates
Always validate your input:
class UserSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$candidate instanceof User) {
throw new InvalidArgumentException(
'Candidate must be a User instance'
);
}
return $candidate->status === 'active';
}
}
Query Errors
Handle database-specific errors:
class SafeUserSpecification extends AbstractSpecification
{
public function toQuery($query): Builder
{
try {
return $query->where('status', 'active')
->whereNotNull('email_verified_at');
} catch (QueryException $e) {
// Log error and return safe fallback
Log::error('Specification query failed', ['error' => $e]);
return $query->whereRaw('1 = 0'); // Return empty result
}
}
}
Testing Your Specifications
Basic Tests
use Tests\TestCase;
use App\Models\User;
use App\Specifications\User\ActiveVerifiedUserSpecification;
class ActiveVerifiedUserSpecificationTest extends TestCase
{
public function test_satisfied_by_active_verified_user()
{
$user = User::factory()->create([
'status' => 'active',
'email_verified_at' => now(),
]);
$spec = new ActiveVerifiedUserSpecification();
$this->assertTrue($spec->isSatisfiedBy($user));
}
public function test_not_satisfied_by_inactive_user()
{
$user = User::factory()->create([
'status' => 'inactive',
'email_verified_at' => now(),
]);
$spec = new ActiveVerifiedUserSpecification();
$this->assertFalse($spec->isSatisfiedBy($user));
}
public function test_generates_correct_query()
{
$spec = new ActiveVerifiedUserSpecification();
$query = User::whereSpecification($spec);
$expectedSql = "select * from \"users\" where \"status\" = ? and \"email_verified_at\" is not null";
$this->assertEquals($expectedSql, $query->toSql());
$this->assertEquals(['active'], $query->getBindings());
}
}
Common Patterns
1. Status-Based Specifications
class PublishedContentSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->status === 'published'
&& $candidate->published_at <= now();
}
}
2. Date-Based Specifications
class RecentActivitySpecification extends AbstractSpecification
{
public function __construct(
private readonly int $days = 30
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->last_activity_at >= now()->subDays($this->days);
}
}
3. Relationship-Based Specifications
class UserWithOrdersSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->orders()->exists();
}
public function toQuery($query): Builder
{
return $query->whereHas('orders');
}
}
Performance Considerations
1. N+1 Query Prevention
class UserWithOrdersSpecification extends AbstractSpecification
{
public function toQuery($query): Builder
{
// Use whereHas instead of loading relationships
return $query->whereHas('orders');
}
public function isSatisfiedBy(mixed $candidate): bool
{
// Only check if relationship is loaded
if ($candidate->relationLoaded('orders')) {
return $candidate->orders->isNotEmpty();
}
// Fallback to database check
return $candidate->orders()->exists();
}
}
2. Index Optimization
Ensure your query methods use indexed columns:
class OptimizedUserSpecification extends AbstractSpecification
{
public function toQuery($query): Builder
{
// Ensure 'status' and 'created_at' are indexed
return $query->where('status', 'active')
->where('created_at', '>', now()->subYear());
}
}
Next Steps
You now understand the fundamentals of Laravel Specifications! Next, learn how to combine them into powerful compositions:
- Composition Techniques → - Combine specifications with AND, OR, NOT
- Query Integration → - Advanced database query patterns
- Testing Strategies → - Comprehensive testing approaches
Key Takeaways
- Specifications encapsulate business rules in testable objects
- They work both for object validation and database queries
- Always implement both
isSatisfiedBy()
andtoQuery()
methods - Use the Artisan generator to create specifications quickly