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.

Sunday, June 3, 2012

Android Development

I have recently started developing a new application for the Android platform. It had been a while since I experimented with Android and this really manifested itself when I started this new project a couple of months ago. Having to deal with new Android features and corresponding backwards compatibility issues, I experienced a steep learning curve. This article highlights the lessons learned during this process.

JDK Selection

I have been a Linux user since 2001 and never looked back since then. So if you are developing on Linux, make sure to replace the default OpenJDK with Oracle's. This will save you a lot of headache dealing with peculiar errors and exceptions during the initial set-up process.

Selecting an IDE

I really like NetBeans and still develop with this IDE when it comes to PHP; it definitely feels much more intuitive than Eclipse. However, when it comes to Android development, it did not take long for me to realize I was fighting an uphill battle. With its excellent Android Development Tools (ADT) plugin integration, Eclipse definitely shines when it comes to programming for Android. In my case, the process of switching from NetBeans to Eclipse definitely took its toll both in terms of learning curve and time spent for customization to get Eclipse to a usable state.

Android API Levels

What API level to support? Well, the answer is not hard to find. On the official Android site, you can find the current statistics for platform versions. To penetrate the majority of the Android market, you need to support API Level 7; however, your application should also perform on the newest handsets running the latest version of the Android SDK at API Level 15. This is accomplished by adding the following line to your AndroidManifest.xml:

<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="15" />

This simply means that when writing your code, you need to be careful in order to maintain backwards compatibility with API Level 7. This is pretty easy to accomplish thanks to Android Lint but what about forward compatibility? How do you utilize the newest features?

Supporting Libraries

New functionality introduced by the latest Android SDK will obviously not be available in older versions. This means that you will have to get comfortable utilizing the support libraries. One of these libraries is the Android Support Package that allows you to utilize certain new features such as fragments.

However, not all new functionality is supported by the Android Support Package. To get action bar support, you will need to download the ActionBarSherlock library.

These extra libraries do make the learning curve a little bit steeper but they are essential tools needed to create a Android application these days. Unfortunately, they are not silver bullets. There are still issues that need to be addressed; one of which manifests itself when you need to embed a MapView inside a fragment.

Other Libraries

If you are planning to integrate your Android application with REST services over OAuth2 , have a look at Spring for Android. In addition, you may be interested in SLF4J Android for your logging needs.

Thoughts on Android

Android reminds me of the early Linux days. The platform still has its fair share of problems but it is an open platform that is improving and maturing constantly with every release. Considering I flashed the one and only Apple product - a 5th generation IPod that was given to me as a birthday present years ago - with Rockbox the next day I received it, it is not surprising to me why I am interested in this platform. Besides, now that Android is slowly being merged into the Linux kernel, do I have a choice?