Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
48 / 48 |
|
100.00% |
9 / 9 |
CRAP | |
100.00% |
1 / 1 |
DtoFactory | |
100.00% |
48 / 48 |
|
100.00% |
9 / 9 |
20 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
setDtoConstructorParamNames | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
makeDto | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
makePropertyMap | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
setPropertyMap | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPropertyMap | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getValueFromArray | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getValueFromEntity | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
makeArrayFromEntity | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * @author: Doug Wilbourne (dougwilbourne@gmail.com) |
5 | */ |
6 | |
7 | declare(strict_types=1); |
8 | |
9 | namespace pvc\struct\dto; |
10 | |
11 | use pvc\interfaces\struct\dto\DtoFactoryInterface; |
12 | use pvc\interfaces\struct\dto\DtoInterface; |
13 | use pvc\struct\dto\err\DtoClassDefinitionException; |
14 | use pvc\struct\dto\err\DtoInvalidArrayKeyException; |
15 | use pvc\struct\dto\err\DtoInvalidEntityGetterException; |
16 | use pvc\struct\dto\err\DtoInvalidPropertyNameException; |
17 | use pvc\struct\dto\err\InvalidDtoReflection; |
18 | use ReflectionClass; |
19 | use ReflectionException; |
20 | use ReflectionParameter; |
21 | use ReflectionProperty; |
22 | use Throwable; |
23 | |
24 | /** |
25 | * Class DtoFactory |
26 | * |
27 | * This class is a singleton. |
28 | * |
29 | * This is a dirt-simple mapper/factory, intended really to facilitate testing of data structures / objects such |
30 | * as those found in this library. Large frameworks and other standalone ORMs obviously have much more |
31 | * sophisticated functionality to get data in and out of the model. |
32 | * |
33 | * a Data Transfer Object (DTO) is a small step up from an associative array, maybe best thought of as an array whose |
34 | * values have prescribed data types. There should not be any logic in the DTO, it should not have |
35 | * setters and getters and all its properties should be public readonly. The property values should be assigned in |
36 | * the constructor. |
37 | * |
38 | * In order to isolate the data structure (model / entity) from the storage mechanism, this mapper provides the ability |
39 | * to move data from the model to a DTO and then from a DTO to an array, transposing property names and key names |
40 | * as necessary. |
41 | * |
42 | * usage: using this class, you can make a DTO from an array. The model can then suck the data from |
43 | * the (publicly accessible) properties and incorporate the DTO's data. In reverse, this mapper/factory can make |
44 | * a dto from a model class, or it can move the data from the model directly to an array. |
45 | * |
46 | * If your name of the property in the dto, the model and the array are all the same, then it is not necessary |
47 | * to create a property map for that property, assuming the getter in the model is in the camel case format |
48 | * "getPropertyName" - the code guesses the name of the getter based on the property name. On the other hand, if the |
49 | * getter does not conform to that practice or if the array key needs to be something different, then add a property |
50 | * map for that property. So the normal usage is 1) create the factory, 2) add property maps as necessary, 3) make |
51 | * your dtos. |
52 | * |
53 | * @template T of ReflectionClass |
54 | */ |
55 | class DtoFactory implements DtoFactoryInterface |
56 | { |
57 | /** |
58 | * @var ReflectionClass<DtoInterface> |
59 | */ |
60 | protected ReflectionClass $dtoReflection; |
61 | |
62 | /** |
63 | * @var ReflectionClass<object> |
64 | */ |
65 | protected ReflectionClass $entityReflection; |
66 | |
67 | /** |
68 | * @var array<string> |
69 | * the getParameters reflection method is guaranteed to return the parameters in the order in which they appear in |
70 | * the constructor. We also ensure that all the public properties of the dto appear in the constructor args. So we |
71 | * can use this array to create a new dto and populate its readonly properties. |
72 | */ |
73 | protected array $dtoConstructorParamNames; |
74 | |
75 | /** |
76 | * @var array<string, PropertyMap> |
77 | * string key is the property name of the dto that is being mapped to an entity property or array key |
78 | */ |
79 | protected array $propertyMappings; |
80 | |
81 | /** |
82 | * @param class-string<Dtointerface> $dtoClassName |
83 | * @param class-string<object> $entityClassName |
84 | * @throws InvalidDtoReflection |
85 | * @throws ReflectionException |
86 | */ |
87 | public function __construct(string $dtoClassName, string $entityClassName) { |
88 | $this->dtoReflection = new ReflectionClass($dtoClassName); |
89 | /** |
90 | * ensure it is a dto |
91 | */ |
92 | if (!$this->dtoReflection->implementsInterface(DtoInterface::class)) { |
93 | throw new InvalidDtoReflection($this->dtoReflection->getName()); |
94 | } |
95 | $this->entityReflection = new ReflectionClass($entityClassName); |
96 | $this->setDtoConstructorParamNames(); |
97 | } |
98 | |
99 | /** |
100 | * @return void |
101 | * @throws InvalidDtoReflection |
102 | * @throws ReflectionException |
103 | * checks to ensure that all the public properties are in the list of constructor args. |
104 | */ |
105 | protected function setDtoConstructorParamNames(): void |
106 | { |
107 | |
108 | $dtoPublicProperties = array_map( |
109 | function (ReflectionProperty $value): string { |
110 | return $value->getName(); |
111 | }, |
112 | $this->dtoReflection->getProperties(ReflectionProperty::IS_PUBLIC), |
113 | ); |
114 | |
115 | $reflectionParams = $this->dtoReflection->getConstructor()?->getParameters() ?? []; |
116 | $callback = function (ReflectionParameter $param) { return $param->getName(); }; |
117 | $constructorParams = array_map($callback, $reflectionParams); |
118 | if (array_diff($dtoPublicProperties, $constructorParams)) { |
119 | throw new DtoClassDefinitionException($this->dtoReflection->getName()); |
120 | } |
121 | |
122 | /** |
123 | * the getParameters method is guaranteed to return the parameters in the order in which they appear in the |
124 | * constructor. |
125 | */ |
126 | $this->dtoConstructorParamNames = $constructorParams; |
127 | } |
128 | |
129 | /** |
130 | * @param array<mixed>|object $source |
131 | * @return DtoInterface |
132 | * @throws DtoInvalidArrayKeyException |
133 | * @throws DtoInvalidEntityGetterException |
134 | * @throws ReflectionException |
135 | */ |
136 | public function makeDto(array|object $source): DtoInterface |
137 | { |
138 | $args = []; |
139 | foreach ($this->dtoConstructorParamNames as $paramName) { |
140 | $propertyMap = $this->getPropertyMap($paramName); |
141 | $args[$paramName] = is_array($source) ? |
142 | $this->getValueFromArray($source, $propertyMap) : |
143 | $this->getValueFromEntity($source, $propertyMap); |
144 | } |
145 | /** @var DtoInterface $dto */ |
146 | $dto = $this->dtoReflection->newInstanceArgs($args); |
147 | return $dto; |
148 | } |
149 | |
150 | /** |
151 | * @throws ReflectionException |
152 | * @throws DtoInvalidArrayKeyException |
153 | * @throws DtoInvalidEntityGetterException |
154 | */ |
155 | protected function makePropertyMap( |
156 | string $dtoPropertyName, |
157 | ?string $entityGetterMethodName = '', |
158 | string $arrayKeyName = '', |
159 | ): PropertyMap { |
160 | |
161 | if (!in_array($dtoPropertyName, $this->dtoConstructorParamNames)) { |
162 | throw new DtoInvalidPropertyNameException($dtoPropertyName); |
163 | } |
164 | |
165 | /** |
166 | * if the entityGetterClassName argument is empty, guess that the getter name follows the standard |
167 | * convention for naming getters. |
168 | */ |
169 | $entityGetterMethodName = empty($entityGetterMethodName) ? |
170 | 'get' . strtoupper(substr($dtoPropertyName, 0, 1)) . substr($dtoPropertyName, 1) : |
171 | $entityGetterMethodName; |
172 | |
173 | if (!$this->entityReflection->hasMethod($entityGetterMethodName)) { |
174 | throw new DtoInvalidEntityGetterException($entityGetterMethodName, $this->entityReflection->getName()); |
175 | } |
176 | |
177 | $arrayKeyName = empty($arrayKeyName) ? $dtoPropertyName : $arrayKeyName; |
178 | |
179 | return new PropertyMap($dtoPropertyName, $entityGetterMethodName, $arrayKeyName); |
180 | } |
181 | |
182 | public function setPropertyMap( string $dtoPropertyName, |
183 | ?string $entityGetterMethodName = '', |
184 | string $arrayKeyName = '', |
185 | ): void{ |
186 | $this->propertyMappings[$dtoPropertyName] = $this->makePropertyMap($dtoPropertyName, $entityGetterMethodName, $arrayKeyName); |
187 | } |
188 | |
189 | /** |
190 | * @param string $dtoPropertyName |
191 | * @return PropertyMap |
192 | * @throws DtoInvalidArrayKeyException |
193 | * @throws DtoInvalidEntityGetterException |
194 | * @throws ReflectionException |
195 | */ |
196 | public function getPropertyMap(string $dtoPropertyName): PropertyMap |
197 | { |
198 | return $this->propertyMappings[$dtoPropertyName] ?? $this->makePropertyMap($dtoPropertyName); |
199 | } |
200 | |
201 | /** |
202 | * @param array<mixed> $array |
203 | * @param PropertyMap $propertyMap |
204 | * @return mixed |
205 | * @throws DtoInvalidArrayKeyException |
206 | */ |
207 | protected function getValueFromArray(array $array, PropertyMap $propertyMap): mixed |
208 | { |
209 | if (!array_key_exists($propertyMap->arrayKeyName, $array)) { |
210 | throw new DtoInvalidArrayKeyException($propertyMap->arrayKeyName); |
211 | } else { |
212 | return $array[$propertyMap->arrayKeyName]; |
213 | } |
214 | } |
215 | |
216 | /** |
217 | * @param object $entity |
218 | * @param PropertyMap $propertyMap |
219 | * @return mixed |
220 | * @throws DtoInvalidEntityGetterException |
221 | */ |
222 | protected function getValueFromEntity(object $entity, PropertyMap $propertyMap): mixed |
223 | { |
224 | try { |
225 | $getterMethodName = $propertyMap->entityGetterMethodName; |
226 | return $entity->$getterMethodName(); |
227 | } catch (Throwable $e) { |
228 | throw new DtoInvalidEntityGetterException($propertyMap->entityGetterMethodName, get_class($entity)); |
229 | } |
230 | } |
231 | |
232 | /** |
233 | * @param object $entity |
234 | * @return array<mixed> |
235 | * @throws DtoInvalidArrayKeyException |
236 | * @throws DtoInvalidEntityGetterException |
237 | * @throws ReflectionException |
238 | */ |
239 | public function makeArrayFromEntity(object $entity): array |
240 | { |
241 | $array = []; |
242 | foreach ($this->dtoConstructorParamNames as $paramName) { |
243 | $propertyMap = $this->getPropertyMap($paramName); |
244 | $array[$propertyMap->arrayKeyName] = $this->getValueFromEntity($entity, $propertyMap); |
245 | } |
246 | return $array; |
247 | } |
248 | } |