Skip to content

NotSpecification

A composite specification that negates another specification, inverting its truth value. If the wrapped specification is satisfied, the NotSpecification is not satisfied, and vice versa.

Namespace

php
DangerWayne\Specification\Specifications\Composite\NotSpecification

Constructor

php
public function __construct(
    SpecificationInterface $specification
)

Parameters

  • $specification (SpecificationInterface) - The specification to negate

Usage Examples

Basic Negation

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

// Users who are NOT admin
$adminSpec = new WhereSpecification('role', 'admin');
$notAdminSpec = new NotSpecification($adminSpec);

// Apply to query
$nonAdminUsers = User::whereSpecification($notAdminSpec)->get();

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

Instead of using NotSpecification directly, use the fluent not() method:

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

// More readable than new NotSpecification($adminSpec)
$notAdminSpec = $adminSpec->not();

$nonAdminUsers = User::whereSpecification($notAdminSpec)->get();

Common Scenarios

Exclusion Filters

php
// Exclude deleted records
$deletedSpec = new WhereNullSpecification('deleted_at', false);
$activeRecordsSpec = $deletedSpec->not();

$activeUsers = User::whereSpecification($activeRecordsSpec)->get();

Status Exclusion

php
// Orders that are NOT completed
$completedSpec = new WhereSpecification('status', 'completed');
$pendingOrdersSpec = $completedSpec->not();

$pendingOrders = Order::whereSpecification($pendingOrdersSpec)->get();

Role-Based Exclusion

php
// Content visible to non-admin users
$adminOnlySpec = new WhereSpecification('visibility', 'admin_only');
$publicContentSpec = $adminOnlySpec->not();

$publicPosts = Post::whereSpecification($publicContentSpec)->get();

Complex Negations

Negating Composite Specifications

php
// NOT (Admin AND Active)
$adminSpec = new WhereSpecification('role', 'admin');
$activeSpec = new WhereSpecification('status', 'active');
$adminActiveSpec = $adminSpec->and($activeSpec);
$notAdminActiveSpec = $adminActiveSpec->not();

// This matches users who are either NOT admin OR NOT active (or both)
$users = User::whereSpecification($notAdminActiveSpec)->get();

Double Negation

php
// NOT (NOT Active) = Active
$activeSpec = new WhereSpecification('status', 'active');
$notActiveSpec = $activeSpec->not();
$doubleNegationSpec = $notActiveSpec->not(); // Equivalent to $activeSpec

$activeUsers = User::whereSpecification($doubleNegationSpec)->get();

De Morgan's Laws in Action

php
// NOT (A AND B) = (NOT A) OR (NOT B)
$aSpec = new WhereSpecification('field_a', 'value_a');
$bSpec = new WhereSpecification('field_b', 'value_b');

// These are logically equivalent:
$notAndSpec = $aSpec->and($bSpec)->not();
$orNotSpec = $aSpec->not()->or($bSpec->not());

// NOT (A OR B) = (NOT A) AND (NOT B)
$notOrSpec = $aSpec->or($bSpec)->not();
$andNotSpec = $aSpec->not()->and($bSpec->not());

Real-World Examples

Content Moderation

php
// Posts that are NOT flagged as inappropriate
$inappropriateSpec = new WhereSpecification('content_flag', 'inappropriate');
$appropriateContentSpec = $inappropriateSpec->not();

// Posts that are NOT from suspended authors
$suspendedAuthorSpec = new WhereHasSpecification('author',
    new WhereSpecification('status', 'suspended')
);
$fromActiveAuthorsSpec = $suspendedAuthorSpec->not();

// Combine: appropriate content from active authors
$displayableSpec = $appropriateContentSpec->and($fromActiveAuthorsSpec);

$displayablePosts = Post::whereSpecification($displayableSpec)->get();

User Segmentation

php
// Users who are NOT in the target demographic
$targetAgeSpec = new WhereBetweenSpecification('age', 25, 45);
$targetLocationSpec = new WhereInSpecification('country', ['US', 'CA', 'GB']);
$premiumUserSpec = new WhereSpecification('subscription_type', 'premium');

$targetDemographicSpec = $targetAgeSpec
    ->and($targetLocationSpec)
    ->and($premiumUserSpec);

$nonTargetUsersSpec = $targetDemographicSpec->not();

// Users for alternative marketing campaign
$alternativeAudienceUsers = User::whereSpecification($nonTargetUsersSpec)->get();

Inventory Management

php
// Products that are NOT out of stock
$outOfStockSpec = new WhereSpecification('stock_quantity', 0);
$inStockSpec = $outOfStockSpec->not();

// Products that are NOT discontinued
$discontinuedSpec = new WhereSpecification('status', 'discontinued');
$activeProductsSpec = $discontinuedSpec->not();

// Available products: in stock and not discontinued
$availableSpec = $inStockSpec->and($activeProductsSpec);

$availableProducts = Product::whereSpecification($availableSpec)->get();

Access Control

php
// Resources NOT restricted to admin users
$adminOnlySpec = new WhereSpecification('access_level', 'admin_only');
$publicResourcesSpec = $adminOnlySpec->not();

// Content NOT requiring premium subscription
$premiumOnlySpec = new WhereSpecification('requires_premium', true);
$freeContentSpec = $premiumOnlySpec->not();

// Publicly accessible content
$publicContentSpec = $publicResourcesSpec->and($freeContentSpec);

$publicContent = Content::whereSpecification($publicContentSpec)->get();

Implementation Details

isSatisfiedBy() Method

php
public function isSatisfiedBy(mixed $candidate): bool
{
    return !$this->specification->isSatisfiedBy($candidate);
}

toQuery() Method

php
public function toQuery(Builder $query): Builder
{
    return $query->whereNot(function ($subQuery) {
        $this->specification->toQuery($subQuery);
    });
}

Performance Considerations

Query Optimization

  1. Index Usage: NOT conditions can prevent index usage in some cases
  2. Subquery Performance: NOT wraps conditions in a subquery which can be expensive
  3. Alternative Approaches: Sometimes positive conditions are more efficient
  4. Database Differences: Different databases handle NOT differently
php
// Consider positive alternatives when possible
// Instead of NOT deleted_at IS NOT NULL
$notDeletedSpec = new WhereNullSpecification('deleted_at', false)->not();

// Use the more direct approach:
$activeSpec = new WhereNullSpecification('deleted_at');

Query Plan Analysis

php
// Complex NOT conditions can be expensive
$complexNotSpec = $spec1->and($spec2)->or($spec3)->not();

// Consider rewriting using De Morgan's laws for better performance
$optimizedSpec = $spec1->not()->or($spec2->not())->and($spec3->not());

Testing

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

class NotSpecificationTest extends TestCase
{
    public function test_it_inverts_specification_result()
    {
        User::factory()->create(['status' => 'active']);
        User::factory()->create(['status' => 'inactive']);
        User::factory()->create(['status' => 'suspended']);
        
        $activeSpec = new WhereSpecification('status', 'active');
        $notActiveSpec = new NotSpecification($activeSpec);
        
        $users = User::whereSpecification($notActiveSpec)->get();
        
        $this->assertCount(2, $users);
        $this->assertFalse($users->contains('status', 'active'));
        $this->assertTrue($users->contains('status', 'inactive'));
        $this->assertTrue($users->contains('status', 'suspended'));
    }
    
    public function test_fluent_not_method_works_identically()
    {
        User::factory()->create(['role' => 'admin']);
        User::factory()->create(['role' => 'user']);
        User::factory()->create(['role' => 'moderator']);
        
        $adminSpec = new WhereSpecification('role', 'admin');
        
        // Direct NotSpecification
        $directNot = new NotSpecification($adminSpec);
        $directResults = User::whereSpecification($directNot)->get();
        
        // Fluent not() method
        $fluentNot = $adminSpec->not();
        $fluentResults = User::whereSpecification($fluentNot)->get();
        
        $this->assertEquals($directResults->count(), $fluentResults->count());
        $this->assertEquals(
            $directResults->pluck('id')->sort()->values(),
            $fluentResults->pluck('id')->sort()->values()
        );
    }
    
    public function test_double_negation_equals_original()
    {
        User::factory()->create(['status' => 'active']);
        User::factory()->create(['status' => 'inactive']);
        
        $activeSpec = new WhereSpecification('status', 'active');
        $doubleNegationSpec = $activeSpec->not()->not();
        
        $originalResults = User::whereSpecification($activeSpec)->get();
        $doubleNegationResults = User::whereSpecification($doubleNegationSpec)->get();
        
        $this->assertEquals($originalResults->count(), $doubleNegationResults->count());
        $this->assertEquals(
            $originalResults->pluck('id')->sort()->values(),
            $doubleNegationResults->pluck('id')->sort()->values()
        );
    }
    
    public function test_de_morgans_law_not_and_equals_or_not()
    {
        User::factory()->create(['role' => 'admin', 'status' => 'active']);
        User::factory()->create(['role' => 'admin', 'status' => 'inactive']);
        User::factory()->create(['role' => 'user', 'status' => 'active']);
        User::factory()->create(['role' => 'user', 'status' => 'inactive']);
        
        $adminSpec = new WhereSpecification('role', 'admin');
        $activeSpec = new WhereSpecification('status', 'active');
        
        // NOT (admin AND active)
        $notAndSpec = $adminSpec->and($activeSpec)->not();
        
        // (NOT admin) OR (NOT active)
        $orNotSpec = $adminSpec->not()->or($activeSpec->not());
        
        $notAndResults = User::whereSpecification($notAndSpec)->get();
        $orNotResults = User::whereSpecification($orNotSpec)->get();
        
        $this->assertEquals($notAndResults->count(), $orNotResults->count());
        $this->assertEquals(
            $notAndResults->pluck('id')->sort()->values(),
            $orNotResults->pluck('id')->sort()->values()
        );
    }
}

Advanced Patterns

Conditional Negation

php
class ConditionalNotSpecification
{
    public static function create(
        SpecificationInterface $spec,
        bool $shouldNegate = false
    ): SpecificationInterface {
        return $shouldNegate ? $spec->not() : $spec;
    }
}

// Usage
$includeInactive = $request->boolean('include_inactive');
$activeSpec = new WhereSpecification('status', 'active');
$finalSpec = ConditionalNotSpecification::create($activeSpec, !$includeInactive);

Exclusion Lists

php
class ExclusionSpecification
{
    private array $exclusions = [];
    
    public function exclude(SpecificationInterface $spec): self
    {
        $this->exclusions[] = $spec;
        return $this;
    }
    
    public function build(): ?SpecificationInterface
    {
        if (empty($this->exclusions)) {
            return null;
        }
        
        return collect($this->exclusions)
            ->map(fn($spec) => $spec->not())
            ->reduce(fn($carry, $spec) => $carry ? $carry->and($spec) : $spec);
    }
}

// Usage
$exclusionSpec = (new ExclusionSpecification())
    ->exclude(new WhereSpecification('status', 'banned'))
    ->exclude(new WhereSpecification('role', 'spam'))
    ->exclude(new WhereNullSpecification('email_verified_at', false))
    ->build();

Whitelist vs Blacklist

php
class AccessControlSpecification
{
    public static function whitelist(array $allowedValues, string $field): SpecificationInterface
    {
        return new WhereInSpecification($field, $allowedValues);
    }
    
    public static function blacklist(array $deniedValues, string $field): SpecificationInterface
    {
        return (new WhereInSpecification($field, $deniedValues))->not();
    }
}

// Usage
$whitelistSpec = AccessControlSpecification::whitelist(['admin', 'moderator'], 'role');
$blacklistSpec = AccessControlSpecification::blacklist(['banned', 'suspended'], 'status');

$allowedUsersSpec = $whitelistSpec->and($blacklistSpec);

Logical Laws and Transformations

De Morgan's Laws

php
// NOT (A AND B) = (NOT A) OR (NOT B)
function notAnd(SpecificationInterface $a, SpecificationInterface $b): SpecificationInterface
{
    // These are equivalent:
    $version1 = $a->and($b)->not();
    $version2 = $a->not()->or($b->not());
    
    return $version2; // Often more efficient
}

// NOT (A OR B) = (NOT A) AND (NOT B)
function notOr(SpecificationInterface $a, SpecificationInterface $b): SpecificationInterface
{
    // These are equivalent:
    $version1 = $a->or($b)->not();
    $version2 = $a->not()->and($b->not());
    
    return $version2; // Often more efficient
}

Double Negation Elimination

php
class OptimizedSpecification
{
    public static function optimize(SpecificationInterface $spec): SpecificationInterface
    {
        // Eliminate double negation: NOT (NOT A) = A
        if ($spec instanceof NotSpecification) {
            $inner = $spec->getSpecification();
            if ($inner instanceof NotSpecification) {
                return self::optimize($inner->getSpecification());
            }
        }
        
        return $spec;
    }
}

Best Practices

1. Use Fluent Interface

php
// Preferred: More readable
$spec = $condition->not();

// Avoid: Constructor approach
$spec = new NotSpecification($condition);

2. Consider Positive Alternatives

php
// Sometimes positive conditions are clearer and more efficient
// Instead of:
$notDeletedSpec = new WhereNullSpecification('deleted_at', false)->not();

// Use:
$activeSpec = new WhereNullSpecification('deleted_at');

3. Apply De Morgan's Laws for Performance

php
// Less efficient: complex NOT
$complexNotSpec = $spec1->and($spec2)->or($spec3)->not();

// More efficient: distributed NOT
$distributedSpec = $spec1->not()->or($spec2->not())->and($spec3->not());

4. Use Descriptive Variable Names

php
// Good: Clear intent
$adminSpec = new WhereSpecification('role', 'admin');
$nonAdminSpec = $adminSpec->not();

// Less clear
$spec = new WhereSpecification('role', 'admin');
$notSpec = $spec->not();

Common Anti-Patterns

Avoid Unnecessary Negation

php
// Anti-pattern: Double negation
$spec = $condition->not()->not(); // Just use $condition

// Anti-pattern: Negating simple conditions
$notActiveSpec = new WhereSpecification('status', 'active')->not();
// Better: Use direct condition
$inactiveSpec = new WhereSpecification('status', 'inactive');

Avoid Complex Nested Negations

php
// Anti-pattern: Hard to understand
$complexSpec = $spec1->not()->and($spec2->not()->or($spec3->not()))->not();

// Better: Break down and use positive logic where possible
$simpleSpec1 = $spec1->or($spec2->and($spec3));

See Also

Released under the MIT License.