Getting error returning HAL From An RPC Service

While returning HAL From an RPC Service I get the following error. What am I missing here.

Response is as pasted below:

{
    "status": 500,
    "title": "Unexpected error",
    "describedBy": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html",
    "detail": "Link from resource provided to Laminas\\ApiTools\\Hal\\Extractor\\LinkExtractor::extract was incomplete; must contain a URL or a route",
    "details": {
        "code": 0,
        "message": "Link from resource provided to Laminas\\ApiTools\\Hal\\Extractor\\LinkExtractor::extract was incomplete; must contain a URL or a route",
    …
}

Can you share the controller and action that produce this error message, please?

This happens when you have an incomplete Link instance associated with an entity or collection, so I need to see how you’re creating the resource you’re trying to represent in order to provide guidance on how to resolve the situation.

<?php
namespace Status\V1\Rpc\ListStatus;

use Laminas\Mvc\Controller\AbstractActionController;
use Status\V1\Service\StatusService;
use Laminas\ApiTools\ContentNegotiation\ViewModel;
use Laminas\ApiTools\Hal\Entity;
use Doctrine\Common\Collections\ArrayCollection;

class ListStatusController extends AbstractActionController
{
    /**
     * Entity manager.
     * @var Doctrine\ORM\EntityManager;
     */
    private $entityManager;

    private $statusService;

    /**
     * Constructor is used for injecting dependencies into the controller.
     */
    public function __construct($entityManager, $statusService)
    {
        $this->entityManager = $entityManager;
        $this->statusService = $statusService;
    }

    public function listStatusAction()
    {
        $statusses = $this->statusService->findStatusses();
        foreach($statusses as $s) echo($s->getMessage());
        //$statusCollection = new ArrayCollection($statusses);

        return new ViewModel([
            'payload' => $this->getPluginManager()->get('Hal')->createCollection($statusses)
        ]);
    }

}

Next: what type is $statusses in that example, and can you share the api-tools-hal.metadata_map configuration? In this particular case, it’s likely that you’re missing a route_name or resource_route_name entry, as those are what tell the LinkExtractor how to generate the URL for the collection and/or embedded resources.

    'api-tools-hal' => [
        'metadata_map' => [
            \Status\V1\Rest\Teststatus\TeststatusEntity::class => [
                'entity_identifier_name' => 'id',
                'route_name' => 'status.rest.teststatus',
                'route_identifier_name' => 'teststatus_id',
                'hydrator' => \Laminas\Hydrator\ObjectPropertyHydrator::class,
            ],
            \Status\V1\Rest\Teststatus\TeststatusCollection::class => [
                'entity_identifier_name' => 'id',
                'route_name' => 'status.rest.teststatus',
                'route_identifier_name' => 'teststatus_id',
                'is_collection' => true,
            ],
        ],
    ],

Regarding $statusses type:

 public function findStatusses()
    {
        return $this->entityManager->getRepository(TestStatus::class)->findAll();
        //else return new ApiProblemResponse(new ApiProblem(400, 'Invoice Document Data is INVALID !!!',null,null, $additionalDetails));
    }

Also sometimes resource files are generated and sometimes these files are not generated, is it a bug or I am missing something here? For example I could not find the resource files in the directory structure.

Resource files are only generated for REST resources, not for RPC, which is why you may not be seeing them in all scenarios.

I see two potential situations here:

  1. statusService::findStatusses() is returning an instance of TeststatusCollection, but that the individual items TeststatusCollection iterates over might not be TeststatusEntity instances. Try adding a 'resource_route_name' => 'status.rest.teststatus' item to the TeststatusCollection metadata map configuration and see if that resolves it.
  2. statusService::findStatusses() is returning an array of TestStatus instances, and thus there’s zero information whatsoever for the metadata map to work with. In that case, you have empty links and likely no embedded resources at all.

Figure out what statusService::findStatusses is actually returning.

As advised I have included

resource_route_name' => 'status.rest.teststatus

in meta config. But still it gives the same error. Since statusService::findStatusses() returns an array of TestStatus instances how do I convert it to Collection so that I can pass it to createCollection()?
As can be seen above I have tried it with ArrayCollection but still it don’t help.
The following in action method does produce the message string:

foreach($statusses as $s) echo($s->getMessage()); so I don’t think it is empty. I am clueless.

The problem you’re running into is that you’re returning something that the metadata map doesn’t have information on how to represent. It then does a best effort, but because pieces are missing (such as the route name), it fails.

The api-tools-hal module (and mezzio-hal, which operates similarly) requires you to return objects. The reason for this is so that the metadata map can figure out how to serialize them and provide links. In your case, there are two types it needs to know about:

  • An entity (your TestStatus instances)
  • A collection (an object that represents a bunch of entities of the same type)

Entities can be any object, but you need them to be unique within your metadata map so that api-tools-hal can map them to a hydrator (which will extract values from them for representation) and to a route (which is used to create a self-referential link in the representation).

Collections can be any iterable object, but, again, need to be unique within your metadata map so that api-tools-hal can map them to a route for purposes of generating the various links used in a collection (which includes “self”, but may also include “prev”, “next”, “first”, and “last” if the class type is a laminas-paginator type).

What you can do now is add an entry to your configuration in order to map your TestStatus class in your metadata map to a hydrator and a route. Use the TeststatusEntity mapping you had previously as a template, but make sure that the route name specified is correct, and that the hydrator is correct. (It looks like you’re using Doctrine, so you may be using one from the Doctrine module for this purpose.)

The next step is defining a type to represent your collection ot TestStatus instances. This can be as simple as something like class TestStatusCollection extends ArrayIterator {}, or you could use a paginator type. You will then do similarly to what you did in your listStatusAction(), where you create the instance from the array of returned TestStatus instances.

Once that’s in place, your code should work.

The take-aways:

  • Define explicit entity classes and map them in your api-tools-hal.metadata_map configuration, providing them with a hydrator, a route, a route identifier name (this is the name of the placeholder representing the identity in the route definition), and an entity identifier name (this is the extracted property name that represents the identity). These will be used to create representations that include a self-referential link.

  • Define explicit collection classes and map them in your api-tools-hal.metadata_map configuration,providing them with a route name.(The route_identifier_name and entity_identifier_name are unnecessary if you have explicit typed objects in the collection.) This information is used to ensure you can create a HAL representation of a collection.

Thanks for very informative description above. I think my existing code meets the criteria you have mentioned above. But there is one clarification that I need. I am not very clear about this statement of yours " You will then do similarly to what you did in your listStatusAction(), where you create the instance from the array of returned TestStatus instances."

Please refer to my screenshot below.

You have previously indicated that the findStatusses() method returns an array of TestStatus instances.

If that’s the case, then your listStatusAction() method will not work the way it’s currently coded, as the metadata map has nothing to go on:

  • You’re passing an array to the HAL plugin’s createCollection() method. When you do this, it returns a Laminas\ApiTools\Hal\Collection instance that has a self link, but no associated route, so it will create an error when generating that link.
  • If the items in that array are TestStatus instances as you noted in a previous message, there is no HAL metadata map entry for that class. (You have one for TeststatusEntity, but that’s a different class.) As such, the renderer will not know how to deal with these instances.

What I was suggesting was that you uncomment the line $statusCollection = new {collection class name}($statusses), but using the correct collection class as mapped in the HAL metadata map.