Skip to content

AndSpecification

A composite specification that combines two or more specifications with AND logic. All specifications must be satisfied for the overall specification to be satisfied.

Namespace

php
DangerWayne\Specification\Specifications\Composite\AndSpecification

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 AND Logic

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

// Active users who are also verified
$activeSpec = new WhereSpecification('status', 'active');
$verifiedSpec = new WhereSpecification('email_verified_at', '!=', null);
$andSpec = new AndSpecification($activeSpec, $verifiedSpec);

// Apply to query
$activeVerifiedUsers = User::whereSpecification($andSpec)->get();

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

Instead of using AndSpecification directly, use the fluent and() method:

php
$activeSpec = new WhereSpecification('status', 'active');
$verifiedSpec = new WhereSpecification('email_verified_at', '!=', null);

// More readable than new AndSpecification($activeSpec, $verifiedSpec)
$combinedSpec = $activeSpec->and($verifiedSpec);

$users = User::whereSpecification($combinedSpec)->get();

Common Scenarios

User Filtering

php
// Active, verified, and adult users
$activeSpec = new WhereSpecification('status', 'active');
$verifiedSpec = new WhereNullSpecification('email_verified_at', false);
$adultSpec = new WhereSpecification('age', '>=', 18);

$eligibleUsersSpec = $activeSpec
    ->and($verifiedSpec)
    ->and($adultSpec);

$eligibleUsers = User::whereSpecification($eligibleUsersSpec)->get();

Product Filtering

php
// Available products in specific price range
$inStockSpec = new WhereSpecification('stock_quantity', '>', 0);
$priceRangeSpec = new WhereBetweenSpecification('price', 10.00, 100.00);
$activeSpec = new WhereSpecification('is_active', true);

$availableProductsSpec = $inStockSpec
    ->and($priceRangeSpec)
    ->and($activeSpec);

$products = Product::whereSpecification($availableProductsSpec)->get();

Order Processing

php
// Paid orders ready for shipping
$paidSpec = new WhereSpecification('payment_status', 'paid');
$notShippedSpec = new WhereNullSpecification('shipped_at');
$validAddressSpec = new WhereNullSpecification('shipping_address_id', false);

$readyToShipSpec = $paidSpec
    ->and($notShippedSpec)
    ->and($validAddressSpec);

$ordersToShip = Order::whereSpecification($readyToShipSpec)->get();

Complex Combinations

Multiple AND Chains

php
// Complex user eligibility
$ageSpec = new WhereBetweenSpecification('age', 21, 65);
$incomeSpec = new WhereSpecification('annual_income', '>=', 30000);
$creditSpec = new WhereSpecification('credit_score', '>=', 600);
$employedSpec = new WhereSpecification('employment_status', 'employed');
$verifiedSpec = new WhereNullSpecification('identity_verified_at', false);

// Chain multiple AND conditions
$loanEligibilitySpec = $ageSpec
    ->and($incomeSpec)
    ->and($creditSpec)
    ->and($employedSpec)
    ->and($verifiedSpec);

$eligibleApplicants = User::whereSpecification($loanEligibilitySpec)->get();

Mixed with OR Logic

php
// (Active OR Premium) AND (Verified AND Adult)
$activeSpec = new WhereSpecification('status', 'active');
$premiumSpec = new WhereSpecification('subscription_type', 'premium');
$verifiedSpec = new WhereNullSpecification('email_verified_at', false);
$adultSpec = new WhereSpecification('age', '>=', 18);

$statusSpec = $activeSpec->or($premiumSpec); // (Active OR Premium)
$requirementsSpec = $verifiedSpec->and($adultSpec); // (Verified AND Adult)
$finalSpec = $statusSpec->and($requirementsSpec); // Combine with AND

$targetUsers = User::whereSpecification($finalSpec)->get();

Real-World Examples

E-commerce Search Filters

php
// Advanced product search with multiple filters
$categorySpec = new WhereInSpecification('category_id', $selectedCategories);
$priceSpec = new WhereBetweenSpecification('price', $minPrice, $maxPrice);
$ratingSpec = new WhereSpecification('average_rating', '>=', $minRating);
$availabilitySpec = new WhereSpecification('stock_quantity', '>', 0);
$brandSpec = new WhereInSpecification('brand_id', $selectedBrands);

$searchSpec = $categorySpec
    ->and($priceSpec)
    ->and($ratingSpec)
    ->and($availabilitySpec)
    ->and($brandSpec);

$searchResults = Product::whereSpecification($searchSpec)
    ->with(['reviews', 'images', 'brand'])
    ->paginate(20);

Content Management

php
// Publishable content filtering
$publishedSpec = new WhereSpecification('status', 'published');
$notExpiredSpec = new WhereSpecification('expires_at', '>', now());
$authorActiveSpec = new WhereHasSpecification('author', 
    new WhereSpecification('status', 'active')
);
$approvedSpec = new WhereSpecification('moderation_status', 'approved');
$featuredSpec = new WhereSpecification('is_featured', true);

$displayableContentSpec = $publishedSpec
    ->and($notExpiredSpec)
    ->and($authorActiveSpec)
    ->and($approvedSpec);

// Featured content (adds one more condition)
$featuredContentSpec = $displayableContentSpec->and($featuredSpec);

$content = Post::whereSpecification($displayableContentSpec)->get();
$featuredContent = Post::whereSpecification($featuredContentSpec)->get();

User Access Control

php
// Complex permission system
$activeAccountSpec = new WhereSpecification('status', 'active');
$notBannedSpec = new WhereSpecification('banned_at', null);
$verifiedEmailSpec = new WhereNullSpecification('email_verified_at', false);
$validSubscriptionSpec = new WhereSpecification('subscription_expires_at', '>', now());
$agreementAcceptedSpec = new WhereNullSpecification('terms_accepted_at', false);

// Admin access requires all conditions
$adminAccessSpec = $activeAccountSpec
    ->and($notBannedSpec)
    ->and($verifiedEmailSpec)
    ->and($validSubscriptionSpec)
    ->and($agreementAcceptedSpec);

$adminUsers = User::whereSpecification($adminAccessSpec)
    ->whereIn('role', ['admin', 'super_admin'])
    ->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
{
    // Apply both specifications to the query
    $query = $this->left->toQuery($query);
    $query = $this->right->toQuery($query);
    
    return $query;
}

Performance Considerations

Query Optimization

  1. Order Matters: Place the most selective conditions first
  2. Index Usage: Ensure all fields in AND conditions have appropriate indexes
  3. Short-Circuit Evaluation: In-memory evaluation stops on first false condition
  4. Query Planning: Database optimizers handle AND conditions efficiently
php
// Good: Most selective condition first
$rareValueSpec = new WhereSpecification('unique_identifier', $specificValue);
$commonConditionSpec = new WhereSpecification('status', 'active');
$optimizedSpec = $rareValueSpec->and($commonConditionSpec);

// Less optimal: Common condition first
$lessOptimalSpec = $commonConditionSpec->and($rareValueSpec);

Memory Usage

php
// For large datasets, consider breaking down complex ANDs
$baseQuery = Model::query();

// Apply conditions progressively rather than building massive specification
$baseQuery = $baseQuery->where('status', 'active');
if ($request->has('category')) {
    $baseQuery = $baseQuery->whereIn('category_id', $request->category);
}
// ... continue building query

Testing

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

class AndSpecificationTest extends TestCase
{
    public function test_it_requires_both_conditions_to_be_true()
    {
        User::factory()->create(['status' => 'active', 'age' => 25]); // Matches both
        User::factory()->create(['status' => 'active', 'age' => 16]); // Matches first only
        User::factory()->create(['status' => 'inactive', 'age' => 25]); // Matches second only
        User::factory()->create(['status' => 'inactive', 'age' => 16]); // Matches neither
        
        $activeSpec = new WhereSpecification('status', 'active');
        $adultSpec = new WhereSpecification('age', '>=', 18);
        $andSpec = new AndSpecification($activeSpec, $adultSpec);
        
        $users = User::whereSpecification($andSpec)->get();
        
        $this->assertCount(1, $users);
        $this->assertEquals('active', $users->first()->status);
        $this->assertTrue($users->first()->age >= 18);
    }
    
    public function test_fluent_and_method_works_identically()
    {
        User::factory()->create(['status' => 'active', 'verified' => true]);
        User::factory()->create(['status' => 'active', 'verified' => false]);
        
        $activeSpec = new WhereSpecification('status', 'active');
        $verifiedSpec = new WhereSpecification('verified', true);
        
        // Direct AndSpecification
        $directAnd = new AndSpecification($activeSpec, $verifiedSpec);
        $directResults = User::whereSpecification($directAnd)->get();
        
        // Fluent and() method
        $fluentAnd = $activeSpec->and($verifiedSpec);
        $fluentResults = User::whereSpecification($fluentAnd)->get();
        
        $this->assertEquals($directResults->count(), $fluentResults->count());
        $this->assertEquals(
            $directResults->pluck('id')->sort()->values(),
            $fluentResults->pluck('id')->sort()->values()
        );
    }
    
    public function test_it_handles_nested_and_specifications()
    {
        User::factory()->create([
            'status' => 'active',
            'verified' => true,
            'age' => 25
        ]); // Should match
        
        User::factory()->create([
            'status' => 'active',
            'verified' => false,
            'age' => 25
        ]); // Should not match
        
        $activeSpec = new WhereSpecification('status', 'active');
        $verifiedSpec = new WhereSpecification('verified', true);
        $adultSpec = new WhereSpecification('age', '>=', 18);
        
        // Chain multiple AND conditions
        $complexSpec = $activeSpec->and($verifiedSpec)->and($adultSpec);
        
        $users = User::whereSpecification($complexSpec)->get();
        
        $this->assertCount(1, $users);
    }
}

Advanced Patterns

Conditional AND Building

php
class ConditionalAndBuilder
{
    private ?SpecificationInterface $spec = null;
    
    public function addCondition(bool $shouldAdd, SpecificationInterface $specification): self
    {
        if ($shouldAdd) {
            $this->spec = $this->spec ? $this->spec->and($specification) : $specification;
        }
        
        return $this;
    }
    
    public function build(): ?SpecificationInterface
    {
        return $this->spec;
    }
}

// Usage
$builder = new ConditionalAndBuilder();
$spec = $builder
    ->addCondition($request->has('status'), new WhereSpecification('status', $request->status))
    ->addCondition($request->has('category'), new WhereInSpecification('category_id', $request->category))
    ->addCondition($request->has('min_price'), new WhereSpecification('price', '>=', $request->min_price))
    ->build();

if ($spec) {
    $results = Model::whereSpecification($spec)->get();
}

Specification Validation

php
class ValidationAndSpecification extends AndSpecification
{
    private array $errors = [];
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        $leftResult = $this->left->isSatisfiedBy($candidate);
        $rightResult = $this->right->isSatisfiedBy($candidate);
        
        if (!$leftResult) {
            $this->errors[] = "Left condition failed: {$this->left}";
        }
        
        if (!$rightResult) {
            $this->errors[] = "Right condition failed: {$this->right}";
        }
        
        return $leftResult && $rightResult;
    }
    
    public function getErrors(): array
    {
        return $this->errors;
    }
}

Best Practices

1. Use Fluent Interface

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

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

2. Order Conditions by Selectivity

php
// Good: Rare condition first
$specificSpec = new WhereSpecification('unique_code', $uniqueValue);
$generalSpec = new WhereSpecification('status', 'active');
$optimized = $specificSpec->and($generalSpec);

// Less optimal: General condition first
$lessOptimal = $generalSpec->and($specificSpec);
php
// Good: Logical grouping
$userValidation = $activeSpec->and($verifiedSpec);
$contentValidation = $publishedSpec->and($approvedSpec);
$finalSpec = $userValidation->and($contentValidation);

// Less clear: Flat chain
$flatSpec = $activeSpec->and($verifiedSpec)->and($publishedSpec)->and($approvedSpec);

See Also

Released under the MIT License.