Passed
Branch master (49bf98)
by Oguzhan
02:52
created

HydratableTrait::hydrateProperty()   B

Complexity

Conditions 6
Paths 25

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 26
rs 8.439
cc 6
eloc 14
nc 25
nop 3
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
     * @param  boolean      $failOnError Throw Exception if any error(s) occur
56
     *
57
     * @return object
58
     *
59
     * @throws \Exception if hydration failes AND $failOnError is true
60
     */
61
    public function hydrateClass($class, $failOnError = false)
62
    {
63
        $reflection = new \ReflectionClass($this);
64
65
        // Iterate over $class for properties
66
        foreach ($class as $itemKey => $itemValue) {
67
            // Check if key exists in $this
68
69
            // Convert key to a propertyname in $this
70
            try {
71
                $this->hydrateProperty($itemKey, $itemValue, $reflection);
72
            } catch (\Exception $e) {
73
                if ($failOnError) {
74
                    throw $e;
75
                }
76
                continue;
77
            }
78
        }
79
80
        return $this;
81
    }
82
83
    /**
84
     * Hydrates property with value.
85
     * Value can be anything. If the property is supposed to be a Class of anykind we will try to instantiate it
86
     * and assign the class to the property
87
     *
88
     * @param                  $key
89
     * @param                  $value
90
     * @param \ReflectionClass $reflection
91
     *
92
     * @throws \Exception
93
     */
94
    protected function hydrateProperty($key, $value, \ReflectionClass $reflection)
95
    {
96
        $propertyName = $this->resolvePropertyName($key);
97
98
        try {
99
            // Check if property exists and assign a ReflectionProperty class to $reflectionProperty
100
            $reflectionProperty = $reflection->getProperty($propertyName);
101
            // Get the expected property class from the property's DocBlock
102
            $propertyClassName = ReflectionTrait::getClassFromDocComment($reflectionProperty->getDocComment(), true, $reflection);
103
            // Set argument for constructor (if any), in case we're dealing with an object (IE: DateTime)
104
            $this->objectConstructorArguments = (in_array($propertyClassName, $this->giveDataInConstructor)) ? $value : null;
105
106
            if (!in_array($propertyClassName, self::$nonObjectTypes)) {
107
                $value = new $propertyClassName($this->objectConstructorArguments);
108
                $this->checkObjectForErrors($value, true);
109
110
                if (method_exists($value, 'hydrateClass') && $this->isHydratableValue($value)) {
111
                    $value->hydrateClass($value);
112
                }
113
            }
114
115
            $this->setPropertyValue($propertyName, $value, true);
116
        } catch (\Exception $e) {
117
            throw $e;
118
        }
119
    }
120
121
    /**
122
     * Resolves and returns propertyname
123
     *
124
     * @param $key
125
     *
126
     * @return mixed|string
127
     */
128
    private function resolvePropertyName($key)
129
    {
130
        $propertyName = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key))));
131
132
        return (property_exists($this, $propertyName)) ? $propertyName :
133
            (property_exists($this, lcfirst($propertyName)) ? lcfirst($propertyName) :
134
                preg_replace_callback('/([A-Z])/', function($match) {
135
                    return strtolower('_' . $match[1]);
136
                }, lcfirst($propertyName))
137
            );
138
    }
139
140
    /**
141
     * Checks if the value can be hydrated for iteration
142
     *
143
     * @param $value
144
     *
145
     * @return bool
146
     */
147
    private function isHydratableValue($value)
148
    {
149
        return (is_array($value) || is_object($value));
150
    }
151
152
    /**
153
     * Checks (and fixes) objects against known errors
154
     *
155
     * @param Object &$object
156
     * @param bool   $fix Fix errors
157
     *
158
     * @return void
159
     *
160
     * @throws \Exception
161
     */
162
    private function checkObjectForErrors(&$object, $fix = false)
163
    {
164
        if (!is_object($object)) {
165
            throw new \Exception('Non-object passed');
166
        } elseif ($object instanceof \DateTime) {
167
            // The constructor (passed from the API) is NULL, indicating an empty value
168
            // PHP DateTime's default value is now()
169
            if ($this->objectConstructorArguments == null) {
170
                $object = null;
171
            } elseif (!$object->getTimestamp() && $fix) {
172
                $object->setTimestamp(0);
173
            }
174
        }
175
    }
176
}