Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.25% covered (warning)
81.25%
91 / 112
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateMapper
81.25% covered (warning)
81.25%
91 / 112
37.50% covered (danger)
37.50%
3 / 8
71.52
0.00% covered (danger)
0.00%
0 / 1
 update
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 execute
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 executeUpdate
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 updateModel
84.85% covered (warning)
84.85%
28 / 33
0.00% covered (danger)
0.00%
0 / 1
20.26
 updateBelongsTo
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 updateOwnsOne
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 updateHasMany
67.65% covered (warning)
67.65%
23 / 34
0.00% covered (danger)
0.00%
0 / 1
22.62
 updateRelationTable
86.96% covered (warning)
86.96%
20 / 23
0.00% covered (danger)
0.00%
0 / 1
9.18
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\Utils\ArrayUtils;
20
21/**
22 * Update mapper (CREATE).
23 *
24 * @package phpOMS\DataStorage\Database\Mapper
25 * @license OMS License 2.0
26 * @link    https://jingga.app
27 * @since   1.0.0
28 */
29final class UpdateMapper extends DataMapperAbstract
30{
31    /**
32     * Create update mapper
33     *
34     * @return self
35     *
36     * @since 1.0.0
37     */
38    public function update() : self
39    {
40        $this->type = MapperType::UPDATE;
41
42        return $this;
43    }
44
45    /**
46     * Execute mapper
47     *
48     * @param mixed ...$options Options to pass to update mapper
49     *
50     * @return mixed
51     *
52     * @since 1.0.0
53     */
54    public function execute(mixed ...$options) : mixed
55    {
56        switch($this->type) {
57            case MapperType::UPDATE:
58                /** @var object ...$options */
59                return $this->executeUpdate(...$options);
60            default:
61                return null;
62        }
63    }
64
65    /**
66     * Execute mapper
67     *
68     * @param object $obj Object to update
69     *
70     * @return mixed
71     *
72     * @since 1.0.0
73     */
74    public function executeUpdate(object $obj) : mixed
75    {
76        $refClass = new \ReflectionClass($obj);
77        $objId    = $this->mapper::getObjectId($obj);
78
79        if ($this->mapper::isNullModel($obj)) {
80            return $objId === 0 ? null : $objId;
81        }
82
83        $this->updateHasMany($refClass, $obj, $objId);
84
85        if (empty($objId)) {
86            return $this->mapper::create(db: $this->db)->execute($obj);
87        }
88
89        $this->updateModel($obj, $objId, $refClass);
90
91        return $objId;
92    }
93
94    /**
95     * Update model
96     *
97     * @param object           $obj      Object to update
98     * @param mixed            $objId    Id of the object to update
99     * @param \ReflectionClass $refClass Reflection of the object ot update
100     *
101     * @return void
102     *
103     * @since 1.0.0
104     */
105    private function updateModel(object $obj, mixed $objId, \ReflectionClass $refClass = null) : void
106    {
107        try {
108            // Model doesn't have anything to update
109            if (\count($this->mapper::COLUMNS) < 2) {
110                return;
111            }
112
113            $query = new Builder($this->db);
114            $query->update($this->mapper::TABLE)
115                ->where($this->mapper::TABLE . '.' . $this->mapper::PRIMARYFIELD, '=', $objId);
116
117            foreach ($this->mapper::COLUMNS as $column) {
118                $propertyName = \stripos($column['internal'], '/') !== false ? \explode('/', $column['internal'])[0] : $column['internal'];
119                if (isset($this->mapper::HAS_MANY[$propertyName])
120                    || $column['internal'] === $this->mapper::PRIMARYFIELD
121                    || (($column['readonly'] ?? false) && !isset($this->with[$propertyName]))
122                    || (($column['writeonly'] ?? false) && !isset($this->with[$propertyName]))
123                ) {
124                    continue;
125                }
126
127                $refClass = $refClass ?? new \ReflectionClass($obj);
128                $property = $refClass->getProperty($propertyName);
129
130                $tValue = $property->isPublic() ? $obj->{$propertyName} : $property->getValue($obj);
131
132                if (isset($this->mapper::OWNS_ONE[$propertyName])) {
133                    $id    = \is_object($tValue) ? $this->updateOwnsOne($propertyName, $tValue) : $tValue;
134                    $value = $this->parseValue($column['type'], $id);
135
136                    $query->set([$column['name'] => $value]);
137                } elseif (isset($this->mapper::BELONGS_TO[$propertyName])) {
138                    $id    = \is_object($tValue) ? $this->updateBelongsTo($propertyName, $tValue) : $tValue;
139                    $value = $this->parseValue($column['type'], $id);
140
141                    $query->set([$column['name'] => $value]);
142                } elseif ($column['name'] !== $this->mapper::PRIMARYFIELD) {
143                    if (\stripos($column['internal'], '/') !== false) {
144                        $path   = \substr($column['internal'], \stripos($column['internal'], '/') + 1);
145                        $tValue = ArrayUtils::getArray($path, $tValue, '/');
146                    }
147
148                    $value = $this->parseValue($column['type'], $tValue);
149
150                    $query->set([$column['name'] => $value]);
151                }
152            }
153
154            // @todo:
155            // @bug: Sqlite doesn't allow table_name.column_name in set queries for whatver reason.
156
157            $sth = $this->db->con->prepare($query->toSql());
158            if ($sth !== false) {
159                $sth->execute();
160            }
161        } catch (\Throwable $t) {
162            // @codeCoverageIgnoreStart
163            \phpOMS\Log\FileLogger::getInstance()->error(
164                \phpOMS\Log\FileLogger::MSG_FULL, [
165                    'message' => $t->getMessage() . ':' . $query->toSql(),
166                    'line'    => __LINE__,
167                    'file'    => self::class,
168                ]
169            );
170            // @codeCoverageIgnoreEnd
171        }
172    }
173
174    /**
175     * Update belongs to
176     *
177     * @param string $propertyName Name of the property to update
178     * @param object $obj          Object to update
179     *
180     * @return mixed
181     *
182     * @since 1.0.0
183     */
184    private function updateBelongsTo(string $propertyName, object $obj) : mixed
185    {
186        /** @var class-string<DataMapperFactory> $mapper */
187        $mapper = $this->mapper::BELONGS_TO[$propertyName]['mapper'];
188
189        /** @var self $relMapper */
190        $relMapper        = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName);
191        $relMapper->depth = $this->depth + 1;
192
193        return $relMapper->execute($obj);
194    }
195
196    /**
197     * Update owns one
198     *
199     * @param string $propertyName Name of the property to update
200     * @param object $obj          Object to update
201     *
202     * @return mixed
203     *
204     * @since 1.0.0
205     */
206    private function updateOwnsOne(string $propertyName, object $obj) : mixed
207    {
208        /** @var class-string<DataMapperFactory> $mapper */
209        $mapper = $this->mapper::OWNS_ONE[$propertyName]['mapper'];
210
211        /** @var self $relMapper */
212        $relMapper        = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName);
213        $relMapper->depth = $this->depth + 1;
214
215        return $relMapper->execute($obj);
216    }
217
218    /**
219     * Update has many relations
220     *
221     * @param \ReflectionClass $refClass Reflection of the object containing the relations
222     * @param object           $obj      Object which contains the relations
223     * @param mixed            $objId    Object id which contains the relations
224     *
225     * @return void
226     *
227     * @throws InvalidMapperException
228     *
229     * @since 1.0.0
230     */
231    private function updateHasMany(\ReflectionClass $refClass, object $obj, mixed $objId) : void
232    {
233        if (empty($this->with) || empty($this->mapper::HAS_MANY)) {
234            return;
235        }
236
237        $objsIds = [];
238
239        foreach ($this->mapper::HAS_MANY as $propertyName => $rel) {
240            if ($rel['readonly'] ?? false === true) {
241                continue;
242            }
243
244            if (!isset($this->mapper::HAS_MANY[$propertyName]['mapper'])) {
245                throw new InvalidMapperException();
246            }
247
248            $property = $refClass->getProperty($propertyName);
249
250            $values = ($isPublic = $property->isPublic()) ? $obj->{$propertyName} : $property->getValue($obj);
251
252            if (!\is_array($values) || empty($values)) {
253                continue;
254            }
255
256            /** @var class-string<DataMapperFactory> $mapper */
257            $mapper                 = $this->mapper::HAS_MANY[$propertyName]['mapper'];
258            $relReflectionClass     = new \ReflectionClass(\reset($values));
259            $objsIds[$propertyName] = [];
260
261            foreach ($values as $key => &$value) {
262                if (!\is_object($value)) {
263                    // Is scalar => already in database
264                    $objsIds[$propertyName][$key] = $value;
265
266                    continue;
267                }
268
269                $primaryKey = $mapper::getObjectId($value);
270
271                // already in db
272                if (!empty($primaryKey)) {
273                    /** @var self $relMapper */
274                    $relMapper        = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName);
275                    $relMapper->depth = $this->depth + 1;
276
277                    $relMapper->execute($value);
278
279                    $objsIds[$propertyName][$key] = $primaryKey;
280
281                    continue;
282                }
283
284                // create if not existing
285                if ($this->mapper::HAS_MANY[$propertyName]['table'] === $this->mapper::HAS_MANY[$propertyName]['mapper']::TABLE
286                    && isset($mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']])
287                ) {
288                    $relProperty = $relReflectionClass->getProperty($mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]['internal']);
289
290                    if (!$isPublic) {
291                        $relProperty->setValue($value, $objId);
292                    } else {
293                        $value->{$mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]['internal']} = $objId;
294                    }
295                }
296
297                $objsIds[$propertyName][$key] = $mapper::create(db: $this->db)->execute($value);
298            }
299        }
300
301        $this->updateRelationTable($objsIds, $objId);
302    }
303
304    /**
305     * Update has many relations if the relation is handled in a relation table
306     *
307     * @param array $objsIds Objects which should be related to the parent object
308     * @param mixed $objId   Parent object id
309     *
310     * @return void
311     *
312     * @since 1.0.0
313     */
314    private function updateRelationTable(array $objsIds, mixed $objId) : void
315    {
316        foreach ($this->mapper::HAS_MANY as $member => $many) {
317            if (isset($many['column']) || !isset($this->with[$member])) {
318                continue;
319            }
320
321            $query = new Builder($this->db);
322            $src   = $many['external'] ?? $many['mapper']::PRIMARYFIELD;
323
324            // @todo: what if a specific column name is defined instead of primaryField for the join? Fix, it should be stored in 'column'
325            $query->select($many['table'] . '.' . $src)
326                ->from($many['table'])
327                ->where($many['table'] . '.' . $many['self'], '=', $objId);
328
329            if ($many['table'] !== $many['mapper']::TABLE) {
330                $query->leftJoin($many['mapper']::TABLE)
331                    ->on($many['table'] . '.' . $src, '=', $many['mapper']::TABLE . '.' . $many['mapper']::PRIMARYFIELD);
332            }
333
334            $sth = $this->db->con->prepare($query->toSql());
335            if ($sth === false) {
336                continue;
337            }
338
339            $sth->execute();
340            $result = $sth->fetchAll(\PDO::FETCH_COLUMN);
341
342            if ($result === false) {
343                return; // @codeCoverageIgnore
344            }
345
346            $removes = \array_diff($result, \array_values($objsIds[$member] ?? []));
347            $adds    = \array_diff(\array_values($objsIds[$member] ?? []), $result);
348
349            if (!empty($removes)) {
350                $this->mapper::remover(db: $this->db)->deleteRelationTable($member, $removes, $objId);
351            }
352
353            if (!empty($adds)) {
354                $this->mapper::writer(db: $this->db)->createRelationTable($member, $adds, $objId);
355            }
356        }
357    }
358}