Skip to content

shipmonk-rnd/input-mapper

Repository files navigation

ShipMonk Input Mapper

Bidirectional mapper for PHP with support for generics, array shapes and nullable types. For each class, input and output mappers are generated at runtime and cached on disk. The mappers are generated only once and then reused on subsequent requests. The generated mappers are highly optimized for performance and designed to be human readable. You can see examples of generated mappers in the tests directory: input mapper, output mapper.

Installation:

composer require shipmonk/input-mapper

Features

Built-in mappers

Input Mapper comes with built-in mappers for the following types:

  • array, bool, float, int, mixed, string, list
  • positive-int, negative-int, int<TMin, TMax>, non-empty-string, non-empty-list
  • array<V>, array<K, V>, list<V>, non-empty-list<V>
  • array{K1: V1, ...}
  • ?T, Optional<T>
  • DateTimeInterface, DateTimeImmutable
  • BackedEnum
  • and most importantly classes with public constructor

All built-in mappers support both input (array → object) and output (object → array) directions.

You can write your own mappers or replace the default mappers with your own.

Built-in validators

Input Mapper comes with some built-in validators (input mapping only):

  • int validators:
    • AssertInt16
    • AssertInt32
    • AssertIntRange
    • AssertPositiveInt
    • AssertNegativeInt
    • AssertNonNegativeInt
    • AssertNonPositiveInt
    • AssertIntMultipleOf
  • float validators:
    • AssertFloatRange
    • AssertPositiveFloat
    • AssertNegativeFloat
    • AssertNonNegativeFloat
    • AssertNonPositiveFloat
    • AssertFloatMultipleOf
  • string validators:
    • AssertStringLength
    • AssertStringMatches
    • AssertStringNonEmpty
    • AssertUrl
  • list validators:
    • AssertListItem
    • AssertListLength
    • AssertUniqueItems (compares items by ===)
  • date time validators:
    • AssertDateTimeRange

You can write your own validators if you need more.

Usage:

Write Input Class

To use Input Mapper, write a class with a public constructor and add either native or PHPDoc types to all constructor parameters.

Optional fields can either be marked with #[Optional] attribute (allowing you to specify a default value), or if you need to distinguish between default and missing values, you can wrap the type with ShipMonk\InputMapper\Runtime\Optional class.

use ShipMonk\InputMapper\Compiler\Mapper\Optional;

class Person
{
    public function __construct(
        public readonly string $name,

        public readonly int $age,

        #[Optional]
        public readonly ?string $email,

        /** @var list<string> */
        public readonly array $hobbies,

        /** @var list<self> */
        #[Optional(default: [])]
        public readonly array $friends,
    ) {}
}

By default, any extra properties are not allowed. You can change that by adding #[AllowExtraKeys] over the class.

Map Input

To map input data (e.g. JSON) to objects, use MapperProvider:

$tempDir = sys_get_temp_dir() . '/input-mapper';
$autoRefresh = true; // MUST be set to false in production
$mapperProvider = new ShipMonk\InputMapper\Runtime\MapperProvider($tempDir, $autoRefresh);
$mapper = $mapperProvider->getInputMapper(Person::class);

try {
    $person = $mapper->map([
        'name' => 'John',
        'age' => 30,
        'hobbies' => ['hiking', 'reading'],
        'friends' => [
            [
                'name' => 'Jane',
                'age' => 28,
                'hobbies' => ['hiking', 'reading'],
            ],
            [
                'name' => 'Jack',
                'age' => 28,
                'hobbies' => ['hiking', 'reading'],
            ],
        ],
    ]);
} catch (\ShipMonk\InputMapper\Runtime\Exception\MappingFailedException $e) {
    // $e->getMessage() // programmer readable error message in English
    // $e->getPath() // path of the problematic field for example ['friends', 0, 'name']
    // ...
}

Map Output

To convert objects back to plain arrays (e.g. for JSON serialization), use the same MapperProvider:

$mapper = $mapperProvider->getOutputMapper(Person::class);

$data = $mapper->map($person);
// ['name' => 'John', 'age' => 30, 'email' => null, 'hobbies' => ['hiking', 'reading'], 'friends' => [...]]

The output mapper converts objects to arrays, enums to their backing values, DateTimeImmutable to formatted strings, and Optional properties are omitted from the output when not defined. All types supported by input mapping are also supported by output mapping.

Adding Validation Rules

You can add validation rules by adding attributes to constructor parameters.

For example, to validate that age is between 18 and 99, you can add the AssertIntRange attribute to the constructor parameter:

use ShipMonk\InputMapper\Compiler\Validator\Int\AssertIntRange;

class Person
{
    public function __construct(
        public readonly string $name,

        #[AssertIntRange(gte: 18, lte: 99)]
        public readonly int $age,
    ) {}
}

Renaming keys

If the input keys do not match the property names, you can use the #[SourceKey] attribute to specify the key name:

use ShipMonk\InputMapper\Compiler\Mapper\Object\SourceKey;

class Person
{
    public function __construct(
        #[SourceKey('full_name')]
        public readonly string $name,
    ) {}
}

Parsing polymorphic classes (subtypes with a common parent)

If you need to parse a hierarchy of classes, you can use the #[Discriminator] attribute. (The discriminator field does not need to be mapped to a property if #[AllowExtraKeys] is used.)

use ShipMonk\InputMapper\Compiler\Mapper\Object\Discriminator;

#[Discriminator(
    key: 'type', // key to use for mapping
    mapping: [
        'car' => Car::class,
        'truck' => Truck::class,
    ]
)]
abstract class Vehicle {
    public function __construct(
        public readonly string $type,
    ) {}
}

class Car extends Vehicle {

    public function __construct(
        string $type,
        public readonly string $color,
    ) {
        parent::__construct($type);
    }

}

class Truck extends Vehicle {

    public function __construct(
        string $type,
        public readonly string $color,
    ) {
        parent::__construct($type);
    }

}

or, with enum:

use ShipMonk\InputMapper\Compiler\Mapper\Object\Discriminator;

enum VehicleType: string {
    case Car = 'car';
    case Truck = 'truck';
}

#[Discriminator(
    key: 'type', // key to use for mapping
    mapping: [
        VehicleType::Car->value => Car::class,
        VehicleType::Truck->value => Truck::class,
    ]
)]
abstract class Vehicle {
    public function __construct(
        VehicleType $type,
    ) {}
}

class Car extends Vehicle {

    public function __construct(
        VehicleType $type,
        public readonly string $color,
    ) {
        parent::__construct($type);
    }

}

class Truck extends Vehicle {

    public function __construct(
        VehicleType $type,
        public readonly string $color,
    ) {
        parent::__construct($type);
    }

}

Using custom mappers

To map classes with your custom mapper, you need to implement the InputMapper or OutputMapper interface and register it with the corresponding provider:

class MyCustomInputMapper implements ShipMonk\InputMapper\Runtime\InputMapper
{
    public function map(mixed $data, array $path = []): mixed
    {
        return MyCustomClass::createFrom($data);
    }
}

$inputMapperProvider->registerFactory(MyCustomClass::class, function () {
    return new MyCustomInputMapper();
});

Customizing default mappers inferred from types

To customize how default mappers are inferred from types, you need to implement ShipMonk\InputMapper\Compiler\MapperFactory\MapperCompilerFactory and MapperCompilerFactoryProvider.

Then register your factory provider with the corresponding provider:

$mapperCompilerFactoryProvider = new MyCustomMapperCompilerFactoryProvider();

$inputMapperProvider = new ShipMonk\InputMapper\Runtime\InputMapperProvider(
    tempDir: $tempDir,
    autoRefresh: $autoRefresh,
    mapperCompilerFactoryProvider: $mapperCompilerFactoryProvider,
);

$outputMapperProvider = new ShipMonk\InputMapper\Runtime\OutputMapperProvider(
    tempDir: $tempDir,
    autoRefresh: $autoRefresh,
    mapperCompilerFactoryProvider: $mapperCompilerFactoryProvider,
);

Contributing

  • Check your code by composer check
  • Autofix coding-style by composer fix:cs
  • All functionality must be tested

About

Performant array-to-object mapper supporting generics, array shapes, optional fields and much more!

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages