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

mp.php ➔ setValuesUsingSetters()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 3
eloc 8
c 2
b 0
f 1
nc 3
nop 2
dl 0
loc 12
ccs 9
cts 9
cp 1
crap 3
rs 9.4285
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