Last active
February 15, 2016 09:07
-
-
Save chvonrohr/f417e7e76b0e0dfb94d4 to your computer and use it in GitHub Desktop.
TYPO3 Flow CopyService for Cloning Models and its relations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Foo\Bar\Annotations; | |
/** | |
* @Annotation | |
* @Target("PROPERTY") | |
*/ | |
final class Copy { | |
/** | |
* type of copy {empty, 'reference'} | |
* @var string | |
*/ | |
public $type; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Foo\Bar\Service; | |
use TYPO3\Flow\Annotations as Flow; | |
/** | |
* @Flow\Scope("singleton") | |
*/ | |
class CopyService { | |
/** | |
* @var \Foo\Cockpit\Service\RecursionService | |
* @Flow\Inject | |
*/ | |
public $recursionService; | |
/** | |
* @var \TYPO3\Flow\Reflection\ReflectionService | |
* @Flow\Inject | |
*/ | |
protected $reflectionService; | |
/** | |
* @var \TYPO3\Flow\Object\ObjectManagerInterface | |
* @Flow\Inject | |
*/ | |
protected $objectManager; | |
/** | |
* Copy a single object based on field annotations about how to copy the object | |
* | |
* @return $copy | |
*/ | |
public function copy($object) { | |
$className = get_class($object); | |
$this->recursionService->in(); | |
$this->recursionService->check($className); | |
$copy = $this->objectManager->get($className); | |
$properties = $this->reflectionService->getClassPropertyNames($className); | |
foreach ($properties as $propertyName) { | |
$propertyAnnotation = $this->reflectionService->getPropertyAnnotation($className, $propertyName, 'Frontal\Cockpit\Annotations\Copy'); | |
if (!$propertyAnnotation) { // ignore if property has no "copy" annotation | |
continue; | |
} | |
$copyMethod = $propertyAnnotation->type; | |
$getter = 'get' . ucfirst($propertyName); | |
$setter = 'set' . ucfirst($propertyName); | |
$originalValue = $object->$getter(); | |
// copy as reference | |
if ($copyMethod == 'reference') { | |
$copiedValue = $this->copyAsReference($originalValue); | |
// clone value itself (if its a model, it will copied again by annotations) | |
} else { | |
$copiedValue = $this->copyAsClone($originalValue); | |
} | |
if ($copiedValue != NULL) { | |
$copy->$setter($copiedValue); | |
} | |
} | |
$this->recursionService->out(); | |
return $copy; | |
} | |
protected function copyAsReference($value) { | |
// collection | |
if ($value instanceof \Doctrine\ORM\PersistentCollection) { | |
$newStorage = new \Doctrine\Common\Collections\ArrayCollection(); | |
foreach ($value as $item) { | |
$newStorage->attach($item); | |
} | |
return $newStorage; | |
// model | |
} else if ($value instanceof Doctrine\ORM\ProxyInterface) { | |
return $value; | |
// other object | |
} else if (is_object($value)) { | |
// fallback case for class copying - value objects and such | |
return $value; | |
} else { | |
// this case is very unlikely: means someone wished to copy hard type as a reference - so return a copy instead | |
return $value; | |
} | |
} | |
protected function copyAsClone($value) { | |
// collection | |
if ($value instanceof \Doctrine\ORM\PersistentCollection) { | |
$newStorage = new \Doctrine\Common\Collections\ArrayCollection(); | |
foreach ($value as $item) { | |
$newItem = $this->copy($item); | |
$newStorage->add($newItem); | |
} | |
return $newStorage; | |
// model | |
} else if ($value instanceof \Doctrine\ORM\ProxyInterface) { | |
return $this->copy($value); | |
// other object | |
} else if (is_object($value)) { | |
// fallback case for class copying - value objects and such | |
return clone $value; | |
} else { | |
// value is probably a string | |
return $value; | |
} | |
} | |
} | |
?> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Foo\Bar\Domain\Model; | |
use TYPO3\Flow\Annotations as Flow; | |
use Foo\Bar\Annotations as CP; | |
/** | |
* @Flow\Entity | |
*/ | |
class Example { | |
/** | |
* @var string | |
* @CP\Copy | |
* => copy content | |
*/ | |
protected $title; | |
/** | |
* @var \Foo\Bar\Domain\Model\ExampleCategory | |
* @CP\Copy(type="reference") | |
* => keep reference to relation in copy | |
*/ | |
protected $category; | |
/** | |
* @var \Foo\Bar\Domain\Model\ExampleChild | |
* @ORM\OneOne(cascade={"persist", "remove"}) | |
* @CP\Copy | |
* => copy related object | |
*/ | |
protected $child; | |
/** | |
* @var \Doctrine\Common\Collections\ArrayCollection<\Foo\Bar\Domain\Model\ExampleChild> | |
* @ORM\OneToMany(mappedBy="project",cascade={"persist", "remove"}) | |
* @CP\Copy | |
* => copy related objects | |
*/ | |
protected $childs; | |
/** | |
* Important: Set cascade=persist | |
* @var \Doctrine\Common\Collections\ArrayCollection<\Foo\Bar\Domain\Model\MmChilds> | |
* @ORM\ManyToMany(mappedBy="example",cascade={"persist", "remove"}) | |
* @CP\Copy(type="reference") | |
* => copy reference to existing objects | |
*/ | |
protected $mmChilds; | |
// ... | |
} | |
?> | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Foo\Bar\Domain\Repository; | |
use TYPO3\Flow\Annotations as Flow; | |
use TYPO3\Flow\Persistence\Repository; | |
/** | |
* @Flow\Scope("singleton") | |
*/ | |
class ExampleRepository extends Repository { | |
/** | |
* @var \TYPO3\Flow\Object\ObjectManagerInterface | |
* @Flow\Inject | |
*/ | |
protected $objectManager; | |
/** | |
* clone existing example | |
* | |
* @param \Foo\Bar\Domain\Model\Example $example | |
* @return \Foo\Bar\Domain\Model\Example cloned example | |
*/ | |
public function copy($example) { | |
// clone project | |
$copyService = $this->objectManager->get("Foo\Bar\Service\CopyService"); | |
$clonedExample = $copyService->copy($example); | |
// set title "xxx (copy)" | |
$clonedExample->setTitle( $clonedExample->getTitle() . " (Copy)"); | |
// persist | |
$this->add($clonedExample); | |
$this->persistenceManager->persistAll(); | |
return $clonedExample; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Foo\Bar\Service; | |
use TYPO3\Flow\Annotations as Flow; | |
/** | |
* @Flow\Scope("singleton") | |
*/ | |
class RecursionService { | |
/** | |
* @var string | |
*/ | |
private $_exceptionMessage = 'Recursion problem occurred'; | |
/** | |
* @var int | |
*/ | |
private $_level = 0; | |
/** | |
* @var int | |
*/ | |
private $_maxLevel = 16; | |
/** | |
* @var int | |
*/ | |
private $_maxEncounters = 1; | |
/** | |
* @var array | |
*/ | |
private $_encountered = array(); | |
/** | |
* @var boolean | |
*/ | |
private $_autoReset = FALSE; | |
/** | |
* Set the message used to prepend Exceptions | |
* @param string $msg | |
*/ | |
public function setExceptionMessage($msg) { | |
$this->_exceptionMessage = $msg; | |
} | |
/** | |
* Get the message used to prepend Exceptions | |
* @return string | |
*/ | |
public function getExceptionMessage() { | |
return $this->_exceptionMessage; | |
} | |
/** | |
* Set automatic resetting of encounters and level (TRUE/FALSE) | |
* @param boolean $reset | |
*/ | |
public function setAutoReset($reset) { | |
$this->_autoReset = $reset; | |
} | |
/** | |
* Set the maximum allowed number of times a particular identifier may be encountered before an Exception is thrown | |
* @param unknown_type $max | |
*/ | |
public function setMaxEncounters($max) { | |
$this->_maxEncounters = $max; | |
} | |
/** | |
* Get the maximum allowed number of encounters | |
* @return int | |
*/ | |
public function getMaxEncounters() { | |
return $this->_maxEncounters; | |
} | |
/** | |
* Set the maximum allowed recursion level | |
* @param int $level | |
*/ | |
public function setMaxLevel($level) { | |
$this->_maxLevel = $level; | |
} | |
/** | |
* Get the maximum allowed recursion level | |
* @return int | |
*/ | |
public function getMaxLevel() { | |
return $this->_maxLevel; | |
} | |
/** | |
* Get the current recursion level | |
* @return int | |
*/ | |
public function getLevel() { | |
return $this->_level; | |
} | |
/** | |
* Get the identifier last encountered | |
* @return mixed | |
*/ | |
public function getLastEncounter() { | |
return array_pop($this->_encountered); | |
} | |
/** | |
* Increase recursion level (start of implementer function) | |
*/ | |
public function in() { | |
$this->_level++; | |
} | |
/** | |
* Decrease recursion level (end of implementer function) | |
*/ | |
public function out() { | |
$this->_level--; | |
} | |
/** | |
* Encounter $data (usually a string), call this when new values are read in your recursive function | |
* @param mixed $data | |
*/ | |
public function encounter($data) { | |
array_push($this->_encountered, $data); | |
$this->check(); | |
} | |
/** | |
* Check the current recursion level and encounter status. Call in each iteration of your function | |
* @param string $exitMsg | |
*/ | |
public function check($exitMsg='<no message>') { | |
$level = $this->getLevel(); | |
$maxEnc = $this->getMaxEncounters(); | |
$message = $this->getExceptionMessage(); | |
if ($this->failsOnLevel()) { | |
$msg = "{$message} at level {$level} with message: {$exitMsg}"; | |
throw new Exception($msg); | |
} | |
if ($this->failsOnMaxEncounters()) { | |
$msg = "{$message} at encounter {$maxEnc} of {$maxEnc} allowed with message: {$exitMsg}"; | |
$this->throwException($msg); | |
} | |
return TRUE; | |
} | |
/** | |
* Reset all counters | |
*/ | |
public function reset() { | |
$this->_level = 0; | |
$this->_encountered = array(); | |
} | |
/** | |
* Throw an Exception - wrapper; check for auto-reset and reset if needed | |
* @param string $message | |
* @throws Exception | |
*/ | |
private function throwException($message) { | |
if ($this->_autoReset === TRUE) { | |
$this->reset(); | |
} | |
throw new Exception($message); | |
} | |
/** | |
* Check if the current iteration violates level restraints | |
* @return boolean | |
*/ | |
private function failsOnLevel() { | |
$level = $this->getLevel(); | |
$max = $this->getMaxLevel(); | |
return (bool) ($level >= $max); | |
} | |
/** | |
* Check if the current iteration violates encounter restraints | |
* @return boolean | |
*/ | |
private function failsOnMaxEncounters() { | |
$lastEncounter = $this->getLastEncounter(); | |
$occurrences = $this->countEncounters($lastEncounter); | |
$max = $this->getMaxEncounters(); | |
return (bool) ($occurrences > $max); | |
} | |
/** | |
* Count number of times the identifier $encounter has been encountered | |
* @param mixed $encounter | |
* @return int | |
*/ | |
private function countEncounters($encounter) { | |
$num = 0; | |
foreach ($this->_encountered as $encountered) { | |
if ($encountered === $encounter) { | |
$num++; | |
} | |
} | |
return (int) $num; | |
} | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment