Skip to content

Instantly share code, notes, and snippets.

@zajca
Created October 11, 2017 09:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zajca/8a7843cf82d8b66e0dc024413929e2db to your computer and use it in GitHub Desktop.
Save zajca/8a7843cf82d8b66e0dc024413929e2db to your computer and use it in GitHub Desktop.
Php class to update doctrine any entity property without any special configuration.
<?php
namespace Zajca\Service;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use MabeEnum\Enum;
use Zajca\DoctrineType\DTOJsonArrayType;
use Zajca\DoctrineType\DTOJsonType;
use Zajca\DTO\DTOArrayDenormalizer;
use Zajca\DTO\DTONormalizer;
use Zajca\Entity\BaseEntity;
use Zajca\Enum\EnumNormalizer;
use Zajca\Helper\Str;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Serializer\Serializer;
/**
* Class EntityUpdater
* @package Zajca\Service
*
* This class is able to update any field in entity including OneToMany, ManyToMany, ManyToOne, Enum,... relation can
* be deleted updated or created new.
*
*/
class EntityUpdater
{
const SIMPLE_TYPES = [
Type::BIGINT,
Type::BOOLEAN,
Type::DECIMAL,
Type::FLOAT,
Type::STRING,
Type::INTEGER,
Type::SMALLINT,
Type::SIMPLE_ARRAY,
Type::TEXT,
];
/**
* @var EntityManager
*/
private $em;
/**
* @var Serializer
*/
private $serializer;
/**
* EntityUpdater constructor.
*
* @param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
*
* TODO: this should be changes for JMS serializer
* @return Serializer
*/
private function getSerializer(): Serializer
{
if (null === $this->serializer) {
$this->serializer = new Serializer(
[
$arrayNormalizer = new DTOArrayDenormalizer(),
$dtoNormalizer = new DTONormalizer(),
new EnumNormalizer(),
]
);
$arrayNormalizer->setSerializer($this->serializer);
$dtoNormalizer->setSerializer($this->serializer);
}
return $this->serializer;
}
/**
* @param BaseEntity $entity
* @param string $property
* @param $data
*
* @return BaseEntity
* @throws \InvalidArgumentException
* @throws BadRequestHttpException
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
* @throws \ReflectionException
*/
public function updateProperty(BaseEntity $entity, string $property, $data): BaseEntity
{
$refClass = new \ReflectionClass($entity);
$metadata = $this->em->getClassMetadata($refClass->getName());
if (!$refClass->hasMethod('set'.ucfirst($property))) {
throw new BadRequestHttpException('Missing method '.'set'.ucfirst($property).' on '.$refClass->getName());
}
$setMethod = $refClass->getMethod('set'.ucfirst($property));
if (array_key_exists($property, $metadata->fieldMappings)) {
$mappings = $metadata->fieldMappings[$property];
if (in_array($mappings['type'], self::SIMPLE_TYPES, true) === true) {
//PRIMITIVE string, int, array,...
//simple value update
$setMethod->invokeArgs($entity, [$data]);
} elseif ($mappings['type'] === DTOJsonArrayType::DTO_JSON_ARRAY) {
//DTO ARRAY
$dtoArr = $this->getSerializer()->denormalize($data, '', 'json');
$setMethod->invokeArgs($entity, [$dtoArr]);
} elseif ($mappings['type'] === DTOJsonType::DTO_JSON) {
//SINGLE DTO
$dto = $this->getSerializer()->denormalize($data, '', 'json');
$setMethod->invokeArgs($entity, [$dto]);
} elseif (Str::endsWith($mappings['type'], 'Enum')) {
//ENUM
$param = $setMethod->getParameters()[0];
$className = $param->getClass()->getName();
$targetClassRef = new \ReflectionClass($className);
/** @var Enum $enum */
$enum = $targetClassRef->newInstanceWithoutConstructor();
$setMethod->invokeArgs($entity, [$enum::byValue($data['value'])]);
} elseif ($mappings['type'] === 'datetime') {
$setMethod->invokeArgs($entity, [new \DateTime($data)]);
} else {
throw new \InvalidArgumentException('Property update can\'t handle '.$mappings['type']);
}
}
if (array_key_exists($property, $metadata->associationMappings)) {
switch ($metadata->associationMappings[$property]['type']) {
case ClassMetadataInfo::ONE_TO_MANY:
$targetEntity = $metadata->associationMappings[$property]['targetEntity'];
$this->updateOneToMany($entity, $property, $data, $refClass, $setMethod, $metadata, $targetEntity);
break;
case ClassMetadataInfo::MANY_TO_ONE:
$this->updateManyToOne($entity, $data, $setMethod);
break;
case ClassMetadataInfo::MANY_TO_MANY:
$this->updateManyToMany(
$entity,
$data,
$setMethod,
$metadata->associationMappings[$property]['targetEntity']
);
break;
default:
throw new \InvalidArgumentException(
'Property update can\'t handle association'.$metadata->associationMappings[$property]['type']
);
}
}
return $entity;
}
/**
* @param BaseEntity $entity
* @param $data
* @param \ReflectionMethod $setMethod
*
* @param string $targetEntity
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*/
private function updateManyToMany(
BaseEntity $entity,
$data,
\ReflectionMethod $setMethod,
string $targetEntity
): void {
if (null === $data || 0 === count($data)) {
$setMethod->invokeArgs($entity, [[]]);
} else {
$references = new ArrayCollection();
foreach ($data as $item) {
$references->add($this->em->find($targetEntity, $item['id']));
}
$setMethod->invokeArgs($entity, [$references]);
}
}
/**
* @param BaseEntity $entity
* @param $data
* @param \ReflectionMethod $setMethod
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*/
private function updateManyToOne(BaseEntity $entity, $data, \ReflectionMethod $setMethod): void
{
if (null === $data) {
$setMethod->invokeArgs($entity, [null]);
} else {
$param = $setMethod->getParameters()[0];
//update reference entity
$newReference = $this->em->find($param->getClass()->getName(), $data['id']);
$setMethod->invokeArgs($entity, [$newReference]);
}
}
/**
* @param BaseEntity $entity
* @param string $property
* @param array $data
* @param \ReflectionClass $entityRefClass
* @param \ReflectionMethod $propertySetMethod
* @param ClassMetadata $entityMetadata
* @param string $targetEntityName
*
* @throws BadRequestHttpException
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
* @throws \ReflectionException
*/
private function updateOneToMany(
BaseEntity $entity,
string $property,
array $data,
\ReflectionClass $entityRefClass,
\ReflectionMethod $propertySetMethod,
ClassMetadata $entityMetadata,
string $targetEntityName
): void {
$mappedBy = null;
foreach ($entityMetadata->associationMappings as $mapping) {
if ($mapping['targetEntity'] === $targetEntityName) {
$mappedBy = $mapping['mappedBy'];
break;
}
}
if (null === $mappedBy) {
throw new BadRequestHttpException('Relation must have defined backwards mappedByField to set source');
}
$relationsToAdd = [];
/** @var $item [] */
foreach ($data as $index => $item) {
if (!array_key_exists('id', $item)) {
$newEntity = new $targetEntityName();
//update content
foreach ($item as $prop => $propData) {
$this->updateProperty($newEntity, $prop, $propData);
}
//set mappedBy reference
$targetReflectionClass = new \ReflectionClass($targetEntityName);
if (!$targetReflectionClass->hasMethod('set'.ucfirst($mappedBy))) {
throw new BadRequestHttpException(
'Missing method '.'set'.ucfirst($mappedBy).' on '.$targetEntityName
);
}
$setMapped = $targetReflectionClass->getMethod('set'.ucfirst($mappedBy));
$setMapped->invokeArgs($newEntity, [$entity]);
$relationsToAdd[] = $newEntity;
//continue only with data to update or keep
unset($data[$index]);
}
}
if (!$entityRefClass->hasMethod('get'.ucfirst($property))) {
throw new BadRequestHttpException(
'Missing method '.'get'.ucfirst($property).' on '.$entityRefClass->getName()
);
}
$getMethod = $entityRefClass->getMethod('get'.ucfirst($property));
/** @var BaseEntity[] $references */
$references = $getMethod->invoke($entity);
$seenReferences = [];
//TODO: this is like worst thing I've ever done :/ this need more thinking, how to do it with less for
foreach ($references as $reference) {
foreach ($data as $item) {
if ($item['id'] === $reference->getId()) {
foreach ($item as $prop => $propData) {
if ($prop !== 'id' && $prop !== $mappedBy) {
//id can't be set and mapped field is same
$this->updateProperty($reference, $prop, $propData);
}
}
$seenReferences[] = $reference;
}
}
}
//TODO: references must be set cascade persist and orphanRemoval check this
$relations = array_merge($relationsToAdd, $seenReferences);
$propertySetMethod->invokeArgs($entity, [$relations]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment