futuretek/data-mapper

A lightweight, reflection-based PHP 8.4+ data mapping library for converting associative arrays into typed PHP objects (POPOs), and back. Ideal for OpenAPI-generated DTOs, form handling, and request/response serialization in modern API architectures.

Maintainers

Package info

github.com/futuretek-solutions-ltd/data-mapper

Homepage

pkg:composer/futuretek/data-mapper

Statistics

Installs: 30

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

1.1.0 2026-03-04 13:12 UTC

This package is auto-updated.

Last update: 2026-03-04 13:16:19 UTC


README

A lightweight PHP 8.4+ utility for mapping associative arrays to plain PHP objects (POPOs) and vice versa, using reflection and PHP attributes.

Features

  • ⚙️ Supports native PHP 8.4+ typed properties (scalars, nullable, mixed, object, iterable)
  • 📌 Declarative mapping via custom attributes
  • 📅 Handles date and datetime format conversions via #[Format]
  • 🗂️ Supports typed arrays of objects via #[ArrayType]
  • 🗺️ Supports associative maps via #[MapType]
  • 📁 Supports file mapping via SplFileObject and PSR-7 UploadedFileInterface
  • 🧩 Backed enum handling with tryFrom
  • 🔄 Nested object mapping (recursive)
  • 🔒 Readonly property support via reflection
  • 🔐 PHP 8.4 asymmetric visibility support (e.g., public private(set))
  • 🔍 Optional strict validation for required properties
  • ✅ Converts objects back to associative arrays (toArray)
  • 🚫 Skips non-public and static properties
  • 💪 Gracefully handles uninitialized properties in toArray

Installation

composer require futuretek/data-mapper

Usage

Define DTO with Attributes

use futuretek\datamapper\attributes\ArrayType;
use futuretek\datamapper\attributes\MapType;
use futuretek\datamapper\attributes\Format;

class BlogPost
{
    public string $title;
    public ?string $subtitle = null;

    #[Format('date-time')]
    public \DateTimeImmutable $publishedAt;

    #[ArrayType(Comment::class)]
    public array $comments;

    #[MapType(valueType: Tag::class)]
    public array $tags;

    public StatusEnum $status;
    public Author $author;

    public readonly string $slug;
}

Map From Array

use futuretek\datamapper\DataMapper;

$dto = DataMapper::toObject($dataArray, BlogPost::class);

Convert Back To Array

$array = DataMapper::toArray($dto);

Configuration

// Throw InvalidArgumentException when a non-nullable property is missing from input
DataMapper::$validateRequiredProperties = true;

// Set a factory for converting file resources to PSR-7 UploadedFileInterface
DataMapper::$fileFactory = new MyFileFactory();

Custom Attributes

#[Format]

Parses date strings into DateTimeImmutable instances.

#[Format('date')]        // Parses "2025-06-17" using Y-m-d format
public \DateTimeInterface $birthDate;

#[Format('date-time')]   // Parses "2025-06-17T15:00:00+00:00" using ISO 8601 / ATOM format
public \DateTimeInterface $createdAt;

When converting back via toArray, a DateTimeInterface property without #[Format] defaults to date-time format.

#[ArrayType]

Declares the item type of an array property — supports both class names and scalar type names.

#[ArrayType(Comment::class)]   // Array of objects — each item is recursively mapped
public array $comments;

#[ArrayType('int')]            // Array of scalars — items are passed through as-is
public array $scores;

#[MapType]

Declares a property as an associative map (string keys to typed values).

#[MapType(valueType: Tag::class)]    // Map of objects — values are recursively mapped
public array $tags;

#[MapType(valueType: 'string')]      // Map of scalars — values are passed through as-is
public array $translations;

Supported Property Types

Type toObject Behavior toArray Behavior
string, int, float, bool Assigned directly Returned as-is
Nullable (?type) null values accepted; missing keys use default null returned
DateTimeInterface + #[Format] Parsed from string via createFromFormat Formatted to string
array + #[ArrayType] Items mapped recursively if class type Items converted recursively
array + #[MapType] Values mapped recursively if class type Values converted recursively
Backed enum Resolved via tryFrom() Serialized to backing value
Nested class Recursively mapped from sub-array Recursively converted to sub-array
object Cast from array via (object) JSON encode/decode to array
mixed Assigned as-is Returned as-is
UploadedFileInterface Assigned directly or via $fileFactory Returned as-is
SplFileObject Assigned directly Returned as-is
readonly Set via reflection Returned normally
public private(set) Set via reflection Returned normally
Untyped (public $x) Assigned as-is Returned as-is

Property Handling Rules

  • Non-public properties (private, protected) are skipped.
  • Static properties are always skipped.
  • Unknown keys in the input array are silently ignored.
  • Missing non-nullable properties with $validateRequiredProperties = true throw InvalidArgumentException.
  • Missing non-nullable properties with $validateRequiredProperties = false remain uninitialized.
  • Uninitialized properties are skipped during toArray conversion.
  • Invalid enum values throw UnexpectedValueException.
  • Malformed date strings result in null (via DateTimeImmutable::createFromFormat returning false).

File Handling

Supports SplFileObject and PSR-7 UploadedFileInterface properties.

For UploadedFileInterface properties, if the input value is already an UploadedFileInterface instance, it is assigned directly. Otherwise, the configured FileFactoryInterface is used to convert the value. If no factory is configured, a RuntimeException is thrown.

Implementing FileFactoryInterface

use futuretek\datamapper\FileFactoryInterface;
use Psr\Http\Message\UploadedFileInterface;

class MyFileFactory implements FileFactoryInterface
{
    public function createFromResource(mixed $resource): UploadedFileInterface
    {
        if (is_string($resource)) {
            // Handle file path
            $stream = fopen($resource, 'r');
            return new MyUploadedFile($stream, filesize($resource), basename($resource));
        }

        throw new \InvalidArgumentException('Unsupported resource type: ' . gettype($resource));
    }
}

Limitations

  • Union types (e.g., int|string) are not supported.
  • Intersection types are not supported.
  • The constructor is bypassed via newInstanceWithoutConstructor() — constructor logic will not run.
  • DateTimeImmutable is always used for date parsing, regardless of whether the property type is DateTime or DateTimeInterface.

License

Apache License 2.0