vouchedfor/rules-bundle
vouchedfor/rules-bundle is a PHP package for defining, organizing, and evaluating business rules in a reusable structure. It helps keep rule logic clean and makes rule-based decisions easier to integrate into Laravel or PHP applications.
Reality check before you install anything. vouchedfor/rules-bundle is a Symfony bundle, not a Laravel package. With 2 stars, 0 dependents and a low score, treat it as unmaintained third-party code: read src/ end to end before you composer require it into a production app. If your stack is Laravel, this will not auto-register, will not boot without symfony/framework-bundle and symfony/dependency-injection, and you will be fighting the container rather than writing rules. Realistically your options are (a) port the rule classes into a plain-PHP/Laravel service provider, or (b) use symfony/expression-language directly, which is almost certainly the actual value here.
If you are on Symfony:
composer require vouchedfor/rules-bundle
Register it manually (old bundles don't ship Symfony Flex recipes):
// config/bundles.php
return [
// ...
VouchedFor\RulesBundle\VouchedForRulesBundle::class => ['all' => true],
];
Then look at, in this order:
README.md — often the only documentation; skim for the service ID and rule definition example.src/DependencyInjection/ — the extension alias tells you the config key (vouched_for_rules, rules, etc.) and any parameters you can override.src/Resources/config/services.* — reveals the public service IDs and which interface you must implement.tests/ — usually the real documentation. Copy a test fixture as your first rule.First use case: write one rule that answers a yes/no business question about a domain object ("can this order be refunded?", "is this review eligible for publication?") and evaluate it from a console command before wiring it into a controller. Keep the first integration read-only and side-effect free.
Rules as tagged services. The standard Symfony shape for this kind of bundle is an interface plus a DI tag. Expect something like:
final class ReviewIsPublishable implements RuleInterface
{
public function supports(object $subject): bool
{
return $subject instanceof Review;
}
public function evaluate(object $subject): bool
{
return $subject->getSubmittedAt() !== null
&& $subject->getWordCount() >= 20
&& !$subject->isFlagged();
}
}
Register with the tag the bundle's compiler pass looks for:
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\Rules\:
resource: '../src/Rules/'
tags: ['vouchedfor.rules.rule'] # confirm exact tag in the compiler pass
With autoconfigure: true, if the interface is properly wired for autoconfiguration, the tag is applied for you — check src/DependencyInjection/CompilerPass/ to confirm.
Evaluate via the injected registry, not by hand. The point of the bundle is that you don't new rules or iterate over them yourself. Inject the evaluator/registry service and pass the subject:
public function __construct(private RuleEvaluator $rules) {}
public function __invoke(Review $review): Response
{
$violations = $this->rules->evaluate($review); // collection of failed rules
// map to form errors / API problem JSON / a 422
}
Use it as a domain guard, not a form validator. The strongest pattern is calling the rule set in a command handler or an entity lifecycle listener so the same policy is applied whether the request came from HTTP, a queue worker, or an artisan/console command. Keep the rules pure and inject anything stateful (clock, config, repository lookups) into the rule's constructor.
Test rules in isolation. Rules should be constructible with mocks and evaluated without a kernel. If evaluating a rule requires a booted container, the design has leaked and you'll pay for it in test runtime. Write a small data provider of subject => expected outcome cases; that's the bulk of the value of a rules engine.
Compose sparingly. If the bundle supports and/or/not composites, use them only for genuinely orthogonal checks. Beyond two levels, a composite becomes unreadable and a flat list of named rules reads far better in logs and error messages.
Integration tip: if you need to enrich a subject before evaluation, do it in the caller, not inside a rule — rules that reach into repositories become untestable and slow.
app/Rules namespace bound via a singleton() in a service provider.bin/console config:dump-reference <alias> to see the actual key names before guessing YAML.bin/console cache:clear. A rule that "isn't being called" is almost always an untagged service silently excluded from the registry.bin/console debug:container --tag=<tag> and debug:container <service-id> tell you in seconds whether your rule made it into the registry and whether the service is public.registerForAutoconfiguration in the bundle's extension; otherwise add the tag by hand.support/supports guards are easy to omit. Without a supports() check, a registry iterating all rules over all subjects gives you silent false positives."vouchedfor/rules-bundle": "1.0.*" or a commit hash), and if you depend on it heavily, fork it into your own vendor namespace — it's small enough to vendor.symfony/expression-language — ExpressionFunctionProviderInterface for custom domain functions.How can I help you explore Laravel packages today?