Completed
Push — master ( f6b031...94e5ca )
by Matthieu
02:33
created

DeepCopy::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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