Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

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