Proposal for clearer rule definition syntax with canBeUsedBy
Feature Request
| Q | A |
|---|---|
| New Feature | yes |
| RFC | yes |
| BC Break | no |
Summary
I have a recurring use case in our codebase where we want to prevent all modules from depending on a specific namespace, except a couple of well-known exceptions.
Today, this is expressed like this:
// No other module should depend on this one.
// We only allow two known exceptions: Logging and RequestHandler.
Rule::allClasses()
->except(
'Acme\Quoting\Requests*',
'Acme\Domain\Logging',
'Acme\Service\RequestHandler',
)
->that(new ResideInOneOfTheseNamespaces('Acme'))
->should(new NotDependsOnTheseNamespaces('Acme\Quoting\Requests'))
->because('no other module except Logging and RequestHandler should depend on the Requests module');
I think this rule is harder to read and easy to get wrong, especially as the number of exceptions grows.
I’d like to propose a second, more declarative syntax to make this kind of intent more readable:
Rule::namespace('Acme\Quoting\Requests')
->canBeUsedBy(
'Acme\Domain\Logging',
'Acme\Service\RequestHandler'
);
Thanks!
I see that the declarative syntax is more readable, but I think it would be difficult to explain when to use Rule::allClasses()->that(new ResideInOneOfTheseNamespaces('Acme')) and when to use Rule::namespace('Acme')...
Maybe we can improve the situation making something like:
Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('Acme\Quoting\Requests'))
->should(new BeUsedOnlyBy(['Acme\Domain\Logging', 'Acme\Service\RequestHandler']));
Or some other ways to fit this new concept in our existing structure.. (I think the implementation of this could be tricky, maybe someone else has better ideas.)
Now that I've read #492, I wonder if Rule::namespace('Acme') might just be a shortcut for Rule::allClasses()->that(new ResideInOneOfTheseNamespaces('Acme'))...
You’re right, but my goal here is to offer a simpler and more expressive DSL to cover the most common architectural rules we encounter in real-world projects.
For example, with this kind of DSL:
Rule::namespace('Acme\Quoting\RequestsForQuote')
->shouldNotDependOnOtherNamespaces()
->except('Acme\Entity\Quote', 'Psr', 'Symfony', 'RdKafka', 'Assert', 'Redis');
Rule::namespace('Acme\Quoting\RequestsForQuote')
->canOnlyBeUsedBy(
'Acme\Domain\Logging',
'Acme\Service\RequestHandler',
);
…I can express most of the rules in our codebase in a very concise and readable way. The intention becomes crystal clear, and the developer experience improves a lot from my POV, especially for engineers who don’t want to dive into the Arkitect APIs every time.
I’d love to evolve this DSL further if there’s interest.
in my opinion we sould try to foloow this idea, it can add more flexibility and options for developers at the end.
I think we can probably keep the current DSL and build on top of it a more high-level DSL that covers the most relevant cases.
An even simpler version of the DSL:
Rule::namespace('Acme\Compliance')
->canDependOnlyOn('Acme\Common', 'Acme\Clock');
Rule::namespace('Acme\Compliance')
->shouldNotBeUsedByAnyOtherModule();
Rule::namespace('Acme\Pricing')
->canOnlyBeUsedBy('Acme\Logging', 'Acme\Service\RequestHandler');