2
votes

How can I resolve contentUrl field in MediaObject entity using the event system or any suitable approach

I have followed the Api-platform file upload documentation. The endpoint works fine with rest operations however the problem emerges when I switch to graphql. The contentUrl field in MediaObject is never resolved in graphql as in conventional rest. A scrutiny on symfony profiler reveals that Kernel::VIEW event is never dispatched and so I reckon ResolveMediaObjectContentUrlSubscriber is not notified to resolve contentUrl.

Graphql query

{
  mediaObjects {
    edges {
      node {
        id
        contentUrl
      }
    }
  }
}

Graphql output

{
  "data": {
    "mediaObjects": {
      "edges": [
        {
          "node": {
            "id": "/api/media_objects/1",
            "contentUrl": null
          }
        }
      ]
    }
  }
}

Using rest with Jsonld content-type the field contentUrl is resolved properly.

{
  "@context": "/api/contexts/MediaObject",
  "@id": "/api/media_objects",
  "@type": "hydra:Collection",
  "hydra:member": [
    {
      "@id": "/api/media_objects/1",
      "@type": "http://schema.org/MediaObject",
      "contentUrl": "/media/my_image.png"
    }
  ],
  "hydra:totalItems": 1
}

I want to get contentUrl field resolved in graphql as well. How best can I achieve it?

MediaObject entity class with reference to section Configuring the Entity Receiving the Uploaded File of the aip-platform documentation.

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Controller\CreateMediaObjectAction;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;

/**
 * Class MediaObject
 *
 * @ORM\Entity(repositoryClass="App\Repository\MediaObjectRepository")
 * @ORM\Table(name="media_object")
 * @ApiResource(
 *     iri="http://schema.org/MediaObject",
 *     normalizationContext={
 *         "groups"={"media_object_read"},
 *     },
 *     collectionOperations={
 *         "post"={
 *             "controller"=CreateMediaObjectAction::class,
 *             "deserialize"=false,
 *
 *             "validation_groups"={"Default", "media_object_create"},
 *             "swagger_context"={
 *                 "consumes"={
 *                     "multipart/form-data",
 *                 },
 *                 "parameters"={
 *                     {
 *                         "in"="formData",
 *                         "name"="file",
 *                         "type"="file",
 *                         "description"="The file to upload",
 *                     },
 *                 },
 *             },
 *         },
 *         "get",
 *     },
 *     itemOperations={
 *         "get",
 *         "delete",
 *     },
 * )
 * @Vich\Uploadable
 */
class MediaObject
{
    use TimestampableEntity;

    /**
     * @var int|null
     *
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     * @ORM\Id
     */
    protected $id;

    /**
     * @var string|null
     *
     * @ApiProperty(iri="http://schema.org/contentUrl")
     * @Groups({"media_object_read"})
     */
    public $contentUrl;

    /**
     * @var File|null
     *
     * @Assert\NotNull(groups={"media_object_create"})
     * @Vich\UploadableField(mapping="media_object", fileNameProperty="filePath")
     */
    public $file;

    /**
     * @var string|null
     *
     * @ORM\Column(nullable=true)
     */
    public $filePath;

    public function getId(): ?int
    {
        return $this->id;
    }
}

The documentation further recommends using the event system to resolve contentUrl field with reference to section Resolving the file URL of the documentation, I have the following event subscriber.

namespace App\EventSubscriber;

use ApiPlatform\Core\EventListener\EventPriorities;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use App\Entity\MediaObject;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Vich\UploaderBundle\Storage\StorageInterface;

final class ResolveMediaObjectContentUrlSubscriber implements EventSubscriberInterface
{
    private $storage;

    public function __construct(StorageInterface $storage)
    {
        $this->storage = $storage;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::VIEW => ['onPreSerialize', EventPriorities::PRE_SERIALIZE],
        ];
    }

    public function onPreSerialize(ViewEvent $event): void
    {
        $controllerResult = $event->getControllerResult();
        $request = $event->getRequest();

        if ($controllerResult instanceof Response || !$request->attributes->getBoolean('_api_respond', true)) {
            return;
        }

        if (!($attributes = RequestAttributesExtractor::extractAttributes($request)) || !\is_a($attributes['resource_class'], MediaObject::class, true)) {
            return;
        }

        $mediaObjects = $controllerResult;

        if (!is_iterable($mediaObjects)) {
            $mediaObjects = [$mediaObjects];
        }

        foreach ($mediaObjects as $mediaObject) {
            if (!$mediaObject instanceof MediaObject) {
                continue;
            }

            $mediaObject->setContentUrl($this->storage->resolveUri($mediaObject, 'file'));
        }
    }
}

I only changed the GetResponseForController event type-hint as it reported deprecated since symfony 4.3 and used the suggested ViewEvent and switching it back does not seem to solve the problem though. When I check the profiler after hitting the media_objects endpoint with a get operation, the given subscriber is only notified with REST API and never with grapqhl.

I suspect the issue is with kernel.view event not firing in grapql, I'm not sure if that's the expected behaviour or not with using graphql (I'm new to graphql). Any improvement or further clarification will be highly appreciated.

1
Post your ApiResource Annotations or the relevant yaml files for Rest and GraphQl, please - Mario 2002
i have a working solution for query Media Object and Media Object Collection but contentUrl is null on nested queries. If your are interested in the solution for the query of MediaObject i can post it here - NMathar

1 Answers

1
votes

I can confirm this issue with the approach described above.

It uses an EventSubscriber for the Symfony\Component\HttpKernel\KernelEvents:VIEW Event which is not supported by API-Platform for GraphQL:
https://api-platform.com/docs/core/events/#custom-event-listeners
Note: using Kernel event with API Platform should be mostly limited to tweaking the generated HTTP response. Also, GraphQL is not supported. For most use cases, better extension points, working both with REST and GraphQL, are available.

https://api-platform.com/docs/core/extending/
Kernel Events customize the HTTP request or response (REST only, other extension points must be preferred when possible)

I'am also looking for a Solution to this Problem, that also has to work in nested GraphQL Queries.

EDIT:

This is the solution I found so far:

Based on this Example: https://api-platform.com/docs/core/file-upload/#resolving-the-file-url

I used DataProviders: https://api-platform.com/docs/core/data-providers/

My DataProviders (one for Item and one for Collection, extend the ORM-DataProviders from API-Platform):

backend/src/DataProvider/DropFileItemDataProvider.php:

<?php
namespace App\DataProvider;
use ApiPlatform\Core\Bridge\Doctrine\Orm\ItemDataProvider;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use App\Entity\DropFile;
use Doctrine\Common\Persistence\ManagerRegistry;
use Vich\UploaderBundle\Storage\StorageInterface;

class DropFileItemDataProvider extends ItemDataProvider
{
    /**
     * @var StorageInterface
     */
    private $storage;

    public function __construct(
        ManagerRegistry $managerRegistry,
        PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
        PropertyMetadataFactoryInterface $propertyMetadataFactory,
        StorageInterface $storage,
        iterable $itemExtensions = []
    )
    {
        $this->storage = $storage;
        parent::__construct($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, $itemExtensions);
    }

    public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
    {
        return DropFile::class == $resourceClass;
    }

    public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
    {
        $dropFile = parent::getItem($resourceClass, $id, $operationName, $context);
        $dropFile->setUri($this->storage->resolveUri($dropFile, 'file'));
        return $dropFile;
    }
}

backend/src/DataProvider/DropFileCollectionDataProvider.php:

<?php
namespace App\DataProvider;
use ApiPlatform\Core\Bridge\Doctrine\Orm\CollectionDataProvider;
use App\Entity\DropFile;
use Vich\UploaderBundle\Storage\StorageInterface;
use Doctrine\Common\Persistence\ManagerRegistry;

class DropFileCollectionDataProvider extends CollectionDataProvider
{
    /**
     * @var StorageInterface
     */
    private $storage;

    public function __construct(
        ManagerRegistry $managerRegistry,
        StorageInterface $storage,
        iterable $collectionExtensions = []
    )
    {
        $this->storage = $storage;
        parent::__construct($managerRegistry, $collectionExtensions);
    }

    public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
    {
        return DropFile::class === $resourceClass;
    }

    public function getCollection(string $resourceClass, string $operationName = null, array $context = [])
    {
        $dropFiles =  parent::getCollection($resourceClass, $operationName, $context);
        foreach ($dropFiles AS $dropFile)
        {
            $dropFile->setUri($this->storage->resolveUri($dropFile, 'file'));
        }
        return $dropFiles;
    }
}

Note: My Entity uses a different Name and Property!