Repository   D
last analyzed

Complexity

Total Complexity 59

Size/Duplication

Total Lines 438
Duplicated Lines 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
eloc 160
dl 0
loc 438
rs 4.08
c 5
b 1
f 0
wmc 59

19 Methods

Rating   Name   Duplication   Size   Complexity  
B query() 0 34 7
A __construct() 0 4 1
A findAllBy() 0 7 2
C insert() 0 59 12
A limit() 0 6 1
A findAll() 0 3 1
A findBy() 0 7 2
A find() 0 3 1
A orderBy() 0 8 1
A create() 0 11 1
B delete() 0 41 7
A save() 0 9 2
A all() 0 3 1
B update() 0 62 11
A filters() 0 9 2
A with() 0 9 2
A existIgnore() 0 5 2
A setFilters() 0 9 2
A exists() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Repository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Repository, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Platine ORM
5
 *
6
 * Platine ORM provides a flexible and powerful ORM implementing a data-mapper pattern.
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine ORM
11
 *
12
 * Permission is hereby granted, free of charge, to any person obtaining a copy
13
 * of this software and associated documentation files (the "Software"), to deal
14
 * in the Software without restriction, including without limitation the rights
15
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
 * copies of the Software, and to permit persons to whom the Software is
17
 * furnished to do so, subject to the following conditions:
18
 *
19
 * The above copyright notice and this permission notice shall be included in all
20
 * copies or substantial portions of the Software.
21
 *
22
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
 * SOFTWARE.
29
 */
30
31
/**
32
 *  @file Repository.php
33
 *
34
 *  The Repository class
35
 *
36
 *  @package    Platine\Orm
37
 *  @author Platine Developers Team
38
 *  @copyright  Copyright (c) 2020
39
 *  @license    http://opensource.org/licenses/MIT  MIT License
40
 *  @link   https://www.platine-php.com
41
 *  @version 1.0.0
42
 *  @filesource
43
 */
44
45
declare(strict_types=1);
46
47
namespace Platine\Orm;
48
49
use Closure;
50
use Platine\Database\Connection;
51
use Platine\Database\Query\Expression;
52
use Platine\Database\Query\Insert;
53
use Platine\Database\Query\Update;
54
use Platine\Orm\Entity;
55
use Platine\Orm\EntityManager;
56
use Platine\Orm\Exception\EntityStateException;
57
use Platine\Orm\Mapper\Proxy;
58
use Platine\Orm\Query\EntityQuery;
59
use Platine\Orm\RepositoryInterface;
60
61
62
/**
63
 * @class Repository
64
 * @package Platine\Orm
65
 * @template TEntity as Entity
66
 * @implements RepositoryInterface<TEntity>
67
 */
68
class Repository implements RepositoryInterface
69
{
70
    /**
71
     * The list of relation to load with the query
72
     * @var array<int, string>|array<string, Closure>
73
     */
74
    protected array $with = [];
75
76
    /**
77
     * Whether need load relation data immediately
78
     * @var bool
79
     */
80
    protected bool $immediate = false;
81
82
    /**
83
     * The order by column(s)
84
     * @var string|Closure|Expression|string[]|Expression[]|Closure[]
85
     */
86
    protected string|Closure|Expression|array $orderColumns = '';
87
88
    /**
89
     * The order direction
90
     * @var string
91
     */
92
    protected string $orderDir = 'ASC';
93
94
    /**
95
     * The offset to use
96
     * @var int
97
     */
98
    protected int $offset = -1;
99
100
    /**
101
     * The limit to use
102
     * @var int
103
     */
104
    protected int $limit = 0;
105
106
    /**
107
     * The filters list
108
     * @var array<string, mixed>
109
     */
110
    protected array $filters = [];
111
112
    /**
113
     * Create new instance
114
     * @param EntityManager<TEntity> $manager
115
     * @param class-string<TEntity> $entityClass
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<TEntity> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<TEntity>.
Loading history...
116
     */
117
    public function __construct(
118
        protected EntityManager $manager,
119
        protected string $entityClass
120
    ) {
121
    }
122
123
    /**
124
     * {@inheritedoc}
125
     */
126
    public function query(string|array $with = [], bool $immediate = false): EntityQuery
127
    {
128
        if (is_string($with)) {
0 ignored issues
show
introduced by
The condition is_string($with) is always false.
Loading history...
129
            $with = [$with];
130
        }
131
132
        if (empty($with) && count($this->with) > 0) {
133
            $with = $this->with;
134
135
            $this->with = [];
136
        }
137
        $this->immediate = false;
138
139
        $query = $this->manager->query($this->entityClass);
140
        $query->with($with, $immediate);
141
142
        if (!empty($this->orderColumns)) {
143
            $query->orderBy($this->orderColumns, $this->orderDir);
144
145
            $this->orderColumns = '';
146
            $this->orderDir = 'ASC';
147
        }
148
149
        if ($this->offset >= 0 && $this->limit >= 0) {
150
            $query->offset($this->offset)
151
                  ->limit($this->limit);
152
153
            $this->offset = -1;
154
            $this->limit = 0;
155
        }
156
157
        $this->setFilters($query);
158
159
        return $query;
160
    }
161
162
163
    /**
164
     * {@inheritedoc}
165
     */
166
    public function with(string|array $with, bool $immediate = false): self
167
    {
168
        if (!is_array($with)) {
0 ignored issues
show
introduced by
The condition is_array($with) is always true.
Loading history...
169
            $with = [$with];
170
        }
171
        $this->with = $with;
172
        $this->immediate = $immediate;
173
174
        return $this;
175
    }
176
177
    /**
178
     * {@inheritedoc}
179
     */
180
    public function orderBy(
181
        string|Closure|Expression|array $columns,
182
        string $order = 'ASC'
183
    ): self {
184
        $this->orderColumns = $columns;
185
        $this->orderDir = $order;
186
187
        return $this;
188
    }
189
190
    /**
191
     * {@inheritedoc}
192
     */
193
    public function limit(int $offset, int $limit): self
194
    {
195
        $this->offset = $offset;
196
        $this->limit = $limit;
197
198
        return $this;
199
    }
200
201
    /**
202
     * {@inheritedoc}
203
     */
204
    public function filters(string|array $filters = []): self
205
    {
206
        if (is_string($filters)) {
0 ignored issues
show
introduced by
The condition is_string($filters) is always false.
Loading history...
207
            $filters = [$filters => true];
208
        }
209
210
        $this->filters = $filters;
211
212
        return $this;
213
    }
214
215
    /**
216
     * {@inheritedoc}
217
     * @return TEntity[]
218
     */
219
    public function all(array $columns = []): array
220
    {
221
        return $this->query()->all($columns);
222
    }
223
224
    /**
225
     * {@inheritedoc}
226
     */
227
    public function create(array $columns = []): Entity
228
    {
229
        $mapper = $this->manager->getEntityMapper($this->entityClass);
230
231
        return new $this->entityClass(
232
            $this->manager,
233
            $mapper,
234
            $columns,
235
            [],
236
            false,
237
            true
238
        );
239
    }
240
241
    /**
242
     * {@inheritedoc}
243
     * @return TEntity|null
0 ignored issues
show
Bug introduced by
The type Platine\Orm\TEntity was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
244
     */
245
    public function find(array|string|int|float $id): ?Entity
246
    {
247
        return $this->query()->find($id);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->query()->find($id) also could return the type Platine\Orm\Entity which is incompatible with the documented return type Platine\Orm\TEntity|null.
Loading history...
248
    }
249
250
    /**
251
     * {@inheritedoc}
252
     */
253
    public function findBy(array $conditions): ?Entity
254
    {
255
        $query = $this->query();
256
        foreach ($conditions as $name => $value) {
257
            $query->where($name)->is($value);
258
        }
259
        return $query->get();
260
    }
261
262
    /**
263
     * {@inheritedoc}
264
     */
265
    public function findAll(mixed ...$ids): array
266
    {
267
        return $this->query()->findAll(...$ids);
268
    }
269
270
    /**
271
     * {@inheritedoc}
272
     */
273
    public function findAllBy(array $conditions): array
274
    {
275
        $query = $this->query();
276
        foreach ($conditions as $name => $value) {
277
            $query->where($name)->is($value);
278
        }
279
        return $query->all();
280
    }
281
282
    /**
283
     * {@inheritedoc}
284
     */
285
    public function save(Entity $entity): array|string|int|float|bool|null
286
    {
287
        $data = Proxy::instance()->getEntityDataMapper($entity);
288
289
        if ($data->isNew()) {
290
            return (bool) $this->insert($entity);
291
        }
292
293
        return $this->update($entity);
294
    }
295
296
    /**
297
     * {@inheritedoc}
298
     */
299
    public function insert(Entity $entity): array|string|int|float|false|null
300
    {
301
        $data = Proxy::instance()->getEntityDataMapper($entity);
302
        $mapper = $data->getEntityMapper();
303
        $eventsHandlers = $mapper->getEventHandlers();
304
305
        if ($data->isDeleted()) {
306
            throw new EntityStateException('The record was deleted');
307
        }
308
309
        if (!$data->isNew()) {
310
            throw new EntityStateException('The record was already saved');
311
        }
312
313
        $connection = $this->manager->getConnection();
314
        $id = $connection->transaction(function (Connection $connection) use ($data, $mapper) {
315
            $columns = $data->getRawColumns();
316
            $pkGenerator = $mapper->getPrimaryKeyGenerator();
317
            if ($pkGenerator !== null) {
318
                $pkData = $pkGenerator($data);
319
320
                if (is_array($pkData)) {
321
                    foreach ($pkData as $pkColumn => $pkValue) {
322
                        $columns[$pkColumn] = $pkValue;
323
                    }
324
                } else {
325
                    $pkColumns = $mapper->getPrimaryKey()->columns();
326
                    $columns[$pkColumns[0]] = $pkData;
327
                }
328
            }
329
330
            if ($mapper->hasTimestamp()) {
331
                list($createdAtCol, $updatedAtCol) = $mapper->getTimestampColumns();
332
                $columns[$createdAtCol] = date($this->manager->getDateFormat());
333
                $columns[$updatedAtCol] = null;
334
            }
335
336
            (new Insert($connection))->insert($columns)->into($mapper->getTable());
337
338
            if ($pkGenerator !== null) {
339
                return isset($pkData) ? $pkData : false;
340
            }
341
342
            return $connection->getPDO()->lastInsertId($mapper->getSequence());
343
        });
344
345
        if ($id === false) {
346
            return false;
347
        }
348
349
        $data->markAsSaved($id);
350
351
        if (isset($eventsHandlers['save'])) {
352
            foreach ($eventsHandlers['save'] as $callback) {
353
                $callback($entity, $data);
354
            }
355
        }
356
357
        return $id;
358
    }
359
360
    /**
361
     * {@inheritedoc}
362
     */
363
    public function update(Entity $entity): bool
364
    {
365
        $data = Proxy::instance()->getEntityDataMapper($entity);
366
        $mapper = $data->getEntityMapper();
367
        $eventsHandlers = $mapper->getEventHandlers();
368
369
        if ($data->isDeleted()) {
370
            throw new EntityStateException('The record was deleted');
371
        }
372
373
        if ($data->isNew()) {
374
            throw new EntityStateException('Can\'t update an unsaved entity');
375
        }
376
377
        if (!$data->wasModified()) {
378
            return true;
379
        }
380
381
        $modified = $data->getModifiedColumns();
382
        if (!empty($modified)) {
383
            $connection = $this->manager->getConnection();
384
            $result = $connection->transaction(function (Connection $connection) use ($data, $mapper, $modified) {
385
                $columns = array_intersect_key($data->getRawColumns(), array_flip($modified));
386
387
                $updatedAt = null;
388
389
                if ($mapper->hasTimestamp()) {
390
                    list(, $updatedAtCol) = $mapper->getTimestampColumns();
391
                    $columns[$updatedAtCol] = $updatedAt = date($this->manager->getDateFormat());
392
                }
393
                $data->markAsUpdated($updatedAt);
394
395
                $update = new Update($connection, $mapper->getTable());
396
397
                $primaryKeys = $mapper->getPrimaryKey()->getValue($data->getRawColumns(), true);
398
                if (is_array($primaryKeys)) {
399
                    foreach ($primaryKeys as $pkColumn => $pkValue) {
400
                        $update->where($pkColumn)->is($pkValue);
401
                    }
402
                }
403
404
                return $update->set($columns) >= 0;
405
            });
406
407
            if ($result === false) {
408
                return false;
409
            }
410
411
            if (isset($eventsHandlers['update'])) {
412
                foreach ($eventsHandlers['update'] as $callback) {
413
                    $callback($entity, $data);
414
                }
415
            }
416
417
            return true;
418
        }
419
420
        $connection = $this->manager->getConnection();
421
        return $connection->transaction(function (Connection $connection) use ($data) {
0 ignored issues
show
Unused Code introduced by
The parameter $connection is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

421
        return $connection->transaction(function (/** @scrutinizer ignore-unused */ Connection $connection) use ($data) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
422
            $data->executePendingLinkage();
423
424
            return true;
425
        });
426
    }
427
428
    /**
429
     * {@inheritedoc}
430
     */
431
    public function delete(Entity $entity, bool $force = false): bool
432
    {
433
        $data = Proxy::instance()->getEntityDataMapper($entity);
434
        $mapper = $data->getEntityMapper();
435
        $eventsHandlers = $mapper->getEventHandlers();
436
        $connection = $this->manager->getConnection();
437
438
        $result = $connection->transaction(function () use ($data, $mapper, $force) {
439
            if ($data->isDeleted()) {
440
                throw new EntityStateException('The record was deleted');
441
            }
442
443
            if ($data->isNew()) {
444
                throw new EntityStateException('Can\'t delete an unsaved entity');
445
            }
446
447
            $delete = new EntityQuery($this->manager, $mapper);
448
449
            foreach ($mapper->getPrimaryKey()->getValue($data->getRawColumns(), true) as $pkColumn => $pkValue) {
450
                $delete->where($pkColumn)->is($pkValue);
451
            }
452
453
            return (bool) $delete->delete($force);
454
        });
455
456
        if ($result === false) {
457
            return false;
458
        }
459
460
        if (isset($eventsHandlers['delete'])) {
461
            foreach ($eventsHandlers['delete'] as $callback) {
462
                $callback($entity, $data);
463
            }
464
        }
465
466
        //Note this need call after events handlers
467
        //because some handlers can't access
468
        //entity attributes after mark as delete
469
        $data->markAsDeleted();
470
471
        return true;
472
    }
473
474
    /**
475
     * {@inheritedoc}
476
     */
477
    public function exists(array $conditions): bool
478
    {
479
        return $this->findBy($conditions) !== null;
480
    }
481
482
    /**
483
     * {@inheritedoc}
484
     */
485
    public function existIgnore(array $conditions, mixed $value, string $field = 'id'): bool
486
    {
487
        $o = $this->findBy($conditions);
488
489
        return $o !== null && $o->{$field} != $value; // don't use ===
490
    }
491
492
    /**
493
     * Set the filters
494
     * @param EntityQuery<TEntity> $query
495
     * @return $this
496
     */
497
    protected function setFilters(EntityQuery $query): self
498
    {
499
        if (!empty($this->filters)) {
500
            $query->filter($this->filters);
501
502
            $this->filters = [];
503
        }
504
505
        return $this;
506
    }
507
}
508