Passed
Branch master (adafdf)
by Oguzhan
02:31
created

HydratableTrait::hydrateClass()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
c 5
b 1
f 0
dl 0
loc 14
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: PBX_g33k
5
 * Date: 24-May-16
6
 * Time: 00:07
7
 */
8
9
namespace Pbxg33k\Traits;
10
11
/**
12
 * Class HydratableTrait
13
 *
14
 * This trait allows you to hydrate a class/object by passing an array or stdObj and let it hydrate itself by
15
 * calling either of the following methods:
16
 *  - hydrateClass($rawData)
17
 *
18
 * @author  Oguzhan Uysal <[email protected]>
19
 * @package Pbxg33k\Traits
20
 */
21
trait HydratableTrait
22
{
23
    use PropertyTrait;
24
    use ReflectionTrait;
25
26
    /**
27
     * List of types which are not used as objects
28
     *
29
     * @var array
30
     */
31
    public static $nonObjectTypes = ['string', 'int', 'integer', 'bool', 'boolean', 'array', 'float', 'mixed', 'null'];
32
33
    /**
34
     * List of classes which will take string arguments in constructor
35
     *
36
     * @var array
37
     */
38
    protected $giveDataInConstructor = ['\DateTime'];
39
40
    /**
41
     * Object constructor arguments to be passed when creating an object during conversion
42
     *
43
     * @var mixed
44
     */
45
    protected $objectConstructorArguments;
46
47
    /**
48
     * Converts a stdClass to models loaded in current context
49
     *
50
     * This method iterates over the passed $class
51
     * For each key, it looks for a setter and type.
52
     * If the value is an object, it initializes the object and assignes the initialized object.
53
     *
54
     * @param  object|array $class class
55
     *
56
     * @return object
57
     */
58
    public function hydrateClass($class)
59
    {
60
        $reflection = new \ReflectionClass($this);
61
62
        // Iterate over $class for properties
63
        foreach ($class as $itemKey => $itemValue) {
64
            // Check if key exists in $this
65
66
            // Convert key to a propertyname in $this
67
            $this->hydrateProperty($itemKey, $itemValue, $reflection);
68
        }
69
70
        return $this;
71
    }
72
73
    /**
74
     * Hydrates property with value.
75
     * Value can be anything. If the property is supposed to be a Class of anykind we will try to instantiate it
76
     * and assign the class to the property
77
     *
78
     * @param                  $key
79
     * @param                  $value
80
     * @param \ReflectionClass $reflection
81
     *
82
     * @throws \Exception
83
     */
84
    protected function hydrateProperty($key, $value, \ReflectionClass $reflection)
85
    {
86
        $propertyName = $this->resolvePropertyName($key);
87
88
        // Check if property exists and assign a ReflectionProperty class to $reflectionProperty
89
        if (property_exists($this, $propertyName) && $reflectionProperty = $reflection->getProperty($propertyName)) {
90
            // Get the expected property class from the property's DocBlock
91
            if ($propertyClassName = ReflectionTrait::getClassFromDocComment($reflectionProperty->getDocComment(), true, $reflection)) {
92
                // Set argument for constructor (if any), in case we're dealing with an object (IE: DateTime)
93
                $this->objectConstructorArguments = (in_array($propertyClassName, $this->giveDataInConstructor)) ? $value : null;
94
95
                if (!$this->isInstantiatable($propertyClassName)) {
96
                    return;
97
                }
98
99
                if (!in_array($propertyClassName, self::$nonObjectTypes)) {
100
                    $value = new $propertyClassName($this->objectConstructorArguments);
101
                    $this->checkObjectForErrors($value, true);
102
103
                    if (method_exists($value, 'hydrateClass') && $this->isHydratableValue($value)) {
104
                        $value->hydrateClass($value);
105
                    }
106
                }
107
108
                $this->setPropertyValue($propertyName, $value, true);
109
            }
110
        }
111
    }
112
113
    /**
114
     * Resolves and returns propertyname
115
     *
116
     * @param $key
117
     *
118
     * @return mixed|string
119
     */
120
    private function resolvePropertyName($key)
121
    {
122
        $propertyName = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key))));
123
124
        return (property_exists($this, $propertyName)) ? $propertyName :
125
            (property_exists($this, lcfirst($propertyName)) ? lcfirst($propertyName) :
126
                preg_replace_callback('/([A-Z])/', function($match) {
127
                    return strtolower('_' . $match[1]);
128
                }, lcfirst($propertyName))
129
            );
130
    }
131
132
    /**
133
     * Checks if the value can be hydrated for iteration
134
     *
135
     * @param $value
136
     *
137
     * @return bool
138
     */
139
    private function isHydratableValue($value)
140
    {
141
        return (is_array($value) || is_object($value));
142
    }
143
144
    /**
145
     * Checks if Classname resolved from docblock can be instantiated
146
     *
147
     * @param $className
148
     *
149
     * @return bool
150
     */
151
    private function isInstantiatable($className)
152
    {
153
        return (!interface_exists($className));
154
    }
155
156
    /**
157
     * Checks (and fixes) objects against known errors
158
     *
159
     * @param Object &$object
160
     * @param bool   $fix Fix errors
161
     *
162
     * @return void
163
     *
164
     * @throws \Exception
165
     */
166
    private function checkObjectForErrors(&$object, $fix = false)
167
    {
168
        if (!is_object($object)) {
169
            throw new \Exception('Non-object passed');
170
        } elseif ($object instanceof \DateTime) {
171
            // The constructor (passed from the API) is NULL, indicating an empty value
172
            // PHP DateTime's default value is now()
173
            if ($this->objectConstructorArguments == null) {
174
                $object = null;
175
            } elseif (!$object->getTimestamp() && $fix) {
176
                $object->setTimestamp(0);
177
            }
178
        }
179
    }
180
}