JMSSerializer. Пишем свой обработчик

Написана 22 Августа, 2013 в 16:44. Автор: borN_free   |   Теги: symfony, jmsserializer, handler Комментарии 3

  • Задача: написать API, отдающее продукты с вложенными картинками (URLs на них) в виде JSON
  • Используется: Symfony2, FOSRestBundle + NelmioApiDocBundle
  • Сериализация через JMSSerializer. В ответе сериализуются сущности (Doctrine Entity).

Необходимость в написании своего обработчика появилась из-за использования Sonata Media Bundle, а именно:

при сериализации сущности, вложенная в продукт сущность картинки (Sonata\Media) сериализуется без URL. Кто сталкивался с этим бандлом, тому известно, что сущность не имеет метода получения URL, а необходимо использовать MediaManager.

Код примерно такой:

$mediaService = $this->container->get('sonata.media.pool');
$provider = $mediaService->getProvider($media->getProviderName());
$format = $provider->getFormatName($media, 'small');
$url = $provider->generatePublicUrl($media, $format);

Очень много действий, да еще и Service Container надо использовать. В Entity это никак не запихнуть в какой-нибудь метод ->getUrl(). Вы ведь не используете контейнер в сущностях, да? :)

Итак, т.к. сериализацией занимается JMSSerializer бандл, которому "на вход" поступает наша сущность Product, мы попробуем написать свой обработчик, в который инъектируем сервисы, ну или сразу весь Service Container. И тогда сможем получить URL для каждой картинки (сущности).

Кода будет не мало, но мы ведь хотим получить ссылки на картинки.

Первым делом создадим сервис. Я предпочитаю YAML:

# App\TestBundle\Resources\config\services.yml
services:
    app_test.serializer.handler:
        class: App\TestBundle\Serializer\Handler
        tags:
            - { name: jms_serializer.subscribing_handler }
        arguments: ["@doctrine.orm.entity_manager", "@router", "@sonata.media.pool", ["@sonata.media.provider.image", "@sonata.media.provider.imagelink"]]

Сервис написали, теперь напишем сам обработчик:


/**
 * Custom serializer for SonataMediaBunlde media objects
 */
namespace App\TestBundle\Serializer;

use Application\Sonata\MediaBundle\Entity\Media;
use Doctrine\ORM\EntityManager;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use Sonata\MediaBundle\Model\MediaInterface;
use Sonata\MediaBundle\Provider\Pool;
use Symfony\Component\Routing\RouterInterface;

/**
 * Class Handler
 * @package TailoredGift\ApiBundle\Serializer
 */
class Handler implements SubscribingHandlerInterface
{
    /**
     * @var EntityManager
     */
    protected $em;

    /**
     * @var \Symfony\Component\Routing\RouterInterface
     */
    protected $router;

    /**
     * @var \Sonata\MediaBundle\Provider\Pool
     */
    protected $mediaService;

    /**
     * @var array
     */
    protected $serializeProviders;

    /**
     * @param EntityManager   $em
     * @param RouterInterface $router
     * @param Pool            $mediaService
     * @param array           $serializeProviders
     */
    public function __construct(
        EntityManager $em,
        RouterInterface $router,
        Pool $mediaService,
        array $serializeProviders = array()
    ) {
        $this->em = $em;
        $this->router = $router;
        $this->mediaService = $mediaService;
        $this->serializeProviders = $serializeProviders;
    }

    /**
     * @return array
     */
    public static function getSubscribingMethods()
    {
        return array(
            array(
                'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
                'format' => 'json',
                'type' => 'Application\Sonata\MediaBundle\Entity\Media',
                'method' => 'serializeImageToJson',
            ),
        );
    }

    /**
     * @return \Sonata\MediaBundle\Provider\Pool
     */
    private function getMediaService()
    {
        return $this->mediaService;
    }

    /**
     * @param \Sonata\MediaBundle\Model\MediaInterface $media
     * @param string                                   $format
     *
     * @return string
     */
    private function path(MediaInterface $media, $format)
    {
        $provider = $this->getMediaService()
            ->getProvider($media->getProviderName());

        $format = $provider->getFormatName($media, $format);

        return $provider->generatePublicUrl($media, $format);
    }

    /**
     * Return if the provider is allowed to be serialized
     *
     * @param  string $name
     * @return bool
     */
    public function serializeProvider($name)
    {
        $providersNames = array_map(
            function ($v) {
                return $v->getName();
            },
            $this->serializeProviders
        );

        return in_array($name, $providersNames);
    }

    /**
     * Get formats for media
     *
     * @param  MediaInterface $media
     * @return array
     */
    public function getFormats(MediaInterface $media)
    {
        return $this->getMediaService()->getFormatNamesByContext($media->getContext());
    }

    /**
     * Get a url safe identifier
     *
     * @param $src
     * @return string
     */
    public function getUrlsafeId($src)
    {
        $src = substr($src, 1);

        return preg_replace('/[\/\.]/', '-', $src);
    }

    /**
     * Handles the serialization of an Image object
     *
     * @param  \JMS\Serializer\JsonSerializationVisitor $visitor
     * @param  \Sonata\MediaBundle\Model\MediaInterface $media
     * @param  array                                    $type
     * @param  Context                                  $context
     * @return array
     */
    public function serializeImageToJson(
        JsonSerializationVisitor $visitor,
        MediaInterface $media,
        array $type,
        Context $context
    ) {
        if (!$this->serializeProvider($media->getProviderName())) {
            return;
        }

        $formats = $context->attributes->get('image_formats')->getOrElse(array());
        $urls = new \stdClass();
        $requestContext = $this->router->getContext();
        $host = $requestContext->getScheme() . '://' . $requestContext->getHost();

        foreach ($formats as $format) {

            switch ($media->getProviderName()) {
                case 'sonata.media.provider.imagelink':
                    $urls->$format = $this->path($media, $format);
                    break;
                default:
                    $urls->$format = $host . $this->path($media, $format);
                    break;
            }
        }

        return array(
            'id' => $media->getId(),
            'width' => $media->getWidth(),
            'height' => $media->getHeight(),
            'size' => $media->getSize(),
            'urls' => $urls,
            'name' => $media->getName(),
            'enabled' => $media->getEnabled(),
            'provider_name' => $media->getProviderName(),
            'provider_status' => $media->getProviderStatus(),
            'provider_reference' => $media->getProviderReference(),
            'provider_metadata' => $media->getProviderMetadata(),
            'context' => $media->getContext(),
            'updated_at' => $media->getUpdatedAt(),
            'created_at' => $media->getCreatedAt(),
            'content_type' => $media->getContentType(),
            'context' => $media->getContext(),
        );
    }
}

На строках 69 и 70 мы определяем, что нужно обрабатывать особым образом сущность Media, и будет это делать метод serializeImageToJson().

Теперь далее сам метод: на строке 160 мы определяем форматы, они задаются из контроллера, код которого представлен чуть ниже. Ну дальше все очевидно, в методе path() мы получаем нужный сервис и генерируем ссылку. В итоге возвращаемый массив - это сериализованная особым образом (как нам нужно) сущность Media.

Код контроллера:

...
class ProductsController extends FOSRestController
{
    public function getProductsAction()
    {
        // ...
        $view = $this->view(
            array(
        // ...
                'products' => $products,
            ),
            200
        )->setFormat('json');
        
        $context = new \JMS\Serializer\SerializationContext();
        $context->setAttribute('image_formats', array('small'));
        $view->setSerializationContext($context);

        return $this->handleView($view);
    }
}

Вот и всё. Теперь вложенные сущности сериализуются так, как мы этого хотели, вместе с сылкой для каждой картинки.

[JMSSerializer: how to write custom handler]

3 comments

-1 ответить
September 7, 2015 at 02:59 pm

add line to file \src\Application\Sonata\MediaBundle\Resources\config\serializer\Entity.Media.xml

    <virtual-property name="referenceFull" type="string" expose="true" since-version="1.0" groups="sonata_api_read,sonata_search" method="getReferenceFull" />

and code...

+1 ответить
March 7, 2017 at 05:20 pm

На сегодня появились аннотации, с помощью которых можно легко решить этот вопрос. Например, данный вопрос можно было решить с помощью

@JMS\Accessor(getter="someGetMethod", setter="someSetMethod")

И просто пишем эти методы. Т.е. можно не описывать полностью целый слушатель (лисеннер), а просто добавить аннотацию.

Возможно я ошибаюсь, но для меня работает. Читайте документацию: http://jmsyst.com/libs/serializer/master/reference/annotations#accessor

ответить
April 25, 2017 at 01:21 pm

А если в другом месте Media нужно serialize иначе?

Оставьте свой комментарий:

Поля с * обязательны.