Updated YAML to PHP config syntax, use new symplify/autowire-array-package.
To be clear: we talk about those tags that only have a name. No priority, no level, no event name, nothing, just the name. If you're not sure why these tags are bad, read Drop all Service Tags in Your Nette and Symfony Applications first.
I'm very happy to see that collectors are getting to the core of DI components of PHP frameworks. Tags, extensions, compiler passes and autoconfigure
now became workarounds. Collectors are now in the best place they can... the PHP code.
Let's say we need to build a tool for releasing a new version of the open-source package. Something like what I use for Symplify and Rector releases, but better.
You want it to be open for extension and closed for modification. How do we do that?
You introduce and a ReleaseWorkerInterface
:
namespace Moses\ReleaseWorker;
interface ReleaseWorkerInterface
{
public function work(string $version): void;
}
Good, now if anyone wants to extend it, they' just create a new service:
namespace Moses\ReleaseWorker;
use Nette\Utils\Strings;
final class CheckBlogHasReleasePostReleaseWorker implements ReleaseWorkerInterface
{
public function work(string $version): void
{
$blogContent = file_get_contents('https://tomasvotruba.com');
// is there a post with this title?
if (Strings::match($blogContent, '#Release of ' . $version . '#')) {
// good
echo 'Good job! The blog post was released.';
// early return
return;
}
// bad
throw new DoThisFirstException(sprintf('Write release post about "%s" version first', $version));
}
}
and register it
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(Moses\ReleaseWorker\CheckBlogHasReleasePostReleaseWorker::class);
};
ReleaseWorkerInterface
?Note: I'll be mixing Nette | Symfony syntax now, but they're almost identical in DI component, so just imagine it's your favorite framework.
How can we get all the services that implement ReleaseWorkerInterface
?
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(Moses\ReleaseWorker\CheckBlogHasReleasePostReleaseWorker::class)
->tag('release_worker');
};
In extension/compiler pass:
$mosesDefinition = $containerBuilder->getDefinition(Moses::class);
foreach ($containerBuilder->findByTags('release_worker') as $workerDefinition) {
$mosesDefinition->addCall('addWorker', [$workerDefinition->getName()]);
}
This is what we would do in 2010. This brings memory-lock on tag name and disables common sense. And we need common sense to create usable code.
What's the next option we have?
byType()
methodsIn extension/compiler pass:
$mosesDefinition = $containerBuilder->getDefinition(Moses::class);
foreach ($containerBuilder->findByType(ReleaseWorkerInterface::class) as $workerDefinition) {
$mosesDefinition->addCall('addWorker', [$workerDefinition->getName()]);
}
This drops memory-lock, good. But we still have to go to extension/compiler-pass, lands that are visited by fractions of framework-users.
What about something "2018"?
All options above hides a contract. Which one? The Moses
class looks like this:
final class Moses
{
// property + setter
public function release(string $version)
{
foreach ($this->releaseWorkers as $releaseWorker) {
$releaseWorker->work($version);
}
}
}
What is wrong with this contract? Have you noticed the constructor? Me neither, it's not there! It needs at least some release workers, it's useless without it, but we lie about this contract:
$moses = new Moses\Moses;
$moses->release('v5.0.0');
// nothing
// ...
// WTF?
We already know that public properties, setters, and drugs are bad. Missing constructor contract and sniffing dependency somewhere else by setters - not good either. Moreover when your other classes keep that contract. What's the point of rules in your code then?
We should make a design that is reliable.
ReleaseWorkerInterface
s? Tell us in the constructor.
$releaseWorkers = [
new Moses\ReleaseWorker\CheckBlogHasReleasePostReleaseWorker,
];
$moses = new Moses\Moses($releaseWorkers);
Now when we call the service, we can actually see some output:
$moses->release('v5.0.0');
// "Good job! The blog post was released."
// ...
// Thanks!
Sound nice, right? Is that even possible? Without that, we could drop tags, the compiler passes, YAML/Neon stringly-typed configuration, anti-conception... The world would finally make sense again!
"Vision over Expectations."
It sounds really nice. But how would that work in PHP? How does container now what we need in the constructor. Yes, Mr. Potter?
namespace Moses;
use Moses\ReleaseWorker\ReleaseWorkerInterface;
final class Moses
{
/**
* @param ReleaseWorkerInterface[] $releaseWorkers
*/
public function __construct(array $releaseWorkers)
{
}
}
No need for magic. Just use typehint in annotation.
Typehint in the annotation. It's that simple.
I have no idea.
But you can install it today:
"nette/di": "v3.0"
with this feature enabled in the core
symplify/autowire-array-parameter
Yes, for the cases above it's 1:1 substitution with 0-configuration. It's part of Symplify since 5.1 and it works flawlessly.
And why Moses? Well, he released thousands of people from slavery :)
Happy coding!
Do you learn from my contents or use open-source packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!