I am starting experimenting with Dependency Injection Containers and I’m weighing pros and cons of using any of the existing DIC libraries and if it really is worth the effort. The idea of a dependency container that instantiates objects and injects all required dependencies to them is nice but has one major drawback to me: it makes my code magical. And while I may understand the simple workings of a DIC, my IDE won’t. So far I have tried Pimple (which lack some features of a full-blown DIC but in the basic implementation is the same) and looked at other libraries and the issue is the same:
// Pimple
$myObject = $pimple['my_object'];
// Symfony DIC
$myObject = $container->get('my_object');
// DICE
$myObject = $container->create('my_object');
In all these case the object is created on run time so my IDE won’t know what type of object is returned by the container and so I will not have auto-completion nor will I be able to make sure while writing code that I didn’t misspell an object name. I’d rather use this:
$myObject = $container->getMyObject();
This makes the usage much clearer and non-magical. The downside is that the container will not be able to create objects automatically in a way that an IDE will know what it’s doing so we would need to write the getters by hand.
This lead me to a reddit post by an anonymous person presenting a solution to use a simple class as a container and update it by hand. This reasoning appears pretty sound to me, I will quote a few parts of his posts:
I use a very simple container, it comes built-in with PHP: a simple class with methods.
Sarcasm aside, I’m willing to demonstrate how you can use a simple
class to achieve the same effect as you would with a container, often
with more understandable, explicit (less magical) behavior, with IDE
autocomplete and static type checking (no “stringly typed” services and
labels) and at times even shorter than the writing needed to configure a
container.
Oh and you can’t beat the performance of a simple class when compared to a container library.
[…]
All you need is a class with a set of one-liner methods returning an instance:
function userRepo() { return new SpecificUserRepo(); }
If that instance requires dependencies, call container’s own methods to resolve them:
new UserRepo($this->userSqlDb());
Every “new” call gets its own one-liner method (except for transient
objects of your choice), so there’s one “new” per dependency. This keeps
your code “DRY”.
Make methods protected or private, except for the 2-3 dependencies you want to fetch at your application root.
To create single instance you have a wide range of options, but I’ll give you the most banal one:
protected $sql; function sql() { return $this->sql ?: $this->sql = new Sql(...); }
When you’re done (5 minutes later) you’ll have one of the shortest
and most-easily written classes in your project and it’s your app
container.
It’s easy to write - you’re simply calling constructors, almost
declaratively. Just like container binds! Except… without the
container libs.
Let’s see what we avoided: third party deps, string configs, array
configs, XML, YAML, annotations, parsers, runtime or build-step code
gen, reflection. Not bad.
When you are ready to start your app, make an instance of your
container, grab an instance of your “starting” dependency, and away you
go:
$container = new AppContainer();
$router = $container->router();
$ctrl = $router->route($url);
$dispatcher = $container->dispatcher();
$dispatcher->dispatch($ctrl, $inputs);
[…]
When any dependency changes in a way that alters its constructor (which will be rare, by the way), typically it’s taken us more than a minute to understand the need for change, come up with an idea, implement it, test it.
The thing you need to change in a container as a result of that… is one-line of code calling “new”, in one place. Hardly even a minute. And you don’t have many containers per app, either. In many cases, you just have one for the whole app.
Even with an “automatic” container, chances are when you declare a
dependency in your component, you’re already keeping in mind what that
“automatic” container would do, and you still might have to edit rules, binds and settings if it doesn’t match your intent.
It’s good to keep things in a perspective. Your container won’t be where you spend most of your time, so it doesn’t make sense to focus so intensely on trying to “optimize” a few new calls to an almost equivalent set of bind
calls utilizing expensive runtime magic. This only only obscures the
flow of control in your application and ties you to a library that does
very little for you, if you think about it.
Since I don’t have experience with DI containers in bigger projects I’m wondering if I were missing something if I used such simple getter classes as containers instead of full-fledged DI containers? Maintaining a single container class doesn’t seem like a lot of work and I’d much prefer to have completely non-magical code with proper IDE autocompletion than having all the fancy automatic dependency injection based on argument hints. To me the very little amount of manual work needed to write a simple container would eventually be less time consuming than having to constantly deal with array-like or ‘stringy’ getter syntax that returns an unknown entity that needs to me manually annotated every time to get IDE to recognize what it is.
Therefore, I’m open to opinions on this!