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);
});
Fluent Interface (Recommended)
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
- Index Strategy: OR conditions can prevent index usage, consider separate queries
- Selectivity: Place more selective conditions first when possible
- Query Planning: OR can be expensive; sometimes UNION is faster
- 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');
}
}
3. Group Logically Related Conditions
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
- AndSpecification - Combine specifications with AND logic
- NotSpecification - Negate a specification
- AbstractSpecification - Base class with composite methods
- SpecificationBuilder - Fluent interface for building specifications