Passed
Push — master ( 648605...b4ff77 )
by Anthony
15:20 queued 01:14
created

DataTransferObject::immutable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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