1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Spatie\DataTransferObject; |
6
|
|
|
|
7
|
|
|
use ReflectionClass; |
8
|
|
|
use ReflectionProperty; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* Class DataTransferObject. |
12
|
|
|
*/ |
13
|
|
|
abstract class DataTransferObject implements DtoContract |
14
|
|
|
{ |
15
|
|
|
/** @var array */ |
16
|
|
|
protected $exceptKeys = []; |
17
|
|
|
|
18
|
|
|
/** @var array */ |
19
|
|
|
protected $onlyKeys = []; |
20
|
|
|
|
21
|
|
|
/** @var Property[] | array */ |
22
|
|
|
protected $properties = []; |
23
|
|
|
|
24
|
|
|
/** @var bool */ |
25
|
|
|
protected $immutable; |
26
|
|
|
|
27
|
|
|
public static function mutable(array $parameters): DtoContract |
28
|
|
|
{ |
29
|
|
|
return new static($parameters, false); |
30
|
|
|
} |
31
|
|
|
|
32
|
|
|
public static function immutable(array $parameters): DtoContract |
33
|
|
|
{ |
34
|
|
|
return new static($parameters, true); |
35
|
|
|
} |
36
|
|
|
|
37
|
|
|
public function __construct(array $parameters, bool $immutable = true) |
38
|
|
|
{ |
39
|
|
|
$this->immutable = $immutable; |
40
|
|
|
$this->boot($parameters); |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Boot the dto and process all parameters. |
45
|
|
|
* @param array $parameters |
46
|
|
|
* @throws \ReflectionException | DataTransferObjectError |
47
|
|
|
*/ |
48
|
|
|
protected function boot(array $parameters): void |
49
|
|
|
{ |
50
|
|
|
foreach ($this->getPublicProperties() as $property) { |
51
|
|
|
|
52
|
|
|
/* |
53
|
|
|
* Do not change the order of the following methods. |
54
|
|
|
* External packages rely on this order. |
55
|
|
|
*/ |
56
|
|
|
|
57
|
|
|
$this->setPropertyDefaultValue($property); |
58
|
|
|
|
59
|
|
|
$property = $this->mutateProperty($property); |
60
|
|
|
|
61
|
|
|
$this->validateProperty($property, $parameters); |
62
|
|
|
|
63
|
|
|
$this->setPropertyValue($property, $parameters); |
64
|
|
|
|
65
|
|
|
/* add the property to an associative array with the name as key */ |
66
|
|
|
$this->properties[$property->getName()] = $property; |
67
|
|
|
|
68
|
|
|
/* remove the property from the parameters array */ |
69
|
|
|
unset($parameters[$property->getName()]); |
70
|
|
|
|
71
|
|
|
/* remove the property from the value object */ |
72
|
|
|
unset($this->{$property->getName()}); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
$this->processRemainingProperties($parameters); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Get all public properties from the current object through reflection. |
80
|
|
|
* @return Property[] |
81
|
|
|
* @throws \ReflectionException |
82
|
|
|
*/ |
83
|
|
|
protected function getPublicProperties(): array |
84
|
|
|
{ |
85
|
|
|
$class = new ReflectionClass(static::class); |
86
|
|
|
$properties = []; |
87
|
|
|
foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $reflectionProperty) { |
88
|
|
|
$properties[$reflectionProperty->getName()] = Property::fromReflection($reflectionProperty); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
return $properties; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Check if property passes the basic conditions. |
96
|
|
|
* @param Property $property |
97
|
|
|
* @param array $parameters |
98
|
|
|
*/ |
99
|
|
View Code Duplication |
protected function validateProperty($property, array $parameters): void |
|
|
|
|
100
|
|
|
{ |
101
|
|
|
if (! array_key_exists($property->getName(), $parameters) |
102
|
|
|
&& is_null($property->getDefault()) |
103
|
|
|
&& ! $property->isNullable() |
104
|
|
|
) { |
105
|
|
|
throw DataTransferObjectError::uninitialized($property); |
106
|
|
|
} |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Set the value if it's present in the array. |
111
|
|
|
* @param Property $property |
112
|
|
|
* @param array $parameters |
113
|
|
|
*/ |
114
|
|
|
protected function setPropertyValue($property, array $parameters): void |
115
|
|
|
{ |
116
|
|
|
if (array_key_exists($property->getName(), $parameters)) { |
117
|
|
|
$property->set($parameters[$property->getName()]); |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Set the value if it's present in the array. |
123
|
|
|
* @param Property $property |
124
|
|
|
* @param array $parameters |
|
|
|
|
125
|
|
|
*/ |
126
|
|
|
protected function setPropertyDefaultValue($property): void |
127
|
|
|
{ |
128
|
|
|
$property->setDefault($property->getValueFromReflection($this)); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Allows to mutate the property before it gets processed. |
133
|
|
|
* @param Property $property |
134
|
|
|
* @param array $parameters |
|
|
|
|
135
|
|
|
* @return Property |
136
|
|
|
*/ |
137
|
|
|
protected function mutateProperty($property) |
138
|
|
|
{ |
139
|
|
|
return $property; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Check if there are additional parameters left. |
144
|
|
|
* Throw error if there are. |
145
|
|
|
* Additional properties are not allowed in a dto. |
146
|
|
|
* @throws DataTransferObjectError |
147
|
|
|
*/ |
148
|
|
|
protected function processRemainingProperties(array $parameters) |
149
|
|
|
{ |
150
|
|
|
if (count($parameters)) { |
151
|
|
|
throw DataTransferObjectError::unknownProperties(array_keys($parameters), static::class); |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Immutable behavior |
157
|
|
|
* Throw error if a user tries to set a property. |
158
|
|
|
* @param $name |
159
|
|
|
* @param $value |
160
|
|
|
* @Throws DataTransferObjectError |
161
|
|
|
*/ |
162
|
|
|
public function __set($name, $value) |
163
|
|
|
{ |
164
|
|
|
if ($this->immutable) { |
165
|
|
|
throw DataTransferObjectError::immutable($name); |
166
|
|
|
} |
167
|
|
|
$this->$name = $value; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Proxy through to the properties array. |
172
|
|
|
* @param $name |
173
|
|
|
* @return mixed |
174
|
|
|
*/ |
175
|
|
|
public function __get($name) |
176
|
|
|
{ |
177
|
|
|
return $this->properties[$name]->getValue(); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
public function all(): array |
181
|
|
|
{ |
182
|
|
|
$data = []; |
183
|
|
|
|
184
|
|
|
foreach ($this->properties as $property) { |
185
|
|
|
$data[$property->getName()] = $property->getValue(); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
return $data; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
public function only(string ...$keys): DtoContract |
192
|
|
|
{ |
193
|
|
|
$this->onlyKeys = array_merge($this->onlyKeys, $keys); |
194
|
|
|
|
195
|
|
|
return $this; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
public function except(string ...$keys): DtoContract |
199
|
|
|
{ |
200
|
|
|
$this->exceptKeys = array_merge($this->exceptKeys, $keys); |
201
|
|
|
|
202
|
|
|
return $this; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
public function toArray(): array |
206
|
|
|
{ |
207
|
|
|
if (count($this->onlyKeys)) { |
208
|
|
|
$array = Arr::only($this->all(), $this->onlyKeys); |
209
|
|
|
} else { |
210
|
|
|
$array = Arr::except($this->all(), $this->exceptKeys); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
$array = $this->parseArray($array); |
214
|
|
|
|
215
|
|
|
return $array; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
protected function parseArray(array $array): array |
219
|
|
|
{ |
220
|
|
|
foreach ($array as $key => $value) { |
221
|
|
|
if ( |
222
|
|
|
$value instanceof DataTransferObject |
223
|
|
|
|| $value instanceof DataTransferObjectCollection |
224
|
|
|
) { |
225
|
|
|
$array[$key] = $value->toArray(); |
226
|
|
|
|
227
|
|
|
continue; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
if (! is_array($value)) { |
231
|
|
|
continue; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
$array[$key] = $this->parseArray($value); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
return $array; |
238
|
|
|
} |
239
|
|
|
} |
240
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.