Building Objects in the API Lifecycle
Tags: php, api, designpatterns, architecture
Published: 2024-04-25 14:10:20

## Table of Content

* [Table of Content](#table-of-content)
* [Preface](#preface)
* [Introduction](#introduction)
  * [Why are entities bad for de-/serialization?](#why-are-entities-bad-for-de-/serialization?)
  * [The Solution](#the-solution)
  * [API Response/Request Schema](#api-response/request-schema)
* [DTO Utilization](#dto-utilization)
* [Builder Pattern](#builder-pattern)
  * [Example of a Builder](#example-of-a-builder)
  * [Builder Composition](#builder-composition)
  * [More Compositions](#more-compositions)
* [Response and Request](#response-and-request)
* [Conclusion](#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
<?php

#[ORM\Index(
    name: 'name_idx',
    columns: ['name'],
)]
#[ORM\Entity(repositoryClass: 'App\Repository\ImageRepository')]
class Image
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    #[Ignore]
    private ?int $id = null;

    #[ORM\Column(type='string')]
    #[Assert\NotBlank()]
    #[Groups(['gallery', 'profile'])]
    #[OA\Property(description='Alternate text')]
    private ?string $alt = null;

    #[ORM\Column(type='string')]
    #[CustomValidator()]
    #[Groups(['gallery', 'post'])]
    #[OA\Property(nullable=true)]
    private ?string $name = null;

    #[ORM\Column(type='string')]
    #[Assert\NotBlank()]
    #[Groups(['gallery', 'profile', 'post'])]
    #[OA\Property(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](https://wiki.adminforge.de/wiki/Data_transfer_object?lang=en). These objects can take any shape you require for the given situation without being coupled with your database schema. These objects are defined per **P**lain **O**ld **P**HP **O**bjects. 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
<?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:

![Student Retrieval Process](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/twmpn8n7ogxj3elq1tle.png)

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](https://wiki.adminforge.de/wiki/Design_Patterns?lang=en).

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:

![Schematic Builder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x3vutthgjira8vlij70a.png)


### Example of a Builder

This is how a builder could look like:

```php
<?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 App\Service\SomeFancyGeoService $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
<?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
<?php

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

    /**
     * @param App\Entity\Student $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:

![Builder Dependencies as UML](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bc272gqc4nze3jkcf6rv.png)

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
<?php

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

The builder will look like this:

```php
<?php

class FacultyLocationBuilder implements BuilderInterface
{
    private ?App\Entity\Faculty $facultyEntity = null;
    private ?App\Entity\Address $addressEntity = null;

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

    /**
     * @param array{App\Entity\Faculty, App\Entity\Address} $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:

```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
<?php

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

```

The JSON would look like this:

```json
{
  "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