DEV Community

dr0bz
dr0bz

Posted on

Building Objects in the API Lifecycle

Table of Content

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

#[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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

Student Retrieval Process

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:

Schematic Builder

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 App\Service\SomeFancyGeoService $fancyService
    ) {  }

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

    public function build(): object
    {
        // ..
        return $buildedAddressObject;
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 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)
        ;
    }
}
Enter fullscreen mode Exit fullscreen mode

A small class UML for better understanding:

Builder Dependencies as UML

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
}
Enter fullscreen mode Exit fullscreen mode

The builder will look like this:

<?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)
        ;
    }
}

Enter fullscreen mode Exit fullscreen mode

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",
      "..."
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

The JSON would look like this:

{
  "city": "Duisburg",
  "facultyName": "Applied Informatics",
  "....": "...."
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)