Expressive HAL problem with entity with underscore and ClassMethods

Hi,

I’ll try to explain my issue…

I have a “customer” table with “customer_id” and not only “id” for the primary key.
All my database tables fields name have “_” underscore when is necessary (user_id, create_date, is_active, …)

In general i use ClassMethods hydrator for all my entity, getCustomerId(), getUserId(), getName(), …

Routes example:

$app->get('/customers', App\Action\AllCustomerAction::class, 'customers.get');
$app->get('/customer/{id}', App\Action\CustomerAction::class, 'customer.get');

Metamap example:

MetadataMap::class => [
        [
            '__class__' => RouteBasedResourceMetadata::class,
            'resource_class' => Customer::class,
            'route' => 'customer.get',
            'extractor' => ClassMethodsHydrator::class,
        ],
        [
            '__class__' => RouteBasedCollectionMetadata::class,
            'collection_class' => EntityCollection::class,
            'collection_relation' => 'customer',
            'route' => 'customers.get',
        ],
    ]

When i request: localhost:8080/customer/1 all is fine, i got a JSON representation with HAL _links (self)

BUT when i try to retrieve all customers with localhost:8080/customers, i got this error:

Zend \ Expressive \ Router \ Exception \ InvalidArgumentException
Route customer.get expects at least parameter values for [id], but received []

What i understand is that expressive try to generate HAL links with “id” value from the route but it not exist because in my case is “customer_id” in my table and also because i use an ClassMethods hydrator he will not find customer_id variable…

All example that i found in documentation use all the time no underscore (_) in entity (id, name, title, …) and use ObjectProperty hydrator.

If i add a new “id” field in my table and define in my entity (getId(), setId()) all works!

I need little help to understand my mistake…

Do i have to stop using ClassMethods and use ObjectProperty hydrator in that case ?
How to specify that link between the query parameter {id} and the entity key “customer_id” in my case ?
May be my ClassMethods hydrator have bad configuration?

Someone or @matthew may be :slight_smile: can help me to clarify that ?

Thanks!

This is a case where you need to do some massaging of your hydrators. Specifically, you will want to create a custom hydrator that uses the underscore mapping naming strategy as described in the zend-hydrator documentation. Map your resources to the hydrator you create, and you should get better results.

Thanks @matthew for your answer, i didn’t knew this naming strategy but sadly it change nothing with my problem… is same result then with ObjectProperty or ClassMethods. Is like it accept only “id” parameter or nothing with “_” underscore for id relation in HAL.

Below i more complete example…

Database table fields:

  • customer_id
  • name

Routes:

$app->get('/customers', App\Action\AllCustomerAction::class, 'customers.get');
$app->get('/customer/{customer_id}', App\Action\CustomerAction::class, 'customer.get');

Entity:

namespace App\Entity;

class Customer
{ 
    public $customerId;
    public $name;
}

Hydrator:

namespace App\Hydrator;

use Zend\Hydrator\ObjectProperty;
use Zend\Hydrator\NamingStrategy\UnderscoreNamingStrategy;

class DefaultHydrator extends ObjectProperty
{
    public function __construct()
    {
        parent::__construct();
        $this->setNamingStrategy(new UnderscoreNamingStrategy());
        
        return $this;
    }
}

Model:

namespace App\Model\Factory;
...

class CustomerTableFactory
{
    public function __invoke($container)
    {
        // Get Adapter
        $dbAdapter  = $container->get(AdapterInterface::class);

        //$hydrator = new ClassMethods(true);
        //$hydrator = new ObjectProperty();
        $hydrator = new DefaultHydrator();
        $resultSet = new HydratingResultSet($hydrator, new Customer());

        // Create TableGateway
        $tableGateway = new TableGateway('customer', $dbAdapter, null, $resultSet);

        return new CustomerTable($tableGateway);
    }
}

namespace App\Model;
...

class CustomerTable
{
    protected $tableGateway;

    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    public function fetchOne($id)
    {
        $id = (int) $id;
        $rowset = $this->tableGateway->select(
            ['customer_id' => $id]
        );
        $row = $rowset->current();
        if (!$row) {
            return false;
        }

        return $row;
    }

    public function fetchAll()
    {
        $select = $this->tableGateway->getSql()->select();
 
        $paginatorAdapter = new DbSelect(
            $select,
            $this->tableGateway->getAdapter(),
            $this->tableGateway->getResultSetPrototype()
        );

        return new EntityCollection($paginatorAdapter);
    }
}

Collection:

namespace App\Collection;

use Zend\Paginator\Paginator;

class EntityCollection extends Paginator
{
}

MetadataMap:

return [
    MetadataMap::class => [
        [
            '__class__' => RouteBasedResourceMetadata::class,
            'resource_class' => Customer::class,
            'route' => 'customer.get',
            'extractor' => DefaultHydrator::class
        ],
        [
            '__class__' => RouteBasedCollectionMetadata::class,
            'collection_class' => EntityCollection::class,
            'collection_relation' => 'customer',
            'route' => 'customers.get',
        ],
    ]
];

Result for localhost:8080/customer/1 → OK

{
    "_links": {
        "self": {
            "href": "http://localhost:8080/customer/1"
        }
    }, 
    "customer_id": "1", 
    "name": "Test"
}

Result for localhost:8080/customers → FAIL

I tried with different hydrators, not working. BUT it works if by example i use the variable “id” instead of “customer_id” (in database, route, entity, …)

> Expressive\Router\Exception\InvalidArgumentException thrown with message “Route customer.get expects at least parameter values for [customer_id], but received []”

Stacktrace:
#40 Zend\Expressive\Router\Exception\InvalidArgumentException in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive-fastroute/src/FastRouteRouter.php:305
#39 Zend\Expressive\Router\FastRouteRouter:generateUri in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive-helpers/src/UrlHelper.php:98
#38 Zend\Expressive\Helper\UrlHelper:__invoke in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive-helpers/src/UrlHelper.php:128
#37 Zend\Expressive\Helper\UrlHelper:generate in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive-hal/src/LinkGenerator/ExpressiveUrlGenerator.php:38
#36 Zend\Expressive\Hal\LinkGenerator\ExpressiveUrlGenerator:generate in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive-hal/src/LinkGenerator.php:36
#35 Zend\Expressive\Hal\LinkGenerator:fromRoute in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive-hal/src/ResourceGenerator/RouteBasedResourceStrategy.php:54
#34 Zend\Expressive\Hal\ResourceGenerator\RouteBasedResourceStrategy:createResource in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive-hal/src/ResourceGenerator.php:142
#33 Zend\Expressive\Hal\ResourceGenerator:fromObject in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive-hal/src/ResourceGenerator/ExtractCollectionTrait.php:118
#32 Zend\Expressive\Hal\ResourceGenerator\RouteBasedCollectionStrategy:extractPaginator in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive-hal/src/ResourceGenerator/ExtractCollectionTrait.php:52
#31 Zend\Expressive\Hal\ResourceGenerator\RouteBasedCollectionStrategy:extractCollection in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive-hal/src/ResourceGenerator/RouteBasedCollectionStrategy.php:39
#30 Zend\Expressive\Hal\ResourceGenerator\RouteBasedCollectionStrategy:createResource in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive-hal/src/ResourceGenerator.php:142
#29 Zend\Expressive\Hal\ResourceGenerator:fromObject in /home/michael/AppTest/api/expressive/src/App/Action/AllCustomerAction.php:33
#28 App\Action\AllCustomerAction:process in /home/michael/AppTest/api/expressive/vendor/zendframework/zend-expressive/src/Middleware/LazyLoadingMiddleware.php:60

Thanks in advance!

Have a look at MapNamingStrategy: https://docs.zendframework.com/zend-hydrator/naming-strategy/map-naming-strategy/

After searching in the source code of zend-expressive-hal i finally found the solution! And was easy…

It exist a key in the MetadataMap config. By default it use id key and like i explained my primary key is customer_id. When HAL try to generate the links (collection), he try to find id for route generation… and in my case it didn’t work because the unique identifier for my object is customer_id (id not exist).

With the configured key below (resource_identifier, route_identifier_placeholder) now all is fine! :grinning:

MetadataMap::class => [
    [
        '__class__' => RouteBasedResourceMetadata::class,
        'resource_class' => Customer::class,
        'route' => 'customer.get',
        'extractor' => DefaultHydrator::class,
        'resource_identifier' => 'customer_id', <--- With this one
        'route_identifier_placeholder' => 'id' <-- Can also use this for the route parameter
    ],
    ...

Considering your trouble finding this information… would you be willing to author a “recipe” to include in the manual so we can make this more clear for users?

Of course @matthew, i think that it can be very useful for people. But how to proceed? I guess by proposing a commit on GitHub?

Yes! The repository is here: https://github.com/zendframework/zend-expressive-hal

Each repository has contributing guidelines and instructions, and they can be found in the docs/ directory.