Completed
Push — master ( d95fc8...954071 )
by Rafael
03:33
created

Array2Object::getClassProperties()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 18
rs 9.2
cc 4
eloc 11
nc 6
nop 1
1
<?php
2
3
/**
4
 * LICENSE: This file is subject to the terms and conditions defined in
5
 * file 'LICENSE', which is part of this source code package.
6
 *
7
 * @copyright 2016 Copyright(c) - All rights reserved.
8
 */
9
10
namespace Rafrsr\LibArray2Object;
11
12
use Symfony\Component\PropertyAccess\PropertyAccessor;
13
14
/**
15
 * Using the property names and the common property annotations
16
 * populate a object instance with the values of the array recursively
17
 */
18
class Array2Object
19
{
20
21
    /**
22
     * createObject
23
     *
24
     * @param string $class class to create object or instance
25
     * @param array  $data  array of data
26
     *
27
     * @return mixed
28
     *
29
     * @throws \InvalidArgumentException
30
     */
31
    static public function createObject($class, array $data)
32
    {
33
        if (is_string($class) && class_exists($class)) {
34
            $object = new $class;
35
        } else {
36
            $object = new $class;
37
        }
38
39
        self::populate($object, $data);
40
41
        return $object;
42
    }
43
44
    /**
45
     * @param object $object object instance to populate
46
     * @param array  $data   array of data to apply
47
     *
48
     * @throws \InvalidArgumentException
49
     */
50
    static public function populate($object, array $data)
51
    {
52
        if (!is_object($object)) {
53
            throw new \InvalidArgumentException('The first param should be a object.');
54
        }
55
56
        $propertyAccessor = new PropertyAccessor();
57
58
        $reflClass = new \ReflectionClass($object);
59
60
        foreach (self::getClassProperties($reflClass) as $property) {
61
            foreach ($data as $key => $value) {
62
                if ($propertyAccessor->isWritable($object, $key) && self::isSameProperty($property->getName(), $key)) {
63
                    $types = self::getPropertyTypes($property);
64
                    $value = self::parseValue($value, $types, new \ReflectionClass($property->class));
65
                    $propertyAccessor->setValue($object, $key, $value);
66
                }
67
            }
68
        }
69
    }
70
71
    /**
72
     * Get array of class properties including parents private properties
73
     *
74
     * @param \ReflectionClass $refClass
75
     *
76
     * @return array|\ReflectionProperty[]
77
     */
78
    static private function getClassProperties(\ReflectionClass $refClass)
79
    {
80
        $props = $refClass->getProperties();
81
        $props_arr = [];
82
        foreach ($props as $prop) {
83
            $f = $prop->getName();
84
85
            $props_arr[$f] = $prop;
86
        }
87
        if ($parentClass = $refClass->getParentClass()) {
88
            $parent_props_arr = self::getClassProperties($parentClass);//RECURSION
89
            if (count($parent_props_arr) > 0) {
90
                $props_arr = array_merge($parent_props_arr, $props_arr);
91
            }
92
        }
93
94
        return $props_arr;
95
    }
96
97
    /**
98
     * @param $propertyName
99
     * @param $givenName
100
     *
101
     * @return bool
102
     */
103
    static private function isSameProperty($propertyName, $givenName)
104
    {
105
        if ($propertyName === $givenName
106
            || $propertyName === self::camelize($givenName) //ErrorCode = error_code
107
            || $propertyName === lcfirst(self::camelize($givenName)) // errorCode => error_code
108
            || $propertyName === strtolower(self::camelize($givenName)) // errorcode => error_code
109
            || strtolower($propertyName) === $givenName // errorCode => errorcode
110
        ) {
111
            return true;
112
        }
113
114
        return false;
115
    }
116
117
    /**
118
     * Parse a value using given types
119
     *
120
     * @param mixed            $value
121
     * @param array            $types
122
     * @param \ReflectionClass $context
123
     *
124
     * @return array|bool|float|int|string
125
     */
126
    static private function parseValue($value, $types, \ReflectionClass $context)
127
    {
128
        foreach ($types as $type) {
129
130
            switch ($type) {
131
                case 'string':
132
                    $value = (string)$value;
133
                    break;
134
                case 'integer':
135
                case 'int':
136
                    $value = (integer)$value;
137
                    break;
138
                case 'float':
139
                    $value = (float)$value;
140
                    break;
141
                case 'bool':
142
                case 'boolean':
143
                    if (is_string($value) && strtolower($value) === 'false') {
144
                        $value = false;
145
                    } elseif (is_string($value) && strtolower($value) === 'true') {
146
                        $value = true;
147
                    } else {
148
                        $value = (boolean)$value;
149
                    }
150
                    break;
151
                case '\DateTime':
152
                case 'DateTime':
153
                    $value = new $type($value);
154
                    break;
155
                case 'array':
156
                    if (is_array($value)) {
157
                        foreach ($value as $key => $index) {
158
                            $value[$key] = self::valueToObject($index, $type, $context);
159
                        }
160
                    }
161
                    break;
162
                default:
163
                    $value = self::valueToObject($value, $type, $context);
164
165
            }
166
        }
167
168
        return $value;
169
    }
170
171
    /**
172
     * Convert a array value into a object or array of objects
173
     *
174
     * @param  mixed           $value
175
     * @param  string          $type
176
     * @param \ReflectionClass $context
177
     *
178
     * @return array
179
     */
180
    static private function valueToObject($value, $type, \ReflectionClass $context)
181
    {
182
        $isArrayOfObjects = (strpos($type, '[]') !== false);
183
        $type = str_replace('[]', null, $type);
184
        $className = null;
185
186
        //use the type as class
187
        if (class_exists($type)) {
188
            $className = $type;
189
        }
190
191
        //try to get the class from use statements in the class file
192
        if (!$className) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className of type string|null is loosely compared to false; 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...
193
            $classContent = file_get_contents($context->getFileName());
194
            preg_match("/use\s+([\w\\\]+$type);/", $classContent, $matches);
195
            if (isset($matches[1]) && class_exists($matches[1])) {
196
                $className = $matches[1];
197
            }
198
        }
199
200
        //use the same namespace as class container
201
        if (!$className && class_exists($context->getNamespaceName() . "\\" . $type)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className of type string|null is loosely compared to false; 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...
202
            $className = $context->getNamespaceName() . "\\" . $type;
203
        }
204
205
        if ($className && class_exists($className)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className 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...
206
            if ($isArrayOfObjects) {
207
                $newValue = [];
208
                if (is_array($value)) {
209
                    foreach ($value as $item) {
210 View Code Duplication
                        if (is_array($item)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
211
                            $childObject = new $className();
212
                            self::populate($childObject, $item);
213
                            $newValue[] = $childObject;
214
                        } else {
215
                            $newValue = $item;
216
                        }
217
                    }
218
                }
219
220 View Code Duplication
            } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
221
                if (is_array($value)) {
222
                    $childObject = new $className();
223
                    self::populate($childObject, $value);
224
                    $newValue = $childObject;
225
                }
226
            }
227
        }
228
229
        if (isset($newValue)) {
230
            $value = $newValue;
231
        }
232
233
        return $value;
234
    }
235
236
    /**
237
     * @param $name
238
     *
239
     * @return string
240
     */
241
    static private function camelize($name)
242
    {
243
        return strtr(ucwords(strtr($name, ['_' => ' '])), [' ' => '']);
244
    }
245
246
    /**
247
     * @param \ReflectionProperty $property
248
     *
249
     * @return array
250
     */
251
    static private function getPropertyTypes(\ReflectionProperty $property)
252
    {
253
        $doc = $property->getDocComment();
254
        preg_match('/@var\s([\w\\\|\[\]]+)/', $doc, $matches);
255
        $types = [];
256
        if (isset($matches[1])) {
257
            $types = explode('|', $matches[1]);
258
        }
259
260
        return $types;
261
    }
262
}