New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to handle dependency injection? #6548
Comments
My thoughts: Piwik functionality is meant to be provided by plugins, so plugins must be able to override/change injections. I think this requires having a config. Ideally, it'd be nice if users could modify this config as well to suit their own needs. Autowiring should still be used since configuration is only required to override Piwik's behavior. So default behavior should be specified via auto-wiring and this should be overridden if the config specifies it. I'd prefer annotations. We can always add getters/setters to make a class more testable. |
Yeah totally agree with that. I didn't go into detail with the config files, but in any case we will have some config files. And at first I thought we could start simple and have like one or two file, then we let plugins have their own config files too later on (so that they can expand or override the main config). What I had in mind looks a lot like what you're suggesting and what already exists with ini configs I guess:
And I'm +1 with the rest of your comment. |
+1000 if you can replace INI config w/ DI config :) |
Yeah that's one of the goals too in the long term :) We'll have to keep BC though, but I think that can easily be done by implementing a |
Nice discussion, thanks for explaining! I have no preference but it sounds good to use both, hopefully it will be clear when to use config.php VS annotation.
+1 |
I'm 100% for constructor injection and sometimes setter injection if needed or appropriate. Wouldn't go with the injection of private / protected properties via annotations.
This often indicates a code smell as a class seems to do too many things and is not a problem of constructor injection. A few nice blog posts about this topic:
In general the less magic the better... more magic is seldom a good thing. Defining the arguments in a normal way is understood by any IDE and any code analysis tool etc. Although PHPStorm refactors comments as well not every IDE/Editor does it and people should have the freedom to choose any IDE they like. We would probably also add like big blocks of comments just for DI? Which can be verbose ;) Ideally, a method is so clear that is does not need a comment (unless it is plugin API). Also to expose dependencies in a clear way is quite important I think. It is also understood by everyone and it will be easier for plugin developers. |
If constructor injection allows allowing parameters to be nullable and it's still possible to type hint constructor parameters w/o having them automatically set to objects in DI, then constructor injection seems ok to me. I would prefer annotations however. Re the IDE issue: I am not aware of an IDE outside of PHPStorm that does any refactoring, so I'm not sure this point stands. And it's not impossible to use an IDE w/o refactoring tools. Re the big blocks of comments: the annotations I've seen (looks like 2 per injectable field) doesn't seem to qualify as big blocks of comments. Re clarity of constructor arguments: Having just constructor arguments does not imply they will be loaded from a DI container. They're just constructor arguments. Annotations on the other hand are very clear; Re ensuring code quality: Is this for plugin developers? As core developers whose time to work on things is already minimal, the code we write will never be perfect. We should strive for constant incremental improvement, not immediate perfectionism. Plugin developers will do whatever they want. Furthermore, injecting by constructor args might encourage more granular classes, or it might encourage object spaghetti. You can't force people to think critically and have the correct insights. |
I should have been clearer, I was mostly talking about controllers here (I mean I'm only advocating annotations for controllers). In Piwik we can only have 2 controllers per plugins: Of course if a service/model class (whatever the name) has a long constructor, that is indeed a code smell and it should probably be refactored. But I'm only advocating annotations for glue code, i.e. controllers and maybe commands. Here are the arguments in the blog posts:
In the case of controllers, the consumer is the front controller, who uses the container. So it doesn't matter if dependencies are hidden because this is the container that takes care of these dependencies.
We don't want to unit test controllers. Here is an extract of http://php-di.org/doc/best-practices.html:
Regarding IDE support, I tend to think the contrary, this usually gets better autocompletion and refactoring support: class Foo {
/**
* @var Logger
*/
private $logger;
public function bar() {
$this->logger->…
}
} Than this: class Foo {
private $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function bar() {
$this->logger->…
}
} Basic IDEs can lack proper static type analysis to provide autocompletion. So then the solution is to add docblock comments for properties, but we're back to an even more verbose solution ;) However I agree with your point about "it's magic". I've never liked magic because you need to understand it to use it. So And the second drawback IMO is that there would be 2 best practices: use annotations in controllers, and use constructor injection elsewhere. Confusing… (not for us, but for casual contributors) |
@diosmosis
A constructor probably defines mandatory dependencies, optional ones as a setter.
Might be bigger for methods etc.
I don't think I wrote about code quality. I only meant plugin developers are used to it the standard way etc and for instance annotations can add additional complexity etc.
Nobody is forcing anyone. Constructor arguments can help you to see / identify problems but it doesn't have to. @mnapoli |
It maybe also makes sense to refactor some controllers into multiple classes when introducing DI |
Something we didn't mention yet is that controllers have a base Controller class. If we use constructor injection that might be a mess: how do we handle the dependencies of the parent class? If we have to copy paste the dependencies of the base class every time that might be boring. Especially when we see all the dependencies of the base controller: use Piwik\Access;
use Piwik\API\Proxy;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\Config as PiwikConfig;
use Piwik\Config;
use Piwik\DataTable\Filter\CalculateEvolutionFilter;
use Piwik\Date;
use Piwik\Exceptions\HtmlMessageException;
use Piwik\FrontController;
use Piwik\Menu\MenuTop;
use Piwik\Menu\MenuUser;
use Piwik\NoAccessException;
use Piwik\Notification\Manager as NotificationManager;
use Piwik\Period\Month;
use Piwik\Period;
use Piwik\Period\Range;
use Piwik\Piwik;
use Piwik\Plugins\CoreAdminHome\CustomLogo;
use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
use Piwik\Plugins\UsersManager\UserPreferences;
use Piwik\Registry;
use Piwik\SettingsPiwik;
use Piwik\Site;
use Piwik\Url;
use Piwik\View;
use Piwik\View\ViewInterface;
use Piwik\ViewDataTable\Factory as ViewDataTableFactory; (some of them might not be objects to inject through the constructor though) When we'll have moved away from singletons, we might have a long list of dependencies to inject. We should probably refactor that though? But still for example CoreAdminHome has 19 dependencies… But yeah we could also split that too. One thing to note is that it's easy to move from constructor injection to annotations though, so we can always start with the constructor injection and see how it goes. |
For now I have disabled the use of annotations. We can give it a try with autowiring + config only and see how it goes. We are anyway mainly using the container as a service locator (i.e. Basically, we are moving from "static" -> "service locator" -> "dependency injection". Moving from SL to DI is much easier than from static/singletons, so we are on the right path. |
In 2020 this still an actual question 🤣 because |
@tassoman in Matomo 4 we have marked StaticContainer no longer as API so it is basically to be considered removed. In plugins you should always use injection through constructor method. This is also best practice for 3.X |
@tsteur I think this means that guides like https://developer.matomo.org/guides/logging#how-to-log-messages need to be updated and maybe matomo-org/developer-documentation#67 needs to be finished. |
If I'm able to tame it, I would like to contribute somehow, lemme study more now |
@tassoman I think a good start would be to change the existing plugins to use DI instead of StaticContainer where possible. There are still some usages left that should be easy to convert. Just created a small PR to demonstrate what I mean: #16023 |
OK this is a RFC to discuss how we want to implement dependency injection and use the container in our codebase.
The options
We have several options with PHP-DI that I'll detail below. If you want to read more on this, there's the getting started guide, or also the reference on how to define injections.
Autowiring and constructor injection
Pros:
SMSProvider
class) -> that's called autowiringCons:
Annotations
PHP-DI supports injecting with annotations, like Spring in Java, or ZF2, or a few other containers. It can inject in protected and private properties too.
In this example,
@Inject
will inject the type defined in@var
. You can also define explicitly which entry you want to see injected if you can't do it with@var
(e.g. it's a configuration value):@Inject('the.name.of.my.entry')
Pros:
Cons:
Configuration
In the case where we don't want any magic to happen and we want complete control over everything, we can define injections manually in config files:
The syntax of the PHP config is explained here: defining injections. If you are curious as to why it's not yaml/why it looks like that/etc. have a look here: Why YAML was a bad choice.
So obviously this option is extremely verbose. It's the path chosen by Symfony for example: define ALL the things! And in my experience that sucks a lot, especially when you have a large codebase. As you can see it's pretty dumb code, and can be avoided most of the time.
Pros:
Cons:
Mixing up options
The good news is that we can mix all 3 options and use each where it makes sense :)
If you have a look at the best practices guide, that's the approach I usually recommend.
Performances
There is no performance difference between all these options if we use a cache.
However, given we have the plugin architecture where plugins are enabled/disabled at will, and given that maybe in the future plugins can have their own DI config, maybe a cache is incompatible with that? Or maybe we could simply clear the DI cache when a plugin is enabled/disabled?
My suggestion
Maybe we can differentiate between:
Code that needs to be unit tested needs to use proper dependency injection, i.e. either autowiring or configuration. Of course, I'd tend towards autowiring and using configuration only when it's necessary (i.e. inject config values for example).
Code like controllers, menus… don't need to be unit tested in theory because they are just application code and don't contain a lot of logic. Covering them with system/ui tests is usually enough. So usually it's best to be as productive and practical as possible so annotations can help a lot here. I've used annotations in the past (in controllers) and it's just fantastic to use.
However, I said in theory because some controllers or API classes can contain a lot of code that we might want to test. So annotations could be a pain. So do we want to unit test controllers and API classes and all? Should we refactor them to move business logic inside model classes/services?
Anyway the discussion is open, I'm open to not use annotations too if that's too confusing to have 2 ways of doing injections.
See also #4917
The text was updated successfully, but these errors were encountered: