Passed
Push — develop ( 1e5fa4...fa47fb )
by nguereza
02:53
created

Repository::limit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 6
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   http://www.iacademy.cf
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
 */
66
class Repository implements RepositoryInterface
67
{
0 ignored issues
show
Coding Style introduced by
Opening brace must not be followed by a blank line
Loading history...
68
69
    /**
70
     * The entity class
71
     * @var class-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
72
     */
73
    protected string $entityClass;
74
75
    /**
76
     *
77
     * @var EntityManager
78
     */
79
    protected EntityManager $manager;
80
81
    /**
82
     * The list of relation to load with the query
83
     * @var array<int, string>|array<string, Closure>
84
     */
85
    protected array $with = [];
86
87
    /**
88
     * Whether need load relation data immediately
89
     * @var bool
90
     */
91
    protected bool $immediate = false;
92
93
    /**
94
     * The order by column(s)
95
     * @var string|Closure|Expression|string[]|Expression[]|Closure[]
96
     */
97
    protected $orderColumns = '';
98
99
    /**
100
     * The order direction
101
     * @var string
102
     */
103
    protected string $orderDir = 'ASC';
104
105
    /**
106
     * The offset to use
107
     * @var int
108
     */
109
    protected int $offset = -1;
110
111
    /**
112
     * The limit to use
113
     * @var int
114
     */
115
    protected int $limit = 0;
116
117
    /**
118
     * The filters list
119
     * @var array<string, mixed>
120
     */
121
    protected array $filters = [];
122
123
    /**
124
     * Create new instance
125
     * @param EntityManager $manager
126
     * @param class-string $entityClass
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
127
     */
128
    public function __construct(EntityManager $manager, string $entityClass)
129
    {
130
        $this->manager = $manager;
131
        $this->entityClass = $entityClass;
132
    }
133
134
    /**
135
     * {@inheritedoc}
136
     */
137
    public function query($with = [], bool $immediate = false): EntityQuery
138
    {
139
        if (empty($with) && !empty($this->with)) {
140
            $with = $this->with;
141
142
            $this->with = [];
143
        }
144
        $this->immediate = false;
145
146
        $query = $this->manager->query($this->entityClass);
147
        $query->with($with, $immediate);
148
149
        if (!empty($this->orderColumns)) {
150
            $query->orderBy($this->orderColumns, $this->orderDir);
151
152
            $this->orderColumns = '';
153
            $this->orderDir = 'ASC';
154
        }
155
156
        if ($this->offset >= 0 && $this->limit >= 0) {
157
            $query->offset($this->offset)
158
                  ->limit($this->limit);
159
160
            $this->offset = -1;
161
            $this->limit = 0;
162
        }
163
164
        if (!empty($this->filters)) {
165
            $query->filter($this->filters);
166
167
            $this->filters = [];
168
        }
169
170
        return $query;
171
    }
172
173
    /**
174
     * {@inheritedoc}
175
     */
176
    public function with($with, bool $immediate = false): self
177
    {
178
        if (!is_array($with)) {
179
            $with = [$with];
180
        }
181
        $this->with = $with;
182
        $this->immediate = $immediate;
183
184
        return $this;
185
    }
186
187
    /**
188
     * {@inheritedoc}
189
     */
190
    public function orderBy($columns, string $order = 'ASC'): self
191
    {
192
        $this->orderColumns = $columns;
193
        $this->orderDir = $order;
194
195
        return $this;
196
    }
197
198
    /**
199
     * {@inheritedoc}
200
     */
201
    public function limit(int $offset, int $limit): self
202
    {
203
        $this->offset = $offset;
204
        $this->limit = $limit;
205
206
        return $this;
207
    }
208
209
    /**
210
     * {@inheritedoc}
211
     */
212
    public function filters(array $filters = []): self
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($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(...$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): bool
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)
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)) {
0 ignored issues
show
introduced by
The condition is_array($primaryKeys) is always true.
Loading history...
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