Starting the APIE project.



Apie Introduction

"Apie" is my personal project that I work on in my spare time. So, how did it start? It actually began when I discovered the API platform and liked many of the solutions it offered. The concept of creating (Doctrine) entities and letting the framework generate endpoints was a real eye-opener for me.

Having worked at Paqt, where most projects utilized Laravel and API platform being Symfony, I decided to create my own library and named it "Apie", a play on words combining "API" and "a pie". I tested it in a project and received positive feedback from my colleagues who had to integrate my brand new library.

Digging deeper, I realized that what API Platform does is not entirely new. I came across terms like 'Restful objects' and 'Naked objects' on Wikipedia. Wouldn't it be amazing to develop your domain objects and have the framework/library automatically create a CMS and a Rest API right out of the box?

The benefits would be substantial. In many of my professional projects, a full CRUD (Create, Read, Update, Delete) is needed for going live, although it often lacks priority for the customer. Frequently, the application needs to have a 'modern website' appearance, leading to a setup where a Single Page Application (SPA) communicates with a Rest API. Consequently, we build a CMS within the SPA along with a Rest API to give administrative tools for the customer. Unfortunately, this frequently results in a flawed implementation with numerous issues (poor error handling, incorrect validation mapping, convoluted business logic between frontend and backend), primarily because the Admin CMS is frequently considered a lower priority.
At times, we resort to using other CMS tools (such as Laravel Nova or the poorly documented API Platform Admin) for the Admin section. However, these tools often lack compatibility with the code used in the Rest API, leading to the creation of two controllers that essentially perform the same tasks, differing mainly in how they present information to the browser.

Making a CMS and Rest API generator.

I believed that developing a CMS generator and a REST API generator would be relatively straightforward, assuming we could utilize reflection (for instance, identifying required fields through constructor arguments and treating setters as optional fields). However, my assumptions turned out to be incorrect.

Several challenges emerge when working on a CMS generator and a REST API generator:
  1. Handling Object Associations: When a setter or constructor argument involves a different object, and that object is another entity, we face the dilemma of whether to present a dropdown with all existing entities or to allow the creation of a new instance of this entity within a form group.
  2. Maintaining Consistent Domain Objects: As we strive to ensure that domain objects remain in a consistent state and avoid anemic domain objects, the generator must determine the appropriate order for calling setters. For instance, if an object encompasses a range with a start date and an end date where the start date must precede the end date, different scenarios might necessitate calling setStartDate before setEndDate or vice versa. This complexity led me to introduce range value objects that solve this in a value object and not in a domain object.
  3. Complexities with Date Objects: Date objects pose a significant challenge, particularly in PHP, where there exist only two classes and a single interface for handling dates. These default classes assume a combination of date, time, and timezone. Unfortunately, customization options are severely limited.
  4. Ensuring Extensibility: Designing the generator to be extensible for other developers results in an extension that ideally works for the CMS or the REST API portion. However, in practice, a developer might solely utilize one of these components, leading to potential neglect of implementing the extension for the other part.

Conclusion

In the end, I first conceptualized how my objects should operate. Subsequently, I crafted an "apie/core" library to manage these aspects, intentionally designing the foundation to be non-extendable. We can still add the option to incorporate extensibility at a later stage. I ended up with these fundamental building blocks:

Entities

Entities need to have an identifier. As a solution, I introduced an "EntityInterface" featuring a "getId()" method that returns a class implementing the "IdentifierInterface". The "Identifier" is a value object, supplemented with an additional method indicating that the value object references a specific entity.

Consequently, we acquire metadata about a value object and the entity it references. This approach aligns with domain objects, as the context of the value object is to reference a distinct entity. While often regarded as over-engineering, it ultimately simplifies the automated process of identifying the referenced object.

In terms of code, a basic entity might resemble the following:

use Apie\Core\Entities\EntityInterface;
use Apie\Core\Identifiers\IdentifierInterface;
use Apie\Core\Identifiers\UuidV4;

class MinimumEntity implements EntityInterface
{
    public function __construct(private MinimumEntityId $id)
    {
    }
    public function getId(): MinimumEntityId
    {
        return $this->id;
    }
}

class MinimumEntityId extends UuidV4 implements IdentifierInterface
{
    public static function getReferenceFor(): ReflectionClass
    {
        return new ReflectionClass(MinimumEntity::class);
    }
}

This code example demonstrates an Apie entity consisting solely of an ID, in this case represented by a UUID version 4 and is always filled in by the caller.

Enums

As PHP natively supports enums in the language, that's the route we chose to take. Nonetheless, there remain a few challenges for the CMS component, particularly regarding translations for enums. Nevertheless, in most cases, enums can be readily employed without the need for any excessively complex enum library. (looking at you bensampo/laravel-enum)

Value objects

Value objects are essentially objects that encapsulate business logic tied to primitive data types like strings, integers, floating points, or booleans. For instance, I can create a "StrongPassword" value object and integrate it into my objects to ensure the use of only robust passwords. These value objects should facilitate conversion both from and to primitive values. Thus, I formulated a "ValueObjectInterface" containing both a static "fromNative()" method and a non-static "toNative()" method. For many value objects, representation as booleans or strings and utilization of common string operations prove sufficient. To streamline typical operations, we've introduced several traits that provide assistance.

Some code examples:
use Apie\Core\ValueObjects\Exceptions\InvalidStringForValueObjectException;
use Apie\Core\ValueObjects\IsPasswordValueObject;
use Apie\Core\ValueObjects\IsStringValueObject;
use Apie\Core\ValueObjects\IsStringWithRegexValueObject;
use Apie\Core\ValueObjects\ValueObjectInterface;
/**
 * Value object for a non-empty string
 */
final class NonEmptyString implements ValueObjectInterface
{
    use IsStringValueObject;
    public static function validate(string $input): void
    {
        if (empty($input)) {
            throw new InvalidStringForValueObjectException(
                $input,
                new ReflectionClass(self::class)
            );
        }
    }
    protected function convert(string $input): string
    {
        return trim($input);
    }
}

/**
 * A simple email to be used as example for regular expressions.
 */
final class SimpleEmail implements ValueObjectInterface
{
    use IsStringWithRegexValueObject;
    public static function getRegularExpression(): string
    {
        return '^[^@]+@[a-z]+(\.[a-z]+)*$';
    }
}
/**
 * An example of a 'strong password'. It depends on the application what the
 * requirements are for a 'strong password'.
 * In this example the password limits are:
 * - password length between 1-42 characters
 * - allow a-z, A-Z, 0-9 and '+' and '-' as allowed characters.
 * - should have one digit
 */
final class StrongPassword implements ValueObjectInterface
{
    use IsPasswordValueObject;
    public static function getMinLength(): int
    {
        return 1;
    }
    public static function getMaxLength(): int
    {
        return 42;
    }

    public static function getAllowedSpecialCharacters(): string
    {
        return '+-';
    }

    public static function getMinSpecialCharacters(): int
    {
        return 0;
    }

    public static function getMinDigits(): int
    {
        return 1;
    }

    public static function getMinLowercase(): int
    {
        return 0;
    }

    public static function getMinUppercase(): int
    {
        return 0;
    }
}
An intriguing concept is that these value objects can also be employed beyond the Apie library, as they are simple PHP objects.

Data Transfer Objects

Data Transfer Objects (DTOs) are meant to exclusively hold data and not incorporate any business logic. Their primary function is to convey various fields. To formalize this principle, I introduced a marker interface called "DtoInterface" and assume that DTOs exclusively comprise public properties.

Here's a code illustration:
use Apie\Core\Dto\DtoInterface;
use App\Apie\ValueObjects\StrongPassword;

class DtoExample implements DtoInterface
{
    public string $username;
    public StrongPassword $password;
    public ?string $totp = null;
}

In this example it contains just 2 fields that are required and one that optional.

Composite value objects

As previously mentioned, I encountered challenges involving fields with constraints or business logic—situations where Data Transfer Objects (DTOs) and entities weren't suitable. An example of this is dealing with ranges or, in some cases, crafting objects that don't warrant the status of an entity, but still encompass multiple fields—take addresses, for instance. This is where composite value objects play a crucial role.

In essence, a composite value object is designed to return an array, transforming an array into a collection of fields. It possesses the ability to validate constraints during its construction.

Allow me to provide an example of a date range, utilizing the "apie/date-value-objects" package to incorporate a Date value object without the time component:

An example for a date range (using apie/date-value-objects for having a Date value object without time)
use Apie\Core\ValueObjects\CompositeObject;
use Apie\DateValueObjects\LocalDate;

class DateRange implements ValueObjectInterface
{
    use CompositeObject;

    public function __construct(
        private LocalDate $startDate,
        private LocalDate $endDate
    ) {
    }

    private function validateState(): void
    {
        if ($this->startDate->toDate() > $this->endDate->toDate()) {
            throw new LogicException('Start date can not be after the end date');
        }
    }
}

Lists and hashmaps

One of the very common issues in PHP when dealing with Rest APIs is that the default json_encode() function incorporates business logic related to converting arrays into objects or arrays based on the content. This leads to complications with empty arrays, as more context is required to determine whether they should be represented as {} or [].

Furthermore, we aim to establish typed lists and hashmaps without resorting to reading PHPDocs or utilizing attributes solely for type hints. As a solution, I arrived at a foundational approach with the "ItemList" and "ItemHashmap" classes, which extend the "ArrayObject". If used directly we assume a list or hashmap that allows everything. If you extend the class the return type used for an extended offsetGet method is used as type restriction.

For the sake of consistency in behavior, array typehints will be mapped as a hashmap. This approach streamlines the process and ensures a uniform behavior throughout.

If I want an immutable list of LocalDate value objects I can easily make a class like this:
class LocalDateList extends ItemList
{
    protected bool $mutable = false;
    public function offsetGet(mixed $key): LocalDate
    {
        return parent::offsetGet($key);
    }
 }
The biggest gain is achieved when you want to use union types, for example I can easily create a string or int hashmap.
class StringOrIntHashmap extends ItemHashmap
{
    public function offsetGet(mixed $key): string|int
    {
        return parent::offsetGet($key);
    }
 }

Comments