Completed
Push — 1.x ( 7a84e1...770e50 )
by Théo
01:33
created

DeepCopy::copyObject()   D

Complexity

Conditions 9
Paths 7

Size

Total Lines 43
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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