Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Dependency Injection Container

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Our project now has services, an interface, and is fully using dependency injection. Nice work! One of the downsides of DI is that all the complexity of creating and configuring objects is now your job. This isn't so bad since it all happens in one place and gives you so much control, but it is something we can improve!

If you want to make this easier, the tool you need is called a dependency injection container. A lot of DI containers exist in PHP, but let's use Composer to grab the simplest one of all, called Pimple. Add a require key to composer.json to include the library:

12 lines composer.json
{
... lines 2 - 3
"require": {
... line 5
"pimple/pimple": "1.0.*"
},
... lines 8 - 10
}

Make sure you've downloaded Composer, and then run php composer.phar install to download Pimple.

Go Deeper!

If you're new to Composer, check out our free The Wonderful World of Composer Tutorial.

Pimple is both powerful, and tiny. Kind of like having one on prom night. It is just a single file taking up around 200 lines. That's one reason I love it!

Create a new Pimple container. This is an object of course, but it looks and acts like an array that we store all of our service objects on:

24 lines app.php
... lines 1 - 7
$container = new Pimple();
... lines 9 - 24

Start by adding the SmtpMailer object under a key called mailer. Instead of setting it directly, wrap it in a call to share() and in an anonymous function. We'll talk more about this in a second, but just return the mailer object from the function for now:

24 lines app.php
... lines 1 - 9
$container['mailer'] = $container->share(function() {
return new SmtpMailer(
'smtp.SendMoneyToStrangers.com',
'smtpuser',
'smtppass',
'465'
);
});
... lines 18 - 24

To access the SmtpMailer object, use the array syntax again:

24 lines app.php
... lines 1 - 21
$friendHarvester = new FriendHarvester($pdo, $container['mailer']);
... lines 23 - 24

It's that simple! Run the application to spam... I mean send great opportunities to our friends!

php app.php

Shared and Lazy Services

We haven't fully seen the awesomeness of the container yet, but there are already some cool things happening. First, wrapping the instantiation of the mailer service in an anonymous function makes its creation "lazy":

24 lines app.php
... lines 1 - 9
$container['mailer'] = $container->share(function() {
... lines 11 - 16
});
... lines 18 - 24

This means that the object isn't created until much later when we reference the mailer service and ask the container to give it to us. And if we never reference mailer, it's never created at all - saving us time and memory.

Second, using the share() method means that no matter how many times we ask for the mailer service, it only creates it once. Each call returns the original object:

$mailer1 = $container['mailer'];
$mailer2 = $container['mailer'];

// there is only 1 mailer, the 2 variables hold the same one
$willBeTrue = $mailer1 === $mailer2;

Tip

The share() method is deprecated and removed since Pimple 2.0. Now, you simply need to use bare anonymous functions instead of wrapping them with share():

$container['session'] = function() {
    return new Session();
};

This is a very common property of a service: you only ever need just one. If we need to send many emails, we don't need many mailers, we just need the one and then we'll call send() on it many times. This also makes our code faster and less memory intensive, since the container guarantees that we only have one mailer. This is another detail that we don't need to worry about.

Now witness the Geek-Awesomeness of this fully armed and operational Container!

Let's keep going and add our other services to the container. But first, I'll add some comments to separate which part of our code is building the container, and which part is our actual application code:

28 lines app.php
... lines 1 - 7
/* START BUILDING CONTAINER */
$container = new Pimple();
$container['mailer'] = $container->share(function() {
return new SmtpMailer(
'smtp.SendMoneyToStrangers.com',
'smtpuser',
'smtppass',
'465'
);
});
$dsn = 'sqlite:'.__DIR__.'/data/database.sqlite';
$pdo = new PDO($dsn);
/* END CONTAINER BUILDING */
... lines 25 - 28

Let's add FriendHarvester to the container next:

32 lines app.php
... lines 1 - 20
$container['friend_harvester'] = $container->share(function() {
return new FriendHarvester($pdo, $container['mailer']);
});
... lines 24 - 32

That's easy, except that we somehow need access to the PDO object and the container itself so we can get two required dependencies. Fortunately, the anonymous function is passed an argument, which is the Pimple container itself:

35 lines app.php
... lines 1 - 20
$container['friend_harvester'] = $container->share(function(Pimple $container) {
return new FriendHarvester($container['pdo'], $container['mailer']);
});
... lines 24 - 35

To fix the missing PDO object, just make it a service as well:

35 lines app.php
... lines 1 - 24
$container['pdo'] = $container->share(function() {
$dsn = 'sqlite:'.__DIR__.'/data/database.sqlite';
return new PDO($dsn);
});
... lines 30 - 35

Now we can easily update the friend_harvester service configuration to use it:

35 lines app.php
... lines 1 - 20
$container['friend_harvester'] = $container->share(function(Pimple $container) {
return new FriendHarvester($container['pdo'], $container['mailer']);
});
... lines 24 - 35

With the new friend_harvester service, update the application code to just grab it out of the container:

35 lines app.php
... lines 1 - 32
$friendHarvester = $container['friend_harvester'];
$friendHarvester->emailFriends();

Now that all three of our services are in the container, you can start to see the power that this gives us. All of the logic of exactly which objects depend on which other object is abstracted away into the container itself. Whenever we need to use a service, we just reference it: we don't care how it's created or what dependencies it may have, it's all handled elsewhere. And if the constructor arguments for a service like the mailer change later, we only need to update one spot in our code. Nobody else knows or cares about this change.

Remember also that the services are constructed lazily. When we ask for the friend_harvester, the pdo and mailer services haven't been instantiated yet. Fortunately, the container is smart enough to create them first, and then pass them into the FriendHarvester constructor. All of that happens automatically, behind the scenes.

Configuration

But a container can hold more than just services, it can house our configuration as well. Create a new key on the container called database.dsn, set it to our configuration, and then use it when we're creating the PDO object:

35 lines app.php
... lines 1 - 11
$container['database.dsn'] = 'sqlite:'.__DIR__.'/data/database.sqlite';
... lines 13 - 26
$container['pdo'] = $container->share(function(Pimple $container) {
return new PDO($container['database.dsn']);
});
... lines 30 - 35

We're not using the share() method or the anonymous function because this is just a scalar value, and we don't need to worry about that lazy-loading stuff.

We can do the same thing with the SMTP configuration parameters. Notice that the name I'm giving to each of these parameters isn't important at all, I'm just inventing a sane pattern and using the name where I need it:

39 lines app.php
... lines 1 - 11
$container['database.dsn'] = 'sqlite:'.__DIR__.'/data/database.sqlite';
$container['smtp.server'] = 'smtp.SendMoneyToStrangers.com';
$container['smtp.user'] = 'smtpuser';
$container['smtp.password'] = 'smtp';
$container['smtp.port'] = 465;
$container['mailer'] = $container->share(function(Pimple $container) {
return new SmtpMailer(
$container['smtp.server'],
$container['smtp.user'],
$container['smtp.password'],
$container['smtp.port']
);
});
... lines 26 - 39

When we're all done, the application works exactly as before. What we've gained is the ability to keep all our configuration together. This would make it very easy to change our database to use MySQL or change the SMTP password.

Move Configuration into a Separate File

Now that we have this flexibility, let's move the configuration and service building into separate files altogether. Create a new app/ directory and config.php and services.php files. Require each of these from the app.php script right after creating the container:

16 lines app.php
... lines 1 - 4
/* START BUILDING CONTAINER */
$container = new Pimple();
require __DIR__.'/app/config.php';
require __DIR__.'/app/services.php';
/* END CONTAINER BUILDING */
... lines 13 - 16

Next, move the configuration logic into config.php and all the services into services.php. Be sure to update the SQLite database path in config.php since we just moved this file:

7 lines app/config.php
... lines 1 - 2
$container['database.dsn'] = 'sqlite:'.__DIR__.'/../data/database.sqlite';
$container['smtp.server'] = 'smtp.SendMoneyToStrangers.com';
$container['smtp.user'] = 'smtpuser';
$container['smtp.password'] = 'smtp';
$container['smtp.port'] = 465;

21 lines app/services.php
... lines 1 - 2
use DiDemo\Mailer\SmtpMailer;
use DiDemo\FriendHarvester;
$container['mailer'] = $container->share(function(Pimple $container) {
return new SmtpMailer(
$container['smtp.server'],
$container['smtp.user'],
$container['smtp.password'],
$container['smtp.port']
);
});
$container['friend_harvester'] = $container->share(function(Pimple $container) {
return new FriendHarvester($container['pdo'], $container['mailer']);
});
$container['pdo'] = $container->share(function(Pimple $container) {
return new PDO($container['database.dsn']);
});

Skinny Controllers and Service-Oriented Architecture

Awesome! We now have configuration, service-building and our actual application code all separated into different files. Notice how clear our actual app code is now - it's just one line to get out a service and another to use it.

If this were a web application, this would live in a controller. You'll often hear that you should have "skinny controllers" and a "fat model". And whether you realize it or not, we've just seen that in practice! When we started, app.php held all of our logic. After refactoring into services and using a service container, app.php is skinny. The "fat model" refers to moving all of your logic into separate, single-purpose classes, which are sometimes referred to collectively as "the model". Another term for this is service-oriented architecture.

In the real world, you may not always have skinny controllers, but always keep this philosophy in your mind. The skinnier your controllers, the more readable, reusable, testable and maintainable that code will be. What's better, a 300 line long chunk of code or 5 lines that use a few well-named and small service objects?

Auto-completion with a Container

One of the downsides to using a container is that your IDE and other developers don't exactly know what type of object a service may be. There's no perfect answer to this, since a container is very dynamic by nature. But what you can do is use PHP documentation whenever possible to explicitly say what type of object something is.

For example, after fetching the friend_harvester service, you can use a single-line comment to tell your IDE and other developers exactly what type of object we're getting back:

19 lines app.php
... lines 1 - 15
/** @var FriendHarvester $friendHarvester */
$friendHarvester = $container['friend_harvester'];
$friendHarvester->emailFriends();

This gives us IDE auto-complete on the $friendHarvester variable. Another common tactic is to create an object or sub-class the container and add specific methods that return different services and have proper PHPDoc on them. I won't show it here, but imagine we've sub-classed the Pimple class and added a getFriendHarvester() method which has a proper @return PHPDoc on it.

Leave a comment!

14
Login or Register to join the conversation
Default user avatar
Default user avatar Maksym Minenko | posted 5 years ago | edited

$di = new Pimple\Container();
$di['db.dsn'] = '...';

$di['db'] = function ($c) {
    return new \PDO($c['db.dsn']);
};

How on earth does this $c variable receive the container instance?? :)

1 Reply

Hey Maksym!

It's a *great* question :). Notice, that when you set $di['db'], you are not setting this *directly* to the PDO object, you are setting it to a callback function. To say it differently, after the code you have posted has executed (but before anything else has happened), the PDO instance has *not* been created yet: we've simply configured a Pimple\Container() object with a bunch of configuration and callback functions.

Later in your code, you will eventually want to *reference* your "db" service (e.g. $di['db']->someFunction()). At the *moment* that you do this, the Pimple\Container object will execute your callback function and pass itself (the container) as the first argument to that function. This whole setup is designed this way so that you can have a nice container full of objects, but can delay actually creating those object (for performance purposes) until (and unless) you actually need them.

Let me know if that helps!

Cheers!

2 Reply
Default user avatar
Default user avatar Maksym Minenko | weaverryan | posted 5 years ago

Hi, Ryan!

Thank you very much for replying!

Why does the Container pass itself as the first argument? I mean, how does it know that it's supposed to? Or are you trying to say that the creators of Pimple made it do so?
Ok, I feel like I'm getting closer now. :)
Could you pinpoint exactly where (like which files in the vendor folder) does this magic happen?

1 Reply

Hey Maksym,

When you call your service for the first time and service declared as a callback (e.g. as an anonymous function) then Pimple call this callback and inject itself to it, and then the value that returns called callback stores under the same service key, i.e. your anonymous function overwrites with a real object it returns. You can see it here. So Pimple always injects itself into the callback, but you can miss this first argument in callback if you don't need it.

Cheers!

2 Reply
Maksym D. Avatar
Maksym D. Avatar Maksym D. | posted 2 years ago

Hi. You used term "service-oriented architecture". Can you point me where where I can read more about this?

Reply

Hey Maksym D.!

Excellent question! I would check out the first few chapters of this tutorial - https://symfonycasts.com/sc... - it's really about object-oriented coding, but it touches on this topic. The short answer is that a "service oriented architecture" if one where you isolate a lot of your "functionality" into individual service classes.

But let me explain that a bit better (hopefully) :p. Imagine you have a complex page that builds a form, has validation rule and maybe sends an email on success. The simplest way to write that is to put all that code right in the same place (inside the "controller" if you're using a framework like Symfony).

A service-oriented architecture says:

> Hey! You should identity the individual "chunks" of functionality (e.g. validation rules or sending the email) and isolate each one into its own, reusable, standalone function. Except that, in modern object-oriented coding, we isolate these into methods inside a class (called a service class) instead of flat functions.

So a service-oriented architecture sounds really cool and amazing, but it's really a simple idea that you're probably already doing: isolate parts of your code their own class so that they can be re-used and tested. This gives you a bunch of "tools" (service classes) for all your functionality.

Let me know if that helps!

Cheers!

Reply
ladia Avatar
ladia Avatar ladia | posted 3 years ago | edited

Hello, I want to tell you that I love your course. however, I have a problem with the challenge when I want to pass it, I get this error:
<blockquote><( ! ) Parse error: syntax error, unexpected '$container' (T_VARIABLE) in sendHappy.php on line 17</blockquote>
the line is :


$container['happy_sender'] = function(Container $container) {
    return new HappyMessageSender($container['email_loader']); // line 17 is herre
};

i have try this but same error again:


$container['happy_sender'] = function() use (Container $container) {
    return new HappyMessageSender($container['email_loader']); // line 17 is herre
};

PS: i am new in your platform, i don't if here is a good place for talk about this but if no tell me where to do it.
thank you

Reply

Hi @Lacina!

This is a great place to talk and ask questions about this! And, I'm sorry you're hitting this error! So, hmm. I don't see anything wrong with the code you have - I even pasted it into my editor to be absolutely sure. It looks perfect! So, this is a mystery! Here are a few possible things:

A) I don't think this is the case, but you may have a syntax error on the line above these (maybe around line 14 or 15) and PHP is mistakenly telling you the problem is on line 17. This can happen, for example, if you forget a ";" at the end of the line. But if this were the cause, I think PHP would report the error on line 16.

B) If you copied and pasted this code form somewhere, it's possible that it contains some invisible whitespace characters. This is rare, but super hard to debug. The best way to see if this is the problem is to delete these lines completely and re-type them by hand.

C) Finally, what version of PHP are you using? You could hack in a phpinfo();die; in any file, and the output would show you the version. But you would need to be using a *pretty old version (maybe 5.3?) in order for this code to be invalid.

Let me know what you find out! And welcome to SymfonyCasts :).

Cheers!

Reply
Ferdinand geerman Avatar
Ferdinand geerman Avatar Ferdinand geerman | posted 3 years ago

Hi,

How bad is it to inject the container itself in a service? or to provide the container as a trait in an object.

$container['my_service'] = $funcion($c){
return MyService($c)
}

trait DIContainer{

public function getDIContainer(){
$containerObj = new DIContainer();
return $containerObj->getContainer();
}
}

I'm not sure this is possible with Pimple, and I can imagine you will then loose the overview of each service' dependencies.

It would be very nice (lazy :) ) to have access to all the services in an object.What's your opinion about this?

Reply

Hey Ferdinand geerman

Injecting the container is not recommended because you are just hiding the dependencies among your services. It's much much better to use DI (Dependency injection) instead :)

Cheers!

Reply
Y a J. Avatar

Checking pimple v.1for learning purposes, I see that this assertion appears on the tests

$serviceOne = $pimple['service'];
$serviceTwo = $pimple['service'];
$this->assertNotSame($serviceOne, $serviceTwo);

Which confuses me with the assertion appearing on the video:

$willBeTrue = $mailer1 === $mailer2;

Could you please elaborate a bit on it. Thanks.

Reply
Osagie Avatar

Hey Calamarino,

Hm, your example a bit out of the context, it's not clear how those service was defined. Probably you're talking about some kind of service factory :) I can check into it if you provide some links where do you see those tests.

In our case, we do suppose that no matter how many times you fetch the mailer service from the container - it will be the same object, that was instantiated on the first fetch.

Cheers!

Reply
Default user avatar
Default user avatar George007 | posted 5 years ago

When you start injecting Pimple into anonymous functions, doesn't it become a Service Locator pattern instead of DI?

Reply

Hey George007!

Ah, very good! Yes, indeed! Well, sort of :). Technically, yes: when you fetch a service from a container directly, that is service location. However, when you pass Pimple into the anonymous function ($container->share(function(Pimple $container) {})), you're really doing this so that you can configure the dependency injection of whatever service is being configured. Basically, Pimple's DI system depends on using Pimple as a service locator in this way. So, yes, this IS service location :). But, it's necessary due to how Pimple is built. When you use $container['some_service_id'] outside of the anonymous function (like we do fetch the friend_harvester), that definitely is service location :).

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

userVoice