Thursday, June 21, 2012

Creating a Custom JMSSerializerBundle Handler

A couple of days ago, I had to create a custom JMSSerializerBundle handler to process the serialization of a certain property of one of my models. In my case, I have a user model that has a single associated avatar stored on Amazon S3. The problem is that this avatar URI consists of just the path section (for example: /avatars/12/34/43/123443.jpg) instead of the full URL. This is required to allow my application to work with multiple buckets, each of which corresponds to a different environment such as development, production, etc.

However, the Android client I am currently working on (or any other REST client for that matter) needs the full URL of each avatar returned from the server. Injecting this logic into the model class was obviously not a really good idea, so I decided to write a custom JMSSerializerBundle handler to achieve my objective. The idea is the have the custom handler access the required system services and transform the avatar path from a relative on to a full one. here is how I have implemented it.

First, create your custom handler class. At this point, we do not care about de-serialization so our class will only contain serialization logic.

src/Acme/AmazonBundle/Serializer/AvatarHandler.php

use Symfony\Component\Yaml\Inline;
use JMS\SerializerBundle\Serializer\YamlSerializationVisitor;
use JMS\SerializerBundle\Serializer\GenericSerializationVisitor;
use JMS\SerializerBundle\Serializer\JsonSerializationVisitor;
use JMS\SerializerBundle\Serializer\XmlSerializationVisitor;
use JMS\SerializerBundle\Serializer\VisitorInterface;
use JMS\SerializerBundle\Serializer\Handler\SerializationHandlerInterface;
use Acme\AmazonBundle\Core\Entity\S3\AvatarManager;
use Acme\AmazonBundle\Core\Entity\S3\Avatar;

class AvatarHandler implements SerializationHandlerInterface
{
    protected $avatarManager;

    public function __construct(AvatarManager $avatarManager)
    {
        $this->avatarManager = $avatarManager;
    }

    public function serialize(VisitorInterface $visitor, $data, $type, &$visited)
    {
        if (!$data instanceof Avatar) {
            return;
        }
                
        $data = sprintf('http://%s/%s', $this->avatarManager->getDomain(), $this->avatarManager->getTarget($data));
                
        if ($visitor instanceof XmlSerializationVisitor) {
            
            if (null === $visitor->document) {
                $visitor->document = $visitor->createDocument(null, null, true);
            }
            
            $visited = true;
            return $visitor->document->createTextNode($data);
            
        } else if ($visitor instanceof GenericSerializationVisitor) {
            
            $visited = true;
            return $data;
            
        } else if ($visitor instanceof YamlSerializationVisitor) {
            
            $visited = true;
            return Inline::dump($data);
            
        }
        
    }

}

Note the injected S3 Manager (AvatarManager) service which handles an S3 Entity (Avatar).

Second, create your handler factory class:

src/Acme/AmazonBundle/DependencyInjection/Factory/AvatarHandlerFactory.php

namespace Acme\AmazonBundle\DependencyInjection\Factory;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use JMS\SerializerBundle\DependencyInjection\HandlerFactoryInterface;

class AvatarHandlerFactory implements HandlerFactoryInterface
{
    public function getConfigKey()
    {
        return 'avatar';
    }

    public function getType(array $config)
    {
        return self::TYPE_SERIALIZATION;
    }

    public function addConfiguration(ArrayNodeDefinition $builder)
    {
        $builder->addDefaultsIfNotSet();
    }

    public function getHandlerId(ContainerBuilder $container, array $config)
    {
        return 'acme_amazon.serializer.avatar_handler';
    }
}

Next, register your service:

src/Acme/AmazonBundle/DependencyInjection/Factory/AvatarHandlerFactory.php

<service id="acme_amazon.serializer.avatar_handler" class="Acme\AmazonBundle\Serializer\AvatarHandler">
            <argument type="service" id="acme_amazon.entity.s3.avatar_manager" />
        </service>

Update your bundle class to register your customer handler factory:

src/Acme/AmazonBundle/AcmeAmazonBundle.php

namespace Acme\AmazonBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use JMS\SerializerBundle\DependencyInjection\JMSSerializerExtension;
use Acme\AmazonBundle\DependencyInjection\Factory\AvatarHandlerFactory;

class AcmeAmazonBundle extends Bundle
{
    public function configureSerializerExtension(JMSSerializerExtension $ext)
    {
        $ext->addHandlerFactory(new AvatarHandlerFactory());
    }
}

Now, create a separate property in your model class to hold a reference to your S3 Entity (Avatar). Basically, in addition to an avatarPath property - which holds the path as a string - create a new avatar property that holds an instance of an S3 entity class that acts as a wrapper when the time comes to serialize this model. This is important as you can not write custom handlers for simple data types; you need an object. Next, create your getter and setter methods to deal with this new property. The getter method is particularly important as it will be used to instantiate an Avatar object from the path string.

public function getAvatar()
{
    if (!$this->avatar) {
       $this->avatar = new Avatar($this->avatarPath);
    }
        
    return $this->avatar;
}
    
public function setAvatar(Avatar $avatar)
{
    $this->avatar = $avatar;
}

Finally, ensure that your serialization configuration is complete:

src/Acme/UserBundle/Resources/config/serializer/Document.User.xml

...
<property name="avatar" type="Avatar" expose="true" access-type="public_method" read-only="true"></property>
...

The type attribute is only required for de-serialization; however, I included it in the configuration file for completeness. At this point, your serialized model should now contain a full URI.

No comments:

Post a Comment