Hydrator   A
last analyzed

Complexity

Total Complexity 19

Size/Duplication

Total Lines 177
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 19
eloc 48
c 1
b 0
f 0
dl 0
loc 177
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
A setProperty() 0 26 4
A generateSetterMethod() 0 5 2
A __construct() 0 4 1
A from() 0 11 3
A fromArray() 0 8 2
A fromMany() 0 3 1
A fromObject() 0 5 1
A newInstance() 0 13 3
A normaliseProperty() 0 4 2
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Handles the hydration of objects/models for results.
7
 */
8
9
namespace Pixie\Hydration;
10
11
use Exception;
12
use stdClass;
13
use Throwable;
14
use function is_object;
15
use function method_exists;
16
use function trim;
17
use function ucfirst;
18
19
/**
20
 * @template T
21
 */
22
class Hydrator
23
{
24
    /**
25
     * The model to hydrate
26
27
     *
28
     * @var class-string<T>
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
29
     */
30
    protected $model;
31
32
    /**
33
     * The arguments used to create the new instance.
34
     *
35
     * @var array<string|int, mixed>
36
     */
37
    protected $constructorArgs;
38
39
    /**
40
41
     * @param class-string<T> $model
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
42
     * @param array<string|int, mixed> $constructorArgs
43
     */
44
    public function __construct(string $model = stdClass::class, array $constructorArgs = [])
45
    {
46
        $this->model           = $model;
47
        $this->constructorArgs = $constructorArgs;
48
    }
49
50
    /**
51
     * Map many models
52
     *
53
     * @param array<int, object|mixed[]> $sources
54
     *
55
     * @return array<T>
56
     */
57
    public function fromMany(array $sources): array
58
    {
59
        return array_map([$this, 'from'], $sources);
60
    }
61
62
    /**
63
     * Map a single model
64
     *
65
     * @param object|mixed[] $source
66
     *
67
     * @return T
0 ignored issues
show
Bug introduced by
The type Pixie\Hydration\T was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
68
     */
69
    public function from($source)
70
    {
71
        switch (true) {
72
            case is_array($source):
73
                return $this->fromArray($source);
74
75
            case is_object($source):
76
                return $this->fromObject($source);
77
78
            default:
79
                throw new Exception('Models can only be mapped from arrays or objects.', 1);
80
        }
81
    }
82
83
    /**
84
     * Maps the model from an array of data.
85
     *
86
     * @param array<string, mixed> $source
87
     *
88
     * @return T
89
     */
90
    protected function fromArray(array $source)
91
    {
92
        $model = $this->newInstance();
93
        foreach ($source as $key => $value) {
94
            $this->setProperty($model, $key, $value);
95
        }
96
97
        return $model;
98
    }
99
100
    /**
101
     * Maps a model from an Object of data
102
     *
103
     * @param object $source
104
     *
105
     * @return T
106
     */
107
    protected function fromObject($source)
108
    {
109
        $vars = get_object_vars($source);
110
111
        return $this->fromArray($vars);
112
    }
113
114
    /**
115
     * Construct an instance of the model
116
117
     *
118
     * @return T
119
     */
120
    protected function newInstance()
121
    {
122
        $class = $this->model;
123
        try {
124
            /** @var T */
125
            $instance = empty($this->constructorArgs)
126
                ? new $class()
127
                : new $class(...$this->constructorArgs);
128
        } catch (Throwable $th) {
129
            throw new Exception("Failed to construct model, {$th->getMessage()}", 1);
130
        }
131
132
        return $instance;
133
    }
134
135
    /**
136
     * Sets a property to the current model
137
     *
138
     * @param T $model
139
     * @param string $property
140
     * @param mixed $value
141
     *
142
     * @return T
143
     */
144
    protected function setProperty($model, string $property, $value)
145
    {
146
        $property = $this->normaliseProperty($property);
147
148
        // Attempt to set.
149
        try {
150
            switch (true) {
151
                case method_exists($model, $this->generateSetterMethod($property)):
0 ignored issues
show
Unused Code introduced by
method_exists($model, $t...etterMethod($property)) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
152
                    $method = $this->generateSetterMethod($property);
153
                    $model->$method($value);
154
                    break;
155
156
                case method_exists($model, $this->generateSetterMethod($property, true)):
157
                    $method = $this->generateSetterMethod($property, true);
158
                    $model->$method($value);
159
                    break;
160
161
                default:
162
                    $model->$property = $value;
163
                    break;
164
            }
165
        } catch (Throwable $th) {
166
            throw new Exception(sprintf('Failed to set %s of %s model, %s', $property, get_class($model), $th->getMessage()), 1);
167
        }
168
169
        return $model;
170
    }
171
172
    /**
173
     * Normalises a property
174
     *
175
     * @param string $property
176
     *
177
     * @return string
178
     */
179
    protected function normaliseProperty(string $property): string
180
    {
181
        return trim(
182
            preg_replace('/[^a-z0-9]+/', '_', strtolower($property)) ?: ''
183
        );
184
    }
185
186
    /**
187
     * Generates a generic setter method using either underscore [set_property()] or PSR2 style [setProperty()]
188
     *
189
     * @param string $property
190
     * @param bool $underscore
191
     *
192
     * @return string
193
     */
194
    protected function generateSetterMethod(string $property, bool $underscore = false): string
195
    {
196
        return $underscore
197
            ? "set_{$property}"
198
            : 'set' . ucfirst($property);
199
    }
200
}
201