Skip to content

OrSpecification

A composite specification that combines two or more specifications with OR logic. Only one specification needs to be satisfied for the overall specification to be satisfied.

Namespace

php
DangerWayne\Specification\Specifications\Composite\OrSpecification

Constructor

php
public function __construct(
    SpecificationInterface $left,
    SpecificationInterface $right
)

Parameters

  • $left (SpecificationInterface) - The first specification to evaluate
  • $right (SpecificationInterface) - The second specification to evaluate

Usage Examples

Basic OR Logic

php
use DangerWayne\Specification\Specifications\Common\WhereSpecification;
use DangerWayne\Specification\Specifications\Composite\OrSpecification;

// Users who are either admin or moderator
$adminSpec = new WhereSpecification('role', 'admin');
$moderatorSpec = new WhereSpecification('role', 'moderator');
$staffSpec = new OrSpecification($adminSpec, $moderatorSpec);

// Apply to query
$staffUsers = User::whereSpecification($staffSpec)->get();

// Use with collection
$users = User::all();
$staffMembers = $users->filter(function($user) use ($staffSpec) {
    return $staffSpec->isSatisfiedBy($user);
});

Instead of using OrSpecification directly, use the fluent or() method:

php
$adminSpec = new WhereSpecification('role', 'admin');
$moderatorSpec = new WhereSpecification('role', 'moderator');

// More readable than new OrSpecification($adminSpec, $moderatorSpec)
$staffSpec = $adminSpec->or($moderatorSpec);

$staffUsers = User::whereSpecification($staffSpec)->get();

Common Scenarios

Role-Based Access

php
// Users with any administrative role
$adminSpec = new WhereSpecification('role', 'admin');
$superAdminSpec = new WhereSpecification('role', 'super_admin');
$managerSpec = new WhereSpecification('role', 'manager');

$administratorSpec = $adminSpec
    ->or($superAdminSpec)
    ->or($managerSpec);

$administrators = User::whereSpecification($administratorSpec)->get();

Status Filtering

php
// Orders that need attention (pending or processing)
$pendingSpec = new WhereSpecification('status', 'pending');
$processingSpec = new WhereSpecification('status', 'processing');
$attentionNeededSpec = $pendingSpec->or($processingSpec);

$ordersNeedingAttention = Order::whereSpecification($attentionNeededSpec)->get();

Contact Methods

php
// Users with any contact method
$hasEmailSpec = new WhereNullSpecification('email', false);
$hasPhoneSpec = new WhereNullSpecification('phone', false);
$contactableSpec = $hasEmailSpec->or($hasPhoneSpec);

$contactableUsers = User::whereSpecification($contactableSpec)->get();

Complex Combinations

Multiple OR Chains

php
// Products in multiple categories or price ranges
$electronicsSpec = new WhereSpecification('category', 'electronics');
$clothingSpec = new WhereSpecification('category', 'clothing');
$booksSpec = new WhereSpecification('category', 'books');
$cheapSpec = new WhereSpecification('price', '<', 25);
$premiumSpec = new WhereSpecification('price', '>', 500);

// Category OR chain
$categorySpec = $electronicsSpec->or($clothingSpec)->or($booksSpec);

// Price OR chain
$priceSpec = $cheapSpec->or($premiumSpec);

// Combined: (Categories) OR (Price ranges)
$productFilterSpec = $categorySpec->or($priceSpec);

$filteredProducts = Product::whereSpecification($productFilterSpec)->get();

Mixed with AND Logic

php
// (Admin OR Moderator) AND (Active AND Verified)
$adminSpec = new WhereSpecification('role', 'admin');
$moderatorSpec = new WhereSpecification('role', 'moderator');
$activeSpec = new WhereSpecification('status', 'active');
$verifiedSpec = new WhereNullSpecification('email_verified_at', false);

$roleSpec = $adminSpec->or($moderatorSpec); // (Admin OR Moderator)
$statusSpec = $activeSpec->and($verifiedSpec); // (Active AND Verified)
$eligibleStaffSpec = $roleSpec->and($statusSpec); // Combine with AND

$eligibleStaff = User::whereSpecification($eligibleStaffSpec)->get();

Real-World Examples

E-commerce Product Discovery

php
// Show products that are either on sale, highly rated, or recently added
$onSaleSpec = new WhereNullSpecification('sale_price', false);
$highlyRatedSpec = new WhereSpecification('average_rating', '>=', 4.5);
$recentSpec = new WhereSpecification('created_at', '>=', now()->subDays(7));
$featuredSpec = new WhereSpecification('is_featured', true);

$discoverableSpec = $onSaleSpec
    ->or($highlyRatedSpec)
    ->or($recentSpec)
    ->or($featuredSpec);

$discoverableProducts = Product::whereSpecification($discoverableSpec)
    ->with(['reviews', 'images'])
    ->orderBy('created_at', 'desc')
    ->get();

User Engagement Targeting

php
// Target users who are either new, active, or high-value
$newUserSpec = new WhereSpecification('created_at', '>=', now()->subDays(30));
$activeSpec = new WhereSpecification('last_login_at', '>=', now()->subDays(7));
$highSpenderSpec = new WhereSpecification('total_spent', '>=', 1000);
$vipSpec = new WhereSpecification('membership_tier', 'vip');

$targetAudienceSpec = $newUserSpec
    ->or($activeSpec)
    ->or($highSpenderSpec)
    ->or($vipSpec);

$marketingTargets = User::whereSpecification($targetAudienceSpec)
    ->with(['orders', 'preferences'])
    ->get();

Content Moderation

php
// Content that needs review: flagged, reported, or from new authors
$flaggedSpec = new WhereSpecification('is_flagged', true);
$reportedSpec = new WhereHasSpecification('reports');
$newAuthorSpec = new WhereHasSpecification('author', 
    new WhereSpecification('created_at', '>=', now()->subDays(30))
);
$suspiciousKeywordsSpec = new WhereSpecification('content', 'like', '%suspicious%');

$needsReviewSpec = $flaggedSpec
    ->or($reportedSpec)
    ->or($newAuthorSpec)
    ->or($suspiciousKeywordsSpec);

$contentForReview = Post::whereSpecification($needsReviewSpec)
    ->with(['author', 'reports'])
    ->orderBy('created_at', 'desc')
    ->get();

Notification Preferences

php
// Users who should receive notifications via any channel
$emailNotificationsSpec = new WhereSpecification('email_notifications', true);
$smsNotificationsSpec = new WhereSpecification('sms_notifications', true);
$pushNotificationsSpec = new WhereSpecification('push_notifications', true);
$browserNotificationsSpec = new WhereSpecification('browser_notifications', true);

$notifiableSpec = $emailNotificationsSpec
    ->or($smsNotificationsSpec)
    ->or($pushNotificationsSpec)
    ->or($browserNotificationsSpec);

$notifiableUsers = User::whereSpecification($notifiableSpec)->get();

Implementation Details

isSatisfiedBy() Method

php
public function isSatisfiedBy(mixed $candidate): bool
{
    return $this->left->isSatisfiedBy($candidate) 
        || $this->right->isSatisfiedBy($candidate);
}

toQuery() Method

php
public function toQuery(Builder $query): Builder
{
    return $query->where(function ($query) {
        // Apply left specification
        $leftQuery = clone $query;
        $this->left->toQuery($leftQuery);
        
        // Apply right specification with OR
        $query->where(function ($subQuery) use ($leftQuery) {
            // Left conditions
            $leftQuery->toQuery($subQuery);
        })->orWhere(function ($subQuery) {
            // Right conditions
            $this->right->toQuery($subQuery);
        });
    });
}

Performance Considerations

Query Optimization

  1. Index Strategy: OR conditions can prevent index usage, consider separate queries
  2. Selectivity: Place more selective conditions first when possible
  3. Query Planning: OR can be expensive; sometimes UNION is faster
  4. Short-Circuit: In-memory evaluation stops on first true condition
php
// Consider this approach for better performance with large datasets
class OptimizedOrQuery
{
    public static function executeOptimized(
        Builder $baseQuery,
        array $orSpecs
    ): Collection {
        $results = collect();
        
        foreach ($orSpecs as $spec) {
            $query = clone $baseQuery;
            $partialResults = $spec->toQuery($query)->get();
            $results = $results->merge($partialResults);
        }
        
        // Remove duplicates
        return $results->unique('id');
    }
}

Database-Specific Considerations

php
// For complex OR conditions, consider using database-specific features
// MySQL: UNION might be faster than OR for some cases
// PostgreSQL: Consider GIN indexes for complex OR conditions
// SQLite: OR optimization is limited

// Example: Converting OR to UNION for better performance
$adminQuery = User::where('role', 'admin');
$moderatorQuery = User::where('role', 'moderator');
$staffUsers = $adminQuery->union($moderatorQuery)->get();

Testing

php
use Tests\TestCase;
use DangerWayne\Specification\Specifications\Common\WhereSpecification;
use DangerWayne\Specification\Specifications\Composite\OrSpecification;

class OrSpecificationTest extends TestCase
{
    public function test_it_matches_either_condition()
    {
        User::factory()->create(['role' => 'admin', 'status' => 'inactive']); // Matches first
        User::factory()->create(['role' => 'user', 'status' => 'active']); // Matches second
        User::factory()->create(['role' => 'admin', 'status' => 'active']); // Matches both
        User::factory()->create(['role' => 'user', 'status' => 'inactive']); // Matches neither
        
        $adminSpec = new WhereSpecification('role', 'admin');
        $activeSpec = new WhereSpecification('status', 'active');
        $orSpec = new OrSpecification($adminSpec, $activeSpec);
        
        $users = User::whereSpecification($orSpec)->get();
        
        $this->assertCount(3, $users);
    }
    
    public function test_fluent_or_method_works_identically()
    {
        User::factory()->create(['role' => 'admin']);
        User::factory()->create(['role' => 'moderator']);
        User::factory()->create(['role' => 'user']);
        
        $adminSpec = new WhereSpecification('role', 'admin');
        $moderatorSpec = new WhereSpecification('role', 'moderator');
        
        // Direct OrSpecification
        $directOr = new OrSpecification($adminSpec, $moderatorSpec);
        $directResults = User::whereSpecification($directOr)->get();
        
        // Fluent or() method
        $fluentOr = $adminSpec->or($moderatorSpec);
        $fluentResults = User::whereSpecification($fluentOr)->get();
        
        $this->assertEquals($directResults->count(), $fluentResults->count());
        $this->assertEquals(
            $directResults->pluck('id')->sort()->values(),
            $fluentResults->pluck('id')->sort()->values()
        );
    }
    
    public function test_it_handles_chained_or_specifications()
    {
        User::factory()->create(['role' => 'admin']);
        User::factory()->create(['role' => 'moderator']);
        User::factory()->create(['role' => 'editor']);
        User::factory()->create(['role' => 'user']);
        
        $adminSpec = new WhereSpecification('role', 'admin');
        $moderatorSpec = new WhereSpecification('role', 'moderator');
        $editorSpec = new WhereSpecification('role', 'editor');
        
        $staffSpec = $adminSpec->or($moderatorSpec)->or($editorSpec);
        
        $staff = User::whereSpecification($staffSpec)->get();
        
        $this->assertCount(3, $staff);
        $this->assertFalse($staff->contains('role', 'user'));
    }
}

Advanced Patterns

Dynamic OR Building

php
class DynamicOrBuilder
{
    private ?SpecificationInterface $spec = null;
    
    public function addOption(SpecificationInterface $specification): self
    {
        $this->spec = $this->spec ? $this->spec->or($specification) : $specification;
        return $this;
    }
    
    public function addConditionalOption(
        bool $condition, 
        SpecificationInterface $specification
    ): self {
        if ($condition) {
            $this->addOption($specification);
        }
        
        return $this;
    }
    
    public function build(): ?SpecificationInterface
    {
        return $this->spec;
    }
}

// Usage
$builder = new DynamicOrBuilder();
$searchSpec = $builder
    ->addConditionalOption($request->has('admin'), new WhereSpecification('role', 'admin'))
    ->addConditionalOption($request->has('moderator'), new WhereSpecification('role', 'moderator'))
    ->addConditionalOption($request->has('editor'), new WhereSpecification('role', 'editor'))
    ->build();

if ($searchSpec) {
    $results = User::whereSpecification($searchSpec)->get();
}

Fallback Specifications

php
class FallbackOrSpecification extends OrSpecification
{
    private string $primaryReason;
    private string $fallbackReason;
    
    public function __construct(
        SpecificationInterface $primary,
        SpecificationInterface $fallback,
        string $primaryReason = 'Primary condition met',
        string $fallbackReason = 'Fallback condition met'
    ) {
        parent::__construct($primary, $fallback);
        $this->primaryReason = $primaryReason;
        $this->fallbackReason = $fallbackReason;
    }
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if ($this->left->isSatisfiedBy($candidate)) {
            $this->setLastReason($this->primaryReason);
            return true;
        }
        
        if ($this->right->isSatisfiedBy($candidate)) {
            $this->setLastReason($this->fallbackReason);
            return true;
        }
        
        return false;
    }
}

Weighted OR Specifications

php
class WeightedOrSpecification
{
    private array $specifications = [];
    
    public function addSpecification(SpecificationInterface $spec, int $weight): self
    {
        $this->specifications[] = ['spec' => $spec, 'weight' => $weight];
        return $this;
    }
    
    public function getMatchingWeight(mixed $candidate): int
    {
        $totalWeight = 0;
        
        foreach ($this->specifications as $item) {
            if ($item['spec']->isSatisfiedBy($candidate)) {
                $totalWeight += $item['weight'];
            }
        }
        
        return $totalWeight;
    }
    
    public function meetsMinimumWeight(mixed $candidate, int $minimumWeight): bool
    {
        return $this->getMatchingWeight($candidate) >= $minimumWeight;
    }
}

Best Practices

1. Use Fluent Interface

php
// Preferred: More readable
$spec = $condition1->or($condition2)->or($condition3);

// Avoid: Nested constructors
$spec = new OrSpecification(
    new OrSpecification($condition1, $condition2),
    $condition3
);

2. Consider Query Performance

php
// For better performance, sometimes separate queries are faster
class PerformantOrQuery
{
    public static function execute(array $specs, Builder $baseQuery): Collection
    {
        // If OR conditions are complex, consider separate queries with UNION
        $results = collect();
        
        foreach ($specs as $spec) {
            $query = clone $baseQuery;
            $results = $results->merge($spec->toQuery($query)->get());
        }
        
        return $results->unique('id');
    }
}
php
// Good: Logical grouping
$roleSpec = $adminSpec->or($moderatorSpec);
$statusSpec = $activeSpec->or($pendingSpec);
$finalSpec = $roleSpec->and($statusSpec);

// Less clear: Flat OR chain
$flatSpec = $adminSpec->or($moderatorSpec)->or($activeSpec)->or($pendingSpec);

4. Use Descriptive Variables

php
// Good: Clear intent
$hasEmailSpec = new WhereNullSpecification('email', false);
$hasPhoneSpec = new WhereNullSpecification('phone', false);
$contactableSpec = $hasEmailSpec->or($hasPhoneSpec);

// Less clear
$spec1 = new WhereNullSpecification('email', false);
$spec2 = new WhereNullSpecification('phone', false);
$combined = $spec1->or($spec2);

See Also

Released under the MIT License.