xref: /plugin/dw2pdf/vendor/myclabs/deep-copy/src/DeepCopy/DeepCopy.php (revision dc4d9dc689082c963d5c1d9ee679553326788c6e)
1*dc4d9dc6SAnna Dabrowska<?php
2*dc4d9dc6SAnna Dabrowska
3*dc4d9dc6SAnna Dabrowskanamespace DeepCopy;
4*dc4d9dc6SAnna Dabrowska
5*dc4d9dc6SAnna Dabrowskause ArrayObject;
6*dc4d9dc6SAnna Dabrowskause DateInterval;
7*dc4d9dc6SAnna Dabrowskause DateTimeInterface;
8*dc4d9dc6SAnna Dabrowskause DateTimeZone;
9*dc4d9dc6SAnna Dabrowskause DeepCopy\Exception\CloneException;
10*dc4d9dc6SAnna Dabrowskause DeepCopy\Filter\Filter;
11*dc4d9dc6SAnna Dabrowskause DeepCopy\Matcher\Matcher;
12*dc4d9dc6SAnna Dabrowskause DeepCopy\Reflection\ReflectionHelper;
13*dc4d9dc6SAnna Dabrowskause DeepCopy\TypeFilter\Date\DateIntervalFilter;
14*dc4d9dc6SAnna Dabrowskause DeepCopy\TypeFilter\Spl\ArrayObjectFilter;
15*dc4d9dc6SAnna Dabrowskause DeepCopy\TypeFilter\Spl\SplDoublyLinkedListFilter;
16*dc4d9dc6SAnna Dabrowskause DeepCopy\TypeFilter\TypeFilter;
17*dc4d9dc6SAnna Dabrowskause DeepCopy\TypeMatcher\TypeMatcher;
18*dc4d9dc6SAnna Dabrowskause ReflectionObject;
19*dc4d9dc6SAnna Dabrowskause ReflectionProperty;
20*dc4d9dc6SAnna Dabrowskause SplDoublyLinkedList;
21*dc4d9dc6SAnna Dabrowska
22*dc4d9dc6SAnna Dabrowska/**
23*dc4d9dc6SAnna Dabrowska * @final
24*dc4d9dc6SAnna Dabrowska */
25*dc4d9dc6SAnna Dabrowskaclass DeepCopy
26*dc4d9dc6SAnna Dabrowska{
27*dc4d9dc6SAnna Dabrowska    /**
28*dc4d9dc6SAnna Dabrowska     * @var object[] List of objects copied.
29*dc4d9dc6SAnna Dabrowska     */
30*dc4d9dc6SAnna Dabrowska    private $hashMap = [];
31*dc4d9dc6SAnna Dabrowska
32*dc4d9dc6SAnna Dabrowska    /**
33*dc4d9dc6SAnna Dabrowska     * Filters to apply.
34*dc4d9dc6SAnna Dabrowska     *
35*dc4d9dc6SAnna Dabrowska     * @var array Array of ['filter' => Filter, 'matcher' => Matcher] pairs.
36*dc4d9dc6SAnna Dabrowska     */
37*dc4d9dc6SAnna Dabrowska    private $filters = [];
38*dc4d9dc6SAnna Dabrowska
39*dc4d9dc6SAnna Dabrowska    /**
40*dc4d9dc6SAnna Dabrowska     * Type Filters to apply.
41*dc4d9dc6SAnna Dabrowska     *
42*dc4d9dc6SAnna Dabrowska     * @var array Array of ['filter' => Filter, 'matcher' => Matcher] pairs.
43*dc4d9dc6SAnna Dabrowska     */
44*dc4d9dc6SAnna Dabrowska    private $typeFilters = [];
45*dc4d9dc6SAnna Dabrowska
46*dc4d9dc6SAnna Dabrowska    /**
47*dc4d9dc6SAnna Dabrowska     * @var bool
48*dc4d9dc6SAnna Dabrowska     */
49*dc4d9dc6SAnna Dabrowska    private $skipUncloneable = false;
50*dc4d9dc6SAnna Dabrowska
51*dc4d9dc6SAnna Dabrowska    /**
52*dc4d9dc6SAnna Dabrowska     * @var bool
53*dc4d9dc6SAnna Dabrowska     */
54*dc4d9dc6SAnna Dabrowska    private $useCloneMethod;
55*dc4d9dc6SAnna Dabrowska
56*dc4d9dc6SAnna Dabrowska    /**
57*dc4d9dc6SAnna Dabrowska     * @param bool $useCloneMethod   If set to true, when an object implements the __clone() function, it will be used
58*dc4d9dc6SAnna Dabrowska     *                               instead of the regular deep cloning.
59*dc4d9dc6SAnna Dabrowska     */
60*dc4d9dc6SAnna Dabrowska    public function __construct($useCloneMethod = false)
61*dc4d9dc6SAnna Dabrowska    {
62*dc4d9dc6SAnna Dabrowska        $this->useCloneMethod = $useCloneMethod;
63*dc4d9dc6SAnna Dabrowska
64*dc4d9dc6SAnna Dabrowska        $this->addTypeFilter(new ArrayObjectFilter($this), new TypeMatcher(ArrayObject::class));
65*dc4d9dc6SAnna Dabrowska        $this->addTypeFilter(new DateIntervalFilter(), new TypeMatcher(DateInterval::class));
66*dc4d9dc6SAnna Dabrowska        $this->addTypeFilter(new SplDoublyLinkedListFilter($this), new TypeMatcher(SplDoublyLinkedList::class));
67*dc4d9dc6SAnna Dabrowska    }
68*dc4d9dc6SAnna Dabrowska
69*dc4d9dc6SAnna Dabrowska    /**
70*dc4d9dc6SAnna Dabrowska     * If enabled, will not throw an exception when coming across an uncloneable property.
71*dc4d9dc6SAnna Dabrowska     *
72*dc4d9dc6SAnna Dabrowska     * @param $skipUncloneable
73*dc4d9dc6SAnna Dabrowska     *
74*dc4d9dc6SAnna Dabrowska     * @return $this
75*dc4d9dc6SAnna Dabrowska     */
76*dc4d9dc6SAnna Dabrowska    public function skipUncloneable($skipUncloneable = true)
77*dc4d9dc6SAnna Dabrowska    {
78*dc4d9dc6SAnna Dabrowska        $this->skipUncloneable = $skipUncloneable;
79*dc4d9dc6SAnna Dabrowska
80*dc4d9dc6SAnna Dabrowska        return $this;
81*dc4d9dc6SAnna Dabrowska    }
82*dc4d9dc6SAnna Dabrowska
83*dc4d9dc6SAnna Dabrowska    /**
84*dc4d9dc6SAnna Dabrowska     * Deep copies the given object.
85*dc4d9dc6SAnna Dabrowska     *
86*dc4d9dc6SAnna Dabrowska     * @param mixed $object
87*dc4d9dc6SAnna Dabrowska     *
88*dc4d9dc6SAnna Dabrowska     * @return mixed
89*dc4d9dc6SAnna Dabrowska     */
90*dc4d9dc6SAnna Dabrowska    public function copy($object)
91*dc4d9dc6SAnna Dabrowska    {
92*dc4d9dc6SAnna Dabrowska        $this->hashMap = [];
93*dc4d9dc6SAnna Dabrowska
94*dc4d9dc6SAnna Dabrowska        return $this->recursiveCopy($object);
95*dc4d9dc6SAnna Dabrowska    }
96*dc4d9dc6SAnna Dabrowska
97*dc4d9dc6SAnna Dabrowska    public function addFilter(Filter $filter, Matcher $matcher)
98*dc4d9dc6SAnna Dabrowska    {
99*dc4d9dc6SAnna Dabrowska        $this->filters[] = [
100*dc4d9dc6SAnna Dabrowska            'matcher' => $matcher,
101*dc4d9dc6SAnna Dabrowska            'filter'  => $filter,
102*dc4d9dc6SAnna Dabrowska        ];
103*dc4d9dc6SAnna Dabrowska    }
104*dc4d9dc6SAnna Dabrowska
105*dc4d9dc6SAnna Dabrowska    public function prependFilter(Filter $filter, Matcher $matcher)
106*dc4d9dc6SAnna Dabrowska    {
107*dc4d9dc6SAnna Dabrowska        array_unshift($this->filters, [
108*dc4d9dc6SAnna Dabrowska            'matcher' => $matcher,
109*dc4d9dc6SAnna Dabrowska            'filter'  => $filter,
110*dc4d9dc6SAnna Dabrowska        ]);
111*dc4d9dc6SAnna Dabrowska    }
112*dc4d9dc6SAnna Dabrowska
113*dc4d9dc6SAnna Dabrowska    public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher)
114*dc4d9dc6SAnna Dabrowska    {
115*dc4d9dc6SAnna Dabrowska        $this->typeFilters[] = [
116*dc4d9dc6SAnna Dabrowska            'matcher' => $matcher,
117*dc4d9dc6SAnna Dabrowska            'filter'  => $filter,
118*dc4d9dc6SAnna Dabrowska        ];
119*dc4d9dc6SAnna Dabrowska    }
120*dc4d9dc6SAnna Dabrowska
121*dc4d9dc6SAnna Dabrowska    private function recursiveCopy($var)
122*dc4d9dc6SAnna Dabrowska    {
123*dc4d9dc6SAnna Dabrowska        // Matches Type Filter
124*dc4d9dc6SAnna Dabrowska        if ($filter = $this->getFirstMatchedTypeFilter($this->typeFilters, $var)) {
125*dc4d9dc6SAnna Dabrowska            return $filter->apply($var);
126*dc4d9dc6SAnna Dabrowska        }
127*dc4d9dc6SAnna Dabrowska
128*dc4d9dc6SAnna Dabrowska        // Resource
129*dc4d9dc6SAnna Dabrowska        if (is_resource($var)) {
130*dc4d9dc6SAnna Dabrowska            return $var;
131*dc4d9dc6SAnna Dabrowska        }
132*dc4d9dc6SAnna Dabrowska
133*dc4d9dc6SAnna Dabrowska        // Array
134*dc4d9dc6SAnna Dabrowska        if (is_array($var)) {
135*dc4d9dc6SAnna Dabrowska            return $this->copyArray($var);
136*dc4d9dc6SAnna Dabrowska        }
137*dc4d9dc6SAnna Dabrowska
138*dc4d9dc6SAnna Dabrowska        // Scalar
139*dc4d9dc6SAnna Dabrowska        if (! is_object($var)) {
140*dc4d9dc6SAnna Dabrowska            return $var;
141*dc4d9dc6SAnna Dabrowska        }
142*dc4d9dc6SAnna Dabrowska
143*dc4d9dc6SAnna Dabrowska        // Object
144*dc4d9dc6SAnna Dabrowska        return $this->copyObject($var);
145*dc4d9dc6SAnna Dabrowska    }
146*dc4d9dc6SAnna Dabrowska
147*dc4d9dc6SAnna Dabrowska    /**
148*dc4d9dc6SAnna Dabrowska     * Copy an array
149*dc4d9dc6SAnna Dabrowska     * @param array $array
150*dc4d9dc6SAnna Dabrowska     * @return array
151*dc4d9dc6SAnna Dabrowska     */
152*dc4d9dc6SAnna Dabrowska    private function copyArray(array $array)
153*dc4d9dc6SAnna Dabrowska    {
154*dc4d9dc6SAnna Dabrowska        foreach ($array as $key => $value) {
155*dc4d9dc6SAnna Dabrowska            $array[$key] = $this->recursiveCopy($value);
156*dc4d9dc6SAnna Dabrowska        }
157*dc4d9dc6SAnna Dabrowska
158*dc4d9dc6SAnna Dabrowska        return $array;
159*dc4d9dc6SAnna Dabrowska    }
160*dc4d9dc6SAnna Dabrowska
161*dc4d9dc6SAnna Dabrowska    /**
162*dc4d9dc6SAnna Dabrowska     * Copies an object.
163*dc4d9dc6SAnna Dabrowska     *
164*dc4d9dc6SAnna Dabrowska     * @param object $object
165*dc4d9dc6SAnna Dabrowska     *
166*dc4d9dc6SAnna Dabrowska     * @throws CloneException
167*dc4d9dc6SAnna Dabrowska     *
168*dc4d9dc6SAnna Dabrowska     * @return object
169*dc4d9dc6SAnna Dabrowska     */
170*dc4d9dc6SAnna Dabrowska    private function copyObject($object)
171*dc4d9dc6SAnna Dabrowska    {
172*dc4d9dc6SAnna Dabrowska        $objectHash = spl_object_hash($object);
173*dc4d9dc6SAnna Dabrowska
174*dc4d9dc6SAnna Dabrowska        if (isset($this->hashMap[$objectHash])) {
175*dc4d9dc6SAnna Dabrowska            return $this->hashMap[$objectHash];
176*dc4d9dc6SAnna Dabrowska        }
177*dc4d9dc6SAnna Dabrowska
178*dc4d9dc6SAnna Dabrowska        $reflectedObject = new ReflectionObject($object);
179*dc4d9dc6SAnna Dabrowska        $isCloneable = $reflectedObject->isCloneable();
180*dc4d9dc6SAnna Dabrowska
181*dc4d9dc6SAnna Dabrowska        if (false === $isCloneable) {
182*dc4d9dc6SAnna Dabrowska            if ($this->skipUncloneable) {
183*dc4d9dc6SAnna Dabrowska                $this->hashMap[$objectHash] = $object;
184*dc4d9dc6SAnna Dabrowska
185*dc4d9dc6SAnna Dabrowska                return $object;
186*dc4d9dc6SAnna Dabrowska            }
187*dc4d9dc6SAnna Dabrowska
188*dc4d9dc6SAnna Dabrowska            throw new CloneException(
189*dc4d9dc6SAnna Dabrowska                sprintf(
190*dc4d9dc6SAnna Dabrowska                    'The class "%s" is not cloneable.',
191*dc4d9dc6SAnna Dabrowska                    $reflectedObject->getName()
192*dc4d9dc6SAnna Dabrowska                )
193*dc4d9dc6SAnna Dabrowska            );
194*dc4d9dc6SAnna Dabrowska        }
195*dc4d9dc6SAnna Dabrowska
196*dc4d9dc6SAnna Dabrowska        $newObject = clone $object;
197*dc4d9dc6SAnna Dabrowska        $this->hashMap[$objectHash] = $newObject;
198*dc4d9dc6SAnna Dabrowska
199*dc4d9dc6SAnna Dabrowska        if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) {
200*dc4d9dc6SAnna Dabrowska            return $newObject;
201*dc4d9dc6SAnna Dabrowska        }
202*dc4d9dc6SAnna Dabrowska
203*dc4d9dc6SAnna Dabrowska        if ($newObject instanceof DateTimeInterface || $newObject instanceof DateTimeZone) {
204*dc4d9dc6SAnna Dabrowska            return $newObject;
205*dc4d9dc6SAnna Dabrowska        }
206*dc4d9dc6SAnna Dabrowska
207*dc4d9dc6SAnna Dabrowska        foreach (ReflectionHelper::getProperties($reflectedObject) as $property) {
208*dc4d9dc6SAnna Dabrowska            $this->copyObjectProperty($newObject, $property);
209*dc4d9dc6SAnna Dabrowska        }
210*dc4d9dc6SAnna Dabrowska
211*dc4d9dc6SAnna Dabrowska        return $newObject;
212*dc4d9dc6SAnna Dabrowska    }
213*dc4d9dc6SAnna Dabrowska
214*dc4d9dc6SAnna Dabrowska    private function copyObjectProperty($object, ReflectionProperty $property)
215*dc4d9dc6SAnna Dabrowska    {
216*dc4d9dc6SAnna Dabrowska        // Ignore static properties
217*dc4d9dc6SAnna Dabrowska        if ($property->isStatic()) {
218*dc4d9dc6SAnna Dabrowska            return;
219*dc4d9dc6SAnna Dabrowska        }
220*dc4d9dc6SAnna Dabrowska
221*dc4d9dc6SAnna Dabrowska        // Apply the filters
222*dc4d9dc6SAnna Dabrowska        foreach ($this->filters as $item) {
223*dc4d9dc6SAnna Dabrowska            /** @var Matcher $matcher */
224*dc4d9dc6SAnna Dabrowska            $matcher = $item['matcher'];
225*dc4d9dc6SAnna Dabrowska            /** @var Filter $filter */
226*dc4d9dc6SAnna Dabrowska            $filter = $item['filter'];
227*dc4d9dc6SAnna Dabrowska
228*dc4d9dc6SAnna Dabrowska            if ($matcher->matches($object, $property->getName())) {
229*dc4d9dc6SAnna Dabrowska                $filter->apply(
230*dc4d9dc6SAnna Dabrowska                    $object,
231*dc4d9dc6SAnna Dabrowska                    $property->getName(),
232*dc4d9dc6SAnna Dabrowska                    function ($object) {
233*dc4d9dc6SAnna Dabrowska                        return $this->recursiveCopy($object);
234*dc4d9dc6SAnna Dabrowska                    }
235*dc4d9dc6SAnna Dabrowska                );
236*dc4d9dc6SAnna Dabrowska
237*dc4d9dc6SAnna Dabrowska                // If a filter matches, we stop processing this property
238*dc4d9dc6SAnna Dabrowska                return;
239*dc4d9dc6SAnna Dabrowska            }
240*dc4d9dc6SAnna Dabrowska        }
241*dc4d9dc6SAnna Dabrowska
242*dc4d9dc6SAnna Dabrowska        $property->setAccessible(true);
243*dc4d9dc6SAnna Dabrowska
244*dc4d9dc6SAnna Dabrowska        // Ignore uninitialized properties (for PHP >7.4)
245*dc4d9dc6SAnna Dabrowska        if (method_exists($property, 'isInitialized') && !$property->isInitialized($object)) {
246*dc4d9dc6SAnna Dabrowska            return;
247*dc4d9dc6SAnna Dabrowska        }
248*dc4d9dc6SAnna Dabrowska
249*dc4d9dc6SAnna Dabrowska        $propertyValue = $property->getValue($object);
250*dc4d9dc6SAnna Dabrowska
251*dc4d9dc6SAnna Dabrowska        // Copy the property
252*dc4d9dc6SAnna Dabrowska        $property->setValue($object, $this->recursiveCopy($propertyValue));
253*dc4d9dc6SAnna Dabrowska    }
254*dc4d9dc6SAnna Dabrowska
255*dc4d9dc6SAnna Dabrowska    /**
256*dc4d9dc6SAnna Dabrowska     * Returns first filter that matches variable, `null` if no such filter found.
257*dc4d9dc6SAnna Dabrowska     *
258*dc4d9dc6SAnna Dabrowska     * @param array $filterRecords Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and
259*dc4d9dc6SAnna Dabrowska     *                             'matcher' with value of type {@see TypeMatcher}
260*dc4d9dc6SAnna Dabrowska     * @param mixed $var
261*dc4d9dc6SAnna Dabrowska     *
262*dc4d9dc6SAnna Dabrowska     * @return TypeFilter|null
263*dc4d9dc6SAnna Dabrowska     */
264*dc4d9dc6SAnna Dabrowska    private function getFirstMatchedTypeFilter(array $filterRecords, $var)
265*dc4d9dc6SAnna Dabrowska    {
266*dc4d9dc6SAnna Dabrowska        $matched = $this->first(
267*dc4d9dc6SAnna Dabrowska            $filterRecords,
268*dc4d9dc6SAnna Dabrowska            function (array $record) use ($var) {
269*dc4d9dc6SAnna Dabrowska                /* @var TypeMatcher $matcher */
270*dc4d9dc6SAnna Dabrowska                $matcher = $record['matcher'];
271*dc4d9dc6SAnna Dabrowska
272*dc4d9dc6SAnna Dabrowska                return $matcher->matches($var);
273*dc4d9dc6SAnna Dabrowska            }
274*dc4d9dc6SAnna Dabrowska        );
275*dc4d9dc6SAnna Dabrowska
276*dc4d9dc6SAnna Dabrowska        return isset($matched) ? $matched['filter'] : null;
277*dc4d9dc6SAnna Dabrowska    }
278*dc4d9dc6SAnna Dabrowska
279*dc4d9dc6SAnna Dabrowska    /**
280*dc4d9dc6SAnna Dabrowska     * Returns first element that matches predicate, `null` if no such element found.
281*dc4d9dc6SAnna Dabrowska     *
282*dc4d9dc6SAnna Dabrowska     * @param array    $elements Array of ['filter' => Filter, 'matcher' => Matcher] pairs.
283*dc4d9dc6SAnna Dabrowska     * @param callable $predicate Predicate arguments are: element.
284*dc4d9dc6SAnna Dabrowska     *
285*dc4d9dc6SAnna Dabrowska     * @return array|null Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and 'matcher'
286*dc4d9dc6SAnna Dabrowska     *                    with value of type {@see TypeMatcher} or `null`.
287*dc4d9dc6SAnna Dabrowska     */
288*dc4d9dc6SAnna Dabrowska    private function first(array $elements, callable $predicate)
289*dc4d9dc6SAnna Dabrowska    {
290*dc4d9dc6SAnna Dabrowska        foreach ($elements as $element) {
291*dc4d9dc6SAnna Dabrowska            if (call_user_func($predicate, $element)) {
292*dc4d9dc6SAnna Dabrowska                return $element;
293*dc4d9dc6SAnna Dabrowska            }
294*dc4d9dc6SAnna Dabrowska        }
295*dc4d9dc6SAnna Dabrowska
296*dc4d9dc6SAnna Dabrowska        return null;
297*dc4d9dc6SAnna Dabrowska    }
298*dc4d9dc6SAnna Dabrowska}
299