zf2 pass arguments to service factory
Asked Answered
S

1

9

I have the following classes:

  • CommonService
  • ClientService
  • InvoiceService

And I would like to load the correct class with a factory (for DI) based on the url:

  • CommonService: localhost/service/common
  • ClientService: localhost/service/client
  • InvoiceService: localhost/service/invoice

For now I'm trying to avoid creating a factory for each one of my services, and I'd like to do this dinamically:

<?php namespace App\Service\Factory;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class ServiceFactory implements FactoryInterface
{
    /**
     * Create service
     * @param ServiceLocatorInterface $serviceLocator
     * @return \App\Service\AbstractService
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $servicename = ''; // how can I get something like this, based on the route ?
        $service = $serviceLocator->get('Service\' . $servicename . 'Service');
    }
}

I'd like to avoid, if possible, to compute the route inside the factory, because if one day this factory will be called from elsewhere, it won't work.

So how do you basically do a factory "deal with the problem of creating objects without specifying the exact class of object that will be created" with zend 2 ?

EDIT - USED SOLUTION

Edited again, here the final solution I preferred based on the accepted answer:

$apiName = str_replace(' ', '', ucwords(str_replace('_', ' ', $this->params('api'))));
$serviceName = str_replace(' ', '', ucwords(str_replace('_', ' ', $this->params('service'))));

$di = new Di();
$di->instanceManager()->setParameters(sprintf('App\Service\%s\%sService', $apiName, $serviceName), [
    'service' => sprintf('%s\Service\%sService', $apiName, $serviceName),
]);

$service = $di->get(sprintf('App\Service\%s\%sService', $apiName, $serviceName));

AbstractService (parent class of any service)

<?php namespace App\Service;

use Zend\Log\Logger;

abstract class AbstractService
{
    /**
     * @var object|mixed
     */
    protected $api;

    /**
     * @var \Zend\Log\Logger
     */
    protected $logger;

    /**
     * Constructor
     * @param mixed $service Api service class
     * @param Logger $logger Logger instance
     */
    public function __construct($service, Logger $logger)
    {
        $this->api = $service;
        $this->logger = $logger;
    }
}

Ideally, the $service parameter for the abstract constructor should be typed at least by interface, I'm working on it.

Zend\Di helps me defining the constructor api dynamically and that's all I wanted. AbstractFactory was easier to read, but as you pointed out, the fact that all abstract factories are invoked each time a

$serviceLocator->get()

is invoked it's not that great.

Snell answered 17/9, 2014 at 6:27 Comment(0)
A
8

It is possible to fetch the Request or even the Zend\Mvc\Controller\Plugin\Param instance from within a factory; from there you could then access the route parameters you require.

// within your factory
$params = $serviceLocator->get('ControllerPluginManager')->get('params');
$serviceName = $params->fromRoute('route_param_name');

Which would then lead to

$service = $serviceLocator->get('Service\' . $servicename . 'Service');

However this will not work as you expect; the call to the service managers get would then mean you would need another service factory to load the 'Service\' . $serviceName . 'Service' - So you would still need to create a factory for said service (if it's not an 'invoakable' class). Back to where you started!

Solutions?

  1. Create a factory for each service

Although you specifically said you didn't want to do this, I can only assume it's because you're being lazy! This is the way you should do so. Why? The services you listed seem unrelated, meaning they should each have different dependencies. The easiest way to inject these specific dependencies would be on a per service basis - It would take you less than 5 minuets (you wrote one in your question), your code will be simpler and you'll have a much better time when your requirements change.

  1. Create an abstract factory.

An abstract factory can be considered a “fallback” – if the service does not exist in the manager, it will then pass it to any abstract factories attached to it until one of them is able to return an object.

If your services are very similar (they have the same dependencies, but differ in configuration) you can create a abstract factory that would internally check the requested service's name (say Service\Client) and inject the required dependencies for that service.

use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\AbstractFactoryInterface;

class FooAbstractFactory implements AbstractFactoryInterface
{
    protected $config;

    public function getConfig(ServiceLocatorInterface $serviceLocator) {
        if (! isset($this->config)) {
            $config = $serviceLocator->get('Config');
            $this->config = $config['some_config_key'];
        }
        return $this->config;
    }

    public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        $config = $this->getConfig($serviceLocator);

        return (isset($config[$requestedName])) ? true : false;
    }

    public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        $config = $this->getConfig($serviceLocator);
        $config = $config[$requestedName];

        $className = $config['class_name'];

        // This could be more complicated
        return new $className($config, $this->getSomethingElseExample($serviceLocator));
    }
} 

Things to consider

  • This will only work if your services have the same constructor signature.
  • The service manager will 'check' if the abstract factory is responsible for creating your class on every call to get, meaning a (unnecessary) performance hit.
  • If one of your services changes you need to hack in this change and then re-test each service can still be created.

    1. Get complicated with Zend\Di

In summary this will allow you to define all your dependencies to your services in configuration, or if your services are well written (arguments are type hinted with standard naming conventions) via Reflection.

I've yet to see the need for this kind of complexity for DI; although check it out, in a very very large application the investment in time might be beneficial.

Edit

Another option would be to create a base factory class (implementing FactoryInterface)

abstract class DefaultServiceFactory implements FactoryInterface {

    public function createService(ServiceLocatorInterface $sl)
    {
        $className = $this->getClassName();

        return new $className(
            $this->getApiService($serviceLocator),
            $this->getFooService($serviceLocator)
        );
    }

    // Abstract or it could return the 'default' class name
    // depending on requirements
    abstract protected function getClassName();

    protected function getApiService(ServiceLocatorInterface $sl)
    {
        return $sl->get('Default/Api/Service');
    }

    protected function getFooService(ServiceLocatorInterface $sl)
    {
        return $sl->get('Default/Foo/Service');
    }
}

Unfortunately you wont be able to avoid writing a concrete class again. However, with most of the code being encapsulated within the base class, it might make your life that little bit easier.

class ApiServiceFactory extends DefaultServiceFactory
{
    protected function getClassName() {
        return '\Custom\Api\Service';
    }

    protected function getApiService(ServiceLocatorInterface $sl) {
        return $sl->get('Another/Api/Service');
    }
}

I would defiantly encourage you to invest in a factory per-class option, although there is really no problem using abstract factories, from my personal experience, at some point they wont cover the 'edge cases' and you'll need to go write a specific factory anyway. The above method would probably be the preferred way when the classes being created are of the same inheritance hierarchy.

Asis answered 17/9, 2014 at 13:23 Comment(5)
Hey, thanks a lot for your answer. My service classes are all the same, with same dependencies and same parent class. The only thing that differs it's the "real api" class that they are referring to. Do you think abstract factory is a bad way to achieve this ? I wanted to avoid factory for each service because all factories would be the same. Maybe I should check Zend\Di. What do you think (I'll update my uestion with a sample service class)Snell
(I will accept your answer btw, but I'd like to have your point of view on my case before doing that)Snell
An Abstract Factory certainly is a valid way to go; just keep in mind the downsides I noted. Another option, which I tend to favor is an 'abstract base factory' (not a AbstractFactoryInterface but abstract class BaseFactory implements FactoryInterface), which you could then extend just for the services that differ (again a class per service). I'll update my answer with another example.Asis
Thanks for your example, but I'm not a great fan of it, it force me again to create one factory per class (not really a factory so), I rather try the Zend\Di which seems so perfect to me, I'll accept your answer for pointing me to a godd direction (which is what I was asking :) ) and update my question with the solution I usedSnell
plus also, ZF itself does not seem to be a huge fan of Zend\Di. docs.zendframework.com/zend-servicemanager/migration/…Fenestra

© 2022 - 2024 — McMap. All rights reserved.