PHPackages                             zjkiza/flat-identity-dto-mapper - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. zjkiza/flat-identity-dto-mapper

ActiveLibrary

zjkiza/flat-identity-dto-mapper
===============================

Build rich DTO graphs from flat SQL result sets — without an ORM. A high-performance mapper that converts flat database/array rows into structured DTO graphs using identity mapping and attribute adapters.

v1.1.1(2mo ago)12MITPHPPHP ^8.2

Since Feb 20Pushed 2mo agoCompare

[ Source](https://github.com/zjkiza/flat-identity-dto-mapper)[ Packagist](https://packagist.org/packages/zjkiza/flat-identity-dto-mapper)[ RSS](/packages/zjkiza-flat-identity-dto-mapper/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (5)Dependencies (16)Versions (8)Used By (0)

Flat Identity DTO Mapper
========================

[](#flat-identity-dto-mapper)

Build rich DTO graphs from flat SQL result sets — without an ORM. A high-performance mapper that converts flat database/array rows into structured DTO graphs using identity mapping and attribute adapters.

Key features
------------

[](#key-features)

- Maps flat SQL JOIN results into deep DTO object graphs
- Identity Map (no duplicated objects, consistent merging)
- Attribute-driven mapping (PHP 8 Attributes)
- Nested objects &amp; collections (including lazy collections)
- Pluggable value transformers
- Circular reference safe
- High performance (reflection cached)
- Low memory footprint when mapping large result sets (single-pass merging, identity map, lazy collections)

Installation
------------

[](#installation)

Install via Composer:

```
composer require zjkiza/flat-identity-dto-mapper
```

Run tests locally (if you want to run the project's tests):

```
composer install
./vendor/bin/phpunit
```

Quickstart
----------

[](#quickstart)

This mapper expects a flat array of associative rows (for example the result of a SQL JOIN) and a DTO class annotated with attributes which describe how columns map to DTO properties.

Important: the root DTO MUST define a `ColumnPrefix` attribute — this tells the mapper which column prefixes to use when extracting scalar values for that DTO and its nested children.

You may pass any flat associative array (for example rows returned by `PDO::fetchAll(PDO::FETCH_ASSOC)` or any other source that produces a list of maps). The mapper's only requirement is that column names follow the prefix conventions used in your DTO attributes (see `ColumnPrefix`, `ObjectDto`, `Collection`).

Example: rows (excerpt from `tests/Resources/Data/data.json`):

```
$rows = json_decode(file_get_contents(__DIR__ . '/tests/Resources/Data/data.json'), true);
```

Database example (SQL)

Below is a real-world example of a JOIN query that returns a flat result set suitable for mapping with this library (column aliases use the `media_`, `media_image_`, `media_author_` prefixes used in the test DTOs):

```
SELECT
  media.id AS media_id,
  media.title AS media_title,
  media_image.id AS media_image_id,
  media_image.name AS media_image_name,
  media_image_tag.id AS media_image_tag_id,
  media_image_tag.name AS media_image_tag_name,
  author.id AS media_author_id,
  author.name AS media_author_name,
  author_image.id AS author_image_id,
  author_image.name AS author_image_name,
  author_image_tag.id AS author_image_tag_id,
  author_image_tag.name AS author_image_tag_name
FROM media AS media
  LEFT JOIN media_author AS mediaAuthor ON mediaAuthor.abstract_media_id = media.id
  LEFT JOIN expert AS author ON author.id = mediaAuthor.expert_id
  LEFT JOIN image AS media_image ON media_image.id = media.image_id
  LEFT JOIN image_tag ON image_tag.image_id = media_image.id
  LEFT JOIN tag AS media_image_tag ON media_image_tag.id = image_tag.tag_id
  LEFT JOIN image AS author_image ON author_image.id = author.image_id
  LEFT JOIN image_tag AS image_tag_auth ON image_tag_auth.image_id = author_image.id
  LEFT JOIN tag AS author_image_tag ON author_image_tag.id = image_tag_auth.tag_id;
```

PDO fetch example (how to obtain a compatible PHP array of rows):

```
$pdo = new \PDO($dsn, $user, $pass, $options);
$stmt = $pdo->query($sql);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); // flat associative arrays

$mapper = new ZJKiza\FlatMapper\UniversalDtoMapper();
$dto = $mapper->map($rows, \ZJKiza\FlatMapper\Tests\Resources\Dto\MediaDto::class, 'media_id');
```

Doctrine example:

```
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

class MediaRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, AbstractMedia::class);
    }

    public function getRows(): array
    {
        $sql = 'SELECT ...'; // same SQL as above
        $stmt = $this->entityManager->getConnection()->executeQuery($sql);

        return $stmt->fetchAllAssociative(); // flat associative arrays
    }
}

use ZJKiza\FlatMapper\UniversalDtoMapper;

class TestController
{
    public function index(MediaRepository $repository, UniversalDtoMapper $mapper)
    {
        $rows = $repository->getRows();
        $dto = $mapper->map($rows, MediaDto::class, 'media_id');
        return new JsonResponse($dto);
    }
}
```

Basic usage — map flat rows to DTO graph:

```
use ZJKiza\FlatMapper\UniversalDtoMapper;
use ZJKiza\FlatMapper\Tests\Resources\Dto\MediaDto;

$mapper = new UniversalDtoMapper();
$dto = $mapper->map($rows, MediaDto::class, 'media_id');

echo json_encode($dto, JSON_THROW_ON_ERROR);
```

This repository includes a functional test that maps the provided test rows into the expected JSON graph. The mapper will group rows by the "root id" (here `media_id`), create DTO instances, and merge nested/scalar values while preserving identity.

DTO definition (attributes)
---------------------------

[](#dto-definition-attributes)

DTOs use PHP Attributes to describe mapping rules. Example DTO (from tests):

```
use ZJKiza\FlatMapper\Attribute\ColumnPrefix;
use ZJKiza\FlatMapper\Attribute\Identifier;
use ZJKiza\FlatMapper\Attribute\Transformer;
use ZJKiza\FlatMapper\Attribute\ObjectDto;
use ZJKiza\FlatMapper\Attribute\Collection;
use ZJKiza\FlatMapper\Enum\Naming;
use ZJKiza\FlatMapper\Transformer\UpperTransformer;

#[ColumnPrefix(name: 'media_', naming: Naming::CamelToSnake)]
final class MediaDto implements \JsonSerializable
{
    #[Identifier]
    public ?string $id = null;

    #[Transformer(UpperTransformer::class)]
    public ?string $title = null;

    #[ObjectDto(className: MediaImageDto::class, columnPrefix: 'media_image_', naming: Naming::CamelToSnake)]
    public ?MediaImageDto $image = null;

    #[Collection(className: AuthorDto::class, columnPrefix: 'media_author_', naming: Naming::CamelToSnake)]
    public iterable|null $author = null;

    public function jsonSerialize(): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'image' => $this->image,
            'author' => \array_values(\iterator_to_array($this->author)),
        ];
    }
}
```

Attribute reference:

- ColumnPrefix(name: string, naming: Naming) — required on the root DTO. Defines the column prefix for root scalar properties and default naming strategy.
- Identifier — mark the property that holds the unique identifier for the entity (used by IdentityMap).
- Column(name: string) — map a property to a different column name under the active column prefix.
- ObjectDto(className: string, columnPrefix: string, naming: Naming) — map a nested object; a new sub-prefix can be used.
- Collection(className: string, columnPrefix: string, naming: Naming, lazy: bool = false) — map a collection of nested objects (supports lazy collections).
- Transformer(className: string) — apply a value transformer when setting the property.
- Ignore — skip a scalar property from mapping.

Notes:

- Naming strategies (CamelToSnake / SnakeToCamel) help convert column names to property names when using default names.
- The root DTO MUST define a ColumnPrefix attribute.

Built-in transformers
---------------------

[](#built-in-transformers)

Transformers implement `ZJKiza\FlatMapper\Contract\DataTransformerInterface` (single method: `transform(mixed $value): mixed`).

Example built-in transformer (UpperTransformer):

```
final class UpperTransformer implements DataTransformerInterface
{
    public function transform(mixed $value): ?string
    {
        if (null === $value) {
            return null;
        }

        if (\\is_string($value)) {
            return \\strtoupper($value);
        }

        throw TransformationFailedException::create($value, $this);
    }
}
```

Apply transformer on a DTO property:

```
#[Transformer(UpperTransformer::class)]
public ?string $title = null;
```

Extending with custom transformers
----------------------------------

[](#extending-with-custom-transformers)

To create a custom transformer, implement `DataTransformerInterface`:

```
use ZJKiza\FlatMapper\Contract\DataTransformerInterface;

final class TrimAndUpperTransformer implements DataTransformerInterface
{
    public function transform(mixed $value): mixed
    {
        if (null === $value) {
            return null;
        }

        if (is_string($value)) {
            return strtoupper(trim($value));
        }

        return $value;
    }
}
```

Registering your transformer depends on your framework (see below). The mapper's transformer resolver will instantiate the class when needed if it can be autoloaded/in the container.

Collections &amp; Lazy Collections
----------------------------------

[](#collections--lazy-collections)

Collections are defined using `Collection` attribute on an iterable property. By default the mapper will populate a PHP array. If `lazy: true` is specified, the mapper returns a `LazyCollection` that defers iteration until needed.

Example (lazy collection):

```
#[Collection(className: AuthorLazyDto::class, columnPrefix: 'media_author_', naming: Naming::CamelToSnake, lazy: true)]
public iterable|null $author = null;
```

In tests, both eager and lazy collections are exercised and produce the same final JSON output.

Identity Map
------------

[](#identity-map)

The mapper uses an Identity Map to guarantee that entities with the same identifier produce a single object instance across the whole mapped graph. This prevents duplicated objects when multiple rows refer to the same nested entity.

API is internal to the mapper; however the identity behavior can be observed in test fixtures where nested images and authors are shared across rows.

Errors and exceptions
---------------------

[](#errors-and-exceptions)

The mapper throws specific exceptions for common errors:

- InvalidArrayKayException — when the provided root id is missing from rows or a required column is not present.
- InvalidAttributeException — when required attributes (like ColumnPrefix) are missing from DTO classes.
- InvalidObjectInstanceException — when a nested object cannot be instantiated with expected type.
- TransformationFailedException — thrown by transformers when an unexpected value is provided.

Symfony integration (example)
-----------------------------

[](#symfony-integration-example)

Below is a minimal example for registering the mapper as services in `services.yaml`.

```
services:

  ZJKiza\FlatMapper\UniversalDtoMapper: ~

  ZJKiza\FlatMapper\Contract\UniversalDtoMapperInterface: '@ZJKiza\FlatMapper\UniversalDtoMapper'
```

Below is a minimal example for registering the mapper and a custom transformer as services in `services.yaml`.

```
services:

  App\Transformer\LowercaseTransformer: ~

  ZJKiza\FlatMapper\Transformer\Transformer:
    calls:
      - [addTransformer, ['@App\Transformer\LowercaseTransformer']]

  ZJKiza\FlatMapper\UniversalDtoMapper:
    arguments:
      $transformer: '@ZJKiza\FlatMapper\Transformer\Transformer'

  # Alias the interface to the implementation for easier injection
  ZJKiza\FlatMapper\Contract\UniversalDtoMapperInterface: '@ZJKiza\FlatMapper\UniversalDtoMapper'
```

Example controller usage:

```
public function index(UniversalDtoMapper $mapper)
{
    $rows = /* fetch rows from DB */;
    $dto = $mapper->map($rows, MediaDto::class, 'media_id');
    return new JsonResponse($dto);
}
```

Laravel integration (example)
-----------------------------

[](#laravel-integration-example)

You can register the mapper in a ServiceProvider (example for Laravel 8+):

```
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use ZJKiza\FlatMapper\UniversalDtoMapper;
use ZJKiza\FlatMapper\Contract\UniversalDtoMapperInterface;

class MapperServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(UniversalDtoMapper::class);

        $this->app->alias(
            UniversalDtoMapper::class,
            UniversalDtoMapperInterface::class
        );
    }
}
```

You can register the mapper and custom transformers in a ServiceProvider (example for Laravel 8+):

```
use Illuminate\Support\ServiceProvider;
use App\Transformer\TrimAndUpperTransformer;
use ZJKiza\FlatMapper\Transformer\Transformer;
use ZJKiza\FlatMapper\UniversalDtoMapper;
use ZJKiza\FlatMapper\Contract\UniversalDtoMapperInterface;

class MapperServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(LowercaseTransformer::class);

        $this->app->singleton(Transformer::class, function ($app) {
            $transformer = new Transformer();
            $transformer->addTransformer(
                $app->make(LowercaseTransformer::class)
            );
            return $transformer;
        });

        $this->app->singleton(UniversalDtoMapper::class, function ($app) {
            return new UniversalDtoMapper(
                $app->make(Transformer::class)
            );
        });

        $this->app->alias(
            UniversalDtoMapper::class,
            UniversalDtoMapperInterface::class
        );
    }
}
```

Usage in a controller:

```
use ZJKiza\FlatMapper\Contract\UniversalDtoMapperInterface;

public function index(UniversalDtoMapperInterface $mapper)
{
    $rows = /* fetch rows */;
    $dto = $mapper->map($rows, MediaDto::class, 'media_id');
    return response()->json($dto);
}
```

Practical example (full mapping snippet from tests)
---------------------------------------------------

[](#practical-example-full-mapping-snippet-from-tests)

This is a compact example using the DTOs and data bundled with the tests.

```
use ZJKiza\FlatMapper\UniversalDtoMapper;
use ZJKiza\FlatMapper\Tests\Resources\Dto\MediaDto;

$rows = json_decode(file_get_contents(__DIR__ . '/tests/Resources/Data/data.json'), true);
$mapper = new UniversalDtoMapper();
$dto = $mapper->map($rows, MediaDto::class, 'media_id');
echo json_encode($dto, JSON_THROW_ON_ERROR);
// Output equals tests/Resources/Data/expected.json
```

Troubleshooting &amp; tips
--------------------------

[](#troubleshooting--tips)

- Ensure your root DTO defines `ColumnPrefix` attribute.
- If you see duplicates in nested arrays, confirm the `Identifier` attribute is present on nested DTOs.
- Use `Column(name: '...')` when a column name does not match the property name after the naming strategy.
- For complex DI needs (transformer factories, custom adapter wiring), register your own `Transformer` service and inject it into a small factory that returns a configured `UniversalDtoMapper`.

Contribution
------------

[](#contribution)

Contributions, bug reports and PRs are welcome. Please run the project's test suite and ensure new changes have unit tests.

###  Health Score

38

—

LowBetter than 85% of packages

Maintenance84

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

7

Last Release

77d ago

Major Versions

v0.6.1 → v1.0.02026-02-21

PHP version history (2 changes)v0.5.0PHP &gt;=8.2

v0.6.0PHP ^8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/5ff452349a91001f15f3a1d5ddf5a3a7382218f9222bc3fbfdd005d6c5f0473c?d=identicon)[zjkiza](/maintainers/zjkiza)

---

Top Contributors

[![zjkiza](https://avatars.githubusercontent.com/u/31966449?v=4)](https://github.com/zjkiza "zjkiza (12 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Psalm, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/zjkiza-flat-identity-dto-mapper/health.svg)

```
[![Health](https://phpackages.com/badges/zjkiza-flat-identity-dto-mapper/health.svg)](https://phpackages.com/packages/zjkiza-flat-identity-dto-mapper)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
