Search code examples
phpdoctrine-ormlaminas-api-tools

Apigility + Doctrine - ManyToMany fetch Eager not working from either side - data present


I have a setup with Country and Currency objects. A Country may have 0 or more Currency objects and vice versa there's the Currency that may be used by 0 or more Country objects. The problem is the data is not returned to the front-end (api requests).

In Country Entity

/**
 * @var Collection|ArrayCollection|Currency[]
 * @ORM\ManyToMany(targetEntity="Currency", inversedBy="countries", fetch="EAGER")
 * @ORM\JoinTable(
 *     name="country_country_currencies",
 *     joinColumns={@ORM\JoinColumn(name="country_id", referencedColumnName="id")},
 *     inverseJoinColumns={@ORM\JoinColumn(name="currency_id", referencedColumnName="id")}
 * )
 */
protected $currencies;

In Currency Entity

/**
 * @var Collection|ArrayCollection|Currency[]
 * @ORM\ManyToMany(targetEntity="Country", mappedBy="currencies", fetch="EAGER")
 */
protected $countries;

Both sides have a __construct() function setting the initial values to new ArrayCollection(). Both have their own get*(), add*() and remove*() functions.

When debugging the DoctrineResource::fetch() function on a single object (e.g. /currencies/45) the $entity object does contain the data when executing return $entity (last line of fethc()), so I know the data is there.

Shows debugging information

However, when it finally returns to the "front-end", I'm missing the data of the ManyToMany relation:

Shows missing data

As you can see above: countries is empty.

When requesting from the other side (from Country), the currencies is empty.


Additional info

Other similar questions had suggestions in comments or answers I'll put here straight away as I already checked those.

Doctrine caching - I ran the following commands:

  • ./vendor/bin/doctrine-module orm:clear-cache:query
  • ./vendor/bin/doctrine-module orm:clear-cache:result
  • ./vendor/bin/doctrine-module orm:clear-cache:metadata

I've made sure that Zend Framework caching is disabled (also, development mode is enabled via composer development-enable).

The Entity proxies generated have also been deleted multiple times to make sure they aren't the issue. I've tried also without fetch="EAGER" (without is the original actually), but that yields the same result.

Using wrong hydrator for Entities: both are configured to use the Doctrine EntityManager (same for both):

'zf-hal' => [
    'metadata_map' => [
        \Country::class => [
            'route_identifier_name' => 'id',
            'entity_identifier_name' => 'id',
            'route_name' => 'country.rest.doctrine.country',
            'hydrator' => 'Country\\V1\\Rest\\Country\\CountryHydrator',
            'max_depth' => 3,
        ],
        \Country\V1\Rest\Country\CountryCollection::class => [
            'entity_identifier_name' => 'id',
            'route_name' => 'country.rest.doctrine.country',
            'is_collection' => true,
        ],
        // other entity
    ],
],
// some more config
'zf-apigility' => [
    'doctrine-connected' => [
        \Country\V1\Rest\Country\CountryResource::class => [
            'object_manager' => 'doctrine.entitymanager.orm_default',
            'hydrator' => 'Country\\V1\\Rest\\Country\\CountryHydrator',
        ],
        // other entity
    ],
],
'doctrine-hydrator' => [
    'Country\\V1\\Rest\\Country\\CountryHydrator' => [
        'entity_class' => \Salesupply\Core\Country\Entity\Country::class,
        'object_manager' => 'doctrine.entitymanager.orm_default',
        'by_value' => true,
        'strategies' => [],
        'use_generated_hydrator' => true,
    ],
    // other entity
],

Another suggested the content negotiation type might be set to json instead of HalJson: both (everything really) is set to HalJson:

'zf-content-negotiation' => [
    'controllers' => [
        'Country\\V1\\Rest\\Country\\Controller' => 'HalJson',
        'Country\\V1\\Rest\\Currency\\Controller' => 'HalJson',
    ],
],

Solution

  • The ApiSkeletons vendor package has this by design. I've opened an issue on Github some months back.

    To ensure you receive back collections:

    • Create a strategy class and extend the AllowRemoveByValue strategy of Doctrine.
    • Overwrite the extract function to return a Collection, either filled or empty

    That's it.

    Full class:

    namespace Application\Strategy;
    
    use DoctrineModule\Stdlib\Hydrator\Strategy\AllowRemoveByValue;
    use ZF\Hal\Collection;
    
    class UniDirectionalToManyStrategy extends AllowRemoveByValue
    {
        public function extract($value)
        {
            return new Collection($value ?: []);
        }
    }
    

    Apply this strategy where you need it. E.g. your Advert as many Applications, so the config should be modified like so:

    'doctrine-hydrator' => [
        'Application\\V1\\Entity\\ApplicationHydrator' => [
            // other config
            'strategies' => [
                'relatedToManyPropertyName' => \Application\Strategy\UniDirectionalToManyStrategy::class,
            ],
        ],
    ],
    

    Now collections should be returned.


    Quick note: this will only work as a strategy for the *ToMany side.