Completed
Push — master ( d3f7b6...bab90a )
by Vitaliy
03:28
created

mp.php ➔ getValueByRef()   C

Complexity

Conditions 10
Paths 8

Size

Total Lines 48
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 10.0328

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 10
eloc 30
c 3
b 1
f 0
nc 8
nop 4
dl 0
loc 48
ccs 27
cts 29
cp 0.931
crap 10.0328
rs 5.3454

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace mp;
4
5
use Nayjest\StrCaseConverter\Str;
6
use ReflectionClass;
7
8
define('MP_USE_SETTERS', 1);
9
define('MP_CREATE_PROPERTIES', 2);
10
11
/**
12
 * Creates class instance using specified constructor arguments.
13
 *
14
 * @param string $class
15
 * @param array $arguments
16
 * @return object
17
 */
18
function instantiate($class, array $arguments = [])
19
{
20 2
    switch (count($arguments)) {
21 2
        case 0:
22 1
            return new $class();
23 1
        case 1:
24 1
            return new $class(array_shift($arguments));
25 1
        case 2:
26 1
            return new $class(
27 1
                array_shift($arguments),
28 1
                array_shift($arguments)
29 1
            );
30 1
        case 3:
31 1
            return new $class(
32 1
                array_shift($arguments),
33 1
                array_shift($arguments),
34 1
                array_shift($arguments)
35 1
            );
36 1
    }
37 1
    $reflection = new ReflectionClass($class);
38 1
    return $reflection->newInstanceArgs($arguments);
39
}
40
41
/**
42
 * Assigns values from array to existing public properties of target object.
43
 *
44
 * By default this function ignores fields having no corresponding properties in target object,
45
 * but this behavior can be changed if TRUE will be passed to third argument.
46
 *
47
 * @param object $targetObject target object
48
 * @param array $fields fields to assign, keys must be same as target object property names
49
 * @param bool $createProperties (optional, default value: false)
50
 *                               allows to create new properties in target object if value is true
51
 * @return string[] names of successfully assigned properties
52
 */
53
function setPublicProperties($targetObject, array $fields, $createProperties = false)
54
{
55 4
    if ($createProperties) {
56 1
        $overwrite = $fields;
57 1
    } else {
58 3
        $existing = get_object_vars($targetObject);
59 3
        $overwrite = array_intersect_key($fields, $existing);
60
    }
61 4
    foreach ($overwrite as $key => $value) {
62 3
        $targetObject->{$key} = $value;
63 4
    }
64 4
    return array_keys($overwrite);
65
}
66
67
/**
68
 * Assigns values from array to corresponding properties of target object using setters.
69
 *
70
 * This function works similar to mp\setPublicProperties(), but uses setter methods instead of public properties.
71
 *
72
 * Field names may be in snake or camel case,
73
 * it will be converted to camel case and prefixed by 'set'
74
 * to check availability of corresponding setter in target object.
75
 *
76
 * Fields having no corresponding setters in target object will be ignored.
77
 *
78
 * This function does not work with magic setters created using __set() php method.
79
 *
80
 * @param object $targetObject target object
81
 * @param array $fields fields to assign, keys are used to check availability of corresponding setters in target object
82
 * @return string[] names of successfully assigned properties
83
 */
84
function setValuesUsingSetters($targetObject, array $fields)
85
{
86 2
    $assignedProperties = [];
87 2
    foreach ($fields as $key => $value) {
88 2
        $methodName = 'set' . Str::toCamelCase($key);
89 2
        if (method_exists($targetObject, $methodName)) {
90 2
            $targetObject->$methodName($value);
91 2
            $assignedProperties[] = $key;
92 2
        }
93 2
    }
94 2
    return $assignedProperties;
95
}
96
97
/**
98
 * Assigns values from $fields array to $target. Target may be object or array.
99
 *
100
 * By default `mp\setValues` ignores fields having no corresponding properties or setters in target object
101
 * but this behavior can be changed if MP_CREATE_PROPERTIES option is used.
102
 *
103
 * Assigning values using setters can be disabled by removing MP_USE_SETTERS option (it's enabled by default).
104
 *
105
 * When target is an array, `mp\setValues` will call array_merge PHP function.
106
 *
107
 * @param object|array $target target object or array
108
 * @param array $fields fields to assign
109
 * @pram int $options (optional, default value: MP_USE_SETTERS) supported options: MP_USE_SETTERS, MP_CREATE_PROPERTIES
110
 * @return string[] names of successfully assigned properties
111
 */
112
function setValues(&$target, array $fields, $options = MP_USE_SETTERS)
113
{
114 3
    if (is_array($target)) {
115 2
        $target = array_merge($target, $fields);
116 2
        return array_keys($fields);
117
    }
118 1
    if ($options & MP_USE_SETTERS) {
119 1
        $assignedBySetters = setValuesUsingSetters(
120 1
            $target,
121
            $fields
122 1
        );
123 1
    } else {
124
        $assignedBySetters = [];
125
    }
126 1
    $assignedProperties = setPublicProperties(
127 1
        $target,
128 1
        array_diff_key($fields, array_flip($assignedBySetters)),
129
        $options & MP_CREATE_PROPERTIES
130 1
    );
131
132 1
    return array_merge($assignedProperties, $assignedBySetters);
133
}
134
135
/**
136
 * Returns names of writable properties for objects and classes or existing keys for arrays.
137
 *
138
 * Only public object properties and properties having setters considered writable.
139
 *
140
 * For setters, this function will return property names based on setter names
141
 * (setter names are converted to snake case, 'set' prefixes are removed).
142
 *
143
 * Detecting properties by setters can be disabled by specifying second argument as FALSE.
144
 *
145
 * @param object|string|array $target object or class name or array
146
 * @param bool $useSetters (optional, default value: true) if true, properties having setters will be added to results
147
 * @return string[] names of writable properties
148
 */
149
function getWritable($target, $useSetters = true)
150
{
151 1
    static $writable = [];
152
153 1
    if (is_array($target)) {
154 1
        return array_keys($target);
155
    }
156 1
    $class = is_object($target) ? get_class($target) : $target;
157 1
    $cacheKey = $class . ($useSetters ? '+s' : '');
158 1
    if (!array_key_exists($cacheKey, $writable)) {
159 1
        $writable[$cacheKey] = array_keys(get_class_vars($class));
160 1
        if ($useSetters) {
161 1
            $setters = getSetters($class);
162 1
            foreach ($setters as $setter) {
163 1
                $writable[$cacheKey][] = Str::toSnakeCase(substr($setter, 3));
164 1
            }
165 1
        }
166 1
    }
167 1
    return $writable[$cacheKey];
168
}
169
170
/**
171
 * Returns method names from target object/class that starts from specified keyword
172
 * and followed by uppercase character.
173
 *
174
 * Examples:
175
 *     - mp\getMethodsPrefixedBy('get', $obj)
176
 *       will return methods that looks like getters.
177
 *
178
 * @param string $keyword method name prefix
179
 * @param object|string $target object or class name
180
 * @return array|string[] method names
181
 */
182
function getMethodsPrefixedBy($keyword, $target)
183
{
184 3
    $res = [];
185 3
    $methods = get_class_methods($target);
186 3
    $keyLength = strlen($keyword);
187 3
    foreach ($methods as $method) {
188
        if (
189 3
            strpos($method, $keyword) === 0
190 3
            && strlen($method) > $keyLength
191 3
            && ctype_upper($method[$keyLength])
192 3
        ) {
193 3
            $res[] = $method;
194 3
        }
195 3
    }
196 3
    return $res;
197
}
198
199
/**
200
 * Returns method names from target object/class that looks like setters.
201
 *
202
 * @param object|string $target object or class name
203
 * @return string[] method names
204
 */
205
function getSetters($target)
206
{
207 2
    return getMethodsPrefixedBy('set', $target);
208
}
209
210
/**
211
 * Returns method names from target object/class that looks like getters.
212
 *
213
 * @param object|string $target object or class name
214
 * @return array|string[] method names
215
 */
216
function getGetters($target)
217
{
218 1
    return getMethodsPrefixedBy('get', $target);
219
}
220
221
/**
222
 * Returns values of properties specified in $propertyNames argument.
223
 *
224
 * This function supports getters, i. e.
225
 * value returned by getSomeValue() method of target object can be requested as 'some_value' property.
226
 *
227
 * @experimental
228
 *
229
 * @param object|array $src
230
 * @param string[] $propertyNames
231
 * @return array
232
 */
233
function getValues($src, array $propertyNames)
234
{
235
236 1
    if (is_array($src)) {
237 1
        return array_intersect_key($src, array_flip($propertyNames));
238
    }
239 1
    $values = array_intersect_key(get_object_vars($src), array_flip($propertyNames));
240 1
    foreach ($propertyNames as $key) {
241 1
        if (array_key_exists($key, $values)) continue;
242 1
        $getter = 'get' . Str::toCamelCase($key);
243 1
        if (method_exists($src, $getter)) {
244 1
            $values[$key] = $src->{$getter}();
245 1
        }
246 1
    }
247 1
    return $values;
248
}
249
250
/**
251
 * Extracts value specified by property / field name from object or array.
252
 *
253
 * This function supports property paths (prop1.prop2.prop3) and getters.
254
 *
255
 * If $propertyName = 'prop_name', this method will try to extract data in following order from:
256
 * - $src['prop_name']
257
 * - $src->prop_name
258
 * - $src->getPropName()
259
 * - $src->prop_name()
260
 * - $src->isPropName()
261
 *
262
 * @experimental
263
 * @param array|object $src
264
 * @param string $propertyName
265
 * @param mixed $default default value
266
 * @param string|null $delimiter (optional, default value: '.') used to specify property paths
267
 * @return mixed
268
 */
269
function getValue($src, $propertyName, $default = null, $delimiter = '.')
270
{
271 2
    return getValueByRef($src, $propertyName, $default, $delimiter);
272
}
273
274
/**
275
 * Extracts value specified by property / field / method name from object or array by reference if possible.
276
 *
277
 * This function acts like `mp\getValue` with only difference that value will be returned by reference if possible.
278
 *
279
 * @experimental
280
 * @param array|object $src
281
 * @param string $propertyName
282
 * @param mixed $default
283
 * @param string|null $delimiter
284
 * @return mixed|null
285
 */
286
function &getValueByRef(&$src, $propertyName, $default = null, $delimiter = '.')
287
{
288 2
    if ($delimiter && $pos = strpos($propertyName, $delimiter)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $delimiter of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
289
        // head(a.b.c) = a
290
        // tail(a.b.c) = b.c
291 2
        $head = substr($propertyName, 0, $pos);
292 2
        $tail = substr($propertyName, $pos + 1);
293 2
        return getValueByRef(
294 2
            getValueByRef($src, $head, $default, null),
295 2
            $tail,
296 2
            $default,
297
            $delimiter
298 2
        );
299
    }
300
301 2
    if (is_array($src)) {
302 2
        if (array_key_exists($propertyName, $src)) {
303 2
            return $src[$propertyName];
304
        }
305 2
    } elseif (is_object($src)) {
306 1
        if (isset($src->{$propertyName})) {
307
            // if it's not magic method, return reference
308 1
            if (property_exists($src, $propertyName)) {
309 1
                return $src->{$propertyName};
310
                // otherwise (it's __get()) calling $src->{$propertyName} will generate PHP notice:
311
                // indirect modification of overloaded property has no effect.
312
                // Therefore we return link to temp variable instead of link to variable itself.
313
            } else {
314
                $tmp = $src->{$propertyName};
315
                return $tmp;
316
            }
317
        }
318 1
        $camelPropName = Str::toCamelCase($propertyName);
319
        $methods = [
320 1
            'get' . $camelPropName,
321 1
            $propertyName,
322 1
            $camelPropName,
323
            'is' . $camelPropName
324 1
        ];
325 1
        foreach ($methods as $method) {
326 1
            if (method_exists($src, $method)) {
327 1
                $result = call_user_func([$src, $method]);
328 1
                return $result;
329
            }
330 1
        }
331 1
    }
332 2
    return $default;
333
}
334
335
/**
336
 * Assigns value, supports property paths (prop1.prop2.prop3).
337
 *
338
 * @experimental
339
 * @param array|object $target
340
 * @param string $propertyName
341
 * @param mixed $value
342
 * @param string|null $delimiter
343
 * @return bool returns TRUE if value was successfully assigned, FALSE otherwise
344
 */
345
function setValue(&$target, $propertyName, $value, $delimiter = '.')
346
{
347 1
    if ($delimiter && $pos = strrpos($propertyName, $delimiter)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $delimiter of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
348
        // head(a.b.c) = a.b
349
        // tail(a.b.c) = c
350 1
        $head = substr($propertyName, 0, $pos);
351 1
        $tail = substr($propertyName, $pos + 1);
352 1
        $container = &getValueByRef($target, $head, null, $delimiter);
353 1
        if (!$container) {
354 1
            return false;
355
        }
356 1
        $res = setValues($container, [$tail => $value]);
357 1
        return count($res) === 1;
358
    }
359 1
    $res = setValues($target, [$propertyName => $value]);
360 1
    return count($res) === 1;
361
}
362