Symfony 2 for PHP developers – Part 3

So, now that we have spoken about the general direction in Part 1 and we talked about the Dependency Injection in Part 2, it’s now time to talk about “wiring”. I previously already mentioned wiring in Part 2 and we’re now going to look into what this actually means.

The term “wiring” is actually very descriptive of the function it performs because we’re literally going to “wire” services together. This is actually the first time we’re going to use Symfony 2.

Remember how we created our program in Part 2? We instantiated classes into objects and we then “injected” those instances into other instances. A pretty verbose exercise if you ask me. It looked something like this:

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...");

Even though this illustrates the point of Dependency Injection pretty accurate, this isn’t something you’d like to be typing in on a regular basis, and even more important, we are still hard coding class names into our program. There must be a better to do this.

If you weren’t using wiring you would probably create a factory of some sort to hold the above example code. It would solve the problem of not having to type all of that in over and over but the classes are still hard coded inside the factory which makes testing problematic. Luckily Symfony 2 is here to help us. Symfony 2 offers us what’s known as a Dependency Injection Container, or better known in Symfony 2 as, the Service Container.

Within the context of Symfony 2, all the classes we defined such as Logger, SmtpDriver and Mailer are known as “services”. In the Java world these services would be known as beans, or Java Beans, which are just POJO’s (Plain Old Java Objects) in the same we we have defined our classes as POPO’s (Plain Old PHP Objects) but in Symfony 2 we refer to these POPO classes as “services”. Not all classes in your application are necessarily services though, only the ones you’re going to be wiring up in the Service Container.

Let’s jump ahead and look at a typical service wiring example in Symfony 2. Here we’ll be wiring up the classes we defined in Part 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
services:
  service_logger:
    class: Logger

  service_driver_mock:
    class: MockDriver
    calls:
      - [setLogger, [@service_logger]]

  service_driver_smtp:
    class: SmtpDriver
    calls:
      - [setLogger, [@service_logger]]

  service_mailer:
    class: Mailer
    calls:
      - [setLogger, [@service_logger]]
      - [setDriver, [@service_driver_smtp]]

What you’re looking at is a YAML representation of the same things we did in our program of part 2 with the difference that we’re not actually instantiating anything. Instead, we’re defining a description on how our services (POPO’s) are used together. I.e, we’re creating a definition of our program and we specify the dependencies for each of the services by referencing other service definitions.

In the example above, four services are defined; service_logger, service_driver_mock, service_driver_smtp and service_mailer. For each of the service definitions we set a class attribute which means that whenever we ask the Service Container for an instance of this service it needs to instantiate an object of that particular class. I.e. when we ask the Container for service_logger it will return an instance of class Logger.

The next thing you would notice is that most services have a calls section defined. This means that whenever we ask the Service Container for this type of service, the Service Container will call those methods after instantiating the class. I.e. When we ask the Service Container for service_driver_mock the Service Container will create an instance of the MockDriver class, call the setLogger method and pass in an instance of the service_logger service. We specify that we want an “instance” of another service by prefixing it with an “@” e.g. @service_logger.

So, now that we have the wiring, or configuration, of our application set up we can start using it. In a Symfony 2 application we can now simply ask the container for the instance of a service, like so:

1
2
$mailer = $container->get('service_mailer');
$mailer->send("john@example.com", "luke@example.com", "Dependency Injection Test", "A message...");

This is the exact same program as the first example in this post. You can see how using a Service Container can safe you from quite some typing in code.

However, not having to type in so much code is actually just a side effect of using the Service Container. More importantly, we can easily test our code AND configure our program based on the context it runs in. E.g. when in development you might not want to send out real emails but simply write to a log file. Using a Service Container allows us to “configure” our application based on the context we run it in. So, let’s change our dependency injection definition to be based on parameters instead:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
services:
  service_logger:
    class: Logger

  service_driver_mock:
    class: MockDriver
    calls:
      - [setLogger, [@service_logger]]

  service_driver_smtp:
    class: SmtpDriver
    calls:
      - [setLogger, [@service_logger]]

  service_mailer:
    class: Mailer
    calls:
      - [setLogger, [@service_logger]]
      - [setDriver, [%mailer_driver%]]

It might be hard to notice but the only thing we changed from our original configuration is the injection of the @service_driver_smtp on the setDriver method of our service_mailer. This is because we don’t always want to inject the SMTP driver, we only want to do this in production mode. So, let’s now create our configurations files, one for development and one for production:

config_dev.yml

1
2
parameters:
  mailer_driver = service_driver_mock

config_prod.yml

1
2
parameters:
  mailer_driver = service_driver_smtp

With our configuration in place, depending on which mode we run in, we’re either sending out emails through SMTP or simply logging entries into the database.

As you can see, dependency injection combined with a service container is a very powerful feature. It allows you to configure your application from the outside by “wiring” your application together making your program less verbose and more testable.

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