arnapou / dto
Library - Simple library to create objects from arrays and vice-versa.
Requires
- php: ~8.3.0
- arnapou/ensure: ^2.3
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.52
- phpstan/extension-installer: ^1.3
- phpstan/phpstan: ^1.10
- phpstan/phpstan-deprecation-rules: ^1.1
- phpstan/phpstan-phpunit: ^1.3
- phpstan/phpstan-strict-rules: ^1.5
- phpunit/php-code-coverage: ^11.0
- phpunit/phpunit: ^11.0
- symfony/serializer: ^6.4
README
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
- integer timestamps <=>
DateTimeInterface
through DateTimeConverter - string datetimes <=>
DateTimeInterface
through DateTimeConverter - int + string <=>
BackedEnum
through EnumConverter - array <=>
stdClass
through StdClassConverter - array <=>
object
through SchemaConverter - array <=>
list<object|array|bool|float|int|string|null>
through CollectionConverter - customisable by implementing a dedicated DataConverter
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:
- use an object to carry the item's list
- implement a dedicated converter
- implement a Collection, or extend an AbstractCollection
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
- strict typing, because performance and less code matter
- objects, because memory is bad with arrays, objects perform better
- simple type validation
- want to discard any useless data to focus only on the needed input
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
Date | Ref | 8.3 | 8.2 |
---|---|---|---|
02/04/2024 | 4.x, main | × | |
25/11/2023 | 3.x | × | |
17/09/2023 | 2.x | × | |
03/09/2023 | 1.x | × |