Serialization, Media, and REST: Creating a Media Instrument in Symfony2
February 4th, 2016
In the article, you will find out how to organize an effective and potentially easily-scalable instrument for working with media using REST.
As a specific example for this, we used one of the latest projects developed by our team. The backend of the application is built using Symfony2. It provides a single REST API for the front-end application created with EmberJS and mobile apps for iOS and Android.
As we were working on an MVP, our team faced the challenge to quickly create an effective and potentially scalable tool for working with images. This tool can also be adapted to storage other media files without major architectural changes.
Additional requirements included the need to resize images as they are uploaded according to the list of used formats, as well as implement serialization of the image entity according to the access to the original image and its versions with different sizes.
SonataMediaBundle can rightly be called one of the most popular solutions for managing media data of various nature because this bundle provides the ability to efficiently organize storage and working with different sources and repositories of information, including local data storage, upload by FTP, working with Amazon S3 storage, and CDN configuration option.
The developer gets a high-level abstraction called Media, which implements a rich set of methods and, being inherited according to the recommendations for the bundle installation, can be extended depending on the project’s requirements.
Given the potential need to work with other media data in the future (in the framework of the described project), we decided to create an individual model for data of given type that has a field for linking with the Media class entity. This entity is responsible for the storage of media data and other attributes required while working with this type.
To manage the images, we created an entity called “Image” that has an object identifier, dates of creation and last modification, and the associated Media entity. It contains a simple set of get / set methods and uses trait for functions called when LifeCircleCallback events happen.
Regarding the Image entity’s additional attributes worth mentioning, we should note status field that is required for recording of the entity’s condition during its lifecycle stages. Because the work with images is done as if with an independent resource, their loading is done regardless of the higher level entities to which they may be linked.
For example, the image linked with a certain declaration or application may be loaded at the initial stage of its creation, which later will not be completed for some reason. This, in turn, can create a certain number of rudimentary database records and files on the disk, which must be subsequently removed. Image entity’s creation date and its current status are sufficient markers to determine the need for its removal if not associated with any one.
We have implemented an ImageEntityManager service on our project that acts as a wrapper over the entity repository and implements methods for its saving / deleting.
The controller responsible for working with Image class is called quite trivial - ImageController. It expands the host controller FOSRestController, provided by FOSRestBundle.
The loadImagesAction and getImageAction methods are of the main interest for us. Within the loadImagesAction, we inject ImageLoader service. It encapsulates in itself the logic of storing the images captured in a single HTTP-request and takes into account the possibility of getting several files simultaneously.
Cyclic processing of the information received from the request happens within loadImages method.
Actual storage of every received file and creation of corresponding entities happens within the private saveImage method called after every iteration.
ImageController controller’s getImageAction method is responsible for generating a response to a request for data on a particular Image class entity example.
Frontend architecture and implemented data storage tools allow operating quite effectively with the data from the server using the browser’s localstorage for minimizing the number of pushed requests. At the same time, it specifies the requirements for the data set that is provided by the server as a result of the entity’s serialization.
For Image entity we should get the following structure:
How does "out of the box" serialization work?
In order to serialize entities, FOSRestBundle leverages JMSSerializerBundle, which allows creating configuration files that determine the entity values would be shown as a serialization result.
Let’s take a look at the simplest example of such a file, created on the basis of information from the bundle’s official documentation.
For the values calculated “on the fly” there is a special mechanism of so-called “virtual property,” which is implemented by declaring the corresponding value in the configuration file and adding a get function into camelcase note in the entity class.
The indisputable advantages of such approach are the simplicity and ease of implementation. However, it is not perfect. The most obvious include the entity’s extension by methods that don’t directly deal with its values (this can lead to a number of inconveniences in the case of entity configuration in YML files and subsequent classes autogeneration). Also, it lacks the canonicity mechanisms for services access that may be needed to form computed valued.
Going back to the desired server response structure for Image entity request, we want to highlight several properties, the value formation of which is connected with the necessity to access the services and data that go beyond the serialized class.
To generate an absolute URL to the file on a server and get access to the array of uploaded image formats available for a given context, we need access to the Pool service provided by SonataMediaBundle.
On StackOverflow, there are questions (some of which even get answers) regarding class injection into the entity class. However, there is a more elegant solution to this problem that we’re going to discuss further.
JMSSerializerBundle provides events, which can be processed by the user’s application. In our case, we are mostly interested in a post_serialize event described in the official library documentation. This event is recommended to use for results extension with additional data such as links and other.
EventSubscriber implements the appropriate interface and we get access to the Visitor class object and a data array, formed by bypassing the fields specified in the entity’s configuration file.
In order to form a unified approach to tasks’ implementation for the entities serialization, we have created a corresponding basic class for EventSubscribers and described the interface implemented by SerializationHelper.
BaseSerializationSubscriber class receives an object for its constructor that implements SerializationHelperInterface and carries out onObjectPostSerialize method, which contains the logic of adding additional data to the entity serialization result object.
SerializationHelperInterface interface provides information that its implementing class will contain a method that receives an example of a serialized entity and sends back a data array, which extends the serialization result.
ImageSerializationSubscriber class extends BaseSerializationSubscriber and implements EventSubscriberInterface, required by the serialization library and that obliges us to implement getSubscribedEvents method.
It returns information about:
- Method that will be called
- Class of the entity, the serialization of which a response is required
- Serialized data format output
ImageSerializationHelper service implements SerializationHelperInterface and, actually, implements the required functionality of dynamic generation of the Image entity field values.
The constructor of this class takes Pool, provided by SonataMediaBundle, and Router service, a basic component of the Symfony2 framework, as input the above-mentioned services.
getAddtionalData method takes an example of the Image class, forms an absolute URL for access to the original file as well as a URL array for access to resizing results based on the context of the associated media file.
Private generateFullPublicUrl method creates a full path to the file taking into account the protocol and domain, getting file path relative to the site root as input.
Looking for Symfony developers?
Looking for Symfony developers?