Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Tool Specification Bundle Laravel Package

sfynx-project/tool-specification-bundle

A Symfony bundle implementing the Specification pattern for reusable, composable business rules. Define specifications once, combine them with logical operators, and apply them to validation or query criteria while keeping domain code clean and testable.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require sfynx-project/tool-specification-bundle

Register the bundle

Symfony 3 / legacy skeleton (app/AppKernel.php):

$bundles = [
    // ...
    new Sfynx\SpecificationBundle\SfynxSpecificationBundle(),
];

Symfony 4+/Flex (config/bundles.php), if the bundle ships a *Bundle class with the usual signature:

return [
    Sfynx\SpecificationBundle\SfynxSpecificationBundle::class => ['all' => true],
];

Sfynx bundles were written for the Symfony 2/3 era, so expect to hand-write this registration and to check the composer.json constraints of the release you pull.

Where to look first (in this order)

  1. DependencyInjection/SfynxSpecificationExtension.php and Resources/config/*.yml|xml — this is the only authoritative list of service IDs and tags. Nothing else in the docs tells you what to inject.
  2. Specification/AbstractSpecification.php and Specification/SpecificationInterface.php — the contract you will implement a hundred times (isSatisfiedBy($candidate) plus the usual and / or / not composition helpers).
  3. The Handler/ namespace — this is the "handlers" the description advertises: the services that hold a specification and evaluate it against a candidate, often with a small registry so specs can be addressed by name.
  4. Tests shipped with the bundle — they double as the shortest working usage example.

First use case: a shipping-eligibility rule

namespace App\Specification;

use Sfynx\SpecificationBundle\Specification\AbstractSpecification;

final class IsPremiumCustomer extends AbstractSpecification
{
    public function isSatisfiedBy($customer): bool
    {
        return $customer->isPremium();
    }
}

final class OrderOverThreshold extends AbstractSpecification
{
    public function __construct(private float $threshold) {}

    public function isSatisfiedBy($order): bool
    {
        return $order->getTotal() >= $this->threshold;
    }
}

Wire it as a service (autowiring usually works for your own classes; the bundle's own services usually are not autowired):

services:
    App\Specification\IsPremiumCustomer: ~
    App\Specification\OrderOverThreshold:
        arguments: ['%app.free_shipping_threshold%']

Then evaluate wherever the rule belongs — a controller, a domain service, or a command:

$eligible = (new IsPremiumCustomer())
    ->and(new OrderOverThreshold(50.0))
    ->isSatisfiedBy($order);

Start with one specification and one consumer. The pattern pays off when a rule is referenced from more than one place (checkout, admin re-quote, reporting), so pick a rule that is already duplicated.

Implementation Patterns

1. Compose rather than nest ifs. The whole value of the package is that and/or/not return specifications, so a rule tree stays a first-class object you can inject, log, name, and test:

$rule = (new IsPremiumCustomer())
    ->and((new OrderOverThreshold(50.0))->or(new HasCouponCode())));

2. Parameterise specifications via the constructor, not via setters. A specification constructed with $threshold is immutable and safe to reuse; the bundle's handlers assume a spec can be evaluated repeatedly. Inject configuration through the service definition (%parameter%) so business thresholds live in config/ and not in PHP.

3. Tag-and-collect for rule sets. If the bundle exposes a tagged handler service (check Resources/config for tags:), register every specification with that tag and inject the collection. That gives you a "run all rules and return the failures" style service without a hard-coded list:

services:
    App\Specification\:
        resource: '../src/Specification/*'
        tags: ['sfynx.specification']

If no tag exists, mimic it: define your own app.specification tag, compile-pass it into a SpecificationRegistry, and address specifications by a string key (free_shipping, fraud_review).

4. Use specifications for validation, not for database queries. isSatisfiedBy runs in PHP against an already-loaded object. For list screens, translate the same rule into a Doctrine Criteria/query-builder condition so the database does the filtering; keep the specification as the definition of the rule and let the repository be the translator. Loading 50k orders to filter in memory is the classic misuse.

5. Validate with a result-carrying wrapper. isSatisfiedBy returns a bool, which is useless for user-facing error messages. Pair each specification with a message (a small SpecificationViolation VO or a reason() method on your subclass) and let a SpecificationValidator iterate the set, collecting violations instead of short-circuiting on the first false.

6. Keep specifications domain-pure and unit-testable. They should take plain objects or small DTOs, never the container, never a repository. This means:

public function test_free_shipping_requires_premium_customer(): void
{
    $spec = (new IsPremiumCustomer())->and(new OrderOverThreshold(50.0));
    self::assertFalse($spec->isSatisfiedBy($this->order(premium: false, total: 100)));
    self::assertFalse($spec->isSatisfiedBy($this->order(premium: true, total: 10)));
    self::assertTrue($spec->isSatisfiedBy($this->order(premium: true, total: 100)));
}

No kernel boot, no fixtures — this is where the bundle earns its keep.

7. Handler workflow for command/query buses. If you use a command bus, the natural integration is a middleware or a handler that runs a named specification before dispatching:

if (!$this->specifications->get('order.cancellable')->isSatisfiedBy($order)) {
    throw new DomainException('Order can no longer be cancelled.');
}

This keeps guards declarative and identical across HTTP controllers, console commands, and message consumers.

8. Group by bounded context. Put specifications in src/{Context}/Domain/Specification/ next to the aggregates they constrain, not in a global Specification/ dump. Rules are domain knowledge; they belong to the model.

Gotchas and Tips

  • The bundle is effectively unmaintained. Two stars, zero dependents, and a NOASSERTION license. Read LICENSE before shipping commercially; if it is absent or ambiguous, treat it as "all rights reserved" and get legal sign-off. Vendor the handful of classes you actually use rather than depending on the package.

  • Symfony version friction. Sfynx bundles target old Symfony (2.x/3.x). Expect DependencyInjection API differences, YAML service definitions that Flex won't load, and possibly a missing *Bundle class under the modern namespace. Check composer.json in the repo before require-ing, and pin the version.

  • No autowiring for bundle services. The library services are declared explicitly, so debug:container is your friend:

    bin/console debug:container Specification --show-private
    bin/console debug:container --tag=sfynx.specification
    

    Use --show-private; several handlers are private and invisible without it.

  • Class-name collisions. The Sfynx\SpecificationBundle\Specification namespace has near-identical names to the Sfynx\ToolBundle helpers. Always check the use statement and the actual FQCN when a class "isn't found" after a copy-paste.

  • and/or short-circuit semantics matter. Composition helpers commonly evaluate left-to-right and stop early. If a right-hand specification has side effects (logging, lazy-loading an association) they may not run. Keep specifications side-effect free and you never have to care.

  • Null and type safety. isSatisfiedBy($candidate) is typically untyped in the interface. Add : bool and a concrete parameter type in your subclasses, and decide explicitly what a null candidate means (return false, never throw from inside a specification — throwing breaks composition).

  • Debugging a rule. Because composition hides the tree, add a tiny decorator that logs which leaf returned false:

    final class TracingSpecification implements SpecificationInterface
    {
        public function __construct(private SpecificationInterface $inner, private LoggerInterface $log, private string $name) {}
    
        public function isSatisfiedBy($candidate): bool
        {
            $result = $this->inner->isSatisfiedBy($candidate);
            $this->log->debug('spec', ['name' => $this->name, 'result' => $result]);
            return $result;
        }
    }
    

    Wrap the leaves, not the composites, so you see the actual failing branch.

  • Extension points, in order of preference: (1) implement SpecificationInterface directly if AbstractSpecification drags in unwanted behaviour; (2) subclass and add reason()/violations(); (3) override the bundle's service definition in your app config to swap the handler; (4) fork the two or three files you need. Do not patch vendor/.

  • Performance ceiling. Every evaluation walks the object graph in PHP. If a specification is called inside a loop over hundreds of entities, hoist its construction out of the loop, and cache sub-results where a branch is expensive (e.g. a remote API check). Also pre-filter the collection with a query so isSatisfiedBy sees the smallest possible set.

  • Don't serialise specifications. They hold closures of behaviour and often constructor arguments that don't round-trip. Persist the name of the rule (and its parameters) and rehydrate the specification from the registry.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin