Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Services and Dependency Injection

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 app is small now, but as it grows, the app.php file will get harder and harder to read. The best way to fix this is to separate each different chunk of functionality into different PHP classes and methods. Each of these classes is called a "service" and the whole idea is sometimes called Service-Oriented Architecture.

Create a new file in src/DiDemo called FriendHarvester.php, which will be responsible for sending the email to every lucky person in the database:

... lines 1 - 2
namespace DiDemo;
class FriendHarvester
{
... lines 7 - 10
}

Add the namespace so that it follows the directory structure and give it an emailFriends method:

... lines 1 - 6
public function emailFriends()
{
}

Copy in all of our logic into this new method:

... lines 1 - 10
$mailer = new SmtpMailer('smtp.SendMoneyToStrangers.com', 'smtpuser', 'smtppass', '465');
$sql = 'SELECT * FROM people_to_spam';
foreach ($pdo->query($sql) as $row) {
$mailer->sendMessage(
$row['email'],
'Yay! We want to send you money for no reason!',
sprintf(<<<EOF
Hi %s! We've decided that we want to send you money for no reason!
Please forward us all your personal information so we can make a deposit and don't ask any questions!
EOF
, $row['name']),
'YourTrustedFriend@SendMoneyToStrangers.com'
);
}
... lines 27 - 28

Go Deeper!

To learn more about PHP namespaces, check out our free PHP Namespaces in 120 Seconds tutorial

Tip

The namespace follows the directory structure so the the class is automatically autoloaded by Composer's autoloader. For more on how this all works, see Autoloading in PHP and the PSR-0 Standard.

And just like that, you've created your first service! Roughly speaking, a service is any PHP class that performs an action. Since this sends emails to our new soon-to-be-rich friends, it's a service.

Tip

An example of a PHP class that's not a service would be something that simply holds data, like a Blog class, with title, author and body fields. These are sometimes called "Model objects".

The app.php code gets pretty simple now: just instantiate the FriendHarvester and call the method:

13 lines app.php
... lines 1 - 5
use DiDemo\FriendHarvester;
... lines 7 - 10
$friendHarvester = new FriendHarvester();
$friendHarvester->emailFriends();

But when we try it:

php app.php

We get a huge error!

Once we've moved the code, we don't have access to the PDO object anymore. So how can we get it?

Accessing External Objects from a Service

This is our first important crossroads. There are a few cheating ways to do this, like using the dreaded global keyword:

... lines 1 - 8
public function emailFriends()
{
// NOOOOOOOO!!!!
global $pdo;
... lines 13 - 28
}

Don't use this. You could also make the $pdo variable available statically, by creating some class and then reference it:

19 lines app.php
... lines 1 - 10
class Registry
{
static public $pdo;
}
Registry::$pdo = $pdo;
$friendHarvester = new FriendHarvester();
... lines 18 - 19

... lines 1 - 8
public function emailFriends()
{
// Still NOOOOOOOO!!!!
$pdo = \Registry::$pdo;
... lines 13 - 28
}

The problem with both approaches is that our FriendHarvester has to assume the $pdo variable has actually been set and is available. Or to say it differently, when you use this class, you need to make sure any global or static variables it needs are setup. And the only way to know what the class needs is to scan the file looking for global or static variable calls. This makes FriendHarvester harder to understand and maintain, and much harder to test.

Our Friend Dependency Injection

Let's get rid of all of that and do this right.

Since FriendHarvester needs the PDO object, add a __construct() method with it as the first argument. Set the value to a new private property and update our code to use it:

... lines 1 - 6
class FriendHarvester
{
private $pdo;
public function __construct($pdo)
{
$this->pdo = $pdo;
}
public function emailFriends()
{
... lines 18 - 20
foreach ($this->pdo->query($sql) as $row) {
... lines 22 - 32
}
}
}

The FriendHarvester now makes a lot of sense: whoever instantiates it must pass us a $pdo variable. Inside this class, we don't care how this will happen, we just know that it will, and we can make use of it.

Tip

You can also type-hint the argument, which is a great practice. We'll talk more about this later:

public function __construct(\PDO $pdo)

This very simple idea is called Dependency Injection, and you just nailed it! Dependency injection means that if a class needs an object or some configuration, we force that information to be passed into that class, instead of reaching outside of it by using a global or static variable.

Back in app.php, we now need to explicitly pass the PDO object when instantiating the FriendHarvester:

13 lines app.php
... lines 1 - 10
$friendHarvester = new FriendHarvester($pdo);
... lines 12 - 13

Run it:

php app.php

Everything works exactly like before, except that we've moved our logic into a service, which makes it testable, reusable, and much more understandable for two reasons.

First, the class and method names (FriendHarvester::emailFriends()) serve as documentation for what our code does. Second, because we're using dependency injection, it's clear what our service might do, because we can see what outside things it needs.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

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

userVoice