Need a secure web application?
REST API
Currently, an increasingly common approach in the implementation of web applications is the separation of the client part and backend part from the one application. There are usually two types of applications:
- frontend application, client - prepared e.g. in Vue.JS
- backend application, server - implemented e.g. in PHP Symfony5
These two separate applications need to communicate with each other, preferably in the simplest way possible. In 2000, the foundations of the so-called RESTful API approach, i.e. communication using the HTTP protocol, without the need to install additional libraries and dependencies. This method of communication is used in the client-server architecture approach described above. We will explore the issue of REST API in a separate article, now we will move on to the issue of the data itself.
In the case of REST API, the data for the client application must be provided in a structured, coherent form, for this purpose the JSON or XML format is used. However, the more popular JSON format, with the importance of a more readable form and a smaller volume. JSON format is also called lightweight data interchange format.
If you've dealt with object-oriented programming and ORM relational mapping systems, you probably already know that everything revolves around objects. We create a class, the so-called entity, we map its contents to tables in the database and we always work based on objects. Sharing, or colloquially speaking, exposing data via REST API is simply an extension of the data chain.
Data serialization
In order to share data from objects using REST API, we must make the so-called serialization, i.e. converting an object into a string in a specific data exchange format. For this purpose, users of the Symfony framework use the built-in serialization mechanism or use the extremely popular external bundle: JMSSerializerBundle https://jmsyst.com/bundles/JMSSerializerBundle.
JMSSerializerBundle, like the built-in data serialization mechanism in the Symfony framework, allows us to convert the object into a string in JSON format, but do we always want to make all the data contained within the object publicly available? Remember that in the case of a server-client architecture, it is possible to suspect full communication between the client (JavaScript application running as part of a web browser) and the server. Therefore, it is worth considering whether all data available within the object should be serialized?
Remember that in the case of ORM systems, all data from the database table will be mapped to the object. Are you sure we want more advanced users of the online store to be able to see internal comments of traders about themselves? Is it really possible?
So please let me explain, if we serialize the entire order object containing a collection of traders' comments, the customer will be able to preview their content by analyzing the XHR request logs (XMLHttpRequest). These types of logs are available in every web browser, and the full content of the server's response to requests can be seen even though the frontend (client) application will not display this data anywhere.
This is where the mechanism of serialization groups and serializer annotations comes in handy, which we can use when creating an entity. We decide to use JMSSerializerBundle, in this variant we have the option to define the serialization policy. We can exclude all class properties from serialization by default or serialize all available data by default.
Exlusion policy
The following example of the class and used serialization policy will exclude all properties of the default serialization group from serialization.
<?php
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
/**
* @ExclusionPolicy("all")
*/
class MySuperClass
{
protected ?int $stage;
protected ?string $name;
/**
* @Expose
*/
protected ?string $slug;
}
?>
Serialization policy
By default, all available properties of a given class are serialized, so it is worth taking care of automatically excluding all available properties. It is safer to exclude all data from serialization and then include individual properties in serialization than to use the reverse approach. Including all properties in serialization and excluding selected properties may seem to simplify work and save time. Nothing could be more wrong, they add new properties, e.g. containing sensitive data, we can easily forget about the need to manually exclude them from serialization. Remember! It's safer to share less data than to share too much.
<?php
/**
* @ExclusionPolicy("all")
*/
class MySuperClass
{
/**
* @Expose(if="canExpose(object, context, property_metadata)")
*/
protected ?string $name;
/**
* @Expose(if="canExpose(object, context, property_metadata)")
*/
protected ?string $slug;
?>
In the example above, the $slug property is marked with the @Expose annotation, this means that the $slug property is manually provided during serialization. As a result, during the serialization process of the entire MySuperClass object, we will only get the contents of the $slug property. There is a downside to the exposing individual properties concept, it can be done dynamically, having access to the serialized object and the serialization context, but managing more properties requires duplication of code.
Serialization groups
Serialization groups work in a way similar to validation groups known from Symfony forms, of course fulfilling a different task, but the principle of operation is similar. How it's working? Before starting serialization, we need to indicate a group or several serialization groups that we will use in the serialization process. The selected serialization groups should be reflected in the serialization configuration contained within the entity (or optionally in yaml format). The serialization mechanism will use those properties that are included in the declared pool of serialization groups of our backend application.
use JMS\Serializer\Annotation\Groups;
class Article
{
/** @Groups({“common”}) */
protected $uuid;
/** @Groups({“common”, “list”, "details"}) */
protected $title;
/** @Groups({“content”, “details”}) */
protected $title;
}
$serializer->serialize(new Article(), 'json', SerializationContext::create()->setGroups([‘common’, ‘list’]));
Serialization groups usage
The selected serialization groups should be reflected in the serialization configuration contained within the entity (or optionally in yaml format). The serialization mechanism will use those properties that are included in the declared pool of serialization groups of our backend application.
But how can we use serialization groups to protect sensitive data? A small auxiliary function, the so-called helper, current user context and verification of assigned user roles. Based on the assigned user roles, we can differentiate the pool of serialization groups. Depending on the user we are dealing with, different properties of the same object are returned. For example, the serialized order object for the administrator will contain additional properties with internal notes of merchants, at the same time the same serialized object for the online store client (regular user) will not contain these properties.
An interesting example of the use of serialization groups is, for example, the approach to hiding prices, e.g. in B2B systems, where not every user of the system is to have an insight into the generated costs. In one B2B project, the client made a requirement to hide product prices for some customers, and hide prices for both products and all other objects in which prices, values or cost summaries were visible. How to accomplish this type of task without preparing too much logic?
It is enough to prepare a common mechanism for all API endpoints, so that it is possible to differentiate serialization groups based on the context of the logged in user.
$groups = [‘common’];
$showPrice = $this->getUser() ? $this->getUser()->getShowPrices() : false;
if ($showPrice) {
$groups[] = ‘showPrices’
}
$serializationContext = new SerializationContext::create()->setGroups($groups);
$serializer->serialize(new Article(), 'json', $serializationContext);
Inside the helper we have an available user object, assigned roles and flags set in the user account of the B2B system. On the basis of all these data, a pool of serialization groups is determined, which is then transferred to the serializer service within the serialization context. Rather than duplicating serialization groups across entities, you can use the small functional group approach.
Then we create the basic common part of the property (Common), which is to be always available during serialization, while in the case of differentiating serialized properties, we create additional serialization groups appropriate for a specific function or assigned role to the user. For example, ShowPrices. All entity properties containing prices will be included in this group. The group should be common to all entities used in the project.
If the user has the appropriate role, the handleSerialize helper will add the group "ShowPrices" to the pool of serialization groups, as a result of the API resource we will get prices for serialized objects or not.
This type of approach allows you to easily limit the globally number of serialized properties of all entities in a given web application. Possible addition of a new serialization group will not be a big problem anymore.
use JMS\Serializer\Annotation\Groups;
class Article
{
/** @Groups({“common”}) */
protected $uuid;
/** @Groups({“title”}) */
protected $title;
/** @Groups({“content”}) */
protected $intro;
/** @Groups({“content”}) */
protected $content;
/** @Groups({“content”}) */
protected $description;
}
In this example, to get the full content of the object, you need to select 3 serialization groups: "common", "title" and "content".
Have an idea? Let's talk