This course is archived!
Injecting Config & Services and using Interfaces
We've already created our first service and used dependency injection, we're
even closer to getting this money out! One problem with the FriendHarvester
is that we've hardcoded the SMTP configuration inside of it:
// ... lines 1 - 15 | |
public function emailFriends() | |
{ | |
$mailer = new SmtpMailer('smtp.SendMoneyToStrangers.com', 'smtpuser', 'smtppass', '465'); | |
// ... lines 19 - 33 | |
} |
What if we want to re-use this class with a different configuration? Or what if our beta and production setups use different SMTP servers? Right now, both are impossible!
Injecting Configuration
When we realized that FriendHarvester
needed the PDO object, we injected
it via the constructor. The same rule applies to configuration. Add a second
constructor argument, which will be an array of SMTP config and update the
code to use it:
// ... lines 1 - 6 | |
class FriendHarvester | |
{ | |
private $pdo; | |
private $smtpConfig; | |
public function __construct($pdo, array $smtpConfig) | |
{ | |
$this->pdo = $pdo; | |
$this->smtpConfig = $smtpConfig; | |
} | |
public function emailFriends() | |
{ | |
$mailer = new SmtpMailer( | |
$this->smtpConfig['server'], | |
$this->smtpConfig['user'], | |
$this->smtpConfig['password'], | |
$this->smtpConfig['port'] | |
); | |
// ... lines 27 - 41 | |
} | |
} |
Back in app.php
, pass the array when creating FriendHarvester
:
// ... lines 1 - 10 | |
$friendHarvester = new FriendHarvester($pdo, array( | |
'server' => 'smtp.SendMoneyToStrangers.com', | |
'user' => 'smtpuser', | |
'password' => 'smtppass', | |
'port' => '465' | |
)); | |
// ... lines 17 - 18 |
When we try it:
php app.php
It still works! Our class is more flexible now, but, let's level up again!
Injecting the Whole Mailer
We can now configure the FriendHarvester
with different SMTP settings,
but what if we wanted to change how mails are sent entirely, like from SMTP
to sendmail? And what if we needed to use the mailer object somewhere else
in our app? Right now, we would need to create it anywhere we need it, since
it's buried inside FriendHarvester
.
In fact, FriendHarvester
doesn't really care how we're sending emails,
it only cares that it has an SmtpMailer
object so that it can call sendMessage()
.
So like with the PDO
object, it's a dependency. Refactor our class to pass
in the whole SmtpMailer
object instead of just its configuration:
// ... lines 1 - 6 | |
class FriendHarvester | |
{ | |
private $pdo; | |
private $mailer; | |
public function __construct($pdo, $mailer) | |
{ | |
$this->pdo = $pdo; | |
$this->mailer = $mailer; | |
} | |
public function emailFriends() | |
{ | |
// ... line 21 | |
foreach ($this->pdo->query($sql) as $row) { | |
$this->mailer->sendMessage( | |
// ... lines 24 - 32 | |
); | |
} | |
} | |
} |
Update app.php
to create the mailer object:
// ... lines 1 - 10 | |
$mailer = new SmtpMailer( | |
'smtp.SendMoneyToStrangers.com', | |
'smtpuser', | |
'smtppass', | |
'465' | |
); | |
$friendHarvester = new FriendHarvester($pdo, $mailer); | |
// ... lines 19 - 20 |
Try it out to make sure it still works:
php app.php
We would hate for our friends to miss this opportunity!
Once again, this makes the FriendHarvester
even more flexible and readable,
and will also make re-using the mailer possible. As a general rule, it's almost
always better to inject a service into another than to create it internally.
When you're in a service, think twice before using the new
keyword, unless
you're instantiating a simple object that exists just to hold data as opposed
to doing some job (i.e. a "model object").
Type-Hinting
One thing we've neglected to do is type-hint our two constructor arguments. Let's do it now:
// ... lines 1 - 6 | |
class FriendHarvester | |
{ | |
// ... lines 9 - 12 | |
public function __construct(\PDO $pdo, SmtpMailer $mailer) | |
// ... lines 14 - 35 | |
} |
This is totally optional, but has a bunch of benefits. First, if you pass something else in, you'll get a much clearer error message. Second, it documents the class even further. A developer now knows exactly what methods she can call on these objects. And third, if you use an IDE, this gives you auto-completion! Type-hinting is optional, but I highly recommend it.
Adding an Interface
Right now we're injecting an SmtpMailer
. But in reality, FriendHarvester
only cares that the mailer has a sendMessage()
method on it. But even if we
had another class with an identical method, like SendMailMailer
, for example,
we couldn't use it because of the specific type-hint.
To make this more awesome, create a new MailerInterface.php
file, which holds
an interface with the single send method that all mailers must have:
// ... lines 1 - 2 | |
namespace DiDemo\Mailer; | |
interface MailerInterface | |
{ | |
public function sendMessage($recipientEmail, $subject, $message, $from); | |
} |
Update SmtpMailer
to implement the interface and change the type-hint
in FriendHarvester
as well:
// ... lines 1 - 7 | |
class SmtpMailer implements MailerInterface | |
{ | |
// ... lines 10 - 59 | |
} |
// ... lines 1 - 4 | |
use DiDemo\Mailer\MailerInterface; | |
class FriendHarvester | |
{ | |
// ... lines 9 - 12 | |
public function __construct(\PDO $pdo, MailerInterface $mailer) | |
{ | |
// ... lines 15 - 16 | |
} | |
// ... lines 18 - 35 | |
} |
When you're finished, try the application again:
php app.php
Everything should still work just fine. And with any luck you will find a place for all of that annoying money.
Just like with every step so far, this has a few great advantages. First,
FriendHarvester
is more flexible since it now accepts any object that
implements MailerInterface
. Second, it documents our code a bit more.
It's clear now exactly what small functionality FriendHarvester
actually
needs. Finally, in SmtpMailer
, the fact that it implements an interface
with a sendMessage()
method tells us that this method is particularly
important. The class could have other methods, but sendMessage()
is
probably an especially important one to focus on.
I'm getting this error on challenge #3. I even copied the answers into the challenge to make sure I didn't miss anything and it didn't fix it.