Passed
Push — master ( 245bf0...8f017c )
by Arthur
11:37
created

DataTransferObject::only()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Larapie\DataTransferObject;
6
7
use ReflectionClass;
8
use ReflectionProperty;
9
use Larapie\DataTransferObject\Contracts\DtoContract;
10
use Larapie\DataTransferObject\Contracts\PropertyContract;
11
use Larapie\DataTransferObject\Exceptions\ImmutableDtoException;
12
use Larapie\DataTransferObject\Exceptions\PropertyNotFoundDtoException;
13
use Larapie\DataTransferObject\Exceptions\ImmutablePropertyDtoException;
14
use Larapie\DataTransferObject\Exceptions\UnknownPropertiesDtoException;
15
use Larapie\DataTransferObject\Exceptions\PropertyAlreadyExistsException;
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
            $property->validate();
150
        }
151
    }
152
153
    /**
154
     * Set the value if it's present in the array.
155
     * @param PropertyContract $property
156
     */
157
    protected function setPropertyDefaultValue(PropertyContract $property): void
158
    {
159
        $property->setDefault($property->getValueFromReflection($this));
160
    }
161
162
    /**
163
     * Allows to mutate the property before it gets processed.
164
     * @param PropertyContract $property
165
     * @return PropertyContract
166
     */
167
    protected function mutateProperty(PropertyContract $property): PropertyContract
168
    {
169
        return $property;
170
    }
171
172
    /**
173
     * Check if there are additional parameters left.
174
     * Throw error if there are.
175
     * Additional properties are not allowed in a dto.
176
     * @param array $parameters
177
     * @throws UnknownPropertiesDtoException
178
     */
179
    protected function processRemainingProperties(array $parameters)
180
    {
181
        if (count($parameters)) {
182
            throw new UnknownPropertiesDtoException(array_keys($parameters), static::class);
183
        }
184
    }
185
186
    /**
187
     * Immutable behavior
188
     * Throw error if a user tries to set a property.
189
     * @param $name
190
     * @param $value
191
     * @throws ImmutableDtoException|ImmutablePropertyDtoException|PropertyNotFoundDtoException
192
     */
193
    public function __set($name, $value)
194
    {
195
        if ($this->immutable) {
196
            throw new ImmutableDtoException($name);
197
        }
198
        if (!isset($this->properties[$name])) {
199
            throw new PropertyNotFoundDtoException($name, get_class($this));
200
        }
201
202
        if ($this->properties[$name]->immutable()) {
203
            throw new ImmutablePropertyDtoException($name);
204
        }
205
        $this->$name = $value;
206
    }
207
208
    /**
209
     * Proxy through to the properties array.
210
     * @param $name
211
     * @return mixed
212
     */
213
    public function &__get($name)
214
    {
215
        return $this->properties[$name]->value;
216
    }
217
218
    public function isImmutable(): bool
219
    {
220
        return $this->immutable;
221
    }
222
223
    public function all(): array
224
    {
225
        $data = [];
226
227
        foreach ($this->properties as $property) {
228
            $data[$property->getName()] = $property->getValue();
229
        }
230
231
        return array_merge($data, $this->with);
232
    }
233
234
    public function only(string ...$keys): DtoContract
235
    {
236
        $this->onlyKeys = array_merge($this->onlyKeys, $keys);
237
238
        return $this;
239
    }
240
241
    public function except(string ...$keys): DtoContract
242
    {
243
        foreach ($keys as $key) {
244
            if (array_key_exists($key, $this->with)) {
245
                unset($this->with[$key]);
246
            }
247
            $property = $this->properties[$key] ?? null;
248
            if (isset($property)) {
249
                $property->setVisible(false);
250
            }
251
        }
252
        return $this;
253
    }
254
255
    public function with(string $key, $value): DtoContract
256
    {
257
        if (array_key_exists($key, $this->properties)) {
258
            throw new PropertyAlreadyExistsException($key);
259
        }
260
        return $this->override($key, $value);
261
    }
262
263
    public function override(string $key, $value): DtoContract
264
    {
265
        if ($this->isImmutable()) {
266
            throw new ImmutableDtoException($key);
267
        }
268
        if (($propertyExists = array_key_exists($key, $this->properties) && $this->properties[$key]->immutable())) {
269
            throw new ImmutablePropertyDtoException($key);
270
        }
271
272
        if ($propertyExists) {
0 ignored issues
show
introduced by
The condition $propertyExists is always false.
Loading history...
273
            $property = $this->properties[$key];
274
            $property->set($value);
275
            $property->validate();
276
        } else {
277
            $this->with[$key] = $value;
278
        }
279
280
        return $this;
281
    }
282
283
    public function toArray(): array
284
    {
285
        $data = $this->all();
286
        $array = [];
287
288
        if (count($this->onlyKeys)) {
289
            $array = array_intersect_key($data, array_flip((array)$this->onlyKeys));
290
        } else {
291
            foreach ($data as $key => $propertyValue) {
292
                if (array_key_exists($key, $this->properties) && $this->properties[$key]->isVisible() && $this->properties[$key]->isInitialized()) {
293
                    $array[$key] = $propertyValue;
294
                }
295
            }
296
        }
297
298
        return $this->parseArray($array);
299
    }
300
301
    protected function parseArray(array $array): array
302
    {
303
        foreach ($array as $key => $value) {
304
            if (
305
                $value instanceof DataTransferObject
306
                || $value instanceof DataTransferObjectCollection
307
            ) {
308
                $array[$key] = $value->toArray();
309
310
                continue;
311
            }
312
313
            if (!is_array($value)) {
314
                continue;
315
            }
316
317
            $array[$key] = $this->parseArray($value);
318
        }
319
320
        return $array;
321
    }
322
}
323