What is Dependency Injection?
This chapter introduces the basic programming practices you should follow when writing any application. These are the fundamentals necessary for writing clean, understandable, and maintainable code.
If you adopt and follow these rules, Nette will support you at every step. It will handle routine tasks for you and provide maximum convenience, allowing you to focus on the logic itself.
The principles we will demonstrate here are quite simple. There's nothing to fear.
Remember Your First Program?
We don't know what language you wrote it in, but if it were PHP, it probably looked something like this:
function addition(float $a, float $b): float
{
return $a + $b;
}
echo addition(23, 1); // prints 24
A few trivial lines of code, yet they hide so many key concepts. That variables exist. That code is divided into smaller units, such as functions. That we pass input arguments to them and they return results. Only conditions and loops are missing.
The fact that we pass input data to a function and it returns a result is a perfectly understandable concept, used in other fields as well, like mathematics.
A function has its signature, consisting of its name, a list of parameters and their types, and finally, the return value type. As users, we are interested in the signature; we usually don't need to know anything about the internal implementation.
Now imagine the function signature looked like this:
function addition(float $x): float
Summation with one parameter? That's strange… How about this?
function addition(): float
That's really strange now, isn't it? How is the function used?
echo addition(); // what will it print?
Looking at such code, we would be confused. Not only would a beginner not understand it, but even a skilled programmer wouldn't understand such code.
Are you wondering what such a function would actually look like inside? Where would it get the numbers to add? It would probably procure them somehow itself, perhaps like this:
function addition(): float
{
$a = Input::get('a');
$b = Input::get('b');
return $a + $b;
}
In the function body, we discovered hidden dependencies on other global functions or static methods. To find out where the numbers actually come from, we need to investigate further.
Not This Way!
The design we just showed is the essence of many negative characteristics:
- The function signature pretended it didn't need the numbers to add, which confused us.
- We have no idea how to make the function sum two different numbers.
- We had to look into the code to find out where it gets the numbers.
- We discovered hidden dependencies.
- Fully understanding requires examining these dependencies as well.
And is it even the task of the summation function to obtain the inputs? Of course not. Its responsibility is only the summation itself.
We don't want to encounter such code, and we certainly don't want to write it. The fix is simple: return to the basics and simply use parameters:
function addition(float $a, float $b): float
{
return $a + $b;
}
Rule #1: Let It Be Passed to You
The most important rule is: all the data that functions or classes require must be provided to them.
Instead of inventing hidden ways for them to obtain the data, simply provide the parameters. You will save time spent inventing hidden pathways that definitely won't improve your code.
If you always follow this rule everywhere, you are on the path towards code without hidden dependencies. Towards code that is understandable not only to the author but also to anyone who reads it later. Where everything is understandable from the signatures of functions and classes, and there's no need to search for hidden details in the implementation.
This technique is professionally called Dependency Injection. And the data are called dependencies. It's just plain parameter passing, nothing more.
Please do not confuse Dependency Injection, which is a design pattern, with a “Dependency Injection container,” which is a tool, something fundamentally different. We will discuss containers later.
From Functions to Classes
And how does this apply to classes? A class is a more complex entity than a simple function, but Rule #1 applies fully here as well. There are just more ways to pass arguments. For example, quite similarly to the function case:
class Math
{
public function sum(float $a, float $b): float
{
return $a + $b;
}
}
$math = new Math;
echo $math->sum(23, 1); // 24
Or using other methods, or directly the constructor:
class Sum
{
public function __construct(
private float $a,
private float $b,
) {
}
public function calculate(): float
{
return $this->a + $this->b;
}
}
$sum = new Sum(23, 1);
echo $sum->calculate(); // 24
Both examples are fully compliant with Dependency Injection.
Real-Life Examples
In the real world, you won't be writing classes for summing numbers. Let's move on to practical examples.
Let's have an Article
class representing a blog article:
class Article
{
public int $id;
public string $title;
public string $content;
public function save(): void
{
// save the article to the database
}
}
and the usage will be as follows:
$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();
The save()
method will save the article to a database table. Implementing it using Nette Database would be straightforward, if not for one catch: where does
Article
get the database connection, i.e., an object of the Nette\Database\Connection
class?
It seems we have many options. It could take it from a static variable. Or by inheriting from a class that provides the database connection. Or use a singleton. Or so-called facades, as used in Laravel:
use Illuminate\Support\Facades\DB;
class Article
{
public int $id;
public string $title;
public string $content;
public function save(): void
{
DB::insert(
'INSERT INTO articles (title, content) VALUES (?, ?)',
[$this->title, $this->content],
);
}
}
Great, we solved the problem.
Or did we?
Let's recall Rule #1: Let It Be Passed to You: all dependencies that the class needs must be passed to it. Because if we break the rule, we've embarked on the path to messy code full of hidden dependencies, lack of clarity, and the result will be an application that is a challenge to maintain and develop.
The user of the Article
class has no idea where the save()
method stores the article. In a database
table? Which one, the production or test database? And how can it be changed?
The user has to look at how the save()
method is implemented and finds the use of the DB::insert()
method. So, they must investigate further how this method obtains the database connection. And hidden dependencies can form a
rather long chain.
In clean and well-designed code, there are never hidden dependencies, Laravel facades, or static variables. In clean and well-designed code, arguments are provided:
class Article
{
public function save(Nette\Database\Connection $db): void
{
$db->query('INSERT INTO articles', [
'title' => $this->title,
'content' => $this->content,
]);
}
}
Even more practical, as we will see later, is using the constructor:
class Article
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
public function save(): void
{
$this->db->query('INSERT INTO articles', [
'title' => $this->title,
'content' => $this->content,
]);
}
}
If you are an experienced programmer, you might think that Article
shouldn't have a
save()
method at all; it should represent purely a data structure, and a separate repository should handle the
saving. That makes sense. But that would take us well beyond the scope of the topic, which is Dependency Injection, and the goal
of providing simple examples.
If you write a class that requires, for example, a database for its operation, don't invent where to get it from, but have it passed to you. Perhaps as a parameter of the constructor or another method. Acknowledge dependencies. Acknowledge them in your class's API. You will get understandable and predictable code.
And what about this class, which logs error messages:
class Logger
{
public function log(string $message)
{
$file = LOG_DIR . '/log.txt';
file_put_contents($file, $message . "\n", FILE_APPEND);
}
}
What do you think, did we follow Rule #1: Let It Be Passed to You?
We did not.
The key piece of information, the directory containing the log file, is obtained by the class itself from a constant.
Look at the usage example:
$logger = new Logger;
$logger->log('Temperature is 23 °C');
$logger->log('Temperature is 10 °C');
Without knowing the implementation, could you answer where the messages are being written? Would it occur to you that the
existence of the LOG_DIR
constant is required for its operation? And could you create a second instance that would
write elsewhere? Certainly not.
Let's fix the class:
class Logger
{
public function __construct(
private string $file,
) {
}
public function log(string $message): void
{
file_put_contents($this->file, $message . "\n", FILE_APPEND);
}
}
The class is now much more understandable, configurable, and thus more useful.
$logger = new Logger('/path/to/log.txt');
$logger->log('Temperature is 15 °C');
But I Don’t Care!
“When I create an Article object and call save(), I don't want to deal with the database; I just want it to be saved in the one I have configured.”
“When I use Logger, I just want the message to be written, and I don't want to deal with where. Let the global settings be used.”
These are valid points.
As an example, let's show a class that distributes newsletters and logs the outcome:
class NewsletterDistributor
{
public function distribute(): void
{
$logger = new Logger(/* ... */);
try {
$this->sendEmails();
$logger->log('Emails have been sent out');
} catch (Exception $e) {
$logger->log('An error occurred during sending');
throw $e;
}
}
}
The improved Logger
, which no longer uses the LOG_DIR
constant, requires the file path in the
constructor. How can this be resolved? The NewsletterDistributor
class is not concerned with where messages are
written; it simply wants to log them.
The solution is again Rule #1: Let It Be Passed to You: we pass all the data the class needs.
So, does this mean we pass the log path through the constructor, which we then use when creating the Logger
object?
class NewsletterDistributor
{
public function __construct(
private string $file, // ⛔ NOT LIKE THIS!
) {
}
public function distribute(): void
{
$logger = new Logger($this->file);
Not like this! Because the path is not data that the NewsletterDistributor
class requires; the
Logger
requires it. Do you perceive the difference? The NewsletterDistributor
class needs the logger
itself. So, we will pass the logger itself:
class NewsletterDistributor
{
public function __construct(
private Logger $logger, // ✅
) {
}
public function distribute(): void
{
try {
$this->sendEmails();
$this->logger->log('Emails have been sent out');
} catch (Exception $e) {
$this->logger->log('An error occurred during sending');
throw $e;
}
}
}
Now it's clear from the signature of the NewsletterDistributor
class that logging is part of its function. And
the task of substituting the logger with another, perhaps for testing, is entirely straightforward. Moreover, if the
Logger
class constructor were to change, it will have no impact on our class.
Rule #2: Take What's Yours
Don't be confused and don't accept the dependencies of your dependencies. Accept only your own dependencies.
Thanks to this, code that uses other objects will be completely independent of changes to their constructors. Its API will be more accurate. And most importantly, it will be straightforward to substitute these dependencies with others.
New Family Member
The development team has decided to create a second logger, one that writes to the database. So, we create a
DatabaseLogger
class. Now we have two classes, Logger
and DatabaseLogger
; one writes to a
file, the other to the database… doesn't the naming seem somewhat odd? Wouldn't it be better to rename Logger
to
FileLogger
? Certainly.
But let's do it cleverly. We create an interface using the original name:
interface Logger
{
function log(string $message): void;
}
… which both loggers will implement:
class FileLogger implements Logger
// ...
class DatabaseLogger implements Logger
// ...
And thanks to this, there will be no need to modify anything in the rest of the code where the logger is utilized. For example,
the NewsletterDistributor
class constructor will still be content requiring Logger
as a parameter. And
it will be up to us which instance we provide to it.
That's why we never add the Interface
suffix or the I
prefix to interface names. Otherwise,
it wouldn't be possible to extend the code so elegantly.
Houston, We Have a Problem
While in the entire application we can get by with a single instance of the logger, whether file-based or database-based, and
simply pass it wherever logging occurs, the situation is quite different with the Article
class. We create its
instances as required, even multiple times. How do we handle the database dependency in its constructor?
An example could be a controller that should save an article to the database after submitting a form:
class EditController extends Controller
{
public function formSubmitted($data)
{
$article = new Article(/* ... */);
$article->title = $data->title;
$article->content = $data->content;
$article->save();
}
}
A potential solution seems obvious: let's have the database object passed via the constructor into
EditController
and use $article = new Article($this->db)
.
Just as in the previous case involving Logger
and the file path, this is not the correct approach. The database is
not a dependency of EditController
, but rather of Article
. Passing the database thus violates #Rule #2: Take What's Yours. If the Article
class constructor changes (a
new parameter is added), you will need to modify the code in all places where instances are created. Oof.
Houston, what's your suggestion?
Rule #3: Let the Factory Handle It
By eliminating hidden dependencies and passing all dependencies as arguments, we have gained more configurable and flexible classes. Therefore, we need something additional to create and configure these more flexible classes for us. We'll call these factories.
The rule is: If a class has dependencies, delegate the creation of its instances to a factory.
Factories are a smarter alternative to the new
operator in the world of Dependency Injection.
Please do not confuse this with the factory method design pattern, which describes a specific way of utilizing factories and is unrelated to this topic.
Factory
A factory is a method or class that creates and configures objects. We will call the class that produces Article
ArticleFactory
, and it might look like this:
class ArticleFactory
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
public function create(): Article
{
return new Article($this->db);
}
}
Its usage in the controller will be as follows:
class EditController extends Controller
{
public function __construct(
private ArticleFactory $articleFactory,
) {
}
public function formSubmitted($data)
{
// let the factory create the object
$article = $this->articleFactory->create();
$article->title = $data->title;
$article->content = $data->content;
$article->save();
}
}
At this point, if the signature of the Article
class constructor changes, the only part of the code that needs to
respond is the ArticleFactory
itself. All other code that interacts with Article
objects, such as
EditController
, will remain unaffected.
You might be scratching your head now, wondering if we've actually improved the situation. The amount of code has grown, and the whole thing is starting to look suspiciously complex.
Don't worry, we'll get to the Nette DI container soon. And it has several tricks up its sleeve that will greatly simplify the
construction of applications using Dependency Injection. For example, instead of the ArticleFactory
class, it will
suffice to write just an interface:
interface ArticleFactory
{
function create(): Article;
}
But we are getting ahead of ourselves, stay tuned :-)
Summary
At the beginning of this chapter, we promised to show a process for designing clean code. Simply ensure that classes:
- are passed the dependencies they need
- and conversely, are not passed what they don't directly need
- and that objects with dependencies are best created in factories
It might not seem apparent at first glance, but these three rules have far-reaching consequences. They lead to a radically different perspective on code design. Is it worthwhile? Programmers who have abandoned old habits and started consistently using Dependency Injection consider this step a defining moment in their professional careers. It opened up a world of clear and maintainable applications for them.
But what if the code doesn't consistently use Dependency Injection? What if it's built upon static methods or singletons? Does this lead to problems? Yes, it does, and very significant ones.