Маппер по reflection v1.1.0
DTO: fromArray, toArray, toJson через AbstractReflection и ReflectionMapper.
Введение
AbstractReflection — базовый класс для DTO с публичными типизированными свойствами. Даёт fromArray(), toArray() и toJson(). Заполнение и выгрузку делает ReflectionMapper.
Зачем: принять массив с API или формы, проверить поля атрибутами валидации, отдать обратно массив или JSON — без ручного присваивания каждого поля.
Предварительные требования
Пример
use Devcraft\Abstracts\AbstractReflection;
use Devcraft\Attributes\ArrayOf;
use Devcraft\Attributes\Range;
final class Address extends AbstractReflection
{
public string $city;
}
final class Profile extends AbstractReflection
{
public string $name;
public Address $address;
#[ArrayOf(Address::class)]
public array $locations = [];
#[Range(min: 0)]
public int $score = 0;
}
$profile = Profile::fromArray([
'name' => 'Ada',
'address' => ['city' => 'Berlin'],
'locations' => [
['city' => 'Berlin'],
],
'score' => '10',
]);
$profile->toArray();
$profile->toJson();Канонический пример Proxy / Address:
use Devcraft\Abstracts\AbstractReflection;
use Devcraft\Attributes\ArrayOf;
use Devcraft\Attributes\Range;
use Devcraft\Attributes\Regex;
final class Address extends AbstractReflection
{
public string $city;
}
final class Proxy extends AbstractReflection
{
#[Regex('/^[0-9a-f-]{36}$/i')]
public string $id;
#[Range(min: 1, max: 65535)]
public int $port;
public Address $address;
#[ArrayOf(Address::class)]
public array $locations = [];
}
$proxy = Proxy::fromArray([
'id' => '550e8400-e29b-41d4-a716-446655440000',
'port' => '8080',
'address' => ['city' => 'Berlin'],
'locations' => [
['city' => 'Berlin'],
['city' => 'Paris'],
],
]);
$proxy->port; // int 8080 (string coerced)
$proxy->address->city; // 'Berlin'
$proxy->toArray(); // nested arrays
echo $proxy->toJson(); // pretty-printed JSONСм. также: AbstractReflection, ReflectionMapper, ArrayOf.
API AbstractReflection
| Метод | Описание |
|---|---|
static fromArray(array $data): static | Создать экземпляр и заполнить public-свойства |
toArray(): array | Выгрузить инициализированные public-свойства (вложенные DTO рекурсивно) |
toJson(): string | Красивый JSON с Unicode; при ошибке — JsonException |
static setLogger(LoggerInterface $logger): void | Свой логгер для сбоев валидации |
static resetLogger(): void | Сбросить логгер (тесты) |
В маппинге участвуют только public, не static свойства. Private/protected mapper игнорирует.
Правила заполнения
ReflectionMapper::hydrate($target, $data) обходит public-свойства:
- Нет ключа или явный
null→ запасной путь для null (см. ниже). - Значение приводится к типу свойства.
#[ArrayOf]обрабатывает списки.- Атрибуты валидации проверяют уже приведённое значение.
- Ошибки собираются; если карта ошибок не пуста →
ValidationException.
Скаляры
| Объявленный тип | Что принимают |
|---|---|
string | только string |
int | int или числовая строка (FILTER_VALIDATE_INT) |
float | float, int или числовая строка (FILTER_VALIDATE_FLOAT) |
bool | bool, 0/1 или булевы строки (FILTER_VALIDATE_BOOLEAN) |
array | array |
object | object |
mixed / без типа | что угодно |
Объединения и пересечения типов свойств не поддерживаются — падают при приведении.
Вложенные DTO
Если тип свойства — наследник AbstractReflection, а на входе массив, mapper создаёт вложенный класс и заполняет его. Путь ошибки — через точку (address.city, locations.0.city).
Если на входе уже экземпляр нужного типа (или интерфейса) — принимается как есть.
Запасной путь для null
Когда ключа нет или значение null:
- свойства с default оставляют default;
- nullable без пригодного значения становятся
null; - обязательные non-nullable без default получают ошибку «is required».
При сбое приведения/валидации у nullable свойства mapper ставит null вместо добавления пути в выброшенную карту ошибок (сбой всё равно пишется в лог).
#[ArrayOf]
ArrayOf — и подсказка для преобразования, и правило валидации.
Значение должно быть списком (array_is_list). Каждый элемент приводится к объявленному типу (встроенный или вложенный AbstractReflection).
#[ArrayOf('int')]
public array $ids = [];
#[ArrayOf(Address::class)]
public array $locations = [];Ассоциативные массивы падают с сообщением must be a list. Ошибки элементов — ids.1, locations.0.city и т.д.
Выгрузка
toArray() включает только инициализированные public-свойства. Вложенные AbstractReflection раскрываются рекурсивно. Обычные массивы обходятся поэлементно.
toJson() кодирует toArray() с:
JSON_THROW_ON_ERRORJSON_UNESCAPED_UNICODEJSON_PRETTY_PRINTJSON_UNESCAPED_SLASHESJSON_INVALID_UTF8_SUBSTITUTE
Логирование
По умолчанию — Analog\Logger. Для тестов или своего лога:
AbstractReflection::setLogger($psrLogger);Каждый сбой пишется с уровнем error и контекстом: class, property (путь через точку), expected, actual_type.
Прямой вызов mapper
Можно заполнить уже созданный объект без fromArray():
use Devcraft\Mapper\ReflectionMapper;
use Psr\Log\NullLogger;
$mapper = new ReflectionMapper(new NullLogger());
$mapper->hydrate($profile, $payload);Второй аргумент конструктора — необязательный свой PropertyValidator.