Core: [Question] Adressing specific subresource in a one-to-many Doctrine relationship

Created on 21 Sep 2017  路  10Comments  路  Source: api-platform/core

_(the use-case seems more complex to me than stuff in the docs so if anybody has a hint, might help others too)_

Let's say I have a Image entity with a one-to-many ImageVariation relation, Image.variations: ImageVariation[].

I'd like to add a way to address one of those (say, by name), like

GET /images?variation=square

[
  {
    image_id: 1,
    variation: {
      name: square,
      // info related to image ID 1 and "square" variation
    }
  },
  {
    image_id: 2,
    variation: {
      name: square,
      // info related to image ID 2 and "square" variation
    }
  },
  ...
]

Seeing Image doesn't have a variation property, this will obviously need to be a method. But the method needs to select the proper variation on a parameter known, not just serialize the existing collection. Seeing it's one-to-many, not many-to-one, the existing solution for embedding relations doesn't apply here.

So, I'd need to:
1) adjust the Doctrine query, probably using QueryCollectionExtensionInterface, using the variation param
2) switch the serialization group dynamically (which will try to serialize the variation method in the first place), embedding the variation to serializer context
3) with ContextAwareNormalizerInterface and serialization context from 2), do custom normalization on ImageVariation instance selected in 1)

As you're much more versed in capabilities which API Platform provides, is this even the right approach for this problem? Am I missing something obvious?

TIA.

All 10 comments

If I understood this correctly you want 1 variation from a N variations array.

To get the results, you can indeed use an extension to alter the query so that it indeed returns the one you want in the array. I don't know the context (and may be not that clean) but you could also filter your relation array in an event listener (see below).

I would use an event listener {event: 'kernel.view'} that alters the $event->getControllerResult() (GetResponseForControllerResultEvent). Kinda depends how you fetch the single variation but this gives you the ability to alter your Image[].

For example, say you have:

class Image {
  // ... code here, $variations is a @ORM\ToMany

  /**
   * @ApiProperty
   * @Groups({"foo"})
   */
  public $variation;
}

You're done. Just populate your single variation in the controllerResult and it'll be serialized for the foo group.

Note my event listener has 20 priority (Our SerializeListener has 16 in the core).

@soyuka thank you for the reply, idea of just setting an additional property on the entity seems to be exactly what I need here instead of all the complexity I had in mind before. :+1:

Closing this.

@soyuka I've (kind of) followed your advice:

  1. implemented an extension which validates & joins the passed variation and
  2. added a public property Image.variation with a custom serialization group (which is only activated if a valid variation reference is provided)

What's left is to actually store the one selected ImageVariation from Image.variations[0] to Image.variation for which serializer normalizer seems to be proper (as it depends on serializer context to know it needs to actually do it) but I cannot enable it as it never gets passed an Image (top-level) instance, it's only getting ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator and other child entities.

I'm guessing API Platform has a normalizer which "takes over" for top-level entities. Does this mean I cannot register a custom serializer normalizer for exposed entities?

<?php
namespace ImageBundle\EventListeners;

use ImageBundle\Entity\Image;

class ImageVariationListener {
    public function onKernelView(GetResponseForControllerResultEvent $event) {
        $controllerResult = $event->getControllerResult();
        // $request = $event->getRequest(); if you want to do more validations for this event
        if (!is_array($controllerResult) || false === $controllerResult[0] instanceof Image) {
           return;
        }

        foreach ($controllerResult as $image) {
            $image->variation = $image->variations[0];
        }
    }
}
// services.yaml
services:
    _defaults:
        autoconfigure: true
        autowire: true
        public: false

    ImageBundle\EventListeners\ImageVariationListener:
        tags:
            - { name: kernel.event_listener, event: kernel.view, method: onKernelView, priority: 20}

This is how I do to alter the results before they get serialized. Still maybe there's a better way (use a DataProvider ?)

This runs into the same problem as the normalizer approach: I'm not getting an array but a Paginator instance, had to getIterator() on it.

Data provider seems like an overkill as I already have all the data I need, only wanted to move it to a better location which should be possible with the normalizer.

Anyway, this works as expected, but it's not pretty. Thanks again, if we ever run into each other on some conference, you're due a beverage of choice. :+1:

You should be able to loop on a paginator no (I mean it's Iterable)?

It doesn't allow for array access so you can't check $result[0].

Oh indeed! I'm not using this with a paginator and I have !isset($controllerResult[0]) in the first condition :p. Sorry 'bout that!

Yeah, I take offence that your help wasn't copy/paste ready. :P

Never copy/paste, always rewrite that's how I learn :smile:

Was this page helpful?
0 / 5 - 0 ratings