Skip to content

Instantly share code, notes, and snippets.

@pasinter
Forked from danieledangeli/Clone Entity
Last active August 29, 2015 14:10
Show Gist options
  • Save pasinter/186ed15b9185e8233f76 to your computer and use it in GitHub Desktop.
Save pasinter/186ed15b9185e8233f76 to your computer and use it in GitHub Desktop.
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Inflector\Inflector;
use Doctrine\Common\Persistence\Mapping\AbstractClassMetadataFactory;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\PersistentCollection;
use Foodity\CoreBundle\Helper\EntityHelper;
/**
* Class CloneableService
* This class is a service to clone a doctrine entity with all the associations.
* The goal is to explore the associations inside an entity and recursively
* clone the entity if needed.
*
* Example: A has many B objects and many to many C objects and one D object ( the relation is Many to One )
* A -> B, A <-> C, D -> A
*
* So clone(A) = Ac must be an entity with this characteristics:
* Ac-> Bc, Ac <-> C, D-> Ac
* We need to clone the entity B ( because the foreign key of the association is stored on the B entity ).
* So we need to apply a recursion over the B entity to obtain Bc entity.
* Obviously if B has a relation to others entities the algorithm still works over
* B's association entities until the exit condition, that is when there aren't other entities to be explored ).
*
* We need only to update the doctrine metadata over the many to many association. The foreign keys are stored
* by doctrine in a split table. So we need just to add a reference of C over the Ac cloned object.
* The Many to one case is not processed ( we clone the base entity A with reflection, so the the reference of D in Ac
* is cloned correctly).
* The one to one case is similar to the one to many case ( with very little bit differences )
*
* @package Foodity\CoreBundle\Service\Cloneable
*/
class CloneableService
{
const CLASS_LABEL = 'class';
const PROPERTY_NAME_LABEL = 'property';
/** @var AbstractClassMetadataFactory */
protected $metadataFactory;
/** @var EntityManager */
protected $em;
/**
* @var EntityHelper
*/
private $entityHelper;
/**
* @param EntityManager $entityManager
* @param EntityHelper $entityHelper
*/
public function __construct(EntityManager $entityManager, EntityHelper $entityHelper)
{
$this->em = $entityManager;
$this->metadataFactory = $this->em->getMetadataFactory();
$this->entityHelper = $entityHelper;
}
/**
* @param mixed $entity
* @param array $exclusionMap
*
* @return Object cloned entity
*/
public function doClone($entity, array $exclusionMap = array())
{
if (!$this->entityHelper->isEntity($entity)) {
throw new \InvalidArgumentException(sprintf('Argument 1 of %s() is not an Entity.', __METHOD__));
}
$className = $this->entityHelper->getEntityClassName($entity);
$associationMap = array();
$this->exploreAssociation(array($className), array(), $associationMap);
$startClone = $this->applyReflection($entity, $className);
$explored = array();
$clonedEntity = $this->exploreMap($associationMap, $startClone, $explored, $exclusionMap);
return $clonedEntity;
}
/**
* @param string $className
* @param string $field
*
* @return mixed
*/
public function getInverseAssociation($className, $field)
{
$associationMapping = $this->getAssociationMapping($className, $field);
return $associationMapping['mappedBy'];
}
/**
* @param string $className
* @param string $field
*
* @return bool
*/
public function isManyToManyAssociation($className, $field)
{
return $this->getAssociationType($className, $field) == ClassMetadataInfo::MANY_TO_MANY;
}
/**
* @param string $className
* @param string $field
*
* @return bool
*/
public function isManyToOneAssociation($className, $field)
{
return $this->getAssociationType($className, $field) == ClassMetadataInfo::MANY_TO_ONE;
}
public function getAssociationType($className, $field)
{
$associationMapping = $this->getAssociationMapping($className, $field);
return $associationMapping['type'];
}
/**
* @param string $className
* @param string $fieldName
*
* @return bool
*/
public function isUniqueField($className, $fieldName)
{
/** @var ClassMetadata $class */
$class = $this->metadataFactory->getMetadataFor($className);
try {
return $class->isUniqueField($fieldName);
} catch (MappingException $e) {
return false;
}
}
/**
* @param array $associationNames
* @param array $explored
* @param array $mapping
*/
public function exploreAssociation(array $associationNames, array $explored, array &$mapping)
{
$found = false;
if (count($associationNames) === 0) {
return;
}
$toExplore = null;
while (!$found) {
$toExplore = array_shift($associationNames);
if (!in_array($toExplore, $explored)) {
$explored[] = $toExplore;
$found = true;
} elseif (count($associationNames) === 0) {
return;
}
}
$class = $this->metadataFactory->getMetadataFor($toExplore);
$newAssociationNames = $class->getAssociationNames();
foreach ($newAssociationNames as $associationName) {
$className = $class->getAssociationTargetClass($associationName);
if (!$this->isManyToOneAssociation($class->getName(), $associationName)) {
$associationNames[] = $className;
if (!array_key_exists($toExplore, $mapping)) {
$mapping[$toExplore] = array();
}
$mapping[$toExplore][] = array(
self::PROPERTY_NAME_LABEL => $associationName,
self::CLASS_LABEL => $className
);
}
}
$this->exploreAssociation($associationNames, $explored, $mapping);
}
/**
* Build a reflection for the cloned object
*
* @param mixed $object
* @param string $className
*
* @return mixed
*/
protected function applyReflection($object, $className)
{
$oldEntity = $object;
$newEntity = new $className();
$oldReflection = new \ReflectionObject($oldEntity);
$newReflection = new \ReflectionObject($newEntity);
foreach ($oldReflection->getProperties() as $property) {
if ($newReflection->hasProperty($property->getName()) && $property->getName() != 'id') {
$newProperty = $newReflection->getProperty($property->getName());
$newProperty->setAccessible(true);
$propertyName = $property->getName();
$accessor = $this->getAccessorFromPropertyName($propertyName, $oldEntity);
$oldValue = $oldEntity->$accessor();
if (!$this->isUniqueField($className, $propertyName)) {
$newProperty->setValue($newEntity, $oldValue);
}
}
}
return $newEntity;
}
/**
* @param string $className
* @param string $field
*
* @return array
* @throws MappingException
*/
private function getAssociationMapping($className, $field)
{
/** @var ClassMetadata $class */
$class = $this->metadataFactory->getMetadataFor($className);
return $class->getAssociationMapping($field);
}
/**
* @param string $className
* @param string $field
*
* @return bool
*/
private function hasAssociation($className, $field)
{
$class = $this->metadataFactory->getMetadataFor($className);
return $class->hasAssociation($field);
}
/**
* @param array $map
* @param Object $father
* @param array $alreadyProcessed
* @param array $exclusionMap
* @param string|null $callerClassName
*
* @return Object
*/
public function exploreMap($map, $father, &$alreadyProcessed, $exclusionMap, $callerClassName = null)
{
$class = $this->metadataFactory->getMetadataFor($this->entityHelper->getEntityClassName($father));
$key = $class->getName();
//exit condition
if (count($alreadyProcessed) === count($map)) {
return $father;
}
//not need to explore
if (!array_key_exists($key, $map)) {
return $father;
}
$mapEntry = $map[$key];
//map Entry to explore
foreach ($mapEntry as $entry) {
$field = $entry[self::PROPERTY_NAME_LABEL];
$sonClassName = $entry[self::CLASS_LABEL];
$inverseField = $this->getInverseAssociation($class->getName(), $field);
$associationType = $this->getAssociationType($key, $field);
switch ($associationType) {
case ClassMetadataInfo::ONE_TO_MANY:
$father = $this->oneToManyStrategy(
$father,
$key,
$field,
$sonClassName,
$inverseField,
$map,
$alreadyProcessed,
$exclusionMap
);
break;
case ClassMetadataInfo::ONE_TO_ONE:
$father = $this->oneToOneStrategy(
$father,
$key,
$field,
$sonClassName,
$inverseField,
$map,
$alreadyProcessed,
$exclusionMap,
$callerClassName
);
break;
case ClassMetadataInfo::MANY_TO_MANY:
$father = $this->manyToManyStrategy($father, $key, $field, $sonClassName, $exclusionMap);
break;
}
$alreadyProcessed[$key] = $father;
}
return $father;
}
/**
* @param Object $father
* @param string $fatherClassName
* @param string $field
* @param string $sonClassName
* @param string $inverseField
* @param array $map
* @param array $alreadyProcessed
* @param array $exclusionMap
*
* @return Object
*/
private function oneToManyStrategy(
$father,
$fatherClassName,
$field,
$sonClassName,
$inverseField,
$map,
&$alreadyProcessed,
$exclusionMap
) {
$setAccessorSonToFather = $this->setAccessorFromPropertyName($inverseField);
$getAccessorFatherToSon = $this->getAccessorFromPropertyName($field, $father);
$setAccessorFatherToSon = $this->setAccessorFromPropertyName($field);
$addAccessorFatherToSon = $this->addAccessorFromPropertyName($field);
//One to many logic
//If one to many, the erase the existent collection and generate a recursive cloning of the
//reflection object. Start from here to resolve it ( in debug check owner field )
$sons = $father->$getAccessorFatherToSon();
$father->$setAccessorFatherToSon(new ArrayCollection());
if (!is_null($sons) && !in_array($sonClassName, $exclusionMap)) {
foreach ($sons as $son) {
//cloning the son ( do recursion )
$sonCloned = $this->exploreMap(
$map,
$this->applyReflection($son, $sonClassName),
$alreadyProcessed,
$exclusionMap
);
//prevent navigability
if ($this->hasAssociation($sonClassName, $inverseField)) {
//set "new/cloned" father ref
$sonCloned->$setAccessorSonToFather($father);
}
//prevent navigability
if ($this->hasAssociation($fatherClassName, $field)) {
//add the son
$father = $father->$addAccessorFatherToSon($sonCloned);
}
}
}
return $father;
}
/**
* @param Object $father
* @param string $fatherClassName
* @param string $field
* @param string $sonClassName
* @param array $exclusionMap
*
* @return Object
*/
private function manyToManyStrategy($father, $fatherClassName, $field, $sonClassName, $exclusionMap)
{
$getAccessorFatherToSon = $this->getAccessorFromPropertyName($field, $father);
$setAccessorFatherToSon = $this->setAccessorFromPropertyName($field);
$addAccessorFatherToSon = $this->addAccessorFromPropertyName($field);
//many to many logic
//if is a many to many, then duplicate the data only in the related table
//be care about the PersistentCollection
$sons = $father->$getAccessorFatherToSon();
$associationMapping = $this->getAssociationMapping($fatherClassName, $field);
$sonsCollection = new PersistentCollection(
$this->em,
$this->metadataFactory->getMetadataFor($sonClassName),
new ArrayCollection()
);
if (!is_array($sons) && !($sons instanceof Collection)) {
$sons = new ArrayCollection();
}
$sonsCollection->setOwner($father, $associationMapping);
foreach ($sons as $son) {
$sonsCollection->hydrateAdd($son);
}
if (count($sons) > 0 && !in_array($sonClassName, $exclusionMap)) {
$father->$setAccessorFatherToSon(new ArrayCollection());
foreach ($sonsCollection->getValues() as $newSon) {
$father->$addAccessorFatherToSon($newSon);
}
} else {
$father->$setAccessorFatherToSon(new ArrayCollection());
}
return $father;
}
/**
* @param Object $father
* @param string $fatherClassName
* @param string $field
* @param string $sonClassName
* @param string $inverseField
* @param array $map
* @param array $alreadyProcessed
* @param array $exclusionMap
* @param null $callerClassName
*
* @return mixed
*/
private function oneToOneStrategy(
$father,
$fatherClassName,
$field,
$sonClassName,
$inverseField,
$map,
&$alreadyProcessed,
$exclusionMap,
$callerClassName = null
) {
$setAccessorSonToFather = $this->setAccessorFromPropertyName($inverseField);
$getAccessorFatherToSon = $this->getAccessorFromPropertyName($field, $father);
$setAccessorFatherToSon = $this->setAccessorFromPropertyName($field);
//One to One logic
$son = $father->$getAccessorFatherToSon(); //is just one son
if (!is_null($son) && $sonClassName != $callerClassName && !in_array($sonClassName, $exclusionMap)) {
//cloning the son ( do recursion )
$sonCloned = $this->exploreMap(
$map,
$this->applyReflection($son, $sonClassName),
$alreadyProcessed,
$exclusionMap,
$fatherClassName
);
//set "new/cloned" father ref
if ($this->hasAssociation($sonClassName, $inverseField)) {
$sonCloned->$setAccessorSonToFather($father);
}
if ($this->hasAssociation($fatherClassName, $field)) {
$father = $father->$setAccessorFatherToSon($sonCloned);
}
} else {
$father = $father->$setAccessorFatherToSon(null);
}
return $father;
}
private function getAccessorFromPropertyName($property, $father)
{
$suffix = ucwords(Inflector::camelize($property));
if (method_exists($father, 'is' . $suffix)) {
return 'is' . $suffix;
}
return 'get' . $suffix;
}
private function setAccessorFromPropertyName($property)
{
return 'set' . ucwords(Inflector::camelize($property));
}
private function addAccessorFromPropertyName($property)
{
$ends = substr($property, strlen($property) - 3, 3);
if ($ends == 'ies') {
$property = substr($property, 0, strlen($property) - 3) . 'y';
return 'add' . ucwords(Inflector::camelize($property));
}
return 'add' . ucwords(substr(Inflector::camelize($property), 0, strlen($property) - 1));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment