Symfony 2 for PHP developers – Part 2

Dependency Injection is at the heart of Symfony 2. To understand Symfony 2 you need to understand Dependency Injection.

Fortunately for us, the principle of Dependency Injection is very simple. Rather than hard coding instantiations of objects into our classes we’ll pass ‘m in, something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Logger {
   public function write( $message ) {
      echo $message;
   }
}
 
class SmtpDriver {
   protected $logger;
 
   public function setLogger( $logger ) {
      $this->logger = $logger;
   }
 
   public send( $to, $from, $subject, $message ) {
      $this->logger->write("Sending SMTP email to {$to}...");
 
      // Sending email through SMTP...
   }
}
 
class Mailer {
   protected $logger;
   protected $driver;
 
   public function setLogger( $logger ) {
      $this->logger = $logger;
   }
 
   public function setDriver( $driver ) {
      $this->driver = $driver;
   }
 
   public function send( $to, $from, $subject, $message ) {
      $this->driver->send( $to, $from, $subject, $message );
   }
}

The above example demonstrates the principle of Dependency Injection. To use the above defined classes and make them into a program we do the following:

1
2
3
4
5
6
7
8
9
$logger = new Logger();
 
$driver = new SmtpDriver();
$driver->setLogger( $logger );
 
$mailer = new Mailer();
$mailer->setLogger( $logger );
$mailer->setDriver( $driver );
$mailer->send("john@example.com", "luke@example.com", "Dependency Injection Test", "A message...");

If you’re new to Dependency Injection then you might have to look at the above a few times but you will realize that the above example demonstrates a few very important principles.

The first is that the Mailer class doesn’t send the email but it uses a “driver” instead. In our case we have an SmtpDriver class which we “inject” into the Mailer class by calling the setDriver method on the $mailer instance. Now, this is very important because it means that the Mailer class doesn’t have a “dependency” on the StmpDriver class, it does have a dependency on a driver of some sort but as we’ll see soon, not the SmtpDriver class in particular. In fact, we could create a new class and call it MockDriver and pass that into the $mailer instance. This would be really handy during testing where we don’t want to actually send out real emails every time we run our tests but maybe just want to log a message.

Let’s look at an example of this:

1
2
3
4
5
6
7
8
9
10
11
class MockDriver {
   protected $logger;
 
   public function setLogger( $logger ) {
      $this->logger = $logger;
   }
 
   public send( $to, $from, $subject, $message ) {
      $this->logger->write("Sending mock email to {$to}...");
   }
}

As you can see, the MockDriver is very similar to the SmtpDriver but there is one big difference and that is the “send” method only calls the logger and doesn’t actually send the email.

To use the MockDriver, our program would look like this:

1
2
3
4
5
6
7
8
9
$logger = new Logger();
 
$driver = new MockDriver();
$driver->setLogger( $logger );
 
$mailer = new Mailer();
$mailer->setLogger( $logger );
$mailer->setDriver( $driver );
$mailer->send("john@example.com", "luke@example.com", "Dependency Injection Test", "A message...");

As you can see, the only difference in our program is the line:

$driver = new MockDriver();

Instead of:

$driver = new SmtpDriver();

This bring us to the second important principle; All the above means is that we’re controlling the functionality of our program from the “outside” at the highest level rather than from the inside at the lowest level. In other words, we’re using Inversion of Control.

If your brain just exploded, don’t worry. All will be fine.

One thing the careful observer might have noticed in the above example is that all our classes are POPO’s, i.e. Plain Old PHP Objects. There’s no Symfony in any of the above and this is exactly what we want. In the first code snippet where we defined the Logger, the SmtpDriver and the Mailer classes, we did just that, we defined classes. We could say we defined the “architecture” of our program but not the actual program itself, i.e, on their own the classes don’t do anything but when you “wire them up” it’s where you create your program. It’s this “wiring” that Symfony helps us with but we’ll get to that later.

There is one major problem with the code we have so far and that is that it’s fragile. By this I mean, the driver classes need to have a “send” method so we like to enforce this. Another problem is the duplication of injecting the logger functionality. So let’s revise these issues:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Logger {
   public function write( $message ) {
      echo $message;
   }
}
 
abstract class AbstractBase {
   protected $logger;
 
   public function setLogger( $logger ) {
      $this->logger = $logger;
   }
}
 
interface MailerDriverInterface {
   public function send( $to, $from, $subject, $message );
}
 
class SmtpDriver extends AbstractBase implements MailerDriverInterface {
   public send( $to, $from, $subject, $message ) {
      $this->logger->write("Sending SMTP email to {$to}...");
 
      // Sending email through SMTP...
   }
}
 
class MockDriver extends AbstractBase implements MailerDriverInterface {
   public send( $to, $from, $subject, $message ) {
      $this->logger->write("Sending mock email to {$to}...");
   }
}
 
class Mailer extends AbstractBase {
   protected $driver;
 
   public function setDriver( MailerDriverInterface $driver ) {
      $this->driver = $driver;
   }
 
   public function send( $to, $from, $subject, $message ) {
      $this->driver->send( $to, $from, $subject, $message );
   }
}

Our program is exactly the same as it was before. The only thing we’ve changed is that we’ve moved the functionality that was shared by all classes (the logger) to an abstract base class. The reason we’re making this class abstract is because we don’t want to allow instances to be created of this class. As you might have noticed, we named our abstract base class AbstractBase. By doing this we communicate the “intent” of the class by saying it’s a “base” class and that it’s “abstract”.

The second thing we did was to create an interface for the drivers that can be used on the Mailer class. In our case, the interface does nothing more than defining the “send” method but you can imagine that when your classes are more complex that an interface will force you to implement all the required functionality. An interface therefore acts like a contract and it will create stability within your architecture. To illustrate this, the setDriver method of the Mailer class now has a typed parameter. I.e. the parameter passed to the setDriver method needs to implement the MailerDriverInterface.

However, still no sign of Symfony. We’re soon going to change that in the next article…

This entry was posted in Programming and tagged , . Bookmark the permalink. Both comments and trackbacks are currently closed.