Why to use a Service on ZF2?

2019-07-15 05:53发布

问题:

I'm trying to understand some principles of the ZendFramework 2, and I want to know why to inject services on the ZF2 module.config.php if I can load instance a object using composer autoloader. The same thing for zend factories, why to use this if I can easily implement a Factory class and call it in a controller?

回答1:

To be clear up front - the ServiceLocator is not a must use way for creating objects. You still can create objects directly, and in quite a few cases this is a perfectly valid approach.

With that being said, there are a few reasons why you want to use the ServiceLocator for some objects. Unfortunately the Wikipedia Page on this doesn't help much with understanding the reasons to use a service locator. I found the description and examples here much more useful.

For the remainder of this, I'll use the terms class and service interchangeable. I'll name the class/object requiring a service Consumer, the factory responsible for creating a service a factory.

There is one very important fact to know about service managers. The service name does not need to be a class name. It can be any string (e.g. the services' name).

The major reasons why you want to use a service locator are:

  • Use the same instance of a service and many places (shared services)
  • Abstract the means of service constructions from the consumer (is it created by a factory or instantiated directly)
  • Decouple modules from each other
  • Create abstract services (identified by any string), and let others decide on the concrete implementation of this service
  • Extend service creation using delegators

Re-use services

This is probably the easiest to see. Typically you have a service and want the same instance to be used across your application. Use-cases for this could be anything from a Configuration service to Caching Service.

We sure could use a Singleton for making sure that only one instance is created globally - and this would work as well. But this doesn't provide the benefits of the other arguments following.

In Zf2, all services are shared by default. So each time you request a service by name, you will get the same instance. This can be turned of on a per-service level (that's the shared config key for in the service_manager configuration).

Abstract means of creation Ideally, the consumer should not need to know how a required service is to be created. This makes it more decoupled from the required service. All it needs to know, is to say I require an instance of this service.

Using a service manager, you can achieve this: you ask the service manager to fetch a specific service, But you don't care how the service manager actually creates this. It could be that the service manager instantiates it directly. It could be a factory. The consumer doesn't care - all it only cares about the result, an instance of that service.

The benefit of doing so is that you can replace the mean of creating that service transparently and don't have to update the consumer code to the new way of creating that service.

Decoupling modules

Hiding the way of how a service is created leads to decoupling modules. Let's assume you have two modules Cache and User. The user module requires a Cache Service from the Cache module. Instead of having to rely on the Cache Module and implementation directly, you hide that fact by a service named cache. The user module doesn't care at all how the Cache module creates that service. In fact we could even replace the Cache module with another module AdvancedCache providing the same service, without having to update the User module. The User module isn't tied to a particular module - only the the existence of a service named cache.

Abstract service names

In the previous example, I already introduced this concept. Each service has a global name - a simple string. In many cases that is the class name, but we are not limited to this.

To stick the the previous example of a cache service: in here we didn't specify which type of cache the User Module actually wants - it doesn't care. It could be an array (in-memory) cache, a file cache, or memcache cache.

In some other place we decide which cache we want to use globally and assign that concrete implementation to the name of cache. All modules requiring that cache service now automatically receive that implementation without needing to be updated.

In Zf2, this is used quite extensively. Take the Request Service for example. In many cases this is an instance of the Http\Request. But when using the CLI, this would be an Console\Request instead. But when requesting the Request service, we literally don't care about which one re receive, as long as they behave the same.

Another example for this would be the Renderer. Depending on the environment we want to render the output for, we receive a different implementation (HTML/Json/Console usage). But our consumer doesn't need to know which concrete implementation we are rendering for/.

Delegators

Delegators can be used to extend a factory. I'll leave this out of scope for now though as this is a more advanced use-case. So much be said, you can extend/proxy service creations upon creation time.



回答2:

In completion of @Fge answer iI will show you an example I experimented myself on how service manager can be useful, and when it's not.

I make a dynamic form wich on a few elements I had to add some filters and some validators.

I show you my custom service's method that doi this job for me :

public function setValidators($form, $name, $validator, $nameFieldset)
{
    $optionsSpace = [
        'translator' => $this->translator,
        'type' => NotEmpty::SPACE
    ];
    $optionsString = [
        'translator' => $this->translator,
        'type' => NotEmpty::STRING
    ];
    $optionsDigits = [
        'translator' => $this->translator,
    ];
    $form->getInputFilter()->get('items')
        ->get($nameFieldset)
        ->get($name)
        ->setRequired(true)
        ->getValidatorChain()
        ->attach($validator, true, 1)
        ->attach(new NotEmpty($optionsSpace), true, 2)
        ->attach(new NotEmpty($optionsString), true, 2)
        ->attach(new Digits($optionsDigits), true, 2);
    return $form;
}

You can see that I use some arrays, and some Zend\Validators. You also see that I use $this->translator on each options Array that I pass to those validators. Why I pass the translator ? Let's have a look to the validator :

class NotEmpty extends AbstractValidator

And the abstract Validator

abstract class AbstractValidator implements
    Translator\TranslatorAwareInterface,
    ValidatorInterface

If you use the service manager the TranslatorAwareInterface will be instanciated by an initializer. Wich, if you declared the new validator directly in you service, will not be the case, so you have to set it in your options.