Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
73.24% |
104 / 142 |
|
25.00% |
2 / 8 |
CRAP | |
0.00% |
0 / 1 |
WriteMapper | |
73.24% |
104 / 142 |
|
25.00% |
2 / 8 |
122.47 | |
0.00% |
0 / 1 |
create | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
executeCreate | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
createModel | |
71.11% |
32 / 45 |
|
0.00% |
0 / 1 |
20.42 | |||
createOwnsOne | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
createBelongsTo | |
63.64% |
7 / 11 |
|
0.00% |
0 / 1 |
6.20 | |||
createHasMany | |
63.04% |
29 / 46 |
|
0.00% |
0 / 1 |
34.35 | |||
createRelationTable | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
8.10 |
1 | <?php |
2 | /** |
3 | * Jingga |
4 | * |
5 | * PHP Version 8.1 |
6 | * |
7 | * @package phpOMS\DataStorage\Database\Mapper |
8 | * @copyright Dennis Eichhorn |
9 | * @license OMS License 2.0 |
10 | * @version 1.0.0 |
11 | * @link https://jingga.app |
12 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\DataStorage\Database\Mapper; |
16 | |
17 | use phpOMS\DataStorage\Database\Exception\InvalidMapperException; |
18 | use phpOMS\DataStorage\Database\Query\Builder; |
19 | use phpOMS\DataStorage\Database\Query\QueryType; |
20 | use phpOMS\Utils\ArrayUtils; |
21 | |
22 | /** |
23 | * Write mapper (CREATE). |
24 | * |
25 | * @package phpOMS\DataStorage\Database\Mapper |
26 | * @license OMS License 2.0 |
27 | * @link https://jingga.app |
28 | * @since 1.0.0 |
29 | */ |
30 | final class WriteMapper extends DataMapperAbstract |
31 | { |
32 | /** |
33 | * Create create mapper |
34 | * |
35 | * @return self |
36 | * |
37 | * @since 1.0.0 |
38 | */ |
39 | public function create() : self |
40 | { |
41 | $this->type = MapperType::CREATE; |
42 | |
43 | return $this; |
44 | } |
45 | |
46 | /** |
47 | * Execute mapper |
48 | * |
49 | * @param mixed ...$options Model to create |
50 | * |
51 | * @return mixed |
52 | * |
53 | * @since 1.0.0 |
54 | */ |
55 | public function execute(mixed ...$options) : mixed |
56 | { |
57 | switch($this->type) { |
58 | case MapperType::CREATE: |
59 | /** @var object ...$options */ |
60 | return $this->executeCreate(...$options); |
61 | default: |
62 | return null; |
63 | } |
64 | } |
65 | |
66 | /** |
67 | * Create object |
68 | * |
69 | * @param object $obj Object to create |
70 | * |
71 | * @return mixed |
72 | * |
73 | * @since 1.0.0 |
74 | */ |
75 | public function executeCreate(object $obj) : mixed |
76 | { |
77 | $refClass = new \ReflectionClass($obj); |
78 | |
79 | if ($this->mapper::isNullModel($obj)) { |
80 | $objId = $this->mapper::getObjectId($obj); |
81 | |
82 | return $objId === 0 ? null : $objId; |
83 | } |
84 | |
85 | if (!empty($id = $this->mapper::getObjectId($obj)) && $this->mapper::AUTOINCREMENT) { |
86 | $objId = $id; |
87 | } else { |
88 | $objId = $this->createModel($obj, $refClass); |
89 | $this->mapper::setObjectId($refClass, $obj, $objId); |
90 | } |
91 | |
92 | $this->createHasMany($refClass, $obj, $objId); |
93 | |
94 | return $objId; |
95 | } |
96 | |
97 | /** |
98 | * Create model |
99 | * |
100 | * @param object $obj Object to create |
101 | * @param \ReflectionClass $refClass Reflection of the object to create |
102 | * |
103 | * @return mixed |
104 | * |
105 | * @since 1.0.0 |
106 | */ |
107 | private function createModel(object $obj, \ReflectionClass $refClass) : mixed |
108 | { |
109 | try { |
110 | $query = new Builder($this->db); |
111 | $query->into($this->mapper::TABLE); |
112 | |
113 | $publicProperties = \get_object_vars($obj); |
114 | |
115 | foreach ($this->mapper::COLUMNS as $column) { |
116 | $propertyName = \stripos($column['internal'], '/') !== false |
117 | ? \explode('/', $column['internal'])[0] |
118 | : $column['internal']; |
119 | |
120 | if (isset($this->mapper::HAS_MANY[$propertyName]) |
121 | || ($column['name'] === $this->mapper::PRIMARYFIELD && $this->mapper::AUTOINCREMENT) |
122 | ) { |
123 | continue; |
124 | } |
125 | |
126 | if (!isset($publicProperties[$propertyName])) { |
127 | $property = $refClass->getProperty($propertyName); |
128 | $property->setAccessible(true); |
129 | $tValue = $property->getValue($obj); |
130 | $property->setAccessible(false); |
131 | } else { |
132 | $tValue = $publicProperties[$propertyName]; |
133 | } |
134 | |
135 | if (isset($this->mapper::OWNS_ONE[$propertyName])) { |
136 | $id = \is_object($tValue) ? $this->createOwnsOne($propertyName, $tValue) : $tValue; |
137 | $value = $this->parseValue($column['type'], $id); |
138 | |
139 | $query->insert($column['name'])->value($value); |
140 | } elseif (isset($this->mapper::BELONGS_TO[$propertyName])) { |
141 | $id = \is_object($tValue) ? $this->createBelongsTo($propertyName, $tValue) : $tValue; |
142 | $value = $this->parseValue($column['type'], $id); |
143 | |
144 | $query->insert($column['name'])->value($value); |
145 | } else { |
146 | if (\stripos($column['internal'], '/') !== false) { |
147 | /** @var array $tValue */ |
148 | $path = \substr($column['internal'], \stripos($column['internal'], '/') + 1); |
149 | $tValue = ArrayUtils::getArray($path, $tValue, '/'); |
150 | } |
151 | |
152 | $value = $this->parseValue($column['type'], $tValue); |
153 | |
154 | $query->insert($column['name'])->value($value); |
155 | } |
156 | } |
157 | |
158 | // if a table only has a single column = primary key column. This must be done otherwise the query is empty |
159 | if ($query->getType() === QueryType::NONE) { |
160 | $query->insert($this->mapper::PRIMARYFIELD)->value(0); |
161 | } |
162 | |
163 | $sth = $this->db->con->prepare($query->toSql()); |
164 | $sth->execute(); |
165 | |
166 | $objId = empty($id = $this->mapper::getObjectId($obj)) ? $this->db->con->lastInsertId() : $id; |
167 | \settype($objId, $this->mapper::COLUMNS[$this->mapper::PRIMARYFIELD]['type']); |
168 | |
169 | return $objId; |
170 | } catch (\Throwable $t) { |
171 | // @codeCoverageIgnoreStart |
172 | \phpOMS\Log\FileLogger::getInstance()->error( |
173 | \phpOMS\Log\FileLogger::MSG_FULL, [ |
174 | 'message' => $t->getMessage() . ':' . $query->toSql(), |
175 | 'line' => __LINE__, |
176 | 'file' => self::class, |
177 | ] |
178 | ); |
179 | |
180 | return -1; |
181 | // @codeCoverageIgnoreEND |
182 | } |
183 | } |
184 | |
185 | /** |
186 | * Create owns one model |
187 | * |
188 | * @param string $propertyName Name of the owns one property |
189 | * @param object $obj Object which contains the owns one model |
190 | * |
191 | * @return mixed |
192 | * |
193 | * @since 1.0.0 |
194 | */ |
195 | private function createOwnsOne(string $propertyName, object $obj) : mixed |
196 | { |
197 | if (!\is_object($obj)) { |
198 | return $obj; |
199 | } |
200 | |
201 | /** @var class-string<DataMapperFactory> $mapper */ |
202 | $mapper = $this->mapper::OWNS_ONE[$propertyName]['mapper']; |
203 | $primaryKey = $mapper::getObjectId($obj); |
204 | |
205 | if (empty($primaryKey)) { |
206 | return $mapper::create(db: $this->db)->execute($obj); |
207 | } |
208 | |
209 | return $primaryKey; |
210 | } |
211 | |
212 | /** |
213 | * Create belongs to model |
214 | * |
215 | * @param string $propertyName Name of the belongs to property |
216 | * @param object $obj Object which contains the belongs to model |
217 | * |
218 | * @return mixed |
219 | * |
220 | * @since 1.0.0 |
221 | */ |
222 | private function createBelongsTo(string $propertyName, object $obj) : mixed |
223 | { |
224 | if (!\is_object($obj)) { |
225 | return $obj; |
226 | } |
227 | |
228 | $mapper = ''; |
229 | $primaryKey = 0; |
230 | |
231 | if (isset($this->mapper::BELONGS_TO[$propertyName]['by'])) { |
232 | // has by (obj is stored as a different model e.g. model = profile but reference/db is account) |
233 | |
234 | $refClass = new \ReflectionClass($obj); |
235 | $refProp = $refClass->getProperty($this->mapper::BELONGS_TO[$propertyName]['by']); |
236 | |
237 | $obj = $refProp->isPublic() ? $obj->{$this->mapper::BELONGS_TO[$propertyName]['by']} : $refProp->getValue($obj); |
238 | } |
239 | |
240 | /** @var class-string<DataMapperFactory> $mapper */ |
241 | $mapper = $this->mapper::BELONGS_TO[$propertyName]['mapper']; |
242 | $primaryKey = $mapper::getObjectId($obj); |
243 | |
244 | // @todo: the $mapper::create() might cause a problem if 'by' is set. because we don't want to create this obj but the child obj. |
245 | return empty($primaryKey) ? $mapper::create(db: $this->db)->execute($obj) : $primaryKey; |
246 | } |
247 | |
248 | /** |
249 | * Create has many models |
250 | * |
251 | * @param \ReflectionClass $refClass Reflection of the object to create |
252 | * @param object $obj Object to create |
253 | * @param mixed $objId Id of the parent object |
254 | * |
255 | * @return void |
256 | * |
257 | * @throws InvalidMapperException |
258 | * |
259 | * @since 1.0.0 |
260 | */ |
261 | private function createHasMany(\ReflectionClass $refClass, object $obj, mixed $objId) : void |
262 | { |
263 | foreach ($this->mapper::HAS_MANY as $propertyName => $_) { |
264 | if (!isset($this->mapper::HAS_MANY[$propertyName]['mapper'])) { |
265 | throw new InvalidMapperException(); // @codeCoverageIgnore |
266 | } |
267 | |
268 | $property = $refClass->getProperty($propertyName); |
269 | $values = $property->isPublic() ? $obj->{$propertyName} : $property->getValue($obj); |
270 | |
271 | /** @var class-string<DataMapperFactory> $mapper */ |
272 | $mapper = $this->mapper::HAS_MANY[$propertyName]['mapper']; |
273 | $internalName = isset($mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]) |
274 | ? $mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]['internal'] |
275 | : 'ERROR'; |
276 | |
277 | if (\is_object($values)) { |
278 | // conditionals |
279 | $publicProperties = \get_object_vars($values); |
280 | |
281 | if (!isset($publicProperties[$internalName])) { |
282 | $relReflectionClass = new \ReflectionClass($values); |
283 | $relProperty = $relReflectionClass->getProperty($internalName); |
284 | |
285 | $relProperty->setAccessible(true); |
286 | $relProperty->setValue($values, $objId); |
287 | $relProperty->setAccessible(false); |
288 | } else { |
289 | $values->{$internalName} = $objId; |
290 | } |
291 | |
292 | $mapper::create(db: $this->db)->execute($values); |
293 | continue; |
294 | } elseif (!\is_array($values)) { |
295 | // @todo: conditionals??? |
296 | continue; |
297 | } |
298 | |
299 | $objsIds = []; |
300 | $relReflectionClass = empty($values) ? null : new \ReflectionClass(\reset($values)); |
301 | |
302 | foreach ($values as $key => $value) { |
303 | if (!\is_object($value)) { |
304 | // Is scalar => already in database |
305 | $objsIds[$key] = $value; |
306 | |
307 | continue; |
308 | } |
309 | |
310 | /** @var \ReflectionClass $relReflectionClass */ |
311 | $primaryKey = $mapper::getObjectId($value); |
312 | |
313 | // already in db |
314 | if (!empty($primaryKey)) { |
315 | $objsIds[$key] = $value; |
316 | |
317 | continue; |
318 | } |
319 | |
320 | // Setting relation value (id) for relation (since the relation is not stored in an extra relation table) |
321 | if (!isset($this->mapper::HAS_MANY[$propertyName]['external'])) { |
322 | $relProperty = $relReflectionClass->getProperty($internalName); |
323 | $isRelPublic = $relProperty->isPublic(); |
324 | |
325 | // todo maybe consider to just set the column type to object, and then check for that (might be faster) |
326 | if (isset($mapper::BELONGS_TO[$internalName]) |
327 | || isset($mapper::OWNS_ONE[$internalName])) { |
328 | if (!$isRelPublic) { |
329 | $relProperty->setValue($value, $this->mapper::createNullModel($objId)); |
330 | } else { |
331 | $value->{$internalName} = $this->mapper::createNullModel($objId); |
332 | } |
333 | } elseif (!$isRelPublic) { |
334 | $relProperty->setValue($value, $objId); |
335 | } else { |
336 | $value->{$internalName} = $objId; |
337 | } |
338 | |
339 | if (!$isRelPublic) { |
340 | $relProperty->setAccessible(false); |
341 | } |
342 | } |
343 | |
344 | $objsIds[$key] = $mapper::create(db: $this->db)->execute($value); |
345 | } |
346 | |
347 | $this->createRelationTable($propertyName, $objsIds, $objId); |
348 | } |
349 | } |
350 | |
351 | /** |
352 | * Create has many relations if the relation is handled in a relation table |
353 | * |
354 | * @param string $propertyName Property which contains the has many models |
355 | * @param array $objsIds Objects which should be related to the parent object |
356 | * @param mixed $objId Parent object id |
357 | * |
358 | * @return void |
359 | * |
360 | * @since 1.0.0 |
361 | */ |
362 | public function createRelationTable(string $propertyName, array $objsIds, mixed $objId) : void |
363 | { |
364 | try { |
365 | if (empty($objsIds) || !isset($this->mapper::HAS_MANY[$propertyName]['external'])) { |
366 | return; |
367 | } |
368 | |
369 | $relQuery = new Builder($this->db); |
370 | $relQuery->into($this->mapper::HAS_MANY[$propertyName]['table']) |
371 | ->insert($this->mapper::HAS_MANY[$propertyName]['external'], $this->mapper::HAS_MANY[$propertyName]['self']); |
372 | |
373 | foreach ($objsIds as $src) { |
374 | if (\is_object($src)) { |
375 | $mapper = (\stripos($mapper = \get_class($src), '\Null') !== false |
376 | ? \str_replace('\Null', '\\', $mapper) |
377 | : $mapper) |
378 | . 'Mapper'; |
379 | |
380 | $src = $mapper::getObjectId($src); |
381 | } |
382 | |
383 | $relQuery->values($src, $objId); |
384 | } |
385 | |
386 | $sth = $this->db->con->prepare($relQuery->toSql()); |
387 | if ($sth !== false) { |
388 | $sth->execute(); |
389 | } |
390 | } catch (\Throwable $t) { |
391 | // @codeCoverageIgnoreStart |
392 | \phpOMS\Log\FileLogger::getInstance()->error( |
393 | \phpOMS\Log\FileLogger::MSG_FULL, [ |
394 | 'message' => $t->getMessage() . ':' . $relQuery->toSql(), |
395 | 'line' => __LINE__, |
396 | 'file' => self::class, |
397 | ] |
398 | ); |
399 | // @codeCoverageIgnoreEnd |
400 | } |
401 | } |
402 | } |