Building Objects in the API Lifecycle

Building Objects in the API Lifecycle

Table of Content

Table of Content
Preface

Introduction

Why are entities bad for de-/serialization?
The Solution
API Response/Request Schema

DTO Utilization

Builder Pattern

Example of a Builder
Builder Composition
More Compositions

Response and Request
Conclusion

Preface

This article consists of two parts. The part one is an introduction to the problem and a presentation of an architectural solution. The second part presents a final solution as a symfony bundle based on the knowledge of the first part.

Introduction

One of the tasks of an API is handling the communication between clients and endpoints. This means serialization and deserialization of data and handling that data within the backends business logic. Now it comes to choose where to deserialize your JSON into and back. Working in a small project you’ll probably directly use entities for de-/serialization. It’ll be just fine to also use serialization groups for the few entities. But when you are working in a large project with a lot of entities and API endpoints using them for de-/serialization is a bad decision.

Why are entities bad for de-/serialization?

For a bigger project there are clear disadvantages of utilizing entities for this task:

The first disadvantage of using entities as an API response is, that coupling the database schema
to the API response schema is a bad architectural decision. You’ll loose the flexibility. Schema
changes on the one or the other side will be propagated to the another side.
Keeping an eye on different groups will be a pain.

I’ve seen projects where one entity was involved in many API endpoint responses. Each response
should present a different shape of the entity. So about five groups were required to
construct different responses for this entity. If you also imagine, these groups are then
spread among other entities which are related to this one and used in other endpoint
responses, you can understand that managing these groups and keeping an eye on them is really
a big adventure.

And the last one is of course the big mess in your entities regarding the amount of meta data to
control: ORM, Serialization and probably validation.

If you are using OpenAPI (Swagger) you’ll need even more meta data in your entities.

Example of such a multipurpose entity:

<?php

#[ORMIndex(
name: ‘name_idx’,
columns: [‘name’],
)]
#[ORMEntity(repositoryClass: ‘AppRepositoryImageRepository’)]
class Image
{
#[ORMId]
#[ORMGeneratedValue]
#[ORMColumn(type: ‘integer’)]
#[Ignore]
private ?int $id = null;

#[ORMColumn(type=’string’)]
#[AssertNotBlank()]
#[Groups([‘gallery’, ‘profile’])]
#[OAProperty(description=’Alternate text’)]
private ?string $alt = null;

#[ORMColumn(type=’string’)]
#[CustomValidator()]
#[Groups([‘gallery’, ‘post’])]
#[OAProperty(nullable=true)]
private ?string $name = null;

#[ORMColumn(type=’string’)]
#[AssertNotBlank()]
#[Groups([‘gallery’, ‘profile’, ‘post’])]
#[OAProperty(format=’url’)]
private ?string $url = null;

// .. setters, getters
}

We have only three property here and thats already too much, right?

The Solution

The solution for this is utilization of DTOs – Data Transfer
Objects
. These objects can take any shape you require for the given situation without being coupled with your database schema. These objects are defined per Plain Old PHP Objects. That way it’s possible to clearly separate the domains: entities for database, DTOs anything else including API responses.

API Response/Request Schema

This is a bigger topic, so I’ll bring only the keypoints. When the API is built, the schema of the requests and responses is designed by usecase. Sure, in some cases it’ll match your database schema, but the overall premise is: the schema of an API is designed to suit the business case or use case in current situation. That is the opposite of using entities for de-/serialization and coupling with the database schema. The consumers of the API will need data in a form according to the use case and not according to the database schema.

DTO Utilization

Lets say our API creates and provides data about students, universities and their addresses. Here are the four DTOs:

<?php
class Student
{
private ?string $name = null;
private ?int $semester = null;
private ?Address $address = null;
private ?Faculty $faculty = null;
// … setters, getters
}

class University
{
private ?string $name = null;
private ?Address $address = null;
/** @var Faculty[] */
private array $faculties = [];
// … setters, getters
}

class Faculty
{
private ?string $name = null;
private array $departments = [];
}

class Address
{
private ?string $street = null;
private ?string $houseNumber = null;
private ?string $city = null;
// … setters, getters
}

The process of retrieving a student with his address in one request would look like this:

The process of retrieving an university with it’s address would look similar. The same createAddressDTO() function would be used here. You’ll probably put this in a separate service, because you also do some fancy calculations on it, like distance to the university where the student is studying or what ever… Lets say building the Address DTO object is a complex piece of your application with a lot of dependencies. Also the Address DTO object is used else where in the API endpoints. This would mean the building service should be generic enough to have the ability to
build an address object for any endpoint.

Builder Pattern

The builder pattern is one of the creational design patterns like it is described by the Gang of Four in their book Design Patterns: Elements of Reusable Object-Oriented Software.

Definition:

Separate the construction of a complex object from its representation so that the same construction process can create different representations.

The builder pattern perfectly suits to our needs to build the different objects: Stundent, University, Address and Faculty:

StudentBuilder
UniversityBuilder
AddressBuilder
FacultyBuilder

Each builder should be represented as a separate class/service and build only one DTO. With this construct we gain following advantages over creating the DTOs directly in related services with the main business logic:

Single point of responsibility: a builder does only one thing – it builds one DTO
Separate the building of those DTOs from the main business logic
We can use them all over API endpoints without injecting the whole unrelated service with
unrelated business logic
Also it allows us a building complex objects by composing different builders with each other

A simplified schematic:

Example of a Builder

This is how a builder could look like:

<?php
interface BuilderInterface
{
public function build(): object;
public function setData(mixed $data): void;
}

class AddressBuilder implements BuilderInterface
{
private mixed $data = null;

public function __construct(
private AppServiceSomeFancyGeoService $fancyService
) { }

public function setData(mixed $data): void
{
$this->data = $data;
}

public function build(): object
{
// ..
return $buildedAddressObject;
}
}

A schematic workflow of a builder usage could look like this:

<?php
// somewhere in StudenService or in a controller action
// $addressBuilder is injected as a dependency

$addressEntity = $addressRepository->find($id);
$addressBuilder->setData($addressEntity);
$addressDTO = $builder->build();

What we achieved here:

The context where the DTO is built doesn’t need to know anything about the representation of the Address DTO
The context where the builder is used benefits from the interface agreement

Builder Composition

Since we are using the same Address and Faculty DTOs in both Student and University we can compose the StundentBuilder service utilizing the AddressBuilder and FacultyBuilder:

<?php

class StudentBuilder implements BuilderInterface
{
public function __construct(
private FacultyBuilder $facultyBuilder,
private AddressBuilder $addressBuilder
) {}

/**
* @param AppEntityStudent $data The student entity
*/

public function setData(mixed $data): void { /* … */ }

public function build(): object
{
$this->addressBuilder->setData($this->entity->getAddress());
$addressDTO = $this->addressBuilder->build();

$this->facultyBuilder->setData($this->entity->getFaculty());
$facultyDTO = $this->facultyBuilder->build();

return (new Student())
->setName($this->entity->getName());
->setSemester($this->entity->getSemester())
->setAddress($addressDTO)
->setFaculty($facultyDTO)
;
}
}

A small class UML for better understanding:

The UniversityBuilder would use the same AddressBuilder and FacultyBuilder to provide the address. I think you get the idea.

More Compositions

As I mentioned before the API response schema may correspond to your database schema. That is already happing above. Student and University will be definitely related to Address and Faculty entities in the database as well. But what if we make a response which has no correlation to the database schema. For example we want to return a Faculty and the Address where this faculty can be studied. We don’t have such a relation in the database. We only have a University which has a ManyToMany relation to Faculty and a OneToOne relation an Address. So we just create a new shape of the response in a manner how the use case requires it:

<?php

class FacultyLocation
{
private ?Address $address = null;
private ?Faculty $faculty = null;
// … settrs, getters
}

The builder will look like this:

<?php

class FacultyLocationBuilder implements BuilderInterface
{
private ?AppEntityFaculty $facultyEntity = null;
private ?AppEntityAddress $addressEntity = null;

public function __construct(
private FacultyBuilder $facultyBuilder,
private AddressBuilder $addressBuilder
) {}

/**
* @param array{AppEntityFaculty, AppEntityAddress} $data
*/

public function setData(mixed $data): void
{
[$this->facultyEntity, $this->addressEntity] = $data;
}

public function build(): object
{
$this->facultyBuilder->setData($this->facultyEntity);
$facultyDTO = $this->facultyBuilder->build();

$this->addressBuilder->setData($this->addressEntity);
$addressDTO = $this->addressBuilder->build();

return (new FacultyLocation())
->setFaculty($facultyDTO)
->setAddress($addressDTO)
;
}
}

This might be stacked together as a collection for a map as geo points or as result list for a search.

The DTO above would be serialized to the following JSON:

{
“address”: {
“city”: “Duisburg”,
“street”: “…”
},
“faculty”: {
“name”: “Applied Informatics”,
“departments”: [
“Human Computer Interaction”,
“…”
]
}
}

If you prefer a flat JSON – you’re welcome to do so:

<?php

class FacultyLocation
{
private ?string $facultyName = null;
private ?string $city = null;
// … other properties
// … settrs, getters
}

The JSON would look like this:

{
“city”: “Duisburg”,
“facultyName”: “Applied Informatics”,
“….”: “….”
}

As you can see, you’re able to form any desired shape of your response.

Response and Request

Until now we only covered how to build DTOs for responses. The same process can be applied to build entities from DTOs, when a request comes in, for example from a form submit. You only need to invert the idea. Previously we used BuilderInterface::setData() to provide an entity as a source of data and BuilderInterface::build() to build the DTO. When building an entity from a DTOs the order is
inversed:

BuilderInterface::setData() takes a DTO – a deserialized JSON from a form, for example

BuilderInterface::build() builds and returns an entity, ready to be saved to database

Conclusion

What we’ve seen here so far is:

How to utilize the DTOs instead of entities for API requests and responses
How to decouple the API schema from the database schema
How to build DTOs and entities without polluting the main business logic with the creational process

Creating a set of builders allows us to make creating DTOs more abstract

How to compose those builder together to form every desired shape of an API response or request schema
independently from the database schema

The part one of this article is more of a concept and architectural pattern, than a real world example, especially the builder interfaces and builder implementations. The part two of the article will introduce a Symfony bundle to:

simplify the process of creating such builders
provide the ability to organize the builders
how to utilize them in real world examples

As soon as the part two of this article is ready, I’ll inject the links here. Till than criticism, suggestions and fixes are welcome in comments.

Best Regards,

dr0bz

Leave a Reply

Your email address will not be published. Required fields are marked *