arnapou/dto

Library - Simple library to create objects from arrays and vice-versa.

v4.4 2024-06-25 23:04 UTC

This package is auto-updated.

Last update: 2024-09-17 06:38:40 UTC


README

pipeline coverage

This library is a simple tool to create objects from arrays and vice-versa.

Installation

composer require arnapou/dto

packagist 👉️ arnapou/dto

Features

This lib aims to be simple.

By "simple", I mean: easy to understand the behaviour, without any magic, by using the core of the php language, no attributes, neither phpdoc, etc ...

This basically achieves conversions

  • [data] to object = fromData =~ denormalize
  • object to [data] = toData =~ normalize
interface DataConverter
{
    public function fromData(array|string|float|int|bool|null $data, string $class): Success|Unsupported;

    public function toData(object $object): array|string|float|int|bool|Unsupported|null;
}

Note about the return values of fromData and toData :

  • if the value is not supported, the converter must return an Unsupported object.
  • fromData can either return a Success object which carries the result.
  • if there is an error, a ConverterException will be raised.
  • if you don't want to manage that, you can use the ConverterWrapper convenience object.

⚠️ Important

This lib does not allow you to do things you cannot directly do from the user-land: this is its strength.

If you need a lot of customization in the name mapping, private properties accessibility, getters/setter calls, manage default values for properties without native default, etc ... Then I suggest you to look at alternatives like :

Properties

  • only public properties are used
  • managed readonly or promoted properties
  • no private/protected magic injection

Methods

  • only the constructor arguments are used when the object is created
  • no other method (getter/setter, ...) are used

Name mapping

  • no mapping at all by default
  • if you want to map something, you need to implement a dedicated DataConverter

Value mapping

Note about arrays and lists

  • arrays are polymorphic in php because they can be hashmaps or lists
  • it is not possible to guess with certainty when to convert into a list of objects
  • thus, you must either:

Nested objects

  • yes, of course, that's one strength of this lib
  • to help to manage nested objects, you may use a BaseConverter which iterates over a list of injected DataConverter

Performance

The reflection done through the SchemaConverter is totally decoupled into a dedicated interface Schema which have the responsibility to retrieve the schema of each class.

This can be implemented by you, or you can let the ReflectionSchema do its job. All the metadata objects are immutables value objects you can also convert with this lib if you want to cache them as arrays somewhere.

If you have a lot (hundreds ?) of dedicated converters for your final objects and want a faster BaseConverter for your converters, I suggest you to use a ClassMappingConverter.

How to start

I suggest to start with the DtoConverter which is a BaseConverter with an automatic setup of nested converters :

Just play with it before going further to implement your own converters : it should be sufficient for 80% of your needs.

You may want to look at running examples into the "example" directory of this repo.

Example

use Arnapou\Dto\Converter\Result\Success;
use Arnapou\Dto\Converter\Result\Unsupported;

// Immutable DTO
readonly class ImmutableUser
{
    public function __construct(
        public int $id,
        public string $name,
        public DateTimeImmutable $createdAt = new DateTimeImmutable(),
    ) {
    }
}

// Array data
$data = [
    'id' => '1',
    'name' => 'Arnaud',
];

// Conversion
$converter = new \Arnapou\Dto\DtoConverter();
$result = $converter->fromData($data, ImmutableUser::class); 
$user = $result->get();

print_r($result->get());
// ImmutableUser Object
// (
//     [id] => 1
//     [name] => Arnaud
//     [createdAt] => DateTimeImmutable Object
//         (
//             [date] => 2023-09-03 18:29:48.118875
//             [timezone_type] => 3
//             [timezone] => Europe/Paris
//         )
// )

Use cases

When you want

Json input

$json = file_get_contents('php://input');
$data = json_decode($json, associative: true);

$myJsonPayload = $converter->fromData($data, MyJsonPayload::class);

Query, Request

$myQuery = $converter->fromData($_GET, MyQuery::class);
$myRequest = $converter->fromData($_POST, MyRequest::class);

Entity, RecordSet

/** @var PDO $pdo */
$rows = $pdo->query($selectQuery, \PDO::FETCH_ASSOC)->fetch();

// Manual entities: beware of memory and performance if a lot of items.
$myEntities = [];
foreach ($rows as $row) {
    $myEntities[] = $converter->fromData($row, MyEntity::class);
}

// Automatic entities: better memory using iterators and on-demand conversion.
$myEntities = new \Arnapou\Dto\ObjectIterator($converter, $rows, MyEntity::class);

Nested objects & Recursion

The entry point of conversion for nested objects is BaseConverter. This class loops over a list of converters to try to convert your data/object.

Because recursion can lead to infinite loops with circular dependencies, we manage that by a simple depth check (see Depth).

To enforce this check, the provided converters which need recursion require a BaseConverter in their constructor:

You can extend the BaseConverter to override the constructor if you need to specialize it. That's the case of DtoConverter. But because inheritance can lead to problems, that's the only method you can override, all others are final.

DataDecorator

If you want to do some custom data mutation before or after conversion, you need to implement your own DataConverter, but considering recursion, it becomes really difficult to design your objects.

This is why I suggest to simply use a BaseConverter with a custom DataDecorator.

The DataDecorator is an interface which allows to mutate the data before fromData or after toData for all the internal converters. It is then executed at each level of the recursion without the need to change the whole object design.

This interface can help you to manage object changes in the project life, can be plugged or unplugged whenever you need.

Example setting the DataDecorator:

// The DtoConverter is a specialized BaseConverter
$converter = new \Arnapou\Dto\DtoConverter();
$converter->dataDecorator = new MyCustomDataDecorator();

Example of a DataConverter which implements a DataDecorator itself to replace - by _ because a property cannot contain a dash in its name:

use Arnapou\Dto\DtoConverter;
use Arnapou\Dto\Converter\DataConverter;

final class MyConverter extends DtoConverter implements DataDecorator
{
    public function __construct()
    {
        parent::__construct();
        $this->dataDecorator = $this;
    }

    public function decorateFromData(float|int|bool|array|string|null $data, string $class): array|string|float|int|bool|null
    {
        if (!\is_array($data)) {
            return $data;
        }

        $modified = [];
        foreach ($data as $key => $value) {
            if (\is_string($key)) {
                $modified[str_replace('-', '_', $key)] = $value;
            } else {
                $modified[$key] = $value;
            }
        }

        return $modified;
    }

    public function decorateToData(float|int|bool|array|string|null $data, string $class): array|string|float|int|bool|null
    {
        return $data;
    }
}

Php versions

DateRef8.38.2
02/04/20244.x, main×
25/11/20233.x×
17/09/20232.x×
03/09/20231.x×