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);
});
Fluent Interface (Recommended)
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
- Order Matters: Place the most selective conditions first
- Index Usage: Ensure all fields in AND conditions have appropriate indexes
- Short-Circuit Evaluation: In-memory evaluation stops on first false condition
- 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);
3. Group Related Conditions
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
- OrSpecification - Combine specifications with OR logic
- NotSpecification - Negate a specification
- AbstractSpecification - Base class with composite methods
- SpecificationBuilder - Fluent interface for building specifications