Skip to content

Use decorated serializer to resolve url of uploaded files #916

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 35 additions & 34 deletions core/file-upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,64 +164,65 @@ final class CreateMediaObjectAction
Returning the plain file path on the filesystem where the file is stored is not useful for the client, which needs a
URL to work with.

An [event subscriber](events.md#custom-event-listeners) could be used to set the `contentUrl` property:
We will be [decorating a serializer](serialization.md#decorating-a-serializer-and-adding-extra-data) to resolve the `contentUrl` property:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That section you reference is wrongly named. It should be "decorating a normalizer".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@teohhanhui What do you mean? I took both the title and the link from the existing documentation.

(If we move away from the decoration, then this section has to be updated anyway.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be renamed, but in another PR as here it's just a link.


```php
<?php
// api/src/EventSubscriber/ResolveMediaObjectContentUrlSubscriber.php
// api/src/Serializer/MediaObjectContentUrlNormalizer.php

namespace App\EventSubscriber;
namespace App\Serializer;

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\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Vich\UploaderBundle\Storage\StorageInterface;

final class ResolveMediaObjectContentUrlSubscriber implements EventSubscriberInterface
final class MediaObjectContentUrlNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just name it MediaObjectNormalizer 😄

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to match the previous name somewhat, but shortening it is fine with me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename then!

{
private $decorated;
private $storage;

public function __construct(StorageInterface $storage)
public function __construct(NormalizerInterface $decorated, StorageInterface $storage)
{
if (!$decorated instanceof DenormalizerInterface) {
throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class));
}

$this->decorated = $decorated;
$this->storage = $storage;
}

public static function getSubscribedEvents(): array
public function supportsNormalization($data, $format = null)
{
return [
KernelEvents::VIEW => ['onPreSerialize', EventPriorities::PRE_SERIALIZE],
];
return $this->decorated->supportsNormalization($data, $format);
}

public function onPreSerialize(GetResponseForControllerResultEvent $event): void
public function normalize($object, $format = null, array $context = [])
{
$controllerResult = $event->getControllerResult();
$request = $event->getRequest();

if ($controllerResult instanceof Response || !$request->attributes->getBoolean('_api_respond', true)) {
return;
$data = $this->decorated->normalize($object, $format, $context);
if ($object instanceof MediaObject) {
$data->contentUrl = $this->storage->resolveUri($data, 'file');
}

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

$mediaObjects = $controllerResult;
return $data;
}

if (!is_iterable($mediaObjects)) {
$mediaObjects = [$mediaObjects];
}
public function supportsDenormalization($data, $type, $format = null)
{
return $this->decorated->supportsDenormalization($data, $type, $format);
}

foreach ($mediaObjects as $mediaObject) {
if (!$mediaObject instanceof MediaObject) {
continue;
}
public function denormalize($data, $class, $format = null, array $context = [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to denormalize a MediaObject. You can only use the NormalizerInterface IMO.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're probably right - I just took the example from the Serialization docs and modified it accordingly. Will update this 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is, if we're declaring this as a decorating service in Symfony DI, then it should implement the same interfaces. I'd suggest that we do NOT declare this as a decorating service (i.e. no decorates).

This way we can also remove SerializerAwareInterface.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@teohhanhui Sounds good - does that change the behavior in any way? Is there any additional configuration required if you do not decorate the existing service?

{
return $this->decorated->denormalize($data, $class, $format, $context);
}

$mediaObject->contentUrl = $this->storage->resolveUri($mediaObject, 'file');
public function setSerializer(SerializerInterface $serializer)
{
if($this->decorated instanceof SerializerAwareInterface) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if($this->decorated instanceof SerializerAwareInterface) {
if ($this->decorated instanceof SerializerAwareInterface) {

I know this CS is in the other part as well.

$this->decorated->setSerializer($serializer);
}
}
}
Expand Down