Git Product home page Git Product logo

automapper-plus's Introduction

AutoMapper+

An automapper for PHP inspired by .NET's automapper. Transfers data from one object to another, allowing custom mapping operations.

Build Status

Table of Contents

Installation

This library is available on packagist:

$ composer require "mark-gerarts/auto-mapper-plus"

If you're using Symfony, check out the AutoMapper+ bundle.

Why?

When you need to transfer data from one object to another, you'll have to write a lot of boilerplate code. For example when using view models, CommandBus commands, working with API responses, etc.

Automapper+ helps you by automatically transferring properties from one object to another, including private ones. By default, properties with the same name will be transferred. This can be overridden as you like.

Example usage

Suppose you have a class Employee and an associated DTO.

<?php

class Employee
{
    private $id;
    private $firstName;
    private $lastName;
    private $birthYear;
    
    public function __construct($id, $firstName, $lastName, $birthYear)
    {
        $this->id = $id;
        $this->firstName = $firstName;
        $this->lastName = $lastName;
        $this->birthYear = $birthYear;
    }

    public function getId()
    {
        return $this->id;
    }

    // And so on...
}

class EmployeeDto
{
    // While the properties are public for this example, we can map to private
    // or protected properties just the same.
    public $firstName;
    public $lastName;
    public $age;
}

The following snippet provides a quick overview on how the mapper can be configured and used:

<?php

use AutoMapperPlus\Configuration\AutoMapperConfig;
use AutoMapperPlus\AutoMapper;

$config = new AutoMapperConfig();

// Simply registering the mapping is enough to convert properties with the same
// name. Custom actions can be registered for each individual property.
$config
    ->registerMapping(Employee::class, EmployeeDto::class)
    ->forMember('age', function (Employee $source) {
        return date('Y') - $source->getBirthYear();
    })
    ->reverseMap(); // Register the reverse mapping as well.
                            
$mapper = new AutoMapper($config);

// With this configuration we can start converting our objects.
$john = new Employee(10, "John", "Doe", 1980);
$dto = $mapper->map($john, EmployeeDto::class);

echo $dto->firstName; // => "John"
echo $dto->lastName; // => "Doe"
echo $dto->age; // => 37

In depth

Instantiating the AutoMapper

The AutoMapper has to be provided with an AutoMapperConfig, which holds the registered mappings. This can be done in 2 ways:

Passing it to the constructor:

<?php

use AutoMapperPlus\Configuration\AutoMapperConfig;
use AutoMapperPlus\AutoMapper;

$config = new AutoMapperConfig();
$config->registerMapping(Source::class, Destination::class);
$mapper = new AutoMapper($config);

Using the static constructor:

<?php

$mapper = AutoMapper::initialize(function (AutoMapperConfig $config) {
    $config->registerMapping(Source::class, Destination::class);
    $config->registerMapping(AnotherSource::class, Destination::class);
    // ...
});

Using the AutoMapper

Once configured, using the AutoMapper is pretty straightforward:

<?php

$john = new Employee("John", "Doe", 1980);

// Map the source object to a new instance of the destination class.
$mapper->map($john, EmployeeDto::class);

// Mapping to an existing object is possible as well.
$mapper->mapToObject($john, new EmployeeDto());

// Map a collection using mapMultiple
$mapper->mapMultiple($employees, EmployeeDto::class);

Registering mappings

Mappings are defined using the AutoMapperConfig's registerMapping() method. By default, every mapping has to be explicitly defined before you can use it.

A mapping is defined by providing the source class and the destination class. The most basic definition would be as follows:

<?php

$config->registerMapping(Employee::class, EmployeeDto::class);

This will allow objects of the Employee class to be mapped to EmployeeDto instances. Since no extra configuration is provided, the mapping will only transfer properties with the same name.

Custom callbacks

With the forMember() method, you can specify what should happen for the given property of the destination class. When you pass a callback to this method, the return value will be used to set the property.

The callback receives the source object as argument.

<?php

$config->registerMapping(Employee::class, EmployeeDto::class)
    ->forMember('fullName', function (Employee $source) {
        return $source->getFirstName() . ' ' . $source->getLastName();
    });

Operations

Behind the scenes, the callback in the previous example is wrapped in a MapFrom operation. Operations represent the action that should be performed for the given property.

The following operations are provided:

Name Explanation
MapFrom Maps the property from the value returned from the provided callback. Gets passed the source object, an instance of the AutoMapper and optionally the current context.
Ignore Ignores the property.
MapTo Maps the property to another class. Allows for nested mappings. Supports both single values and collections.
MapToAnyOf Similar to MapTo, but maps the property to the first match from a list of classes. This can be used for polymorphic properties. Supports both single values and collections.
FromProperty Use this to explicitly state the source property name.
DefaultMappingOperation Simply transfers the property, taking into account the provided naming conventions (if there are any).
SetTo Always sets the property to the given value

You can use them with the same forMember() method. The Operation class can be used for clarity.

<?php

$getName = function ($source, AutoMapperInterface $mapper) { return 'John'; };

$mapping->forMember('name', $getName);
// The above is a shortcut for the following:
$mapping->forMember('name', Operation::mapFrom($getName));
// Which in turn is equivalent to:
$mapping->forMember('name', new MapFrom($getName));

// Other examples:
// Ignore this property.
$mapping->forMember('id', Operation::ignore());
// Map this property to the given class.
$mapping->forMember('employee', Operation::mapTo(EmployeeDto::class));
// Explicitly state what the property name is of the source object.
$mapping->forMember('name', Operation::fromProperty('unconventially_named_property'));
// The `FromProperty` operation can be chained with `MapTo`, allowing a
// differently named property to be mapped to a class.
$mapping->forMember(
    'address',
    Operation::fromProperty('adres')->mapTo(Address::class)
);
// SetTo sets the property to the given value.
$mapping->forMember('type', Operation::setTo('employee'));

// An extended example showing you can access the mapper in `MapFrom`.
$getColorPalette = function(SimpleXMLElement $XMLElement, AutoMapperInterface $mapper) {
    /** @var SimpleXMLElement $palette */
    $palette = $XMLElement->xpath('/product/specification/palette/colour');
    return $mapper->mapMultiple($palette, Color::class);
};
$mapping->forMember('palette', $getColorPalette);

MapTo requires some extra explanation. Since lists and maps are the same data structure in PHP (arrays), we can't reliably distinct between the two. MapTo therefore accepts a second parameter, $sourceIsObjectArray, a boolean value that indicates whether the source value should be interpreted as a collection, or as an associative array representing an object. By default we assume a collection or a single non-array value.

<?php

// This assumes address is an object, or a collection of mappable
// objects if the source is an array/iterable.
$mapping->forMember('address', Operation::mapTo(Address::class));
// This is equivalent to:
$mapping->forMember('address', Operation::mapTo(Address::class, false));
// If you want to be very specific about the source being a collection, you
// can use `mapCollectionTo`. This is purely syntactic sugar; it is equivalent
// to the declarations above as well.
$mapping->forMember('addresses', Operation::mapCollectionTo(Address::class));

// On the other hand, if the source is an array that represents an object, you
// can use the following:
$mapping->forMember('address', Operation::mapTo(Address::class, true));
// Or nicer
$mapping->forMember('address', Operation::mapArrayTo(Address::class));

You can create your own operations by implementing the MappingOperationInterface. Take a look at the provided implementations for some inspiration.

If you need to have the automapper available in your operation, you can implement the MapperAwareInterface, and use the MapperAwareTrait. The default MapTo and MapFrom operations use these.

Dealing with polymorphic properties

Sometimes you have properties which contain a list of different types of objects e.g. when you load all entities with single table inheritance. You can use MapToAnyOf to map every object to a possible different one. Keep in mind that the mapping for the child class has to be registered as well. The source property can be both a single value or a collection.

<?php

// This iterates over every property of the source property
// 'polymorphicChildren'. Each value will be mapped to the first existing
// mapping from the value to one of the given classes.
$config->createMapping(ChildA::class, ChildADto::class);
$config->createMapping(ChildB::class, ChildBDto::class);
$config->createMapping(Parent::class, ParentDto::class)
    ->forMember(
        'polymorphicChildren',
        Operation::mapToAnyOf([ChildADto::class, ChildBDto::class]
    ));

Dealing with nested mappings

Nested mappings can be registered using the MapTo operation. Keep in mind that the mapping for the child class has to be registered as well.

MapTo supports both single entities and collections.

<?php

$config->registerMapping(Child::class, ChildDto::class);
$config->registerMapping(Parent::class, ParentDto::class)
    ->forMember('child', Operation::mapTo(ChildDto::class));

Handling object construction

You can specify how the new destination object will be constructed (this isn't relevant if you use mapToObject). You can do this by registering a factory callback. This callback will be passed both the source object and an instance of the AutoMapper.

<?php

$config->registerMapping(Source::class, Destination::class)
    ->beConstructedUsing(function (Source $source, AutoMapperInterface $mapper): Destination {
        return new Destination($source->getProperty());
    });

Another option is to skip the constructor all together. This can be set using the options.

<?php

// Either set it in the options:
$config->getOptions()->skipConstructor();
$mapper = new AutoMapper($config);

// Or set it on the mapping directly:
$config->registerMapping(Source::class, Destination::class)->skipConstructor();

ReverseMap

Since it is a common use case to map in both directions, the reverseMap() method has been provided. This creates a new mapping in the alternate direction.

reverseMap will keep the registered naming conventions into account, if there are any.

<?php

// reverseMap() returns the new mapping, allowing to continue configuring the
// new mapping.
$config->registerMapping(Employee::class, EmployeeDto::class)
    ->reverseMap()
    ->forMember('id', Operation::ignore());

$config->hasMappingFor(Employee::class, EmployeeDto::class); // => True
$config->hasMappingFor(EmployeeDto::class, Employee::class); // => True

Note: reverseMap() simply creates a completely new mapping in the reverse direction, using the default options. However, every operation you defined with forMember that implements the Reversible interface, gets defined for the new mapping as well. Currently, only fromProperty supports being reversed.

To make things more clear, take a look at the following example:

<?php

// Source class properties:         Destination class properties:
// - 'some_property',               - 'some_property'
// - 'some_alternative_property'    - 'some_other_property'
// - 'the_last_property'            - 'the_last_property'
//
$config->registerMapping(Source::class, Destination::class)
    ->forMember('some_property', Operation::ignore())
    ->forMember('some_other_property', Operation::fromProperty('some_alternative_property'))
    ->reverseMap();

// When mapping from Source to Destination, the following will happen:
// - some_property gets ignored
// - some_other_property gets mapped by using the value form some_alternative_property
// - the_last_property gets mapped because the names are equal.
//
// Now, when we go in the reverse direction things are different:
// - some_property gets mapped, because Ignore is not reversible
// - some_alternative_property gets mapped because FromProperty is reversible
// - the_last_property gets mapped as well

Copying a mapping

When defining different view models, it can occur that you have lots of similar properties. For example, with a ListViewModel and a DetailViewModel. This means that the mapping configuration will be similar as well.

For this reason, it is possible to copy a mapping. In practice this means that all the options will be copied, and all the explicitly defined mapping operations.

After copying the mapping, you're free to override operations or options on the new mapping.

<?php

$detailMapping = $config->registerMapping(Employee::class, EmployeeDetailView::class)
    // Define operations and options ...
    ->forMember('age', function () {
        return 20;
    });

// You can copy a mapping by passing source and destination class. This will
// search the config for the relevant mapping.
$listMapping = $config->registerMapping(Employee::class, EmployeeListView::class)
    ->copyFrom(Employee::class, EmployeeDetailView::class)
    // Alternatively, copy a mapping by passing it directly.
    // ->copyFromMapping($detailMapping)
    //
    // You can now go ahead and define new operations, or override existing
    // ones.
    ->forMember('name', Operation::ignore())
    ->skipConstructor();

Automatic creation of mappings

When you're dealing with very simple mappings that don't require any configuration, it can be quite cumbersome the register a mapping for each and every mapping. For these cases it is possible to enable the automatic creation of mappings:

<?php

$config->getOptions()->createUnregisteredMappings();

With this configuration the mapper will generate a very basic mapping on the fly instead of throwing an exception if the mapping is not configured.

Resolving property names

Unless you define a specific way to fetch a value (e.g. mapFrom), the mapper has to have a way to know which source property to map from. By default, it will try to transfer data between properties of the same name. There are, however, a few ways to alter this behaviour.

If a source property is specifically defined (e.g. FromProperty), this will be used in all cases.

Naming conventions

You can specify the naming conventions followed by the source & destination classes. The mapper will take this into account when resolving names.

For example:

<?php

use AutoMapperPlus\NameConverter\NamingConvention\CamelCaseNamingConvention;
use AutoMapperPlus\NameConverter\NamingConvention\SnakeCaseNamingConvention;

$config->registerMapping(CamelCaseSource::class, SnakeCaseDestination::class)
    ->withNamingConventions(
        new CamelCaseNamingConvention(), // The naming convention of the source class.
        new SnakeCaseNamingConvention() // The naming convention of the destination class.
    );

$source = new CamelCaseSource();
$source->propertyName = 'camel';

$result = $mapper->map($source, SnakeCaseDestination::class);
echo $result->property_name; // => "camel"

The following conventions are provided (more to come):

  • CamelCaseNamingConvention
  • PascalCaseNamingConvention
  • SnakeCaseNamingConvention

You can implement your own by using the NamingConventionInterface.

Explicitly state source property

As mentioned earlier, the operation FromProperty allows you to explicitly state what property of the source object should be used.

<?php

$config->registerMapping(Source::class, Destination::class)
    ->forMember('id', Operation::fromProperty('identifier'));

You should read the previous snippet as follows: "For the property named 'id' on the destination object, use the value of the 'identifier' property of the source object".

FromProperty is Reversible, meaning that when you apply reverseMap(), AutoMapper will know how to map between the two properties. For more info, read the section about reverseMap.

Resolving names with a callback

Should naming conventions and explicitly stating property names not be sufficient, you can resort to a CallbackNameResolver (or implement your own NameResolverInterface).

This CallbackNameResolver takes a callback as an argument, and will use this to transform property names.

<?php

class Uppercase
{
    public $IMAPROPERTY;
}

class Lowercase
{
    public $imaproperty;
}

$uppercaseResolver = new CallbackNameResolver(function ($targetProperty) {
    return strtolower($targetProperty);
});

$config->registerMapping(Uppercase::class; Lowercase::class)
    ->withNameResolver($uppercaseResolver);

$uc = new Uppercase();
$uc->IMAPROPERTY = 'value';

$lc = $mapper->map($uc, Lowercase::class);
echo $lc->imaproperty; // => "value"

The Options object

The Options object is a value object containing the possible options for both the AutoMapperConfig and the Mapping instances.

The Options you set for the AutoMapperConfig will act as the default options for every Mapping you register. These options can be overridden for every mapping.

For example:

<?php

$config = new AutoMapperConfig();
$config->getOptions()->setDefaultMappingOperation(Operation::ignore());

$defaultMapping = $config->registerMapping(Source::class, Destination::class);
$overriddenMapping = $config->registerMapping(AnotherSource::class, Destination::class)
    ->withDefaultOperation(new DefaultMappingOperation());

$defaultMapping->getOptions()->getDefaultMappingOperation(); // => Ignore
$overriddenMapping->getOptions()->getDefaultMappingOperation(); // => DefaultMappingOperation

The available options that can be set are:

Name Default value Comments
Source naming convention null The naming convention of the source class (e.g. CamelCaseNamingConversion). Also see naming conventions.
Destination naming convention null See above.
Skip constructor true whether or not the constructor should be skipped when instantiating a new class. Use $options->skipConstructor() and $options->dontSkipConstructor() to change.
Property accessor PropertyAccessor Use this to provide an alternative implementation of the property accessor. A property accessor combines the reading and writing of properties
Property reader PropertyAccessor Use this to provide an alternative implementation of the property reader.
Property writer PropertyAccessor Use this to provide an alternative implementation of the property writer.
Default mapping operation DefaultMappingOperation the default operation used when mapping a property. Also see mapping operations
Default name resolver NameResolver The default class to resolve property names
Custom Mapper null Grants the ability to use a custom mapper.
Object crates [\stdClass::class] See the dedicated section.
Ignore null properties false Sets whether or not a source property should be mapped to the destination object if the source value is null
Use substitution true Whether or not the Liskov substitution principle should be applied when resolving a mapping.
createUnregisteredMappings false Whether or not an exception should be thrown for unregistered mappings, or a mapping should be generated on the fly.

Setting the options

For the AutoMapperConfig

You can set the options for the AutoMapperConfig by retrieving the object:

<?php

$config = new AutoMapperConfig();
$config->getOptions()->dontSkipConstructor();

Alternatively, you can set the options by providing a callback to the constructor. The callback will be passed an instance of the default Options:

<?php

// This will set the options for this specific mapping.
$config = new AutoMapperConfig(function (Options $options) {
    $options->dontSkipConstructor();
    $options->setDefaultMappingOperation(Operation::ignore());
    // ...
});

For the Mappings

A mapping also has the getOptions method available. However, chainable helper methods exist for more convenient overriding of the options:

<?php

$config->registerMapping(Source::class, Destination::class)
    ->skipConstructor()
    ->withDefaultOperation(Operation::ignore());

Setting options via a callable has been provided for mappings as well, using the setDefaults() method:

<?php

$config->registerMapping(Source::class, Destination::class)
    ->setDefaults(function (Options $options) {
        $options->dontSkipConstructor();
        // ...
    });

Mapping with stdClass

As a side note it is worth mentioning that it is possible to map from and to stdClass. Mapping from stdClass happens as you would expect, copying properties to the new object.

<?php

// Register the mapping.
$config->registerMapping(\stdClass::class, Employee::class);
$mapper = new AutoMapper($config);

$employee = new \stdClass();
$employee->firstName = 'John';
$employee->lastName = 'Doe';

$result = $mapper->map($employee, Employee::class);
echo $result->firstName; // => "John"
echo $result->lastName; // => "Doe"

Mapping to \stdClass requires some explanation. All properties available on the provided source object are copied to the \stdClass as public properties. It's still possible to define operations for individual properties (for example, to ignore a property).

<?php

// Operations can still be registered.
$config->registerMapping(Employee::class, \stdClass::class)
    ->forMember('id', Operation::ignore());
$mapper = new AutoMapper($config);

$employee = new Employee(5, 'John', 'Doe', 1978);
$result = $mapper->map($employee, \stdClass::class);

echo $result->firstName; // => "John"
echo $result->lastName; // => "Doe"
var_dump(isset($result->id)); // => bool(false)

Naming conventions will be taken into account, so keep this in mind when defining operations. The property name has to match the naming convention of the target.

<?php

$config->registerMapping(CamelCaseSource::class, \stdClass::class)
    ->withNamingConventions(
        new CamelCaseNamingConvention(),
        new SnakeCaseNamingConvention()
    )
    // Operations have to be defined using the target property name.
    ->forMember('some_property', function () { return 'new value'; });
$mapper = new AutoMapper($config);

$source = new CamelCaseSource();
$source->someProperty = 'original value';
$source->anotherProperty = 'Another value';
$result = $mapper->map($employee, \stdClass::class);

var_dump(isset($result->someProperty)); // => bool(false)
echo $result->some_property; // => "new value"
echo $result->another_property; // => "Another value"

The concept of object crates

As suggested and explained in this issue, AutoMapper+ uses object crates to allow mapping to \stdClass. This means you can register your own classes as well to be an object crate. This makes the mapper handle it exactly as \stdClass, writing all source properties to public properties on the target.

Registering object crates can be done using the Options.

<?php

class YourObjectCrate { }

$config = new AutoMapperConfig(); // (Or pass a callable to the constructor)
$config->getOptions()->registerObjectCrate(YourObjectCrate::class);
$config->registerMapping(Employee::class, YourObjectCrate::class);
$mapper = new AutoMapper($config);

$employee = new Employee(5, 'John', 'Doe', 1978);
$result = $mapper->map($employee, YourObjectCrate::class);

echo $result->firstName; // => "John"
echo $result->lastName; // => "Doe"
echo get_class($result); // => "YourObjectCrate"

Mapping with arrays

It is possible to map associative arrays into objects (shout-out to @slava-v). This can be done just like you would declare a regular mapping:

<?php

$config->registerMapping('array', Employee::class); // Alternatively, use the enum DataType::ARRAY
// Adding operations works just as you would expect.
$config->registerMapping(DataType::ARRAY, Employee::class)
    ->forMember('id', Operation::ignore())
    ->forMember('type', Operation::setTo('employee'))
    // Since arrays are oftentimes snake_case'd.
    ->withNamingConventions(
        new SnakeCaseNamingConvention(),
        new CamelCaseNamingConvention()
    );

// It is now possible to map an array to an employee:
$employee = [
    'id' => 5,
    'first_name' => 'John',
    'last_name' => 'Doe'
];
$result = $mapper->map($employee, Employee::class);
echo $result->firstName; // => "John"
echo $result->id; // => null
echo $result->type; // => "employee"

See the MapTo section under Operations for some more details about the intricacies involving this operation in combination with arrays.

As for now, it is not possible to map to an array. While this is relatively easy to implement, it would introduce a breaking change. It is part of version 2.x, so check there if you need this feature.

Using a custom mapper

This library attempts to make registering mappings painless, with as little configuration as possible. However, cases exist where a mapping requires a lot of custom code. This code would look a lot cleaner if put in its own class. Another reason to resort to a custom mapper would be performance.

It is therefore possible to specify a custom mapper class for a mapping. This mapper has to implement the MapperInterface. For your convenience, a CustomMapper class has been provided that implements this interface.

<?php

// You can either extend the CustomMapper, or just implement the MapperInterface
// directly.
class EmployeeMapper extends CustomMapper
{
    /**
     * @param Employee $source
     * @param EmployeeDto $destination
     * @return EmployeeDto
     */
    public function mapToObject($source, $destination)
    {
        $destination->id = $source->getId();
        $destination->firstName = $source->getFirstName();
        $destination->lastName = $source->getLastName();
        $destination->age = date('Y') - $source->getBirthYear();

        return $destination;
    }
}

$config->registerMapping(Employee::class, EmployeeDto::class)
    ->useCustomMapper(new EmployeeMapper());
$mapper = new AutoMapper($config);

// The AutoMapper can now be used as usual, but your custom mapper class will be
// called to do the actual mapping.
$employee = new Employee(10, 'John', 'Doe', 1980);
$result = $mapper->map($employee, EmployeeDto::class);

Adding context

Sometimes a mapping should behave differently based on the context. It is therefore possible to pass a third argument to the map methods to describe the current context. Both the MapFrom and MapTo operations can make use of this context to alter their behaviour.

The context argument is an array that can contain any arbitrary value. Note that this argument isn't part of the AutoMapperInterface yet, since it would break backwards compatibility. It will be added in the next major release.

<?php

// This example shows how for example the current locale can be passed to alter
// the mapping behaviour.
$config->registerMapping(Employee::class, EmployeeDto::class)
    ->forMember(
        'honorific',
        function ($source, AutoMapperInterface $mapper, array $context): string {
            $translationKey = "honorific.{$source->getGender()}";
            return $this->translator->trans($translationKey, $context['locale']);
        }
    );

// Usage:
$mapper->map($employee, EmployeeDto::class, ['locale' => $request->getLocale()]);

When using the mapToObject method, the context will contain the destination object by default. It is accessible using $context[AutoMapper::DESTINATION_CONTEXT]. This is useful in scenarios where you need data from the destination object to populate the object you're mapping.

When implementing a custom constructor, the context will contain the destination class by default. It is accessible using $context[AutoMapper::DESTINATION_CLASS_CONTEXT].

When mapping an object graph, the context will also contain arrays for property name paths, ancestor source objects and ancestor destination objects. Those arrays are accessible using $context[AutoMapper::PROPERTY_STACK_CONTEXT], $context[AutoMapper::SOURCE_STACK_CONTEXT] and $context[AutoMapper::DESTINATION_STACK_CONTEXT]. They can be used to implement custom mapping function based on the hierarchy level and current position inside the object graph being mapped.

Misc

  • Passing NULL as an argument for the source object to map returns NULL.

Similar libraries

When picking a library, it's important to see what options are available. No library is perfect, and they all have their pro's and con's.

A few other object mappers exist for PHP. They're listed here with a short description, and are definitely worth checking out!

  • Jane automapper:
    • Similar API
    • Compiles mappings, resulting in near-native performance
  • Nylle/PHP-AutoMapper:
    • Only maps public properties
    • Requires some conventions to be met
    • Does some interesting stuff with types
  • Papper:
    • Convention based
    • High performance
    • Lacks in documentation
  • BCCAutoMapperBundle:
    • Only available as a Symfony bundle (<3.0)
    • Very similar to this project
    • Does some cool stuff with graph mapping

Performance benchmarks (credit goes to idr0id):

Runtime: PHP 7.2.9-1
Host: Linux 4.18.0-2-amd64 #1 SMP Debian 4.18.10-2 (2018-11-02) x86_64
Collection size: 100000

package duration (MS) MEM (B)
native php 32 123736064
mark-gerarts/auto-mapper-plus (custom mapper) 92 123736064
jane-php/automapper (optimized) 100 123736064
jane-php/automapper 136 123736064
idr0id/papper 310 123736064
trismegiste/alkahest 424 113250304
mark-gerarts/auto-mapper-plus 623 123736064
nylle/php-automapper 642 123736064
bcc/auto-mapper-bundle 2874 123736064

Up-to-date benchmarks can be found here.

Note that using a custom mapper is very fast. So when performance really starts to matter in your application, you can easily implement a custom mapper where needed, without needing to change the code that uses the mapper.

See also

A note on PHPStan

Because of an issue described here, PHPStan reports the following error if you use the $context parameter:

Method AutoMapperPlus\MapperInterface::map() invoked with 3 parameters, 2 required.

If you see this error, you should enable the AutoMapper+ extension. Please note that this is a temporary solution. The issue will be fixed in the 2.0 release.

Roadmap

  • Provide a more detailed tutorial
  • Create a sample app demonstrating the automapper
  • Allow mapping from stdClass,
  • or perhaps even an associative array (could have)
  • Allow mapping to stdClass
  • Provide options to copy a mapping
  • Allow setting of prefix for name resolver (see automapper)
  • Create operation to copy value from property
  • Allow passing of contructor function
  • Allow configuring of options in AutoMapperConfig -> error when trying with a registered mapping
  • Consider: passing of options to a single mapping operation
  • MapTo: allow mapping of collection
  • Clean up the property checking in the Mapping::forMember() method.
  • Refactor tests
  • Allow setting a maximum depth, see #14
  • Provide a NameResolver that accepts an array mapping, as an alternative to multiple FromPropertys
  • Make use of a decorated Symfony's PropertyAccessor (see #16)
  • Allow adding of middleware to the mapper
  • Allow mapping to array

Version 2 is in the works, check there for new features as well

automapper-plus's People

Contributors

aarong416 avatar boshurik avatar gabbanaesteban avatar iborysenko avatar kozlice avatar mark-gerarts avatar thebouv avatar toilal avatar vasilake-v avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

automapper-plus's Issues

Allow guessing of property names

As it is now, the classes need to follow strict naming conventions in order to be mapped. A nice feature would be to be able to change this behaviour via the Options, making the mapper instead guess the source property name.

For example, when looking for the property firstName, the mapper would match the first occurrence of either [firstName, first_name, FirstName, ...].

Credit

Contributions welcome

This is a young library, meaning there is lots of room for improvements, bugfixes and new features. Contributions of any kind are very much appreciated!

Some things that need work & general ideas:

  • While the documentation is pretty thorough, I feel there are some things that can be explained better. There are bound to be spelling mistakes as well.
  • The tests definitely need a look. They don't follow best practices. A nudge in the right direction would be very nice.
  • The open issues need to be looked at, as well as the features on the roadmap.
  • Submitting feature requests and bug reports is encouraged!

Combine Operation::fromProperty and Operation::mapTo

I'm trying to map a nested property (using mapTo) where the source property name and the target property name are different but I can't seem to find a way.

I tried mapTo and fromProperty combined:

 ->forMember('address', Operation::fromProperty('adres'))
 ->forMember('address', Operation::mapTo(Address::class))

I tried a mapTo combined with a name resolver:

->forMember('address', Operation::mapTo(Address::class))
->withNameResolver(new CallbackNameResolver(function ($targetProperty) {
    if ($targetProperty == 'address') {
        return 'adres';
    }
    return $targetProperty;
}))

None of them seem to work.

I suspect that MapFromWithMapper would do the trick but that change has not been published to packagist it seems. Am I missing something here?

Mapping::forMember(...) incorrectly throws an error

The InvalidPropertyException is thrown when the provided propertyname doesn't exist on the source object. This should take into account the provided naming conventions (and the FromProperty operation).

For example registering a mapping from snake_case to camelCase and then calling forMember with a camelCase propertname will throw an error when it shouldn't

How to handle support for mapping to stdClass

PHPStan has configuration for something it calls "object crates". These are classes that are like stdClass, that just accept whatever you read and write to them by way of __get() and __set().

I'd lile to suggest that the best way to support mapping to stdClass would be a generic system such as this, where you configure a destination class as an object crate, and everything is just written as-is. This makes it easy to extend when integrating it into frameworks and other dynamic code.

Mapping to child classes

Hi,

I'm trying to use automapper-plus in the latest version (1.2.1) with Entity classes used in doctrine ORM. Doctrine ORM creates Proxy-Classes that inherit from the Entity class for adding additional stuff (e.g. loading assosiations).

My problem is, that automapper-plus seems not to work in this case. When trying to map from the proxy class to the target object, it says:

No mapping registered for converting an instance of class Proxies\__CG__\App\Entity\UserAddress into one of App\FormModel\UserAddressFormModel

I think this should be working, because every child class is an instance of the base class and should provide the same interface (Liscov Substitution Principle). The mapping should therefore apply to all sub classes.

Best Regards
mschop

CustomConstructor callable should have destinationClass parameter

Maybe I miss something, but I try to implement a custom constructor that grabs entities from a Doctrine repository.

But it doesn't seem to be possible because the constructor callback doesn't get $destinationClass as parameter to retrieve which class to instanciate.

$mapping = $this->getMapping($sourceClass, $destinationClass);
if ($mapping->providesCustomMapper()) {
return $this->getCustomMapper($mapping)->map($source, $destinationClass);
}
if ($mapping->hasCustomConstructor()) {
$destinationObject = $mapping->getCustomConstructor()($source, $this, $context);
}

I think $destinationClass should be passed as a parameter to the constructor callable.

Mapper add property to class which not have this properties

I've a parent class and its child

EntityA {
   private $name;
   private $entityB;
}

EntityB {
   private $name;
}

With doctrine, when I "findAll" entityA, it returns:

[
EntityA {
    name: Foo
    entityB: {
        name: Bar
        entitiesA: {
            ........
        }
    }
}
]

Then I want to map the response Doctrine to this Model

ModelA {
    private $name;
    private $modelB;
}

ModelB {
    private $name;
}

When I want to map the doctrine response to the ModelA I've this result:

ModelA {
    name: Foo
    modelB: {
        name: bar
        entitiesA: {
           ........
        }
    }
}

while the expected result should be :

ModelA {
    name: Foo
    modelB: {
        name: bar
    }
}

The mapper copy "entitiesA" property (come from doctrine) in the class ModelB, while my class ModelB hasn't a property named "entitiesA"

Why the mapper create properties which not exists in the class ?

code example :

$config->registerMapping(EntityA::class, ModelA::class);
$mapperResponse = $mapper->mapMultiple($doctrineResultWithEntityAList, ModelA::class);

What about polymorphism?

Is there any easy way to map polymorph property?

The polymorph property can be class A or B or C. Both classes extends the same abstract classes.
Unfortunately I can not make a mapper for the abstract class itself.

DataLoader for avoid N+1 problems

I need to avoid a N+1 problem uses DataLoader or similar solution.

I have collection of Entities and each model load from DB or from REST some data.

->forMember('path', function (Category $category, AutoMapperInterface $mapper) {

    $paths = $this->pathProvider->findByPath($category->path);

    return $mapper->mapMultiple($paths, CategoryPathDTO::class);
})

I want to collect all paths, load in batch and then resolve for each object. Do you have any suggestions how I can do this?

Map NULL objects

Hi, in this case:
$dto = $mapper->map($john, EmployeeDto::class);

if $john is null, the mapper throws an exception:

"No mapping registered for converting an instance of class AutoMapperPlus\AutoMapper into one of App\DTOs\EmployeeDTO"

Shouldn't it just set $dto to NULL as well?

Problem with mapping substr names properties

Hello! I updated from 1.2.1 to 1.2.2 and found a bug, example code:

final class FooDTO {
	/** @var int */
	private $foo_another_id = 2;
	/** @var int */
	private $id = 1;
}

final class BarDTO {
	/** @var int */
	private $id;
	/** @var int */
	private $bar_another_id;
}

$config = (new \AutoMapperPlus\Configuration\AutoMapperConfig());
$config
	->registerMapping(FooDTO::class, BarDTO::class)
	->forMember('id', \AutoMapperPlus\MappingOperation\Operation::fromProperty('id'))
	->forMember('bar_another_id', \AutoMapperPlus\MappingOperation\Operation::fromProperty('foo_another_id'))
	->withDefaultOperation(\AutoMapperPlus\MappingOperation\Operation::ignore());
$mapper = new \AutoMapperPlus\AutoMapper($config);
var_dump($mapper->map((new FooDTO()), BarDTO::class));

Output:

object(BarDTO)[539]
  private 'id' => int 2
  private 'bar_another_id' => int 2

Problem in \AutoMapperPlus\PropertyAccessor\PropertyAccessor::getPrivate

Nested mapping, where collection element needs to have parent assigned to its property

In following example, we didn't find a way how to replicate following in Automapper. We don't think it's possible without very custom things.

$answerSetDTO = ...
$duplicatedAnswerSetDTO = $this->autoMapper->map($answerSetDTO, AnswerSetDTO::class);

$optionsDTO = [];
foreach ($answerSetDTO->options as $optionDTO) {
    $optionDTO = $this->autoMapper->map($optionDTO, AnswerSetOptionDTO::class);
    $optionDTO->answerSet = $duplicatedAnswerSetDTO; // <- here is a complication
    $optionsDTO[] = $optionDTO;
}

$duplicatedAnswerSetDTO->options = new ArrayCollection($optionsDTO);

Using Operation::mapTo like following is close, but $optionsDTO#answerSet will be null, or it will just copy same object over:

// cloning
$autoMapperConfig->registerMapping(AnswerSetDTO::class, AnswerSetDTO::class)
    ->forMember('id', Operation::ignore())
    ->forMember('options', Operation::mapTo(AnswerSetOptionDTO::class))
    ->forMember('questions', Operation::ignore());

$autoMapperConfig->registerMapping(AnswerSetOptionDTO::class, AnswerSetOptionDTO::class)
    ->forMember('id', Operation::ignore())
// here is second culprit. Causes answerSet to be null, 
// or same object instead of parent one if Operation::ignore() is removed
// how to assign parent object id instead?
    ->forMember('answerSet', Operation::ignore()); 

So, if $answerSetDTO#options[0]#answerSet#id = 1 and $duplicateAnswerSetDTO#id = 2, we want $duplicateAnswerSetDTO#options[0]#answerSet#id = 2. Currently, we can achieve it to be null or 1 only.

Operation::setTo(true)

Description of this operation says: "Always sets the property to the given value" but actually its not true - it will only set property to the given value in the target class if the source class has the same property name defined.. It doesnt make sense to me. I would like my mapper to always set given property to a given value, but I cant do it using setTo operation. Solution to this problem would be to always allow mappings for this operation \AutoMapperPlus\MappingOperation\Implementations\SetTo::canMapProperty:

/**
 * @inheritdoc
 */
protected function canMapProperty(string $propertyName, $source): bool
{
    return true;
}

Update object without creating new

Given $object1 and $object2 with same type.
How can I update $object1 attributes with the not null values of $object2. Is it possible?

Possible typo In README File

Checking how to create nested DTO mappings, I came across the section in the README file Dealing with nested Mappings. I was confused for some time with the line that reads

$config->createMapping(Child::class, ChildDto::class);

As far as I am aware, there doesn't seem to be a method name createMapping, but rather registerMapping. Everywhere else in the README file has the method registerMapping, so I tried that in my code and it works great.

I know it's small thing, but if it could be fixed, then hopefully it'll cause less confusion for people reading the docs for the library xD

Encode a password during mapping in forMember()

To encode a password in symfony, we have to use UserPasswordEncoderInterface to encode the password. It will need to pass the User entity as first parameter for the method encodePassword().

It will be cool if we could do something like

$config->registerMapping(UserDto::class, User::class)
    ->forMember("password", function($source, $destination) use ($passwordEncoder) {
         return $passwordEncoder->encodePassword($destination, $source->password);  
   });

Is Automapper-plus able to convert array of object to object

Hi,
Example : the array
$array =

array (2) {
  [0]=> 
    object(App\Foo) {
      id => 1
    }
  [1]=>
    object(App\Foo) {
      id => 2
    }
}

Is automapper able to convert this array to an object like this ?

object(FooList) (2) {
  [0]=> 
    object(App\Foo) {
      id => 1
    }
  [1]=>
    object(App\Foo) {
      id => 2
    }
}

Map to nested objects

Hello! I have the following code:

$order = new Order();
$item = new Item();
$item->setName('Item Name');
$item->setPrice(1000);
$order->setItems([$item]);

dump($order);

// DTO haven't price property*
$dto = $mapper->map($order, OrderDTO::class);

dump($dto);

$order = $mapper->mapToObject($dto, $order);

dump($order);

Output:

App\Model\Order {#23
  -items: array:1 [
    0 => App\Model\Item {#24
      -name: "Item Name"
      -price: 1000
    }
  ]
}
App\DTO\OrderDTO {#33
  +items: array:1 [
    0 => App\DTO\ItemDTO {#28
      +name: "Item Name"
    }
  ]
}
App\Model\Order {#23
  -items: array:1 [
    0 => App\Model\Item {#31
      -name: "Item Name"
      -price: null
    }
  ]
}

Expected output:

App\Model\Order {#23
  -items: array:1 [
    0 => App\Model\Item {#24
      -name: "Item Name"
      -price: 1000
    }
  ]
}
App\DTO\OrderDTO {#33
  +items: array:1 [
    0 => App\DTO\ItemDTO {#28
      +name: "Item Name"
    }
  ]
}
App\Model\Order {#23
  -items: array:1 [
    0 => App\Model\Item {#31
      -name: "Item Name"
      -price: 1000
    }
  ]
}

Reproducer: https://github.com/BoShurik/automapper-plus-issue

Map object to array?

There is often a need to transform an object to associative array and I haven't found any good solutions for this. Even having some libraries they change a standard way to do such manipulations it in the project as we use the automapper-plus ;)
I've read somewhere that there is a plan to implement such a feature but in the next version because it has some breaking changes. Is it correct?
Anyway, I just wanted to ask whether this is a coming-soon feature and I can live with my hacks for now or should I better get some additional library for this only feature?
Kinds regards and thanks for the awesome library!

Allow callable as alternative to custom mapper

It would be nice to be able to map an entire object with a callable/closure, as an alternative to creating a custom mapper. E.g. the following:

$config->registerMapping(SomeClass::class, SomeOther::class)
            ->useCustomMapper(new class extends CustomMapper {
                public function mapToObject($source, $destination)
                {
                     // do stuff
                     return $destination;
                }
            });

Would become something like this:

$config->registerMapping(SomeClass::class, SomeOther::class)
            ->fromCallable(function ($source, $destination) {
                     // do stuff
                     return $destination;
                }});

Optionally the mapper can be passed as the third parameter. On a related note, the CustomMapper could maybe use a MapperAwareInterface as well.

Dto Object's property mapped in the wrong destination field

Hi,

Since update 1.3.0, I perceive a mapping problem.

I have two objects represented as follows :

  • UserDto
  • User (model object)
UserDto {#705
    -id: 269
    -name: "Doe"
    -firstname: "John"
    -email: "[email protected]"
    -cellphone: "04"
    -phone: "05"
  }

/** -------------------------- */

User {#1163
  -id: 269
  -name: "DOE"
  -firstname: "John"
  -email: "[email protected]"
  -cellphone: "05"
  -phone: null
  -fax: "06"
}

/** Mapping */

$this->config->registerMapping(UserDto::class, User::class)

The above configuration is very simple. Despite this, the library has a rather strange behavior since, as we can see, the phone property is mapped in cellphone. The above configuration is however simple.

In another case, if I pass only the cellphone value, cellphone property in UserModel will then be null.

Can you help me to see things more clearly?

Thanks you.

AutoMapperConfig::getClassDistance should consider interfaces

Hello,

It would be nice registering mapping for an interface, making the code below possible for unknown types:

$config->registerMapping(ObjectInterface::class, MyDTO::class);


$dtos = [];
foreach ($objects as $object) {
    $dtos[] = $automapper->map($object, MyDTO::class);
}

AutoMapperConfig::getClassDistance should consider interfaces with class_implements if no result with class_parents

Mapping to \stdClass fails with private properties

Config:

$config->registerMapping(Category::class, \stdClass::class);

Entity:

class Category
{
    private $id;

    private $name;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }
}

Mapping:

$em = $this->getDoctrine()->getRepository(Category::class);
$category = $em->findOneBy([]);
$stdclass = $this->mapper->map($category, \stdClass::class);

Expected: stdClass with ID and name properties
Actual: Cannot access private property App\Entity\Category::$id

Map to array/list of objects

Is it possible to map to a typed list resp. an array of objects?

Imagine a class like this:

class MyObject{
    /**
     * @var Thing[]
     */
    private $things;

    /**
     * @return Thing[]
     */
    public function getThings(): array
    {
        return $this->things;
    }

    /**
     * @param Thing[] $things
     * @return MyObject
     */
    public function setThings(array $things): MyObject
    {
        $this->things = $things;

        return $this;
    }

}

When mapping from an array i did this:

$config->registerMapping(DataType::ARRAY, Thing::class);
$config->registerMapping(DataType::ARRAY, MyObject::class)
       ->forMember(
           'things',
           function (array $things, AutoMapperInterface $mapper) {
               $ret = [];
               foreach ($things['things'] as $thing) {
                   $ret[] = $mapper->map($thing, Thing::class);
               }
               return $ret;
           }
       );

I guess it would be convenient to have a operation for this custom callback to map to a list.
Like:
Operation::mapArrayToArray(Things::class);

Custom Mapper with reverse map

Hello everyone,

This package looks amazing and I am currently trying to use it for a project. I was wondering if it was possible to use the method reverseMap() on a custom mapper if it only maps attributes to other attributes without any work on the values itself ?

Or doing a custom mapper for this isn't the best practice with this package ? I wanted to use a custom mapper since I will have a lot of mappers to create and it seemed cleaner to do it that way.

Thanks in advance,
Have a nice day !

Map to multiple operation and map from method operation

Hi, I would like to add 2 operations and wanted to know if it is wanted or can't be done already with some other operations. I'll start with the easier one:

fromProperty

we have quite a few mappings like this

        $config->registerMapping(Hosting::class, HostingDto::class)
               ->forMember('isActive', static function (Hosting $hosting) {
                   return $hosting->canBeStarted();
               })
        ;

this could be quite easily done with something like

 $config->registerMapping(Hosting::class, HostingDto::class)
               ->forMember('isActive', Operation::fromMethod('canBeStarted')
 ;

mapToMultiple

the more complicated one - support polymorphism. Sometimes we have a collection of different objects (e.g. with single table inheritance) and we would like to map them to different set of objects. Currently we are doing it like this:

               ->forMember(
                   'supportedParameters',
                   static function (Package $package, AutoMapperInterface $mapper) {
                       // Automapper is not supporting polymorphism, so we have to do it like this
                       $supportedParameters = [];

                    foreach ($package->getSupportedParameters() as $hostingTypeParameters) {
                        switch (get_class($hostingTypeParameters)) {
                            case LinuxHostingParameters::class:
                                $supportedParameters[] = $mapper->map(
                                    $hostingTypeParameters,
                                    LinuxHostingParametersDto::class
                                );

                                break;
                            case WindowsHostingParameters::class:
                                $supportedParameters[] = $mapper->map(
                                    $hostingTypeParameters,
                                    WindowsHostingParametersDto::class
                                );

                                break;                                
                            default:
                                throw new \RuntimeException(
                                    sprintf('Unsupported hosting parameters "%s"', get_class($hostingTypeParameters))
                                );
                        }
                    }

                       return $supportedParameters;
                   }
               )

My idea is to create an operation which would look like this

 $config->registerMapping(Package::class, PackageDto::class)
               ->forMember('supportedParameters', Operation::mapToMultiple([
                   LinuxHostingParameters::class,
                   WindowsHostingParameters::class
               ])
 ;

Not mapping proxy classes from doctrine lazy load

When you do lazy loading sometimes the objects come back as a proxy class which breaks since there's no mapping config registered for the proxy classes.

Example of the error:
No mapping registered for converting an instance of class Proxies\CG\Bundle\Entity\ProductItem into one of \ProductItemDto

Flattening objects and reusability

Hi, I have a question regarding flattening objects and reusability of mappings for child objects. Imagine having structure like this:

class Tyre
{
    /** @var string */
    public $name;

    /** @var int */
    public $size;
}

class Wheel
{
    /** @var Tyre */
    public $tyre;
}

class UniCycle
{
    /** @var Wheel */
    public $wheel;

}

class UniCycleDto implements Vehicle
{
    /** @var string */
    public $tyreName;
}

class CarWithOneWheelDto implements Vehicle
{
    /** @var string */
    public $tyreName;

    /** @var string */
    public $tyreSize;
}


$config->registerMapping(UniCycle::class, UniCycleDto::class)
       ->forMember(
           'tyreName',
           static function (UniCycle $uniCycle) {
               return $uniCycle->wheel->tyre->name;
           }
       );

This can result in a pretty big configuration, keeping me wondering, if this is not smaller/easier/faster (especially when I can't reuse my mapping, since I only have one unicycle in my system)

$uniCycleDto = new UniCycleDto();
$uniCycleDto->tyreName = $uniCycle->wheel->tyre->name;

Now first question is, is it possible to write it some compact way? e.g.

$config->registerMapping(UniCycle::class, UniCycleDto::class)
       ->forMember('tyreName', Operation::mapFrom('wheel.tyre.name'));

Second question is connected with my above example and this issue regarding reusability of those small value objects. I can have other objects, which have wheels and I'm interested in their tyre names (maybe some other properties). Is it possible to do something like this? I would like to have this kind of automatic, every time when I encounter Wheel, just do the mapping. However I'm not sure how to achieve that especially without recursive property accessor.

$config->registerMapping(Wheel::class, Vehicle::class)
       ->forMember('tyreName', Operation::mapFrom('tyre.name'))
       ->forMember('tyreSize', Operation::mapFrom('tyre.size'));

$config->registerMapping(UniCycle::class, UniCycleDto::class)
       ->useMapping(UniCycle::class, Vehicle::class);

$config->registerMapping(UniCycle::class, CarWithOneWheelDto::class)
       ->useMapping(UniCycle::class, Vehicle::class);

Psalm annotations

I'm currently using this project and Psalm. Obviously static analysis has some issues to understand what this mapper does, but I think that with a couple of annotations it would be fairly easy to explain what's happening type-wise.

Would you be interested in a PR that adds annotations for improving static analysis?
Would you prefer replacing the current annotations or just adding @psalm-* ones beside the existing ones?

Not mapping extending classes

if classA entends classBase. All attributes lived in classBase. After mapping, will get an empty class A object.

In my case, I have classA extends classBase.
I want map classA to ClassADto. my class have a list of classA.
after mapping, I get a list of empty classADto.

Last version 1.3.11 is probably not minor upgrade :)

I experience a problem after composer updated the minor version of the amazing library you develop.
As far as I understood it's because we don't use typed variables yet. We are still sticking to the annotations.
A very nice change but I'd suggest you consider implementing backwards capability or upgrade a major version of the library.
I can't achieve proper formatting of the stack trace so I will provide it using screenshots, hope it helps
image
image
image
image
image
image
image
image
image
image

Typed properties break mapping operations

Having a typed property (PHP 7.4+), such as:

class Employee
{
    public string $name;
    public int $age;
}

Causes mapping operations to fail when mapping to Employee::class. The same mapping operating works fine if the properties are declared without types.

Mapping operations without source property required

There is a @TODO on AutoMapperPlus\Configuration\Mapping::shouldCheckForSourceProperty() method.

Indeed certain operations needs extra Interface to skip property existence check.

For instance, I don't want certain properties to be mapped into destination object. The way it works now is perfect (with Ignore operation) but there is one problem: when I specify this operation on virtual property (i.e. in Laravel there is a lot of magic happens on models) forMember method will try to ensure source property exist. But it has no sense due to meaning of Ignore operation.

Also you have great PropertyAccessorInterface object, please utilize it to check property can be reached instead of default PHP function within forMember method.

Thanks in advance, great Tool!

Operation::setTo omits target property if the source property doesn't exist

My target class has a property which the source class is lacking. To this target property I would like to be able to assign a particular value during the mapping. According to the documentation Operation::setTo seems to do the job, but at the moment it ignores target properties for which there are no equivalent source properties. Is there any reason behind this behaviour? I looked at the Options object too, but see no relevant setting. I know I could define a callback, but Operation::setTo is way more elegant in this case.

$context is commented in interfaces

Is there any reason for $context parameter to be commented in interfaces ?

I'm using PHPStan and it raise an error when typing with AutoMapperInterface and trying to use context.

/**, array $context = [] */
public function map($source, string $targetClass/**, array $context = [] */);

public function mapToObject($source, $destination/**, array $context = [] */);

Proxies with Doctrine

Hi !

When we use Doctrine wich returns proxies, the mapper could not map : it maps property to null.

Is there a solution ?

example dump doctrine result :

App\Entity\Company {#584
  -id: 81
  -secteur: Proxies\__CG__\App\Entity\Secteur {
    -id: 1
    -name: null
  }

dump($company->getSecteur->getName()) : returns a string

but the mapper found "null"

Can't blank properties

First of all I am a big fan of the .NET AutoMapper and thank you so much for creating this for PHP!

Is there a way to blank out properties?
For example if my source object has a property like this:

[
    'description' => ''
]

the AutoMapper will ignore it. My goal is to ignore it if null, but to overwrite it if it's an empty string.

Basically if someone wants to delete an optional text field, they can't.
Maybe there is already a way to do that, if so, could you let me know?
Or otherwise can it be implemented?

Thanks!

Registering mapping for child properties

I'm currently trying to use the automapper to solve this problem:

I have multiple objects to map, and I have multiple \DateTime properties in those; I would like to map all of those to the same format\DTO. I have set up everything correctly, but every time that a source object has a \DateTime property, I'm forced to call ->forMember('field', Operation::mapTo(DateTimeDto)).

Is there any way to map that just once and for all?

doctrine gets entities one by one

I have entities that are related:
building, house, properties

        $config
            ->registerMapping(DataCollectorBuilding::class, AggregateBuilding::class)
            ->forMember('houses', new MapTo(AggregateHouse::class));

        $config
            ->registerMapping(DataCollectorHouse::class, AggregateHouse::class)
            ->forMember('building', function(DataCollectorHouse $house) {
                return $house->getBuilding()->getId();
            })
            ->forMember('properties', new MapTo(AggregateProperty::class));
$config
            ->registerMapping(DataCollectorProperty::class, AggregateProperty::class)
            ->forMember('presetImage', function(DataCollectorProperty $property) {
                return !is_null($property->getPreset()) ? $property->getPreset()->getImage() : null;
            })
        ;

the property has a Preset and when I just ask for Building, the auto-mapper starts filling in related entities and the Preset fills in one by one

[2020-09-14 07:10:36] doctrine.DEBUG: SELECT t0.id AS id_1, t0.code AS code_2, t0.image AS image_3, t0.created_at AS created_at_4, t0.updated_at AS updated_at_5, t0.house_id AS house_id_6 FROM preset t0 WHERE t0.id = ? [1636] []
[2020-09-14 07:10:36] doctrine.DEBUG: SELECT t0.id AS id_1, t0.code AS code_2, t0.image AS image_3, t0.created_at AS created_at_4, t0.updated_at AS updated_at_5, t0.house_id AS house_id_6 FROM preset t0 WHERE t0.id = ? [1637] []
[2020-09-14 07:10:36] doctrine.DEBUG: SELECT t0.id AS id_1, t0.code AS code_2, t0.image AS image_3, t0.created_at AS created_at_4, t0.updated_at AS updated_at_5, t0.house_id AS house_id_6 FROM preset t0 WHERE t0.id = ? [1638] []
[2020-09-14 07:10:36] doctrine.DEBUG: SELECT t0.id AS id_1, t0.code AS code_2, t0.image AS image_3, t0.created_at AS created_at_4, t0.updated_at AS updated_at_5, t0.house_id AS house_id_6 FROM preset t0 WHERE t0.id = ? [1639] []
[2020-09-14 07:10:36] doctrine.DEBUG: SELECT t0.id AS id_1, t0.code AS code_2, t0.image AS image_3, t0.created_at AS created_at_4, t0.updated_at AS updated_at_5, t0.house_id AS house_id_6 FROM preset t0 WHERE t0.id = ? [1640] []

how can you get rid of this behavior?

[RFC] Context aware operations

Hello!

I have the following use case: https://gist.github.com/BoShurik/7aafc932e9dcf74be2196a09d6da15bd

To build children elements I need access to parent object

$config
    ->registerMapping(ItemDTO::class, Item::class)
    ->forMember('value', Operation::mapFrom(function (ItemDTO $dto) {
        return new Value(/*OrderDTO::$currency*/, $dto->value);
    }))
;

I suggest to use context:

$config
    ->registerMapping(OrderDTO::class, Order::class)
    ->forMember('value', Operation::mapFrom(function (OrderDTO $dto) {
        return new Value($dto->currency, $dto->value);
    }))
    ->forMember('items', Operation::mapFromWithMapper(function (OrderDTO $dto, AutoMapper $mapper) {
        $items = [];
        foreach ($dto->items as $item) {
            $items[] = $mapper->map($item, Item::class, [
               'currency' => $dto->currency,
            ]);
        }

        return $items;
    }))
;
$config
    ->registerMapping(ItemDTO::class, Item::class)
    ->forMember('value', Operation::mapFromWithContext(function (ItemDTO $dto, array $context) {
        return new Value($context['currency'] ?? 'EUR', $dto->value);
    }))
;

Not sure, but may be it solves problem described in #16

Doctrine proxies are not supported

The problem is here: https://github.com/mark-gerarts/automapper-plus/blob/master/src/PropertyAccessor/PropertyAccessor.php#L95-L102

    protected function setPrivate($object, string $propertyName, $value): void
    {
        $setter = function($value) use ($propertyName) {
            $this->{$propertyName} = $value;
        };
        $boundSetter = \Closure::bind($setter, $object, get_class($object));
        $boundSetter($value);
    }

get_class($object) returns proxy class name. This scope does not have access to private properties. Sadly this code does not throw any exception.

Operation::fromProperty with property path with dot notation

This is probably a feature request:
Is it possible to use a property path with dot notation with Operation::fromProperty?

Operation::fromProperty('children[0].firstName')

Its possible with the symfony PropertyAccessor:
https://symfony.com/doc/current/components/property_access.html#accessing-public-properties

Of course this can be achieved by a callback, but its quite tedious to create the callback and also check for existance of the property path.
It would be quite handy if this could be done automatically within fromProperty().

Nested mapping with array as a source

Hi! I have a problem with nested object filling with array as a source. Is it possible with AutoMapper? Here is what I'd like to do, I attach a sample code. Could you have a look, please?

class Test
{
    public function test() {
        $mapper = $this->getParentClassMapper();

        $source = ["id" => 1, "name" => "Super name"];

        $parentObject = $mapper->map($source, ParentClass::class);
        var_dump($parentObject); die;
    }

    private function getParentClassMapper() {
        $config = new AutoMapperConfig();
        $config->registerMapping(DataType::ARRAY, ChildClass::class);
        $config->registerMapping(DataType::ARRAY, ParentClass::class)
            ->forMember('child', Operation::mapTo(ChildClass::class));
        return new AutoMapper($config);
    }
}


class ParentClass
{
    private $id;
    private $child;
}

class ChildClass
{
    private $name;
}

I know that the example looks strange and therefore give you some context. I have an address table in my database. It has fields like countryId and cityId. I don't want to get the city and country with separate queries and write an sql with joins to get all info. My sql looks like this (you can also see the tables and fields there):

SELECT address.id,
                       address.countryId,
                       address.stateId,
                       address.cityId,
                       address.street,
                       address.house,
                       address.zipCode,
                       address.longitude,
                       address.latitude,
                       address_country.nameEn as countryNameEn,
                       address_country.nameDe as countryNameDe,
                       address_country.region as countryRegion,
                       address_country.code as countryCode,
                       address_state.nameEn as stateNameEn,
                       address_state.nameDe as stateNameDe,
                       address_state.countryId as stateCountryId,
                       address_city.nameEn as cityNameEn,
                       address_city.nameDe as cityNameDe,
                       address_city.countryId as cityCountryId,
                       address_city.stateId as cityStateId
        
                FROM address
                JOIN address_country on (address_country.id = address.countryId)
                JOIN address_state on (address_state.id = address.stateId)
                JOIN address_city on (address_city.id = address.cityId)
                    
                WHERE address.id = ?;

My Address model is the following:

/**
     * @var $id int
     */
    private $id;

    /**
     * @var $country Country|null
     */
    private $country;

    /**
     * @var $state State|null
     */
    private $state;

    /**
     * @var $city City|null
     */
    private $city;

    /**
     * @var $street string|null
     */
    private $street;

    /**
     * @var $house string|null
     */
    private $house;

    /**
     * @var $zipCode string|null
     */
    private $zipCode;

    /**
     * @var $longitude string|null
     */
    private $longitude;

    /**
     * @var $latitude string|null
     */
    private $latitude;

    /**
     * @var $countryId int|null
     */
    private $countryId;

    /**
     * @var $stateId int|null
     */
    private $stateId;

    /**
     * @var $cityId int|null
     */
    private $cityId;

As you can see, I would like to have city and country fields that are City and Country types. I would like to map the fields like cityNameEn to field nameEn of child city. Is that possible?
Thanks a lot for your effort, the library is awesomely useful!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.