Skip to content

Instantly share code, notes, and snippets.

@chvonrohr
Last active February 15, 2016 09:07
Show Gist options
  • Save chvonrohr/f417e7e76b0e0dfb94d4 to your computer and use it in GitHub Desktop.
Save chvonrohr/f417e7e76b0e0dfb94d4 to your computer and use it in GitHub Desktop.
TYPO3 Flow CopyService for Cloning Models and its relations
<?php
namespace Foo\Bar\Annotations;
/**
* @Annotation
* @Target("PROPERTY")
*/
final class Copy {
/**
* type of copy {empty, 'reference'}
* @var string
*/
public $type;
}
<?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;
}
}
}
?>
<?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;
// ...
}
?>
<?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;
}
}
<?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