247 lines
6.3 KiB
PHP
247 lines
6.3 KiB
PHP
<?php
|
|
|
|
namespace DeepCopy;
|
|
|
|
use DeepCopy\Exception\CloneException;
|
|
use DeepCopy\Filter\Filter;
|
|
use DeepCopy\Matcher\Matcher;
|
|
use DeepCopy\TypeFilter\Spl\SplDoublyLinkedList;
|
|
use DeepCopy\TypeFilter\TypeFilter;
|
|
use DeepCopy\TypeMatcher\TypeMatcher;
|
|
use ReflectionProperty;
|
|
use DeepCopy\Reflection\ReflectionHelper;
|
|
|
|
/**
|
|
* DeepCopy
|
|
*/
|
|
class DeepCopy
|
|
{
|
|
/**
|
|
* @var array
|
|
*/
|
|
private $hashMap = [];
|
|
|
|
/**
|
|
* Filters to apply.
|
|
* @var array
|
|
*/
|
|
private $filters = [];
|
|
|
|
/**
|
|
* Type Filters to apply.
|
|
* @var array
|
|
*/
|
|
private $typeFilters = [];
|
|
|
|
private $skipUncloneable = false;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
private $useCloneMethod;
|
|
|
|
/**
|
|
* @param bool $useCloneMethod If set to true, when an object implements the __clone() function, it will be used
|
|
* instead of the regular deep cloning.
|
|
*/
|
|
public function __construct($useCloneMethod = false)
|
|
{
|
|
$this->useCloneMethod = $useCloneMethod;
|
|
|
|
$this->addTypeFilter(new SplDoublyLinkedList($this), new TypeMatcher('\SplDoublyLinkedList'));
|
|
}
|
|
|
|
/**
|
|
* Cloning uncloneable properties won't throw exception.
|
|
* @param $skipUncloneable
|
|
* @return $this
|
|
*/
|
|
public function skipUncloneable($skipUncloneable = true)
|
|
{
|
|
$this->skipUncloneable = $skipUncloneable;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Perform a deep copy of the object.
|
|
* @param mixed $object
|
|
* @return mixed
|
|
*/
|
|
public function copy($object)
|
|
{
|
|
$this->hashMap = [];
|
|
|
|
return $this->recursiveCopy($object);
|
|
}
|
|
|
|
public function addFilter(Filter $filter, Matcher $matcher)
|
|
{
|
|
$this->filters[] = [
|
|
'matcher' => $matcher,
|
|
'filter' => $filter,
|
|
];
|
|
}
|
|
|
|
public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher)
|
|
{
|
|
$this->typeFilters[] = [
|
|
'matcher' => $matcher,
|
|
'filter' => $filter,
|
|
];
|
|
}
|
|
|
|
|
|
private function recursiveCopy($var)
|
|
{
|
|
// Matches Type Filter
|
|
if ($filter = $this->getFirstMatchedTypeFilter($this->typeFilters, $var)) {
|
|
return $filter->apply($var);
|
|
}
|
|
|
|
// Resource
|
|
if (is_resource($var)) {
|
|
return $var;
|
|
}
|
|
// Array
|
|
if (is_array($var)) {
|
|
return $this->copyArray($var);
|
|
}
|
|
// Scalar
|
|
if (! is_object($var)) {
|
|
return $var;
|
|
}
|
|
// Object
|
|
return $this->copyObject($var);
|
|
}
|
|
|
|
/**
|
|
* Copy an array
|
|
* @param array $array
|
|
* @return array
|
|
*/
|
|
private function copyArray(array $array)
|
|
{
|
|
foreach ($array as $key => $value) {
|
|
$array[$key] = $this->recursiveCopy($value);
|
|
}
|
|
|
|
return $array;
|
|
}
|
|
|
|
/**
|
|
* Copy an object
|
|
* @param object $object
|
|
* @return object
|
|
*/
|
|
private function copyObject($object)
|
|
{
|
|
$objectHash = spl_object_hash($object);
|
|
|
|
if (isset($this->hashMap[$objectHash])) {
|
|
return $this->hashMap[$objectHash];
|
|
}
|
|
|
|
$reflectedObject = new \ReflectionObject($object);
|
|
|
|
if (false === $isCloneable = $reflectedObject->isCloneable() and $this->skipUncloneable) {
|
|
$this->hashMap[$objectHash] = $object;
|
|
return $object;
|
|
}
|
|
|
|
if (false === $isCloneable) {
|
|
throw new CloneException(sprintf(
|
|
'Class "%s" is not cloneable.',
|
|
$reflectedObject->getName()
|
|
));
|
|
}
|
|
|
|
$newObject = clone $object;
|
|
$this->hashMap[$objectHash] = $newObject;
|
|
if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) {
|
|
return $object;
|
|
}
|
|
|
|
if ($newObject instanceof \DateTimeInterface) {
|
|
return $newObject;
|
|
}
|
|
foreach (ReflectionHelper::getProperties($reflectedObject) as $property) {
|
|
$this->copyObjectProperty($newObject, $property);
|
|
}
|
|
|
|
return $newObject;
|
|
}
|
|
|
|
private function copyObjectProperty($object, ReflectionProperty $property)
|
|
{
|
|
// Ignore static properties
|
|
if ($property->isStatic()) {
|
|
return;
|
|
}
|
|
|
|
// Apply the filters
|
|
foreach ($this->filters as $item) {
|
|
/** @var Matcher $matcher */
|
|
$matcher = $item['matcher'];
|
|
/** @var Filter $filter */
|
|
$filter = $item['filter'];
|
|
|
|
if ($matcher->matches($object, $property->getName())) {
|
|
$filter->apply(
|
|
$object,
|
|
$property->getName(),
|
|
function ($object) {
|
|
return $this->recursiveCopy($object);
|
|
}
|
|
);
|
|
// If a filter matches, we stop processing this property
|
|
return;
|
|
}
|
|
}
|
|
|
|
$property->setAccessible(true);
|
|
$propertyValue = $property->getValue($object);
|
|
|
|
// Copy the property
|
|
$property->setValue($object, $this->recursiveCopy($propertyValue));
|
|
}
|
|
|
|
/**
|
|
* Returns first filter that matches variable, NULL if no such filter found.
|
|
* @param array $filterRecords Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and
|
|
* 'matcher' with value of type {@see TypeMatcher}
|
|
* @param mixed $var
|
|
* @return TypeFilter|null
|
|
*/
|
|
private function getFirstMatchedTypeFilter(array $filterRecords, $var)
|
|
{
|
|
$matched = $this->first(
|
|
$filterRecords,
|
|
function (array $record) use ($var) {
|
|
/* @var TypeMatcher $matcher */
|
|
$matcher = $record['matcher'];
|
|
|
|
return $matcher->matches($var);
|
|
}
|
|
);
|
|
|
|
return isset($matched) ? $matched['filter'] : null;
|
|
}
|
|
|
|
/**
|
|
* Returns first element that matches predicate, NULL if no such element found.
|
|
* @param array $elements
|
|
* @param callable $predicate Predicate arguments are: element.
|
|
* @return mixed|null
|
|
*/
|
|
private function first(array $elements, callable $predicate)
|
|
{
|
|
foreach ($elements as $element) {
|
|
if (call_user_func($predicate, $element)) {
|
|
return $element;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|