Subversion-Projekte lars-tiefland.laravel_shop

Revision

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