Test Failed
Push — master ( 72a6c5...9329b7 )
by Arun
03:52
created

DeepCopy::addFilter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
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\Spl\SplDoublyLinkedList;
9
use DeepCopy\TypeFilter\TypeFilter;
10
use DeepCopy\TypeMatcher\TypeMatcher;
11
use ReflectionProperty;
12
use DeepCopy\Reflection\ReflectionHelper;
13
14
/**
15
 * DeepCopy
16
 */
17
class DeepCopy
18
{
19
    /**
20
     * @var array
21
     */
22
    private $hashMap = [];
23
24
    /**
25
     * Filters to apply.
26
     * @var array
27
     */
28
    private $filters = [];
29
30
    /**
31
     * Type Filters to apply.
32
     * @var array
33
     */
34
    private $typeFilters = [];
35
36
    private $skipUncloneable = false;
37
38
    /**
39
     * @var bool
40
     */
41
    private $useCloneMethod;
42
43
    /**
44
     * @param bool $useCloneMethod   If set to true, when an object implements the __clone() function, it will be used
45
     *                               instead of the regular deep cloning.
46
     */
47
    public function __construct($useCloneMethod = false)
48
    {
49
        $this->useCloneMethod = $useCloneMethod;
50
51
        $this->addTypeFilter(new SplDoublyLinkedList($this), new TypeMatcher('\SplDoublyLinkedList'));
52
    }
53
54
    /**
55
     * Cloning uncloneable properties won't throw exception.
56
     * @param $skipUncloneable
57
     * @return $this
58
     */
59
    public function skipUncloneable($skipUncloneable = true)
60
    {
61
        $this->skipUncloneable = $skipUncloneable;
62
        return $this;
63
    }
64
65
    /**
66
     * Perform a deep copy of the object.
67
     * @param mixed $object
68
     * @return mixed
69
     */
70
    public function copy($object)
71
    {
72
        $this->hashMap = [];
73
74
        return $this->recursiveCopy($object);
75
    }
76
77
    public function addFilter(Filter $filter, Matcher $matcher)
78
    {
79
        $this->filters[] = [
80
            'matcher' => $matcher,
81
            'filter'  => $filter,
82
        ];
83
    }
84
85
    public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher)
86
    {
87
        $this->typeFilters[] = [
88
            'matcher' => $matcher,
89
            'filter'  => $filter,
90
        ];
91
    }
92
93
94
    private function recursiveCopy($var)
95
    {
96
        // Matches Type Filter
97
        if ($filter = $this->getFirstMatchedTypeFilter($this->typeFilters, $var)) {
98
            return $filter->apply($var);
99
        }
100
101
        // Resource
102
        if (is_resource($var)) {
103
            return $var;
104
        }
105
        // Array
106
        if (is_array($var)) {
107
            return $this->copyArray($var);
108
        }
109
        // Scalar
110
        if (! is_object($var)) {
111
            return $var;
112
        }
113
        // Object
114
        return $this->copyObject($var);
115
    }
116
117
    /**
118
     * Copy an array
119
     * @param array $array
120
     * @return array
121
     */
122
    private function copyArray(array $array)
123
    {
124
        foreach ($array as $key => $value) {
125
            $array[$key] = $this->recursiveCopy($value);
126
        }
127
128
        return $array;
129
    }
130
131
    /**
132
     * Copy an object
133
     * @param object $object
134
     * @return object
135
     */
136
    private function copyObject($object)
137
    {
138
        $objectHash = spl_object_hash($object);
139
140
        if (isset($this->hashMap[$objectHash])) {
141
            return $this->hashMap[$objectHash];
142
        }
143
144
        $reflectedObject = new \ReflectionObject($object);
145
146
        if (false === $isCloneable = $reflectedObject->isCloneable() and $this->skipUncloneable) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
147
            $this->hashMap[$objectHash] = $object;
148
            return $object;
149
        }
150
151
        if (false === $isCloneable) {
152
            throw new CloneException(sprintf(
153
                'Class "%s" is not cloneable.',
154
                $reflectedObject->getName()
0 ignored issues
show
Bug introduced by
Consider using $reflectedObject->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
155
            ));
156
        }
157
158
        $newObject = clone $object;
159
        $this->hashMap[$objectHash] = $newObject;
160
        if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) {
161
            return $object;
162
        }
163
164
        if ($newObject instanceof \DateTimeInterface) {
165
            return $newObject;
166
        }
167
        foreach (ReflectionHelper::getProperties($reflectedObject) as $property) {
168
            $this->copyObjectProperty($newObject, $property);
169
        }
170
171
        return $newObject;
172
    }
173
174
    private function copyObjectProperty($object, ReflectionProperty $property)
175
    {
176
        // Ignore static properties
177
        if ($property->isStatic()) {
178
            return;
179
        }
180
181
        // Apply the filters
182
        foreach ($this->filters as $item) {
183
            /** @var Matcher $matcher */
184
            $matcher = $item['matcher'];
185
            /** @var Filter $filter */
186
            $filter = $item['filter'];
187
188
            if ($matcher->matches($object, $property->getName())) {
189
                $filter->apply(
190
                    $object,
191
                    $property->getName(),
192
                    function ($object) {
193
                        return $this->recursiveCopy($object);
194
                    }
195
                );
196
                // If a filter matches, we stop processing this property
197
                return;
198
            }
199
        }
200
201
        $property->setAccessible(true);
202
        $propertyValue = $property->getValue($object);
203
204
        // Copy the property
205
        $property->setValue($object, $this->recursiveCopy($propertyValue));
206
    }
207
208
    /**
209
     * Returns first filter that matches variable, NULL if no such filter found.
210
     * @param array $filterRecords Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and
211
     *                             'matcher' with value of type {@see TypeMatcher}
212
     * @param mixed $var
213
     * @return TypeFilter|null
214
     */
215
    private function getFirstMatchedTypeFilter(array $filterRecords, $var)
216
    {
217
        $matched = $this->first(
218
            $filterRecords,
219
            function (array $record) use ($var) {
220
                /* @var TypeMatcher $matcher */
221
                $matcher = $record['matcher'];
222
223
                return $matcher->matches($var);
224
            }
225
        );
226
227
        return isset($matched) ? $matched['filter'] : null;
228
    }
229
230
    /**
231
     * Returns first element that matches predicate, NULL if no such element found.
232
     * @param array    $elements
233
     * @param callable $predicate Predicate arguments are: element.
234
     * @return mixed|null
235
     */
236
    private function first(array $elements, callable $predicate)
237
    {
238
        foreach ($elements as $element) {
239
            if (call_user_func($predicate, $element)) {
240
                return $element;
241
            }
242
        }
243
244
        return null;
245
    }
246
}
247