December 14, 2014
by Jason Roman





Categories:
Protips

Tags:
PHP  Symfony


Finding a Real-World Use Case for Setter Injection in Symfony

Despite being my least preferred method of injection, I discover a tangible reason for its use.

If you've played around with creating services in Symfony2, you've undoubtedly had to include some services and parameters (the entity manager, templating engine, etc.). Symfony offers three methods to inject these, and there's even the JMSDiExtraBundle which allows you to inject these via annotations.

I usually opt for constructor injection - I know the services and parameters have to exist when the class is created and they cannot be changed. With type hinting I can be sure everything is injected exactly as I intend. Take, for example, a service I created that generically handles some CRUD operations across all of my entities, with my initial implementation using constructor injection:

class AbstractCrudService
{
protected $logger;
protected $em;
protected $validator;

/**
* @param LoggerInterface $logger
* @param EntityManager $em
* @param ExceptionValidator $validator
*/
public function __construct(LoggerInterface $logger, EntityManager $em, ExceptionValidator $validator)
{
$this->logger = $logger;
$this->em = $em;
$this->validator = $validator;
}
Clean and simple, right? I created a few services that extended from this class, and all was well. However, I started to create some new services that extended this but required more. For a PayPal service, I needed to add the Guzzle client and a container parameter that held configuration data. This became:
class PayPalService extends AbstractCrudService
{
private $client;
private $config;

/**
* @param LoggerInterface $logger
* @param EntityManager $em
* @param ExceptionValidator $validator
* @param Client $client
* @param array $config
*/
public function __construct(
LoggerInterface $logger,
EntityManager $em,
ExceptionValidator $validator,
Client $client,
$config
) {
$this->logger = $logger;
$this->em = $em;
$this->validator = $validator;
$this->client = $client;
$this->config = $config;
}
That's all fine and dandy, but it feels really messy to me. If something changes in my base service I then have to change it in all of its extended classes. Once I got to over 5 services extending the base, I saw how this would be an issue. Plus, I only want to see my PayPal service-specific dependencies in my constructor; it is simpler to decipher what else the class requires. So, I reconfigured my base service to use setter injection:
abstract class AbstractCrudService
{
protected $logger;
protected $em;
protected $validator;

/**
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}

/**
* @param EntityManager $em
*/
public function setEntityManager(EntityManager $em)
{
$this->em = $em;
}

/**
* @param ExceptionValidator $validator
*/
public function setValidator(ExceptionValidator $validator)
{
$this->validator = $validator;
}
Now, obviously there's the disadvantage that these isn't set in the constructor so I cannot guarantee they have all been set, and they can also be overridden (which I don't necessarily want). However, I'm only using this service via Symfony's Dependency Injection component to make sure that the injection takes place when the service is called. I also declare the base service as abstract in its configuration, which means it cannot be retrieved directly from the container or passed into another service. This was a suitable compromise.

Now, my PayPalService constructor looks much cleaner and only contains its class-specific dependencies:
class PayPalService extends AbstractCrudService
{
private $client;
private $config;

/**
* @param Client $client
* @param array $config
*/
public function __construct(Client $client, array $config)
{
$this->client = $client;
$this->config = $config;
}
I find this implementation much cleaner. I know exactly what my PayPalService needs, and any changes to the base service only have to occur once. At PayAnywhere, we used constructor injection by default, but all of our base abstract services used setter injection.

You can also do the reverse and keep constructor injection in your parent class and use setter injection in your child classes if you prefer.

For further reading, checking out Symfony's documentation for managing dependencies with parent services.


comments powered by Disqus