Passed
Push — master ( 5a0799...7de705 )
by Arthur
14:17
created

DataTransferObject::__set()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 4
nop 2
dl 0
loc 13
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Larapie\DataTransferObject;
6
7
use Larapie\DataTransferObject\Exceptions\PropertyAlreadyExistsException;
8
use ReflectionClass;
9
use ReflectionProperty;
10
use Larapie\DataTransferObject\Contracts\DtoContract;
11
use Larapie\DataTransferObject\Contracts\PropertyContract;
12
use Larapie\DataTransferObject\Exceptions\ImmutableDtoException;
13
use Larapie\DataTransferObject\Exceptions\PropertyNotFoundDtoException;
14
use Larapie\DataTransferObject\Exceptions\ImmutablePropertyDtoException;
15
use Larapie\DataTransferObject\Exceptions\UnknownPropertiesDtoException;
16
use Larapie\DataTransferObject\Exceptions\UninitialisedPropertyDtoException;
17
18
/**
19
 * Class DataTransferObject.
20
 */
21
abstract class DataTransferObject implements DtoContract
22
{
23
    /** @var array */
24
    protected $onlyKeys = [];
25
26
    /** @var array */
27
    protected $with = [];
28
29
    /** @var Property[] | array */
30
    protected $properties = [];
31
32
    /** @var bool */
33
    protected $immutable = false;
34
35
    public function __construct(array $parameters)
36
    {
37
        $this->boot($parameters);
38
    }
39
40
    /**
41
     * Boot the dto and process all parameters.
42
     * @param array $parameters
43
     * @throws \ReflectionException
44
     */
45
    protected function boot(array $parameters): void
46
    {
47
        foreach ($this->getPublicProperties() as $property) {
48
49
            /*
50
             * Do not change the order of the following methods.
51
             * External packages rely on this order.
52
             */
53
54
            $this->setPropertyDefaultValue($property);
55
56
            $property = $this->mutateProperty($property);
57
58
            $this->validateProperty($property, $parameters);
59
60
            $this->setPropertyValue($property, $parameters);
61
62
            /* add the property to an associative array with the name as key */
63
            $this->properties[$property->getName()] = $property;
64
65
            /* remove the property from the value object and parameters array  */
66
            unset($parameters[$property->getName()], $this->{$property->getName()});
67
        }
68
69
        $this->processRemainingProperties($parameters);
70
        $this->determineImmutability();
71
    }
72
73
    protected function determineImmutability()
74
    {
75
        /* If the dto itself is not immutable but some properties are chain them immutable  */
76
        foreach ($this->properties as $property) {
77
            if ($property->immutable()) {
78
                $this->chainPropertyImmutable($property);
79
            }
80
        }
81
    }
82
83
    protected function setImmutable(): void
84
    {
85
        if (!$this->isImmutable()) {
86
            $this->immutable = true;
87
            foreach ($this->properties as $property) {
88
                $this->chainPropertyImmutable($property);
89
            }
90
        }
91
    }
92
93
    protected function chainPropertyImmutable(PropertyContract $property)
94
    {
95
        $dto = $property->getValue();
96
        if ($dto instanceof DataTransferObject) {
97
            $dto->setImmutable();
98
        } elseif (is_iterable($dto)) {
99
            foreach ($dto as $aPotentialDto) {
100
                if ($aPotentialDto instanceof DataTransferObject) {
101
                    $aPotentialDto->setImmutable();
102
                }
103
            }
104
        }
105
    }
106
107
    /**
108
     * Get all public properties from the current object through reflection.
109
     * @return Property[]
110
     * @throws \ReflectionException
111
     */
112
    protected function getPublicProperties(): array
113
    {
114
        $class = new ReflectionClass(static::class);
115
116
        $properties = [];
117
        foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $reflectionProperty) {
118
            $properties[$reflectionProperty->getName()] = new Property($reflectionProperty);
119
        }
120
121
        return $properties;
122
    }
123
124
    /**
125
     * Check if property passes the basic conditions.
126
     * @param PropertyContract $property
127
     * @param array $parameters
128
     */
129
    protected function validateProperty(PropertyContract $property, array $parameters): void
130
    {
131
        if (!array_key_exists($property->getName(), $parameters)
132
            && is_null($property->getDefault())
133
            && !$property->nullable()
134
            && !$property->isOptional()
135
        ) {
136
            throw new UninitialisedPropertyDtoException($property);
137
        }
138
    }
139
140
    /**
141
     * Set the value if it's present in the array.
142
     * @param PropertyContract $property
143
     * @param array $parameters
144
     */
145
    protected function setPropertyValue(PropertyContract $property, array $parameters): void
146
    {
147
        if (array_key_exists($property->getName(), $parameters)) {
148
            $property->set($parameters[$property->getName()]);
149
        }
150
    }
151
152
    /**
153
     * Set the value if it's present in the array.
154
     * @param PropertyContract $property
155
     */
156
    protected function setPropertyDefaultValue(PropertyContract $property): void
157
    {
158
        $property->setDefault($property->getValueFromReflection($this));
159
    }
160
161
    /**
162
     * Allows to mutate the property before it gets processed.
163
     * @param PropertyContract $property
164
     * @return PropertyContract
165
     */
166
    protected function mutateProperty(PropertyContract $property): PropertyContract
167
    {
168
        return $property;
169
    }
170
171
    /**
172
     * Check if there are additional parameters left.
173
     * Throw error if there are.
174
     * Additional properties are not allowed in a dto.
175
     * @param array $parameters
176
     * @throws UnknownPropertiesDtoException
177
     */
178
    protected function processRemainingProperties(array $parameters)
179
    {
180
        if (count($parameters)) {
181
            throw new UnknownPropertiesDtoException(array_keys($parameters), static::class);
182
        }
183
    }
184
185
    /**
186
     * Immutable behavior
187
     * Throw error if a user tries to set a property.
188
     * @param $name
189
     * @param $value
190
     * @throws ImmutableDtoException|ImmutablePropertyDtoException|PropertyNotFoundDtoException
191
     */
192
    public function __set($name, $value)
193
    {
194
        if ($this->immutable) {
195
            throw new ImmutableDtoException($name);
196
        }
197
        if (!isset($this->properties[$name])) {
198
            throw new PropertyNotFoundDtoException($name, get_class($this));
199
        }
200
201
        if ($this->properties[$name]->immutable()) {
202
            throw new ImmutablePropertyDtoException($name);
203
        }
204
        $this->$name = $value;
205
    }
206
207
    /**
208
     * Proxy through to the properties array.
209
     * @param $name
210
     * @return mixed
211
     */
212
    public function &__get($name)
213
    {
214
        return $this->properties[$name]->value;
215
    }
216
217
    public function isImmutable(): bool
218
    {
219
        return $this->immutable;
220
    }
221
222
    public function all(): array
223
    {
224
        $data = [];
225
226
        foreach ($this->properties as $property) {
227
            $data[$property->getName()] = $property->getValue();
228
        }
229
230
        return $data;
231
    }
232
233
    public function only(string ...$keys): DtoContract
234
    {
235
        $this->onlyKeys = array_merge($this->onlyKeys, $keys);
236
237
        return $this;
238
    }
239
240
    public function except(string ...$keys): DtoContract
241
    {
242
        foreach ($keys as $key) {
243
            $property = $this->properties[$key] ?? null;
244
            if (isset($property)) {
245
                $property->setVisible(false);
246
            }
247
        }
248
249
        return $this;
250
    }
251
252
    public function with(string $key, $value): DtoContract
253
    {
254
        if (array_key_exists($key, $this->getPublicProperties())) {
255
            throw new PropertyAlreadyExistsException($key);
256
        }
257
        return $this->override($key, $value);
258
    }
259
260
    public function override(string $key, $value): DtoContract
261
    {
262
        if ($this->isImmutable())
263
            throw new ImmutableDtoException($key);
264
265
        if ((array_key_exists($key, $this->properties) && $this->properties[$key]->immutable())) {
266
            throw new ImmutablePropertyDtoException($key);
267
        }
268
269
        $this->with[$key] = $value;
270
        return $this;
271
    }
272
273
    public function toArray(): array
274
    {
275
        $data = $this->all();
276
        $array = [];
277
278
        if (count($this->onlyKeys)) {
279
            $array = array_intersect_key($data, array_flip((array)$this->onlyKeys));
280
        } else {
281
            foreach ($data as $key => $propertyValue) {
282
                if ($this->properties[$key]->isVisible() && $this->properties[$key]->isInitialized()) {
283
                    $array[$key] = $propertyValue;
284
                    $array[$key] = $propertyValue;
285
                }
286
            }
287
        }
288
289
        return $this->parseArray(array_merge($array, $this->with));
290
    }
291
292
    protected function parseArray(array $array): array
293
    {
294
        foreach ($array as $key => $value) {
295
            if (
296
                $value instanceof DataTransferObject
297
                || $value instanceof DataTransferObjectCollection
298
            ) {
299
                $array[$key] = $value->toArray();
300
301
                continue;
302
            }
303
304
            if (!is_array($value)) {
305
                continue;
306
            }
307
308
            $array[$key] = $this->parseArray($value);
309
        }
310
311
        return $array;
312
    }
313
}
314