Open leroy0211 opened 5 years ago
Hi @leroy0211! Yes, this would be pretty useful. For this to work we need to be able to map from and to scalars. From there it would simply be a matter of registering a mapping that uses some entity repository. However, mapping with scalars is currently not possible, as I always started from the idea of mapping from and to an object.
That being said, I think it might be possible to work this into the 2.0 branch. As a matter in fact, I was able to create a small POC by just expanding the allowed data types (see the feature/scalar-mapping branch, more specifically this commit).
This then works using custom constructor factories:
$config = new AutoMapperConfig();
// This only works on feature/scalar-mappings
$config->registerMapping(DataType::INTEGER, Post::class)
->beConstructedUsing(function (int $id): ?Post {
return $this->postRepository->find($id);
})
->withDefaultOperation(Operation::ignore());
$config->registerMapping(Post::class, DataType::INTEGER)
->beConstructedUsing(function (Post $post): int {
return $post->getId();
});
$mapper = new AutoMapper($config);
$mapper->map(1, Post::class);
$mapper->map($this->postRepository->find(1), DataType::INTEGER);
This isn't really clean of course, expanding the API of the config would be better. This API is currently built around defining operations for properties, whereas here we are defining a single operation for the entire object. We could use something like $config->registerScalarMapping(...)
or $config->registerMapping(...)->mapUsing(<callable>)
(ideas are welcome :)).
I'll have to think about it a bit. I'm having a busy few months ahead, so I don't know how much time I'm going to be able to spend on this, but I'll see what I can do :)
I've come up with a different approach by using MappingOperations
, because I like to map A relation's id or other attribute to a Dto's scalar value.
For example, a Shop which has Stock, and that Stock comes from multiple Locations. I only want to configure a shop's stock identifier, but want to display the location names.
For example (as json):
{
"name": "My Physical shop",
"address": {
"street": "Lorem ipsum"
},
"stockId": 5, <-- This is an entity id
"stockLocations": ["Warehouse 1", "Store stock", "Store warehouse"] <-- These are the location names
}
So I've come up with 4 MappingOperations for this to work. However, to convert the integer to an entity, you need a repository or an EntityManager as dependency. The Entity to property MappingOpertions can take any property, not only just the identifiers.
For the entity to a property mapping I've created:
class MapPropertyFromEntity extends DefaultMappingOperation
{
/** @var string */
private $valueProperty;
public function __construct(string $valueProperty)
{
$this->valueProperty = $valueProperty;
}
protected function getSourceValue($source, string $propertyName)
{
$entity = $this->propertyReader->getProperty(
$source,
$this->getSourcePropertyName($propertyName)
);
return $this->propertyReader->getProperty($entity, $this->valueProperty);
}
}
And for a Collection I've created:
class MapPropertyFromCollection extends DefaultMappingOperation
{
private $valueProperty;
public function __construct($valueProperty)
{
$this->valueProperty = $valueProperty;
}
protected function getSourceValue($source, string $propertyName)
{
$values = [];
foreach ($this->propertyReader->getProperty($source, $propertyName) ?: [] as $item) {
$values[] = $this->getPropertyReader()->getProperty($item, $this->valueProperty);
}
return $values;
}
}
For the reverse version (mapping id's to entities or a collection of entities) I've created:
class MapIdToEntity extends DefaultMappingOperation
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
protected function setDestinationValue($destination, string $propertyName, $value): void
{
if (empty($value)) {
return;
}
$metadata = $this->entityManager->getClassMetadata(get_class($destination));
$destinationClass = $metadata->getAssociationTargetClass($propertyName);
$entity = $this->entityManager->find($destinationClass, $value);
if (null === $entity) {
return;
}
$metadata->setFieldValue($destination, $propertyName, $entity);
}
}
AND
class MapIdsToCollection extends DefaultMappingOperation
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* {@inheritdoc}
*/
protected function setDestinationValue($destination, string $propertyName, $value): void
{
if (empty($value)) {
return;
}
$metadata = $this->entityManager->getClassMetadata(get_class($destination));
$destinationClass = $metadata->getAssociationTargetClass($propertyName);
$items = $this->getEntities($value, $destinationClass);
if ($metadata->isCollectionValuedAssociation($propertyName)) {
$metadata->setFieldValue($destination, $propertyName, new ArrayCollection(iterator_to_array($items)));
} else {
parent::setDestinationValue($destination, $propertyName, iterator_to_array($items));
}
}
private function getEntities($value, string $className): \Generator
{
foreach ($value as $item) {
if ($entity = $this->entityManager->find($className, $item)) {
yield $entity;
}
}
}
}
What about adding Doctrine Support with some additional operations? For example: