Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.24% covered (warning)
73.24%
104 / 142
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
WriteMapper
73.24% covered (warning)
73.24%
104 / 142
25.00% covered (danger)
25.00%
2 / 8
122.47
0.00% covered (danger)
0.00%
0 / 1
 create
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 execute
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 executeCreate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 createModel
71.11% covered (warning)
71.11%
32 / 45
0.00% covered (danger)
0.00%
0 / 1
20.42
 createOwnsOne
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 createBelongsTo
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
6.20
 createHasMany
63.04% covered (warning)
63.04%
29 / 46
0.00% covered (danger)
0.00%
0 / 1
34.35
 createRelationTable
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
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 */
13declare(strict_types=1);
14
15namespace phpOMS\DataStorage\Database\Mapper;
16
17use phpOMS\DataStorage\Database\Exception\InvalidMapperException;
18use phpOMS\DataStorage\Database\Query\Builder;
19use phpOMS\DataStorage\Database\Query\QueryType;
20use 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 */
30final 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}