What is Dependency Injection?

This chapter will introduce you to the basic programming practices that you should follow when writing any application. These are the fundamentals needed for writing clean, understandable, and maintainable code.

If you learn and follow these rules, Nette will be there for you every step of the way. It will handle routine tasks for you and provide maximum comfort, so you can focus on the logic itself.

The principles we will show here are quite simple. You don't have to worry about anything.

Remember Your First Program?

We don't know what language you wrote it in, but if it was PHP, it might have 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, but so many key concepts hidden in them. That there are variables. That code is broken down into smaller units, which are functions, for example. That we pass them input arguments and they return results. All that's missing are conditions and loops.

The fact that a function takes input data and returns a result is a perfectly understandable concept, which is also used in other fields, such as mathematics.

A function has its signature, which consists of its name, a list of parameters and their types, and finally the type of the return value. As users, we are interested in the signature, and we usually don't need to know anything about the internal implementation.

Now imagine that the function signature looked like this:

function addition(float $x): float

An addition with one parameter? That's strange… What about this?

function addition(): float

Now that's really weird, right? How is the function used?

echo addition(); // what does it prints?

Looking at such code, we would be confused. Not only would a beginner not understand it, but even an experienced programmer would not understand such code.

Are you wondering what such a function would actually look like inside? Where would it get the summands? It would probably somehow get them by itself, perhaps like this:

function addition(): float
{
	$a = Input::get('a');
	$b = Input::get('b');
	return $a + $b;
}

It turns out that there are hidden bindings to other functions (or static methods) in the body of the function, and to find out where the addends actually come from, we have to dig further.

Not This Way!

The design we just showed is the essence of many negative features:

  • the function signature pretended that it didn't need the summands, which confused us
  • we have no idea how to make the function calculate with two other numbers
  • we had to look at the code to find out where the summands came from
  • we found hidden dependencies
  • a full understanding requires examining these dependencies as well

And is it even the job of the addition function to procure inputs? Of course it isn't. Its responsibility is only to add.

We don't want to encounter such code, and we certainly don't want to write it. The remedy is simple: go back to basics and just 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 data that functions or classes need must be passed to them.

Instead of inventing hidden ways for them to access the data themselves, simply pass the parameters. You will save time that would be spent inventing hidden paths that certainly won't improve your code.

If you always and everywhere follow this rule, you are on your way to code without hidden dependencies. To code that is understandable not only to the author but also to anyone who reads it afterward. Where everything is understandable from the signatures of functions and classes, and there is no need to search for hidden secrets in the implementation.

This technique is professionally called dependency injection. And those data are called dependencies. It's just ordinary 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 diametrically different. We will deal with containers later.

From Functions to Classes

And how are classes related? A class is a more complex unit than a simple function, but rule #1 applies entirely here as well. There are just more ways to pass arguments. For example, quite similar to the case of a function:

class Math
{
	public function addition(float $a, float $b): float
	{
		return $a + $b;
	}
}

$math = new Math;
echo $math->addition(23, 1); // 24

Or through other methods, or directly through the constructor:

class Addition
{
	public function __construct(
		private float $a,
		private float $b,
	) {
	}

	public function calculate(): float
	{
		return $this->a + $this->b;
	}

}

$addition = new Addition(23, 1);
echo $addition->calculate(); // 24

Both examples are completely in compliance with dependency injection.

Real-Life Examples

In the real world, you won't be writing classes for adding numbers. Let's move on to practical examples.

Let's have a Article class representing a blog post:

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 will be a piece of cake, were it not for one hitch: where does Article get the database connection, i.e., an object of class Nette\Database\Connection?

It seems we have plenty of options. It can take it from a static variable somewhere. Or inherit from a class that provides a database connection. Or take advantage of a singleton. Or use so-called facades, which are 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've solved the problem.

Or have we?

Let's recall rule #1: Let It Be Passed to You: all the dependencies the class needs must be passed to it. Because if we break the rule, we have embarked on a path to dirty code full of hidden dependencies, incomprehensibility, and the result will be an application that will be painful 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, production or testing? 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, he has to search further to find out how this method obtains a database connection. And hidden dependencies can form quite a long chain.

In clean and well-designed code, there are never any hidden dependencies, Laravel facades, or static variables. In clean and well-designed code, arguments are passed:

class Article
{
	public function save(Nette\Database\Connection $db): void
	{
		$db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

An even more practical approach, as we will see later, will be through 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 should not have a save() method at all; it should represent a purely data component, and a separate repository should take care of saving. That makes sense. But that would take us far beyond the scope of the topic, which is dependency injection, and the effort to provide 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. Either as a parameter of the constructor or another method. Admit dependencies. Admit them in the API of your class. 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 didn't.

The key information, i.e., the directory with the log file, is obtained by the class itself from the constant.

Look at the example of usage:

$logger = new Logger;
$logger->log('The temperature is 23 °C');
$logger->log('The temperature is 10 °C');

Without knowing the implementation, could you answer the question of where the messages are written? Would you guess that the existence of the LOG_DIR constant is necessary for its functioning? And could you create a second instance that would write to a different location? 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 therefore more useful.

$logger = new Logger('/path/to/log.txt');
$logger->log('The 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 set in the configuration.”

“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 look at a class that sends newsletters and logs how it went:

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 the sending');
			throw $e;
		}
	}
}

The improved Logger, which no longer uses the LOG_DIR constant, requires specifying the file path in the constructor. How to solve this? The NewsletterDistributor class doesn't care where the messages are written; it just wants to write them.

The solution is again rule #1: Let It Be Passed to You: pass all the data that the class needs.

So does that mean we pass the path to the log through the constructor, which we then use when creating the Logger object?

class NewsletterDistributor
{
	public function __construct(
		private string $file, // ⛔ NOT THIS WAY!
	) {
	}

	public function distribute(): void
	{
		$logger = new Logger($this->file);

No, not like this! The path doesn't belong among the data that the NewsletterDistributor class needs; in fact, the Logger needs it. Do you see the difference? The NewsletterDistributor class needs the logger itself. So that's what we'll pass:

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 the sending');
			throw $e;
		}
	}
}

Now it is clear from the signatures of the NewsletterDistributor class that logging is also part of its functionality. And the task of exchanging the logger for another, perhaps for testing, is completely trivial. Moreover, if the constructor of the Logger class changes, it will not affect our class.

Rule #2: Take What's Yours

Don't be misled and don't let yourself pass the dependencies of your dependencies. Just pass your own dependencies.

Thanks to this, the code using other objects will be completely independent of changes in their constructors. Its API will be more truthful. And above all, it will be trivial to replace these dependencies with others.

New Family Member

The development team decided to create a second logger that writes to the database. So we create a DatabaseLogger class. So we have two classes, Logger and DatabaseLogger, one writes to a file, the other to a database … doesn't the naming seem strange to you? Wouldn't it be better to rename Logger to FileLogger? Definitely yes.

But let's do it smartly. We create an interface under the original name:

interface Logger
{
	function log(string $message): void;
}

… which both loggers will implement:

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

And because of this, there will be no need to change anything in the rest of the code where the logger is used. For example, the constructor of the NewsletterDistributor class will still be satisfied with requiring Logger as a parameter. And it will be up to us which instance we pass.

That's why we never add the Interface suffix or I prefix to interface names. Otherwise, it would not be possible to develop the code so nicely.

Houston, We Have a Problem

While we can get by with a single instance of the logger, whether file-based or database-based, throughout the entire application and simply pass it wherever something is logged, it's quite different for the Article class. We create its instances as needed, even multiple times. How to deal with the database dependency in its constructor?

An example can 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 possible solution is obvious: pass the database object to the EditController constructor and use $article = new Article($this->db).

Just as in the previous case with Logger and the file path, this is not the right approach. The database is not a dependency of the EditController, but of the Article. Passing the database goes against rule #2: take what's yours. If the Article class constructor changes (a new parameter is added), you will need to modify the code wherever instances are created. Ufff.

Houston, what do you suggest?

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. And therefore, we need something else to create and configure those more flexible classes for us. We will call it factories.

The rule of thumb is: if a class has dependencies, leave the creation of their instances to the factory.

Factories are a smarter replacement for the new operator in the world of dependency injection.

Please do not confuse with the factory method design pattern, which describes a specific way of using factories and is not related to this topic.

Factory

A factory is a method or class that creates and configures objects. We will name the class producing Article as ArticleFactory, and it could 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 an 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 react is the ArticleFactory itself. All other code working with Article objects, such as the EditController, will not be affected.

You might be wondering if we have actually made things better. The amount of code has increased, and it all starts to look suspiciously complicated.

Don't worry, soon we will get to the Nette DI container. And it has several tricks up its sleeve, which will greatly simplify building applications using dependency injection. For example, instead of the ArticleFactory class, you will only need to write a simple interface:

interface ArticleFactory
{
	function create(): Article;
}

But we're getting ahead of ourselves; please be patient :-)

Summary

At the beginning of this chapter, we promised to show you a process for designing clean code. All it takes is for classes to:

At first glance, these three rules may not seem to have far-reaching consequences, but they lead to a radically different perspective on code design. Is it worth it? Developers who have abandoned old habits and started consistently using dependency injection consider this step a crucial moment in their professional lives. It has opened the world of clear and maintainable applications for them.

But what if the code does not consistently use dependency injection? What if it relies on static methods or singletons? Does that cause any problems? Yes, it does, and very fundamental ones.

version: 3.x 2.x