Search code examples
phpzend-framework2laminas-api-toolshal

How to register a custom hydrator in Zend Framework 2?


In an Apigility driven ZF2 application I want to use a custom Hydrator.

Module class

class Module implements ApigilityProviderInterface {
    ...
    public function getServiceConfig() {
        return array(
            'factories' => array(
                ...
                'MyNamespace\\Hydrator\\ProjectHydrator' => function(ServiceManager $serviceManager) {
                    $projectHydrator = new ProjectHydrator();
                    $projectHydrator->setImageService($serviceManager->get('Portfolio\V2\Rest\ImageService'));
                    return $projectHydrator;
                }
            ),
            ...
        );
    }
}

module.config.php

...
'zf-hal' => array(
    'metadata_map' => array(
        ...
        'Portfolio\\V2\\Rest\\Project\\ProjectEntity' => array(
            'entity_identifier_name' => 'id',
            'route_name' => 'portfolio.rest.project',
            'route_identifier_name' => 'id',
            // 'hydrator' => 'Zend\\Stdlib\\Hydrator\\ClassMethods',
            'hydrator' => 'MyNamespace\\Hydrator\\ProjectHydrator',
        ),
        ...
    ),
),
...

It's ignored, when a collection get retrieved, but it's another issue (s. here). When a single entity is required, the hydratin mechanism starts, but it doesn't use my factory, in order to create an instance.

After some debugging I came to this place in the ZF\Hal\Metadata\Metadata#setHydrator(...) code:

if (is_string($hydrator)) {
    if (null !== $this->hydrators
        && $this->hydrators->has($hydrator)
    ) {
        $hydrator = $this->hydrators->get($hydrator);
    } elseif (class_exists($hydrator)) {
        $hydrator = new $hydrator(); // <-- here
    }
}

The custom Hydrator gets created directly. (In my case it causes a fatal error, since it's tried then to execute a method on ProjectHydrator#imageService, that is not set). I took a look at the Metadata#hydrators (of type Zend\Stdlib\Hydrator\HydratorPluginManager) and found only four default invocables, that's why null !== $this->hydrators && $this->hydrators->has($hydrator) is false and a direct instantiating is tried.

So, I guess, I have to register my custom hydrator. Where/how can I do this?


EDIT:

I move the factory code from Module#getServiceConfig() to Module#getHydratorConfig():

public function getHydratorConfig() {
    return array(
        'factories' => array(
            // V2
            'MyNamespace\\Hydrator\\ProjectHydrator' => function(ServiceManager $serviceManager) {
                $projectHydrator = new ProjectHydrator();
                $projectHydrator->setImageService($serviceManager->get('Portfolio\V2\Rest\ImageService'));
                return $projectHydrator;
            }
        ),
    );
}

The same over the config array in the module.config.php (needs an Factory class):

module.config.php

return array(
    ...
    'hydrators' => array(
        'factories' => array(
            'MyNamespace\\Hydrator\\ProjectHydrator' => 'MyNamespace\\Hydrator\\ProjectHydratorFactory',
        ), 
    ),
);

But its ends with an error:

Zend\ServiceManager\Exception\ServiceNotFoundException

File: /var/www/my-project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:550 Message: Zend\Stdlib\Hydrator\HydratorPluginManager::get was unable to fetch or create an instance for Portfolio\V2\Rest\ImageService


Solution

  • What you did in your edit using getHydratorConfig was correct, the error message you're seeing is caused because within your factory method you're attempting to fetch your image service from the hydrator plugin manager.

    The solution is simple, as with other plugin managers you need to call getServiceLocator() on the hydrator manager instance in order to get the main service locator (aka the service manager)

    So, a small change to your factory method should fix the issue ...

    public function getHydratorConfig() {
        return array(
            'factories' => array(
                // V2
                'MyNamespace\\Hydrator\\ProjectHydrator' => function(ServiceManager $serviceManager) {
                    $projectHydrator = new ProjectHydrator();
                    // get the image service from the main service locator
                    $imageService = $serviceManager->getServiceLocator()->get('Portfolio\V2\Rest\ImageService');
                    $projectHydrator->setImageService($imageService);
                    return $projectHydrator;
                }
            ),
        );
    }
    

    How it works: The instance passed to the closure as argument is a Zend\Stdlib\Hydrator\HydratorPluginManager. The HydratorPluginManager inherits from the Zend\ServiceManager\AbstractPluginManager, that is a child of the Zend\ServiceManager\ServiceManager. But the method ServiceManager#get(...) gets logically overridden in the HydratorPluginManager and provides only hydrators. Nevertheless it's parent class implements the Zend\ServiceManager\ServiceLocatorAwareInterface, so we access to its internal ServiceLocator and over the ServiceLocator to the entire set of the available services.

    As an aside, I usually prefer to name the serviceLocator variable in the factory method ($serviceManager in your code) so that it reflects the actual plugin manager in use, so for a hydrator factory, $hydrators, for a form factory, $formElemements, and so forth. I reserve use of $services to refer to the main service manager only. I find doing this is a helpful reminder that a getServiceLocator() call is necessary for any dependencies not located in that particular manager.