Repository::find()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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
     * @return self<TEntity>
166
     */
167
    public function with(string|array $with, bool $immediate = false): self
168
    {
169
        if (!is_array($with)) {
0 ignored issues
show
introduced by
The condition is_array($with) is always true.
Loading history...
170
            $with = [$with];
171
        }
172
        $this->with = $with;
173
        $this->immediate = $immediate;
174
175
        return $this;
176
    }
177
178
    /**
179
     * {@inheritedoc}
180
     * @return self<TEntity>
181
     */
182
    public function orderBy(
183
        string|Closure|Expression|array $columns,
184
        string $order = 'ASC'
185
    ): self {
186
        $this->orderColumns = $columns;
187
        $this->orderDir = $order;
188
189
        return $this;
190
    }
191
192
    /**
193
     * {@inheritedoc}
194
     * @return self<TEntity>
195
     */
196
    public function limit(int $offset, int $limit): self
197
    {
198
        $this->offset = $offset;
199
        $this->limit = $limit;
200
201
        return $this;
202
    }
203
204
    /**
205
     * {@inheritedoc}
206
     * @return self<TEntity>
207
     */
208
    public function filters(string|array $filters = []): self
209
    {
210
        if (is_string($filters)) {
0 ignored issues
show
introduced by
The condition is_string($filters) is always false.
Loading history...
211
            $filters = [$filters => true];
212
        }
213
214
        $this->filters = $filters;
215
216
        return $this;
217
    }
218
219
    /**
220
     * {@inheritedoc}
221
     */
222
    public function all(array $columns = []): array
223
    {
224
        return $this->query()->all($columns);
225
    }
226
227
    /**
228
     * {@inheritedoc}
229
     */
230
    public function create(array $columns = []): Entity
231
    {
232
        $mapper = $this->manager->getEntityMapper($this->entityClass);
233
234
        return new $this->entityClass(
235
            $this->manager,
236
            $mapper,
237
            $columns,
238
            [],
239
            false,
240
            true
241
        );
242
    }
243
244
    /**
245
     * {@inheritedoc}
246
     */
247
    public function find(array|string|int|float $id): ?Entity
248
    {
249
        return $this->query()->find($id);
250
    }
251
252
    /**
253
     * {@inheritedoc}
254
     */
255
    public function findBy(array $conditions): ?Entity
256
    {
257
        $query = $this->query();
258
        foreach ($conditions as $name => $value) {
259
            $query->where($name)->is($value);
260
        }
261
        return $query->get();
262
    }
263
264
    /**
265
     * {@inheritedoc}
266
     */
267
    public function findAll(mixed ...$ids): array
268
    {
269
        return $this->query()->findAll(...$ids);
270
    }
271
272
    /**
273
     * {@inheritedoc}
274
     */
275
    public function findAllBy(array $conditions): array
276
    {
277
        $query = $this->query();
278
        foreach ($conditions as $name => $value) {
279
            $query->where($name)->is($value);
280
        }
281
        return $query->all();
282
    }
283
284
    /**
285
     * {@inheritedoc}
286
     */
287
    public function save(Entity $entity): array|string|int|float|bool|null
288
    {
289
        $data = Proxy::instance()->getEntityDataMapper($entity);
290
291
        if ($data->isNew()) {
292
            return (bool) $this->insert($entity);
293
        }
294
295
        return $this->update($entity);
296
    }
297
298
    /**
299
     * {@inheritedoc}
300
     */
301
    public function insert(Entity $entity): array|string|int|float|false|null
302
    {
303
        $data = Proxy::instance()->getEntityDataMapper($entity);
304
        $mapper = $data->getEntityMapper();
305
        $eventsHandlers = $mapper->getEventHandlers();
306
307
        if ($data->isDeleted()) {
308
            throw new EntityStateException('The record was deleted');
309
        }
310
311
        if (!$data->isNew()) {
312
            throw new EntityStateException('The record was already saved');
313
        }
314
315
        $connection = $this->manager->getConnection();
316
        $id = $connection->transaction(function (Connection $connection) use ($data, $mapper) {
317
            $columns = $data->getRawColumns();
318
            $pkGenerator = $mapper->getPrimaryKeyGenerator();
319
            if ($pkGenerator !== null) {
320
                $pkData = $pkGenerator($data);
321
322
                if (is_array($pkData)) {
323
                    foreach ($pkData as $pkColumn => $pkValue) {
324
                        $columns[$pkColumn] = $pkValue;
325
                    }
326
                } else {
327
                    $pkColumns = $mapper->getPrimaryKey()->columns();
328
                    $columns[$pkColumns[0]] = $pkData;
329
                }
330
            }
331
332
            if ($mapper->hasTimestamp()) {
333
                list($createdAtCol, $updatedAtCol) = $mapper->getTimestampColumns();
334
                $columns[$createdAtCol] = date($this->manager->getDateFormat());
335
                $columns[$updatedAtCol] = null;
336
            }
337
338
            (new Insert($connection))->insert($columns)->into($mapper->getTable());
339
340
            if ($pkGenerator !== null) {
341
                return isset($pkData) ? $pkData : false;
342
            }
343
344
            return $connection->getPDO()->lastInsertId($mapper->getSequence());
345
        });
346
347
        if ($id === false) {
348
            return false;
349
        }
350
351
        $data->markAsSaved($id);
352
353
        if (isset($eventsHandlers['save'])) {
354
            foreach ($eventsHandlers['save'] as $callback) {
355
                $callback($entity, $data);
356
            }
357
        }
358
359
        return $id;
360
    }
361
362
    /**
363
     * {@inheritedoc}
364
     */
365
    public function update(Entity $entity): bool
366
    {
367
        $data = Proxy::instance()->getEntityDataMapper($entity);
368
        $mapper = $data->getEntityMapper();
369
        $eventsHandlers = $mapper->getEventHandlers();
370
371
        if ($data->isDeleted()) {
372
            throw new EntityStateException('The record was deleted');
373
        }
374
375
        if ($data->isNew()) {
376
            throw new EntityStateException('Can\'t update an unsaved entity');
377
        }
378
379
        if (!$data->wasModified()) {
380
            return true;
381
        }
382
383
        $modified = $data->getModifiedColumns();
384
        if (!empty($modified)) {
385
            $connection = $this->manager->getConnection();
386
            $result = $connection->transaction(function (Connection $connection) use ($data, $mapper, $modified) {
387
                $columns = array_intersect_key($data->getRawColumns(), array_flip($modified));
388
389
                $updatedAt = null;
390
391
                if ($mapper->hasTimestamp()) {
392
                    list(, $updatedAtCol) = $mapper->getTimestampColumns();
393
                    $columns[$updatedAtCol] = $updatedAt = date($this->manager->getDateFormat());
394
                }
395
                $data->markAsUpdated($updatedAt);
396
397
                $update = new Update($connection, $mapper->getTable());
398
399
                $primaryKeys = $mapper->getPrimaryKey()->getValue($data->getRawColumns(), true);
400
                if (is_array($primaryKeys)) {
401
                    foreach ($primaryKeys as $pkColumn => $pkValue) {
402
                        $update->where($pkColumn)->is($pkValue);
403
                    }
404
                }
405
406
                return $update->set($columns) >= 0;
407
            });
408
409
            if ($result === false) {
410
                return false;
411
            }
412
413
            if (isset($eventsHandlers['update'])) {
414
                foreach ($eventsHandlers['update'] as $callback) {
415
                    $callback($entity, $data);
416
                }
417
            }
418
419
            return true;
420
        }
421
422
        $connection = $this->manager->getConnection();
423
        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

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