Problem/Motivation
Follow-up to #2729643-15: Create LoggerChannelTrait
Creating a new trait for logger service discovery has led us to a problem:
If we want to require that the service trait use an injected service object, we end up breaking various callers.
Existing code which relies on, for instance, ControllerBase::getLogger(), but which does not properly use the ContainerInjectionInterface pattern, might break when the logger service wasn't previously injected.
This leads to a chicken/egg scenario where we have to figure out what to break and what to fix first. Some classes that use a service location trait will use the service setter of the trait during the factory method or during construction in order to inject the service. Others won't, which necessitates using \Drupal within the trait in order to discover the service. If we enforce the injection of the service by not using \Drupal in the getter, we then allow for poor Inversion of Control (IoC) practices. If we enforce IoC practices, however, we break existing code.
Proposed resolution
Note that this is a proposed resolution. :-)
Ensure that all service discovery traits have a setter for the service and make it clear that it's the best practice to use the setter in the class' factory method to perform injection.
Continue to use the \Drupal service pattern in these traits until classes which use them have been converted to the 'proper' IoC injection pattern. This means that traits will continue to have service accessors, and those accessors will use \Drupal to discover the service if it hasn't happened already.
Once those classes have been converted, have service traits enforce the IoC pattern. This means altering the behavior of the service accessors so that they don't use \Drupal, and instead fail if the service hasn't already been injected.
Comments
Comment #4
mile23Comment #8
mparker17Changed the issue summary to clarify that IoC stands for "Inversion of Control" in the issue summary (for more information on the concept, see Martin Fowler's "Inversion of Control Containers and the Dependency Injection pattern").
@Mile23, does the following code demonstrate the "proper" Inversion of Control Dependency Injection Pattern?
... or if MyClass was a service ...
Comment #9
mile23Thanks for the update.
Yes, that's the pattern for how to inject all the things using service traits: Inject by calling the setter in the factory method.
If I was reviewing code that defined a service that was composed of service traits, I'd send it back and say it shouldn't use those traits without a good reason. But it's not the end of the world, and the service definition lets us get away with it.
Comment #19
solideogloria commentedShould the
LoggerChannelTraitbe deprecated, then, to encourage service injection and discourage new uses of the trait?Comment #20
mile23Searching through Drupal 11, I see that: A) LoggerChannelTrait is unused within core, and B) therefore no services follow this pattern. That's a step forward, I'd say... I assume the trait is still in use in contrib, which is why it's not deprecated at this point though maybe it should be.
Also, core now defines logger channels as services, rather than the factory, so you see this pattern in core.services.yml:
So yay, even better.
Comment #21
solideogloria commentedYes, there are plenty of uses in contrib.
Deprecating it would probably help encourage contrib to use injection.
Comment #22
donquixote commented@mile23
I do find it used in the following places in core:
core/lib/Drupal/Core/Controller/ControllerBase.php
core/lib/Drupal/Core/Form/FormBase.php
core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php
core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
core/modules/views/src/ViewExecutable.php
core/modules/language/src/LanguageNegotiator.php
At least LanguageNegotiator is a service.
But the bigger usage is through base classes ControllerBase and FormBase, which also have other Drupal::*() calls.
I wonder if we should stop using these base classes with their hidden magic.
EDIT: Actually, ControllerBase and FormBase already have comments saying exactly what needs to be said.