Skip to content
This repository has been archived by the owner on Jan 21, 2020. It is now read-only.

When using a closure in the "route_params" config section the metadata_map in zf-hal the config isn't re-rendered correctly when making changes #95

Closed
benwaine opened this issue Apr 8, 2015 · 8 comments

Comments

@benwaine
Copy link

benwaine commented Apr 8, 2015

I needed to add a route parameter to a nested collection. I'm using doctrine so the objects returned from my controllers are entities. Entities have a reference to their parents so can be used to find the correct url parameters.

Eg - To obtain the the url: /list/:list_id/items[/:item_id]

I used the following zf-hal config:

    'zf-hal' => array(
        'metadata_map' => array(
            'List\\List\\List' => array(
                'entity_identifier_name' => 'id',
                'route_name' => 'lists.rest.list',
                'route_identifier_name' => 'list_id',
                'hydrator' => 'List\\List\\ListHydrator',
            ),
            'List\\List\\ListCollection' => array(
                'entity_identifier_name' => 'id',
                'route_name' => 'lists.rest.list',
                'route_identifier_name' => 'list_id',
                'is_collection' => true,
            ),
            'Lists\\V1\\Rest\\ListItem\\ListItemEntity' => array(
                'entity_identifier_name' => 'id',
                'route_name' => 'lists.rest.list-item',
                'route_identifier_name' => 'list_item_id',
                'hydrator' => 'Zend\\Stdlib\\Hydrator\\ArraySerializable',
            ),
            'Lists\\V1\\Rest\\ListItem\\ListItemCollection' => array(
                'entity_identifier_name' => 'id',
                'route_name' => 'lists.rest.list-item',
                'route_identifier_name' => 'list_item_id',
                'is_collection' => true,
            ),
            'List\\List\\ListItem' => array(
                'entity_identifier_name' => 'id',
                'route_name' => 'lists.rest.list-item',
                'route_identifier_name' => 'list_item_id',
                'hydrator' => 'Zend\\Stdlib\\Hydrator\\ArraySerializable',
                'route_params' => array(
                    'list_id' => function($obj) {
                        return $obj->getList()->getId();
                }
                ),
            ),
            ),
        ),
    )

When I make any change to any endpoint and the content is re-rendered:

    'zf-hal' => array(
        'metadata_map' => array(
            'List\\List\\List' => array(
                'entity_identifier_name' => 'id',
                'route_name' => 'lists.rest.list',
                'route_identifier_name' => 'list_id',
                'hydrator' => 'List\\List\\ListHydrator',
            ),
            'List\\List\\ListCollection' => array(
                'entity_identifier_name' => 'id',
                'route_name' => 'lists.rest.list',
                'route_identifier_name' => 'list_id',
                'is_collection' => true,
            ),
            'Lists\\V1\\Rest\\ListItem\\ListItemEntity' => array(
                'entity_identifier_name' => 'id',
                'route_name' => 'lists.rest.list-item',
                'route_identifier_name' => 'list_item_id',
                'hydrator' => 'Zend\\Stdlib\\Hydrator\\ArraySerializable',
            ),
            'Lists\\V1\\Rest\\ListItem\\ListItemCollection' => array(
                'entity_identifier_name' => 'id',
                'route_name' => 'lists.rest.list-item',
                'route_identifier_name' => 'list_item_id',
                'is_collection' => true,
            ),
            'List\\List\\ListItem' => array(
                'entity_identifier_name' => 'id',
                'route_name' => 'lists.rest.list-item',
                'route_identifier_name' => 'list_item_id',
                'hydrator' => 'Zend\\Stdlib\\Hydrator\\ArraySerializable',
                'route_params' => array(
                    'list_id' => Closure::__set_state(array(
                )),
                ),
            ),
            ),
        ),
    )

The following error is output:

Fatal error: Call to undefined method Closure::__set_state() in /vagrant/src/module/Lists/config/module.config.php on line 188 Call Stack: 0.0009 246960 1. {main}() /vagrant/src/public/index.php:0 0.0404 745696 2. Zend\Mvc\Application::init() /vagrant/src/public/index.php:51 0.1079 1902896 3. Zend\ModuleManager\ModuleManager->loadModules() /vagrant/src/vendor/zendframework/zendframework/library/Zend/Mvc/Application.php:252 0.1079 1903280 4. Zend\EventManager\EventManager->trigger() /vagrant/src/vendor/zendframework/zendframework/library/Zend/ModuleManager/ModuleManager.php:115 0.1080 1903840 5. Zend\EventManager\EventManager->triggerListeners() /vagrant/src/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207 0.1091 1926376 6. call_user_func() /vagrant/src/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468 0.1091 1926544 7. Zend\ModuleManager\ModuleManager->onLoadModules() /vagrant/src/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468 0.1186 2044184 8. Zend\ModuleManager\ModuleManager->loadModule() /vagrant/src/vendor/zendframework/zendframework/library/Zend/ModuleManager/ModuleManager.php:96 0.1225 2058304 9. Zend\EventManager\EventManager->trigger() /vagrant/src/vendor/zendframework/zendframework/library/Zend/ModuleManager/ModuleManager.php:174 0.1226 2058344 10. Zend\EventManager\EventManager->triggerListeners() /vagrant/src/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207 0.1231 2067064 11. call_user_func() /vagrant/src/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468 0.1231 2067096 12. Zend\ModuleManager\Listener\ConfigListener->onLoadModule() /vagrant/src/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468 0.1231 2067096 13. Lists\Module->getConfig() /vagrant/src/vendor/zendframework/zendframework/library/Zend/ModuleManager/Listener/ConfigListener.php:126 0.1246 2093928 14. include('/vagrant/src/module/Lists/config/module.config.php') /vagrant/src/module/Lists/src/Lists/Module.php:17

PHP Version:

vagrant@vagrant-ubuntu-trusty-64:/vagrant/src/tests/behavioural$ php -v
PHP 5.5.9-1ubuntu4.7 (cli) (built: Mar 16 2015 20:47:39) 
Copyright (c) 1997-2014 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2014 Zend Technologies
    with Zend OPcache v7.0.3, Copyright (c) 1999-2014, by Zend Technologies
    with Xdebug v2.2.3, Copyright (c) 2002-2013, by Derick Rethans

Modules:

vagrant@vagrant-ubuntu-trusty-64:/vagrant/src/tests/behavioural$ php -m
[PHP Modules]
bcmath
bz2
calendar
Core
ctype
curl
date
dba
dom
ereg
exif
fileinfo
filter
ftp
gettext
hash
iconv
intl
json
libxml
mbstring
mhash
mysql
mysqli
openssl
pcntl
pcre
PDO
pdo_mysql
Phar
posix
readline
Reflection
session
shmop
SimpleXML
soap
sockets
SPL
standard
sysvmsg
sysvsem
sysvshm
tokenizer
wddx
xdebug
xml
xmlreader
xmlwriter
Zend OPcache
zip
zlib

[Zend Modules]
Xdebug
Zend OPcache

Any one have an idea what could be going on?

@benwaine benwaine changed the title When using a closure in the "route_params" config section of zf-hal the config isn't re-rendered correctly when making changes When using a closure in the "route_params" config section the metadata_map in zf-hal the config isn't re-rendered correctly when making changes Apr 8, 2015
@benwaine
Copy link
Author

benwaine commented Apr 8, 2015

Additional info from composer.lock

            "name": "zfcampus/zf-apigility",
            "version": "1.0.2",
            "source": {
                "type": "git",
                "url": "https://github.com/zfcampus/zf-apigility.git",
                "reference": "1bd864875d1b23e3db4923849f0c2e16fd820538"
            }
            "name": "zfcampus/zf-hal",
            "version": "1.0.4",
            "source": {
                "type": "git",
                "url": "https://github.com/zfcampus/zf-hal.git",
                "reference": "7f64cd827c1b0c11a7d92789840b996e4f7cbcf3"
            },

@weierophinney
Copy link
Member

This is why we do not recommend using closures in configuration, particularly with Apigility; the UI is serializing and writing to those files directly!

To be honest, I'm surprised you can use a closure there at all, and am trying to determine how that works; I don't see anywhere where we call the closure to get the value anywhere in zf-hal or ZF2!

Until I can figure that out, one possible way to make this work is to modify the value during configuration merging.

@benwaine
Copy link
Author

benwaine commented Apr 9, 2015

Here is where it happens in ZF-Hal:

https://github.com/zfcampus/zf-hal/blob/master/src/Plugin/Hal.php#L1329

I found it when looking through the tests:

https://github.com/zfcampus/zf-hal/blob/master/test/Plugin/HalTest.php#L844

What's the recommended approach to generate links from a complex object if not using closures?

Thanks

Ben

@benwaine
Copy link
Author

benwaine commented Apr 9, 2015

Digging into this a little more:

In ZF-Hal metadata map route_parameters are used when creating the 'self' link. These route parameters can be closures. However where links are specified in the metadata map and a route name and params are specified, closures may not be used in the params.

This is due to the behaviour of Hal::marshalSelfLinkFromMetadata (called when creating the self link) vs Hal::marshalMetadataLinks the former calls any closures passed as route_parameters where the later does not.

When an entity with an embedded link (not self) hits the Hal::renderEntity method any parameters which are closures have not had their values swapped for the result of calling the closure.

Would you accept a pull request to fix this behaviour?

@Wilt
Copy link

Wilt commented Apr 10, 2015

I would also like support for rendering closures for other links then self links.
I also made a pull request for generalizing the rendering of links from metadata other then self links. This has been merged already in the develop branch. With the $relation param you can now render links other then self links with this method...

To solve my rendering of closures in the metadata correctly I now I made a RenderLinksListener that does this resolving of the route parameters for me. This listener has a method resolveRouteParams like this:

    /**
     * @param $entity
     * @param LinkCollection $halLinks
     */
    public function resolveRouteParams($entity, LinkCollection &$halLinks)
    {
        /** @var Link $link */
        foreach($halLinks as $link)
        {
            $params = $link->getRouteParams();

            // process any callbacks
            foreach ($params as $key => $param) {
                // bind to the object if supported
                if ($param instanceof Closure
                    && version_compare(PHP_VERSION, '5.4.0') >= 0
                ) {
                    $param = $param->bindTo($entity);
                }

                // pass the object for callbacks and non-bound closures
                if (is_callable($param)) {
                    $params[$key] = call_user_func_array($param, array($entity));
                    if($params[$key] === false || $params[$key] === null){
                        $relation = $link->getRelation();
                        $halLinks->remove($relation);
                    }
                }
            }

            $link->setRouteParams($params);
        }
    }

Maybe this could be a (temporary) solution for @benwaine too...

@benwaine
Copy link
Author

@Wilt Thanks - I'll take a look at this when I'm back in the office on Monday. Looks like what I need.

@benwaine
Copy link
Author

@Wilt - fixed my problem. Look forward to using your real fix in the next point release!

@Wilt
Copy link

Wilt commented Apr 14, 2015

Good to hear it worked for you. Not so sure about this real fix you are mentioning. I think you misinterpreted me, no real fix there yet...

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants