DataMapper
DataMapper provides a modern, fluent API for transforming data between different structures. It supports template-based mapping, queries with SQL-like operators, property-specific filters and much more.
Quick Example
Section titled “Quick Example”use event4u\DataHelpers\DataMapper;
$source = [ 'user' => [ 'name' => 'John Doe', 'email' => 'john@example.com', ], 'orders' => [ ['id' => 1, 'total' => 100, 'status' => 'shipped'], ['id' => 2, 'total' => 200, 'status' => 'pending'], ['id' => 3, 'total' => 150, 'status' => 'shipped'], ],];
// Approach 1: Fluent API with query builder$result = DataMapper::source($source) ->query('orders.*') ->where('status', '=', 'shipped') ->orderBy('total', 'DESC') ->end() ->template([ 'customer_name' => '{{ user.name }}', 'customer_email' => '{{ user.email }}', 'shipped_orders' => [ '*' => [ 'id' => '{{ orders.*.id }}', 'total' => '{{ orders.*.total }}', ], ], ]) ->map() ->getTarget();
// Approach 2: Template-based with WHERE/ORDER BY operators (recommended)$template = [ 'customer_name' => '{{ user.name }}', 'customer_email' => '{{ user.email }}', 'shipped_orders' => [ 'WHERE' => [ '{{ orders.*.status }}' => 'shipped', ], 'ORDER BY' => [ '{{ orders.*.total }}' => 'DESC', ], '*' => [ 'id' => '{{ orders.*.id }}', 'total' => '{{ orders.*.total }}', ], ],];
$result = DataMapper::source($source) ->template($template) ->map() ->getTarget();
// Both approaches produce the same result:// [// 'customer_name' => 'John Doe',// 'customer_email' => 'john@example.com',// 'shipped_orders' => [// ['id' => 3, 'total' => 150],// ['id' => 1, 'total' => 100],// ],// ]Why Use Template-Based Approach?
Section titled “Why Use Template-Based Approach?”The template-based approach (Approach 2) has a significant advantage: templates can be stored in a database and created with a drag-and-drop editor, enabling no-code data mapping:
// Store templates in database$source = [ 'user' => [ 'name' => 'John Doe', 'email' => 'john@example.com', ], 'orders' => [ ['id' => 1, 'total' => 100, 'status' => 'shipped'], ['id' => 2, 'total' => 200, 'status' => 'pending'], ['id' => 3, 'total' => 150, 'status' => 'shipped'], ],];
// Load template from database (created with drag-and-drop editor)$template = Mappings::find(3)->template;
$result = DataMapper::source($source) ->template($template) ->map() ->getTarget();This makes it possible to map import files, API responses, etc. without any programming.
Use cases:
- Import Wizards - Let users map CSV/Excel columns to your data structure
- API Integration - Store API response mappings in database
- Multi-Tenant Systems - Each tenant can have custom mappings
- Dynamic ETL - Build data transformation pipelines without code
- Form Builders - Map form submissions to different data structures
Fluent API Overview
Section titled “Fluent API Overview”The DataMapper uses a fluent, chainable API.
You can start with DataMapper::from($source) (alias: DataMapper::source($source)).
DataMapper::from($source) // Start with source data (alias: DataMapper::source()) ->target($target) // Optional: Set target object/array ->template($template) // Define mapping template ->query($path) // Start query builder ->where($field, $op, $val) // Add WHERE condition ->orderBy($field, $dir) // Add ORDER BY ->limit($n) // Add LIMIT ->end() // End query builder ->property($name) // Access property API ->setFilter($filter) // Set property filter ->end() // End property API ->pipeline($filters) // Set global filters ->skipNull() // Skip null values ->map() // Execute mapping ->getTarget(); // Get resultBasic Usage
Section titled “Basic Usage”Simple Template Mapping
Section titled “Simple Template Mapping”$source = ['user' => ['name' => 'John', 'email' => 'john@example.com', 'profile' => ['age' => 30]]];$result = DataMapper::source($source) ->template([ 'name' => '{{ user.name }}', 'email' => '{{ user.email }}', 'age' => '{{ user.profile.age }}', ]) ->map() ->getTarget();Template Syntax
Section titled “Template Syntax”Templates use {{ }} for dynamic values:
- Dynamic values:
'{{ user.name }}'- Fetches value from source - Static values:
'admin'- Used as literal string (no{{ }}) - Dot-notation:
'{{ user.profile.address.street }}'- Nested access - Wildcards:
'{{ users.*.email }}'- Array operations
Loading Data from Files
Section titled “Loading Data from Files”DataMapper can load data directly from JSON and XML files using sourceFile():
// Load from JSON file$result = DataMapper::sourceFile('/path/to/data.json') ->template([ 'name' => '{{ user.name }}', 'email' => '{{ user.email }}', ]) ->map() ->getTarget();
// Load from XML file$result = DataMapper::sourceFile('/path/to/data.xml') ->template([ 'name' => '{{ company.name }}', 'email' => '{{ company.email }}', ]) ->map() ->getTarget();⚠️ Important: XML Root Element Preservation
Section titled “⚠️ Important: XML Root Element Preservation”When loading XML files, the root element name is always preserved and must be included in your mapping paths:
// XML file content:// <?xml version="1.0"?>// <company>// <name>TechCorp</name>// <email>info@techcorp.com</email>// </company>
// ✅ Correct: Include root element in path$mapping = [ 'company_name' => '{{ company.name }}', 'company_email' => '{{ company.email }}',];
// ❌ Wrong: Missing root element (will return null)$mapping = [ 'company_name' => '{{ name }}', 'company_email' => '{{ email }}',];Different root elements require different paths:
// For <VitaCost>...</VitaCost>'number' => '{{ VitaCost.ConstructionSite.nr_lv }}'
// For <Datafields>...</Datafields>'salutation' => '{{ Datafields.contact_persons.contact_person.salutation }}'
// For <company>...</company>'name' => '{{ company.name }}'Example with nested XML arrays:
// XML: <company><departments><department>...</department></departments></company>$mapping = [ 'company_name' => '{{ company.name }}', 'departments' => [ '*' => [ 'name' => '{{ company.departments.department.*.name }}', 'code' => '{{ company.departments.department.*.code }}', ], ],];XML Files with Multiple Root Elements
Section titled “XML Files with Multiple Root Elements”In some cases, you may need to work with XML files that have multiple root elements (technically invalid XML, but sometimes necessary):
// XML file with multiple roots:// <LVDATA><LV>...</LV></LVDATA>// <POSDATA><POS>...</POS></POSDATA>
// Both root elements are preserved and accessible$mapping = [ 'lv_id' => '{{ LVDATA.LV.ID_LV }}', 'lv_number' => '{{ LVDATA.LV.NR_LV }}', 'positions' => [ '*' => [ 'position_id' => '{{ POSDATA.POS.*.ID_POSITION }}', 'lv_id' => '{{ POSDATA.POS.*.ID_LV }}', ], ],];
$result = DataMapper::sourceFile('/path/to/multi-root.xml') ->template($mapping) ->map() ->getTarget();The FileLoader automatically detects and handles multiple root elements by wrapping them in a temporary container during parsing.
💡 See the complete example: Run php examples/data-mapper/xml-file-mapping.php for a comprehensive demonstration of XML file loading with different root elements.
Mapping to Objects
Section titled “Mapping to Objects”class UserDto{ public string $name; public string $email;}
$result = DataMapper::source($source) ->target(UserDto::class) ->template([ 'name' => '{{ user.name }}', 'email' => '{{ user.email }}', ]) ->map() ->getTarget(); // Returns UserDto instanceWorking with Readonly Properties
Section titled “Working with Readonly Properties”PHP 8.1+ introduced readonly properties that can only be initialized once. The DataMapper provides the modifyReadOnly() method to handle these properties intelligently.
Default Behavior (modifyReadOnly disabled)
Section titled “Default Behavior (modifyReadOnly disabled)”By default, readonly properties that are already initialized will be skipped:
class UserDto{ public function __construct( public readonly int $id, public readonly string $name, ) {}}
// Create instance with initialized readonly properties$dto = new UserDto(999, 'Original');
$source = ['id' => 123, 'name' => 'John'];
// Map to existing object - readonly properties are skipped$result = DataMapper::source($source) ->target($dto) ->template([ 'id' => '{{ id }}', 'name' => '{{ name }}', ]) ->map() ->getTarget();
// Result: id=999, name='Original' (unchanged)Enabling Readonly Modification
Section titled “Enabling Readonly Modification”When modifyReadOnly(true) is enabled, the mapper will create a new instance to allow setting readonly properties:
class UserDto{ public function __construct( public readonly int $id, public readonly string $name, ) {}}
$dto = new UserDto(999, 'Original');$source = ['id' => 123, 'name' => 'John'];
// Enable readonly modification$result = DataMapper::source($source) ->target($dto) ->modifyReadOnly(true) // Creates new instance ->template([ 'id' => '{{ id }}', 'name' => '{{ name }}', ]) ->map() ->getTarget();
// Result: id=123, name='John' (new instance created)// Original $dto remains unchanged: id=999, name='Original'Mapping to Class Names
Section titled “Mapping to Class Names”When the target is a class name (string) instead of an object instance, readonly properties can always be set:
class UserDto{ public function __construct( public readonly int $id, public readonly string $name, ) {}}
$source = ['id' => 123, 'name' => 'John'];
// Pass class name as target$result = DataMapper::source($source) ->target(UserDto::class) // Class name, not instance ->modifyReadOnly(true) ->template([ 'id' => '{{ id }}', 'name' => '{{ name }}', ]) ->map() ->getTarget();
// Result: id=123, name='John' (new instance created via constructor)Performance Optimization
Section titled “Performance Optimization”The mapper only creates a new instance when necessary:
- Class name target: Uses constructor when possible (best performance)
- Object target + no readonly properties to modify: Reuses existing object (preserves reference)
- Object target + readonly properties to modify: Creates new instance via reflection
class UserDto{ public int $id = 0; // Mutable property public string $name = ''; // Mutable property}
$dto = new UserDto();$source = ['id' => 123, 'name' => 'John'];
// No readonly properties - reuses existing object$result = DataMapper::source($source) ->target($dto) ->modifyReadOnly(false) ->template([ 'id' => '{{ id }}', 'name' => '{{ name }}', ]) ->map() ->getTarget();
// $result === $dto (same object reference)Nested Structures
Section titled “Nested Structures”$source = [ 'user' => ['name' => 'John', 'email' => 'john@example.com', 'phone' => '555-1234'], 'orders' => [['id' => 1, 'total' => 100], ['id' => 2, 'total' => 200]],];$result = DataMapper::source($source) ->template([ 'customer' => [ 'name' => '{{ user.name }}', 'contact' => [ 'email' => '{{ user.email }}', 'phone' => '{{ user.phone }}', ], ], 'orders' => [ '*' => [ 'id' => '{{ orders.*.id }}', 'total' => '{{ orders.*.total }}', ], ], ]) ->map() ->getTarget();Query Builder
Section titled “Query Builder”The query builder provides SQL-like operators for filtering and transforming data during mapping.
Basic Queries
Section titled “Basic Queries”// Fluent API approach$result = DataMapper::source($source) ->query('orders.*') ->where('total', '>', 100) ->orderBy('total', 'DESC') ->limit(5) ->end() ->template([ 'items' => [ '*' => [ 'id' => '{{ orders.*.id }}', 'total' => '{{ orders.*.total }}', ], ], ]) ->map() ->getTarget();
// Template-based approach (same result)$result = DataMapper::source($source) ->template([ 'items' => [ 'WHERE' => [ '{{ orders.*.total }}' => ['>', 100], ], 'ORDER BY' => [ '{{ orders.*.total }}' => 'DESC', ], 'LIMIT' => 5, '*' => [ 'id' => '{{ orders.*.id }}', 'total' => '{{ orders.*.total }}', ], ], ]) ->map() ->getTarget();WHERE Conditions
Section titled “WHERE Conditions”// Simple comparison->where('status', '=', 'active')->where('price', '>', 100)->where('stock', '<=', 10)
// Multiple conditions (AND logic)->where('status', '=', 'active')->where('price', '>', 100)
// BETWEEN->where('price', 'BETWEEN', [50, 150])
// IN->where('status', 'IN', ['active', 'pending'])
// LIKE (pattern matching)->where('name', 'LIKE', 'John%')
// NULL checks->where('deleted_at', 'IS NULL')->where('email', 'IS NOT NULL')ORDER BY
Section titled “ORDER BY”// Single field->orderBy('price', 'DESC')
// Multiple fields->orderBy('category', 'ASC')->orderBy('price', 'DESC')LIMIT and OFFSET
Section titled “LIMIT and OFFSET”// Limit results->limit(10)
// Skip items->offset(20)
// Pagination->offset(20)->limit(10)DISTINCT
Section titled “DISTINCT”// Remove duplicates->distinct('email')GROUP BY
Section titled “GROUP BY”// Group and aggregate->groupBy('category', [ 'total' => 'SUM(price)', 'count' => 'COUNT(*)', 'avg_price' => 'AVG(price)',])Pipeline Filters
Section titled “Pipeline Filters”Apply filters to all mapped values globally.
Global Filters
Section titled “Global Filters”use event4u\DataHelpers\DataMapper\Pipeline\Filters\TrimStrings;use event4u\DataHelpers\DataMapper\Pipeline\Filters\UppercaseStrings;
$result = DataMapper::source($source) ->pipeline([ new TrimStrings(), new UppercaseStrings(), ]) ->template([ 'name' => '{{ user.name }}', 'email' => '{{ user.email }}', ]) ->map() ->getTarget();
// All string values are trimmed and uppercasedAdding Filters
Section titled “Adding Filters”$mapper = DataMapper::source($source) ->template($template);
// Add single filter$mapper->addPipelineFilter(new TrimStrings());
// Add multiple filters$mapper->pipeline([ new TrimStrings(), new UppercaseStrings(),]);Built-in Filters
Section titled “Built-in Filters”Data Helpers includes 40+ built-in filters:
- String Filters: TrimStrings, UppercaseStrings, LowercaseStrings, etc.
- Number Filters: RoundNumbers, FormatCurrency, etc.
- Date Filters: FormatDate, ParseDate, etc.
- Array Filters: FlattenArray, UniqueValues, etc.
- Validation Filters: ValidateEmail, ValidateUrl, etc.
See Filters Documentation for complete list.
Property-Specific Filters
Section titled “Property-Specific Filters”Apply filters to specific properties only.
Using setFilter()
Section titled “Using setFilter()”$result = DataMapper::source($source) ->setFilter('name', new TrimStrings(), new UppercaseStrings()) ->setFilter('email', new TrimStrings(), new LowercaseStrings()) ->template([ 'name' => '{{ user.name }}', 'email' => '{{ user.email }}', 'bio' => '{{ user.bio }}', ]) ->map() ->getTarget();
// Only 'name' and 'email' are filtered, 'bio' is notUsing Property API
Section titled “Using Property API”$result = DataMapper::source($source) ->property('name') ->setFilter(new TrimStrings(), new UppercaseStrings()) ->end() ->property('email') ->setFilter(new TrimStrings(), new LowercaseStrings()) ->end() ->template($template) ->map() ->getTarget();Nested Properties
Section titled “Nested Properties”// Works with dot-notation->setFilter('user.profile.bio', new TrimStrings())
// Works with wildcards->setFilter('items.*.name', new TrimStrings())Property API
Section titled “Property API”The Property API provides focused access to individual properties.
Get Property Target
Section titled “Get Property Target”$source = ['user' => ['name' => 'John', 'email' => 'john@example.com']];$mapper = DataMapper::source($source) ->template([ 'name' => '{{ user.name }}', 'email' => '{{ user.email }}', ]);
// Get mapping target for property$target = $mapper->property('name')->getTarget();// $target = '{{ user.name }}'Get Property Filters
Section titled “Get Property Filters”use event4u\DataHelpers\DataMapper\Pipeline\Filters\TrimStrings;
$mapper->setFilter('name', new TrimStrings());
$filters = $mapper->property('name')->getFilter();// $filters = [TrimStrings]Get Mapped Value
Section titled “Get Mapped Value”// Execute mapping and get value for specific property$value = $mapper->property('name')->getMappedValue();// $value = 'John Doe' (after applying filters)Reset Property Filters
Section titled “Reset Property Filters”$mapper->property('name') ->setFilter(new TrimStrings()) ->resetFilter() // Remove all filters ->setFilter(new UppercaseStrings()) // Set new filter ->end();Discriminator (Polymorphic Mapping)
Section titled “Discriminator (Polymorphic Mapping)”Automatically select target class based on a discriminator field (Liskov Substitution Principle).
Basic Usage
Section titled “Basic Usage”abstract class Animal{ public string $name; public int $age;}
class Dog extends Animal{ public string $breed;}
class Cat extends Animal{ public int $lives;}
$source = [ 'type' => 'dog', 'name' => 'Rex', 'age' => 5, 'breed' => 'Golden Retriever',];
$result = DataMapper::source($source) ->target(Animal::class) ->discriminator('type', [ 'dog' => Dog::class, 'cat' => Cat::class, ]) ->template([ 'name' => '{{ name }}', 'age' => '{{ age }}', 'breed' => '{{ breed }}', ]) ->map() ->getTarget();
// Returns Dog instance (because type='dog')Nested Discriminator
Section titled “Nested Discriminator”// Discriminator field can be nested->discriminator('meta.classification.type', [ 'premium' => PremiumUser::class, 'basic' => BasicUser::class,])Fallback Behavior
Section titled “Fallback Behavior”// If discriminator value not found, falls back to original target$result = DataMapper::source(['type' => 'unknown']) ->target(Animal::class) ->discriminator('type', [ 'dog' => Dog::class, 'cat' => Cat::class, ]) ->template($template) ->map() ->getTarget();
// Returns Animal instance (fallback)Copy and Extend
Section titled “Copy and Extend”Create independent copies of mapper configurations.
Copy Configuration
Section titled “Copy Configuration”$baseMapper = DataMapper::source($source) ->target(User::class) ->template([ 'name' => '{{ name }}', ]);
// Create independent copy$extendedMapper = $baseMapper->copy() ->extendTemplate([ 'email' => '{{ email }}', ]) ->addPipelineFilter(new TrimStrings());
// $baseMapper is unchanged// $extendedMapper has extended configExtend Template
Section titled “Extend Template”$source = ['user' => ['name' => 'John', 'email' => 'john@example.com', 'phone' => '555-1234']];$mapper = DataMapper::source($source) ->template([ 'name' => '{{ user.name }}', ]);
// Extend with additional fields$mapper->extendTemplate([ 'email' => '{{ user.email }}', 'phone' => '{{ user.phone }}',]);
// Template now has all three fieldsReset and Delete
Section titled “Reset and Delete”Manage template operators dynamically.
Reset to Original
Section titled “Reset to Original”$source = ['products' => [['id' => 1, 'status' => 'active', 'price' => 100], ['id' => 2, 'status' => 'inactive', 'price' => 50]]];$mapper = DataMapper::source($source) ->template([ 'items' => [ 'WHERE' => ['{{ products.*.status }}' => 'active'], 'ORDER BY' => ['{{ products.*.price }}' => 'DESC'], '*' => ['id' => '{{ products.*.id }}'], ], ]);
// Modify with query$mapper->query('products.*') ->where('price', '>', 75) ->orderBy('price', 'ASC') ->end();
// Reset WHERE to original template value$mapper->reset()->where();
// Reset entire template$mapper->reset()->all();Delete Operators
Section titled “Delete Operators”// Delete specific operator$mapper->delete()->where();
// Delete all operators$mapper->delete()->all();Chainable
Section titled “Chainable”// Chain multiple operations$mapper->reset()->where()->orderBy();$mapper->delete()->limit()->offset();Performance
Section titled “Performance”DataMapper is optimized for performance:
- 3.7x faster than Symfony Serializer for Dto mapping
- Zero reflection overhead for template-based mapping
- Efficient caching for path resolution and reflection
- Minimal overhead (7.1%) for Fluent API wrapper
See Performance Benchmarks for detailed comparison.
Code Examples
Section titled “Code Examples”The following working examples demonstrate DataMapper in action:
- Simple Mapping - Basic template-based mapping
- Template-Based Queries - WHERE/ORDER BY in templates (recommended for database-stored templates)
- With Hooks - Using hooks for custom logic
- Pipeline - Filter pipelines and transformations
- Mapped Data Model - Using MappedDataModel class
- Template Expressions - Advanced template syntax
- Reverse Mapping - Bidirectional mapping
- Dto Integration - Integration with SimpleDto
All examples are fully tested and can be run directly:
php examples/main-classes/data-mapper/simple-mapping.phpphp examples/main-classes/data-mapper/template-based-queries.phpphp examples/main-classes/data-mapper/with-hooks.phpRelated Tests
Section titled “Related Tests”The functionality is thoroughly tested. Key test files:
- DataMapperTest.php - Core functionality tests
- DataMapperHooksTest.php - Hook system tests
- DataMapperPipelineTest.php - Pipeline tests
- MapperQueryTest.php - Query integration tests
- MultiSourceFluentTest.php - Multi-source mapping tests
- MultiTargetMappingTest.php - Multi-target mapping tests
- DataMapperIntegrationTest.php - End-to-end scenarios
Run the tests:
# Run all DataMapper teststask test:unit -- --filter=DataMapper
# Run specific test filevendor/bin/pest tests/Unit/DataMapper/DataMapperTest.phpSee Also
Section titled “See Also”- DataAccessor - Read nested data
- DataMutator - Modify nested data
- DataFilter - Query and filter data
- Core Concepts: Wildcards - Wildcard operators
- Examples - 90+ code examples