Skip to content

zhalil/mapper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Zhalil Mapper

PHP-библиотека для маппинга данных в объекты с автоматической валидацией. Требует PHP 8.2+.

Установка

composer require zhalil/mapper

Быстрый старт

use Zhalil\Mapper\Validation\Attribute\{Required, Email, Min};
use function Zhalil\Mapper\map;

class UserDTO
{
    #[Required]
    #[Min(2)]
    public string $name;

    #[Required]
    #[Email]
    public string $email;

    public ?int $age = null;
}

$user = map([
    'name' => 'John',
    'email' => 'john@example.com',
])->to(UserDTO::class);

echo $user->name; // John

Содержание


Маппинг

Базовое использование

use function Zhalil\Mapper\map;

class Product
{
    public string $name;
    public float $price;
    public ?string $description = null;
}

$product = map([
    'name' => 'iPhone',
    'price' => 999.99,
])->to(Product::class);

Вложенные объекты

Маппер автоматически создаёт вложенные объекты по типу свойства:

class Address
{
    public string $city;
    public string $street;
}

class User
{
    public string $name;
    public Address $address;
}

$user = map([
    'name' => 'John',
    'address' => [
        'city' => 'Moscow',
        'street' => 'Tverskaya',
    ],
])->to(User::class);

echo $user->address->city; // Moscow

Массивы объектов

Используйте атрибут #[ArrayOf] или PHPDoc @var ClassName[]:

use Zhalil\Mapper\Attribute\ArrayOf;

class Order
{
    public int $id;

    /** @var Item[] */
    public array $items;
}

class Item
{
    public string $name;
    public int $quantity;
}

$order = map([
    'id' => 1,
    'items' => [
        ['name' => 'Product 1', 'quantity' => 2],
        ['name' => 'Product 2', 'quantity' => 1],
    ],
])->to(Order::class);

Или с атрибутом:

class Order
{
    public int $id;

    #[ArrayOf(Item::class)]
    public array $items;
}

Стратегии именования

Доступные стратегии

Стратегия Описание Пример
CamelCaseNamingStrategy По умолчанию, без изменений firstNamefirstName
SnakeCaseNamingStrategy Преобразует в snake_case firstNamefirst_name

Использование на уровне класса

use Zhalil\Mapper\Attribute\MapInputName;
use Zhalil\Mapper\NamingStrategy\SnakeCaseNamingStrategy;

#[MapInputName(SnakeCaseNamingStrategy::class)]
class UserDTO
{
    public string $firstName;
    public string $lastName;
}

$user = map([
    'first_name' => 'John',
    'last_name' => 'Doe',
])->to(UserDTO::class);

Переопределение на уровне свойства

#[MapInputName(SnakeCaseNamingStrategy::class)]
class UserDTO
{
    public string $firstName;

    #[MapInputName(CamelCaseNamingStrategy::class)]
    public string $lastName;
}

$user = map([
    'first_name' => 'John',
    'lastName' => 'Doe',
])->to(UserDTO::class);

Атрибуты маппинга

#[MapFrom]

Явное указание ключа во входных данных:

use Zhalil\Mapper\Attribute\MapFrom;

class User
{
    #[MapFrom('user_name')]
    public string $name;

    #[MapFrom('user_email')]
    public string $email;
}

#[MapTo]

Явное указание ключа при сериализации:

use Zhalil\Mapper\Attribute\MapTo;

class User
{
    #[MapTo('user_name')]
    public string $name;
}

$user = map(['name' => 'John'])->to(User::class);
$data = map($user)->toArray();
// ['user_name' => 'John']

#[Strict]

Строгий режим — выбрасывает исключение при отсутствии обязательного поля:

use Zhalil\Mapper\Attribute\Strict;

#[Strict]
class User
{
    public string $name; // Обязательно
    public ?string $email = null; // Опционально (nullable)
}

// Выбросит MappingException, т.к. 'name' отсутствует
map([])->to(User::class);

Можно применять к отдельным свойствам:

class User
{
    #[Strict]
    public string $name;

    public ?string $email = null;
}

#[Hidden]

Скрыть свойство при сериализации:

use Zhalil\Mapper\Attribute\Hidden;

class User
{
    public string $name;

    #[Hidden]
    public string $password;
}

$user = map(['name' => 'John', 'password' => 'secret'])->to(User::class);
map($user)->toArray(); // ['name' => 'John'] — password скрыт

#[MapOutputName]

Стратегия именования при сериализации (объект → массив):

use Zhalil\Mapper\Attribute\MapOutputName;
use Zhalil\Mapper\NamingStrategy\SnakeCaseNamingStrategy;

#[MapOutputName(SnakeCaseNamingStrategy::class)]
class User
{
    public string $firstName;
    public string $lastName;
}

$user = map(['firstName' => 'John'])->to(User::class);
map($user)->toArray(); // ['first_name' => 'John', 'last_name' => null]

#[CastWith]

Кастомное преобразование значения:

use Zhalil\Mapper\Attribute\CastWith;
use Zhalil\Mapper\Contract\CasterInterface;

class UpperCaseCaster implements CasterInterface
{
    public function cast(mixed $input): mixed
    {
        return strtoupper($input);
    }
}

class User
{
    #[CastWith(UpperCaseCaster::class)]
    public string $name;
}

$user = map(['name' => 'john'])->to(User::class);
echo $user->name; // JOHN

Валидация

Правила валидации

Атрибут Параметры Описание
#[Required] Поле обязательно
#[Nullable] Поле может быть null
#[Email] Валидный email
#[Url] Валидный URL
#[Min(int|float)] $min Минимальное значение/длина
#[Max(int|float)] $max Максимальное значение/длина
#[Between(int, int)] $min, $max Диапазон значений
#[Size(int)] $size Точное значение/длина
#[In(...$values)] $values Значение должно быть в списке
#[NotIn(...$values)] $values Значение не должно быть в списке
#[Regex(string)] $pattern Соответствие регулярному выражению
#[Enum(string)] $enumClass Значение из enum
#[Confirmed] Подтверждение (field + field_confirmation)
#[StringType] Строка
#[IntegerType] Целое число
#[Numeric] Число (int или float)
#[BooleanType] Булево значение
#[ArrayType] Массив
#[Date] Валидная дата
#[DateFormat(string)] $format Дата в формате
#[After(string)] $date Дата после указанной
#[Before(string)] $date Дата до указанной
#[AfterOrEqual(string)] $date Дата после или равна
#[BeforeOrEqual(string)] $date Дата до или равна
#[DateEquals(string)] $date Дата равна
#[Uuid] Валидный UUID
#[Ip] Валидный IP
#[Ipv4] Валидный IPv4
#[Ipv6] Валидный IPv6
#[MacAddress] Валидный MAC-адрес
#[Ulid] Валидный ULID
#[Timezone] Валидный часовой пояс
#[Json] Валидный JSON
#[Alpha] Только буквы
#[AlphaNumeric] Буквы и цифры
#[AlphaDash] Буквы, цифры, дефис, подчёркивание
#[Uppercase] Верхний регистр
#[Lowercase] Нижний регистр
#[StartsWith(string)] $prefix Начинается с
#[EndsWith(string)] $suffix Заканчивается на
#[DoesntStartWith(string)] $prefix Не начинается с
#[DoesntEndWith(string)] $suffix Не заканчивается на
#[Digits(int)] $count Точное количество цифр
#[DigitsBetween(int, int)] $min, $max Диапазон цифр
#[Same(string)] $field Равно другому полю
#[Different(string)] $field Отлично от другого поля
#[GreaterThan(string)] $field Больше другого поля
#[GreaterThanOrEqualTo(string)] $field Больше или равно
#[LessThan(string)] $field Меньше другого поля
#[LessThanOrEqualTo(string)] $field Меньше или равно
#[MultipleOf(int)] $value Кратно числу
#[Prohibited] Запрещено
#[ProhibitedIf(string, ...$values)] $field, $values Запрещено, если поле = значение
#[ProhibitedUnless(string, ...$values)] $field, $values Запрещено, если поле ≠ значение

Password

use Zhalil\Mapper\Validation\Attribute\Password;

class UserDTO
{
    #[Password(
        min: 8,          // Минимум 8 символов
        letters: true,   // Хотя бы одна буква
        mixedCase: true, // Верхний и нижний регистр
        numbers: true,   // Хотя бы одна цифра
        symbols: true    // Хотя бы один символ
    )]
    public string $password;
}

Условные правила

use Zhalil\Mapper\Validation\Attribute\Required;
use Zhalil\Mapper\Validation\Rule\{RequiredIf, RequiredWith, RequiredWithout};

class PaymentDTO
{
    public ?string $paymentMethod = null;

    #[Required]
    public string $cardNumber;
}

// Или через атрибуты условий (требуют реализации атрибутов):
// #[RequiredIf('paymentMethod', 'card')]
// #[RequiredWith('otherField')]
// #[RequiredWithout('otherField')]

Примеры валидации

use Zhalil\Mapper\Validation\Attribute\{Required, Email, Min, Max, Between, In, Regex};

class CreateUserDTO
{
    #[Required]
    #[Min(2)]
    #[Max(50)]
    public string $name;

    #[Required]
    #[Email]
    public string $email;

    #[Between(18, 100)]
    public int $age;

    #[In('admin', 'user', 'guest')]
    public string $role;

    #[Regex('/^[A-Z]{2}\d{6}$/')]
    public ?string $passportNumber = null;
}

Коллекции

Для маппинга массива объектов используйте collection():

class User
{
    public string $name;
    public string $email;
}

$users = map([
    ['name' => 'John', 'email' => 'john@example.com'],
    ['name' => 'Jane', 'email' => 'jane@example.com'],
])->collection()->to(User::class);

// $users — массив объектов User

Сериализация (объект → массив)

Маппер поддерживает преобразование объектов обратно в массивы с учётом атрибутов.

Базовое использование

class User
{
    public string $name;
    public string $email;
}

$user = map(['name' => 'John', 'email' => 'john@example.com'])->to(User::class);
$data = map($user)->toArray();
// ['name' => 'John', 'email' => 'john@example.com']

toJson()

$json = map($user)->toJson();

Атрибуты сериализации

#[MapTo] — переименование ключа

use Zhalil\Mapper\Attribute\MapTo;

class User
{
    #[MapTo('user_name')]
    public string $name;
}

$user = map(['name' => 'John'])->to(User::class);
$data = map($user)->toArray();
// ['user_name' => 'John']

#[Hidden] — скрыть свойство

use Zhalil\Mapper\Attribute\Hidden;

class User
{
    public string $name;

    #[Hidden]
    public string $password;
}

$user = map(['name' => 'John', 'password' => 'secret'])->to(User::class);
$data = map($user)->toArray();
// ['name' => 'John'] — password скрыт

#[MapOutputName] — стратегия именования

Преобразует имена свойств при сериализации:

use Zhalil\Mapper\Attribute\MapOutputName;
use Zhalil\Mapper\NamingStrategy\SnakeCaseNamingStrategy;

#[MapOutputName(SnakeCaseNamingStrategy::class)]
class User
{
    public string $firstName;
    public string $lastName;
}

$user = map(['firstName' => 'John', 'lastName' => 'Doe'])->to(User::class);
$data = map($user)->toArray();
// ['first_name' => 'John', 'last_name' => 'Doe']

Переопределение на уровне свойства:

#[MapOutputName(SnakeCaseNamingStrategy::class)]
class User
{
    public string $firstName;

    #[MapOutputName(CamelCaseNamingStrategy::class)]
    public string $lastName;
}

$user = map(['firstName' => 'John', 'lastName' => 'Doe'])->to(User::class);
$data = map($user)->toArray();
// ['first_name' => 'John', 'lastName' => 'Doe']

Автоматическая сериализация типов

Тип Результат
BackedEnum $enum->value
UnitEnum $enum->name
DateTimeInterface ISO 8601 формат
Вложенный объект Рекурсивный toArray()
Массив объектов Рекурсивная обработка
enum Status: string
{
    case Active = 'active';
}

class Address
{
    public string $city;
}

class User
{
    public string $name;
    public Status $status;
    public \DateTime $createdAt;
    public Address $address;
}

$user = map([
    'name' => 'John',
    'status' => 'active',
    'createdAt' => '2024-01-01',
    'address' => ['city' => 'Moscow'],
])->to(User::class);

$data = map($user)->toArray();
// [
//     'name' => 'John',
//     'status' => 'active',
//     'createdAt' => '2024-01-01T00:00:00+00:00',
//     'address' => ['city' => 'Moscow']
// ]

Сериализация коллекций

$users = [/* массив объектов User */];
$data = map($users)->collection()->toArray();
// Массив массивов

Обработка ошибок

MappingException

Выбрасывается при ошибках маппинга:

use Zhalil\Mapper\Exception\MappingException;

try {
    $user = map([])->to(User::class);
} catch (MappingException $e) {
    echo $e->getMessage();
    // "Failed to map data to User. Missing required fields: ['name', 'email']"
    
    $missing = $e->getMissingFields(); // ['name', 'email']
    $provided = $e->getProvidedData(); // []
}

ValidationException

Выбрасывается при ошибках валидации:

use Zhalil\Mapper\Exception\ValidationException;

try {
    $user = map([
        'name' => 'J',
        'email' => 'invalid',
    ])->to(UserDTO::class);
} catch (ValidationException $e) {
    $errors = $e->errors(); // ErrorBag
    
    $errors->all();      // ['name' => ['...'], 'email' => ['...']]
    $errors->has('email'); // true
    $errors->get('email'); // ['This value is not a valid email address.']
    $errors->isEmpty();    // false
    $errors->count();      // 2
    $errors->toJson();     // JSON-представление
}

Кастомные кастеры

Создайте класс, реализующий CasterInterface:

use Zhalil\Mapper\Contract\CasterInterface;

class DateTimeCaster implements CasterInterface
{
    public function cast(mixed $input): mixed
    {
        if ($input instanceof \DateTimeInterface) {
            return $input;
        }
        
        return new \DateTime($input);
    }
}

Использование:

use Zhalil\Mapper\Attribute\CastWith;

class Event
{
    public string $title;

    #[CastWith(DateTimeCaster::class)]
    public \DateTime $startsAt;
}

$event = map([
    'title' => 'Conference',
    'startsAt' => '2024-06-15 10:00:00',
])->to(Event::class);

echo $event->startsAt->format('Y-m-d'); // 2024-06-15

Полный пример

use Zhalil\Mapper\Attribute\{MapInputName, MapOutputName, MapFrom, Hidden, Strict, ArrayOf};
use Zhalil\Mapper\NamingStrategy\SnakeCaseNamingStrategy;
use Zhalil\Mapper\Validation\Attribute\{Required, Email, Min, Max, Password};
use function Zhalil\Mapper\map;

class Address
{
    public string $city;
    public string $street;
}

#[MapInputName(SnakeCaseNamingStrategy::class)]
#[MapOutputName(SnakeCaseNamingStrategy::class)]
#[Strict]
class CreateUserDTO
{
    #[Required]
    #[Min(2)]
    #[Max(50)]
    public string $firstName;

    #[Required]
    #[Min(2)]
    #[Max(50)]
    public string $lastName;

    #[Required]
    #[Email]
    public string $email;

    #[Password(min: 8, letters: true, numbers: true)]
    public string $password;

    #[Hidden]
    public string $passwordHash;

    #[ArrayOf(Address::class)]
    public array $addresses = [];

    public function __construct()
    {
        $this->passwordHash = '';
    }
}

try {
    $dto = map([
        'first_name' => 'John',
        'last_name' => 'Doe',
        'email' => 'john@example.com',
        'password' => 'Secure123',
        'addresses' => [
            ['city' => 'Moscow', 'street' => 'Tverskaya'],
        ],
    ])->to(CreateUserDTO::class);

    echo $dto->firstName; // John
    echo $dto->addresses[0]->city; // Moscow

    $data = map($dto)->toArray();
    // passwordHash скрыт, ключи в snake_case:
    // ['first_name' => 'John', 'last_name' => 'Doe', 'email' => '...', ...]

} catch (\Zhalil\Mapper\Exception\ValidationException $e) {
    foreach ($e->errors()->all() as $field => $messages) {
        foreach ($messages as $message) {
            echo "$field: $message\n";
        }
    }
} catch (\Zhalil\Mapper\Exception\MappingException $e) {
    echo "Mapping error: " . $e->getMessage();
}

Тестирование

composer test

Лицензия

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages