Issues (56)

src/Mapper/EntityMapper.php (7 issues)

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 EntityMapper.php
33
 *
34
 *  The Entity Mapper class
35
 *
36
 *  @package    Platine\Orm\Mapper
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\Mapper;
48
49
use Platine\Orm\Relation\ForeignKey;
50
use Platine\Orm\Relation\PrimaryKey;
51
use Platine\Orm\Relation\Relation;
52
use Platine\Orm\Relation\RelationFactory;
53
54
/**
55
 * @class EntityMapper
56
 * @package Platine\Orm\Mapper
57
 * @template TEntity as \Platine\Orm\Entity
58
 * @implements EntityMapperInterface<TEntity>
59
 */
60
class EntityMapper implements EntityMapperInterface
61
{
62
    /**
63
     * The name of the entity
64
     * @var string
65
     */
66
    protected string $name = '';
67
68
    /**
69
     * The name of the table
70
     * @var string
71
     */
72
    protected string $table = '';
73
74
    /**
75
     * The name of the sequence
76
     * @var string|null
77
     */
78
    protected ?string $sequence = null;
79
80
    /**
81
     * The entity primary key
82
     * @var PrimaryKey<TEntity>|null
83
     */
84
    protected ?PrimaryKey $primaryKey = null;
85
86
    /**
87
     * The entity foreign keys
88
     * @var ForeignKey|null
89
     */
90
    protected ?ForeignKey $foreignKey = null;
91
92
    /**
93
     * The primary key generator callback
94
     * @var callable|null
95
     */
96
    protected $primaryKeyGenerator = null;
97
98
    /**
99
     * The list of columns getter
100
     * @var array<string, callable>
101
     */
102
    protected array $getters = [];
103
104
    /**
105
     * The list of columns setter
106
     * @var array<string, callable>
107
     */
108
    protected array $setters = [];
109
110
    /**
111
     * The list of columns casts
112
     * @var array<string, string>
113
     */
114
    protected array $casts = [];
115
116
    /**
117
     * The list of entity relations
118
     * @var array<string, Relation<TEntity>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, Relation<TEntity>> at position 4 could not be parsed: Expected '>' at position 4, but found 'Relation'.
Loading history...
119
     */
120
    protected array $relations = [];
121
122
    /**
123
     * The list of fillable columns
124
     * @var array<int, string>
125
     */
126
    protected array $fillable = [];
127
128
    /**
129
     * The list of guarded columns
130
     * @var array<int, string>
131
     */
132
    protected array $guarded = [];
133
134
    /**
135
     * The list of query filters
136
     * @var array<string, callable>
137
     */
138
    protected array $filters = [];
139
140
    /**
141
     * Whether to use soft delete
142
     * @var bool
143
     */
144
    protected bool $useSoftDelete = true;
145
146
    /**
147
     * The name of the soft delete column
148
     * @var string
149
     */
150
    protected string $softDeleteColumn = 'deleted_at';
151
152
    /**
153
     * Whether to use timestamp
154
     * @var bool
155
     */
156
    protected bool $useTimestamp = true;
157
158
    /**
159
     * The list of timestamp columns
160
     * @var array<int, string>
161
     */
162
    protected array $timestampColumns = ['created_at', 'updated_at'];
163
164
165
    /**
166
     * The list of events handlers
167
     * @var array<string, array<callable>>
168
     */
169
    protected array $eventHandlers = [];
170
171
172
    /**
173
     * Create new instance
174
     * @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...
175
     */
176
    public function __construct(protected string $entityClass)
177
    {
178
    }
179
180
    /**
181
     * {@inheritedoc}
182
     * @return EntityMapper<TEntity>
183
     */
184
    public function name(string $name): self
185
    {
186
        $this->name = $name;
187
188
        return $this;
189
    }
190
191
    /**
192
     * {@inheritedoc}
193
     * @return EntityMapper<TEntity>
194
     */
195
    public function table(string $table): self
196
    {
197
        $this->table = $table;
198
199
        return $this;
200
    }
201
202
    /**
203
     * {@inheritedoc}
204
     * @return EntityMapper<TEntity>
205
     */
206
    public function primaryKey(string ...$primaryKey): self
207
    {
208
        $this->primaryKey = new PrimaryKey(...$primaryKey);
209
210
        return $this;
211
    }
212
213
    /**
214
     * {@inheritedoc}
215
     * @return EntityMapper<TEntity>
216
     */
217
    public function primaryKeyGenerator(callable $generator): self
218
    {
219
        $this->primaryKeyGenerator = $generator;
220
221
        return $this;
222
    }
223
224
     /**
225
     * {@inheritedoc}
226
     * @return EntityMapper<TEntity>
227
     */
228
    public function sequence(string $sequence): self
229
    {
230
        $this->sequence = $sequence;
231
232
        return $this;
233
    }
234
235
    /**
236
     * {@inheritedoc}
237
     * @return EntityMapper<TEntity>
238
     */
239
    public function casts(array $columns): self
240
    {
241
        $this->casts = $columns;
242
243
        return $this;
244
    }
245
246
    /**
247
     * {@inheritedoc}
248
     * @return EntityMapper<TEntity>
249
     */
250
    public function fillable(array $columns): self
251
    {
252
        $this->fillable = $columns;
253
254
        return $this;
255
    }
256
257
     /**
258
     * {@inheritedoc}
259
     * @return EntityMapper<TEntity>
260
     */
261
    public function guarded(array $columns): self
262
    {
263
        $this->guarded = $columns;
264
265
        return $this;
266
    }
267
268
    /**
269
     * {@inheritedoc}
270
     * @return EntityMapper<TEntity>
271
     */
272
    public function filter(string $name, callable $filter): self
273
    {
274
        $this->filters[$name] = $filter;
275
276
        return $this;
277
    }
278
279
    /**
280
     * {@inheritedoc}
281
     * @return EntityMapper<TEntity>
282
     */
283
    public function getter(string $column, callable $getter): self
284
    {
285
        $this->getters[$column] = $getter;
286
287
        return $this;
288
    }
289
290
    /**
291
     * {@inheritedoc}
292
     * @return EntityMapper<TEntity>
293
     */
294
    public function setter(string $column, callable $setter): self
295
    {
296
        $this->setters[$column] = $setter;
297
298
        return $this;
299
    }
300
301
    /**
302
     * {@inheritedoc}
303
     * @return EntityMapper<TEntity>
304
     */
305
    public function on(string $name, callable $handler): self
306
    {
307
        if (!isset($this->eventHandlers[$name])) {
308
            $this->eventHandlers[$name] = [];
309
        }
310
311
        $this->eventHandlers[$name][] = $handler;
312
313
        return $this;
314
    }
315
316
    /**
317
     * {@inheritedoc}
318
     * @return RelationFactory<TEntity>
319
     */
320
    public function relation(string $name): RelationFactory
321
    {
322
        /** @var RelationFactory<TEntity> $factory */
323
        $factory = new RelationFactory($name, function ($name, Relation $relation) {
324
            return $this->relations[$name] = $relation;
325
        });
326
327
        return $factory;
328
    }
329
330
    /**
331
     * {@inheritedoc}
332
     * @return EntityMapper<TEntity>
333
     */
334
    public function useSoftDelete(
335
        bool $value = true,
336
        string $column = 'deleted_at'
337
    ): self {
338
        $this->useSoftDelete = $value;
339
        $this->softDeleteColumn = $column;
340
341
        return $this;
342
    }
343
344
    /**
345
     * {@inheritedoc}
346
     * @return EntityMapper<TEntity>
347
     */
348
    public function useTimestamp(
349
        bool $value = true,
350
        string $createdAt = 'created_at',
351
        string $updatedAt = 'updated_at'
352
    ): self {
353
        $this->useTimestamp = $value;
354
        $this->timestampColumns = [$createdAt, $updatedAt];
355
356
        return $this;
357
    }
358
359
    /**
360
     * Return the entity class
361
     * @return class-string<TEntity>
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...
362
     */
363
    public function getEntityClass(): string
364
    {
365
        return $this->entityClass;
366
    }
367
368
    /**
369
     *
370
     * @return string
371
     */
372
    public function getName(): string
373
    {
374
        if (empty($this->name)) {
375
            $entityClass = $this->entityClass;
376
377
            $pos = strrpos($entityClass, '\\');
378
379
            if ($pos !== false) {
380
                $entityClass = substr($entityClass, $pos + 1);
381
            }
382
383
            $name = preg_replace('/([^A-Z])([A-Z])/', "$1_$2", $entityClass);
384
385
            if ($name !== null) {
386
                $nameLower = strtolower($name);
387
                $name = str_replace('-', '_', $nameLower);
388
                $this->name = $name;
389
            }
390
        }
391
392
        return $this->name;
393
    }
394
395
    /**
396
     *
397
     * @return string
398
     */
399
    public function getTable(): string
400
    {
401
        if (empty($this->table)) {
402
            $this->table = $this->getName() . 's';
403
        }
404
405
        return $this->table;
406
    }
407
408
    /**
409
     *
410
     * @return PrimaryKey<TEntity>
411
     */
412
    public function getPrimaryKey(): PrimaryKey
413
    {
414
        if ($this->primaryKey === null) {
415
            //Default Primary key if user not set it
416
            $this->primaryKey = new PrimaryKey('id');
417
        }
418
419
        return $this->primaryKey;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->primaryKey could return the type null which is incompatible with the type-hinted return Platine\Orm\Relation\PrimaryKey. Consider adding an additional type-check to rule them out.
Loading history...
420
    }
421
422
    /**
423
     *
424
     * @return string
425
     */
426
    public function getSequence(): string
427
    {
428
        if ($this->sequence === null) {
429
            $primaryKey = $this->getPrimaryKey()->columns();
430
431
            $this->sequence = sprintf(
432
                '%s_%s_seq',
433
                $this->getTable(),
434
                $primaryKey[0]
435
            );
436
        }
437
438
        return $this->sequence;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->sequence could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
439
    }
440
441
    /**
442
     *
443
     * @return ForeignKey
444
     */
445
    public function getForeignKey(): ForeignKey
446
    {
447
        if ($this->foreignKey === null) {
448
            $primaryKey = $this->getPrimaryKey();
449
            $prefix = $this->getName();
450
451
            $this->foreignKey = new class ($primaryKey, $prefix) extends ForeignKey
452
            {
453
                /**
454
                 *
455
                 * @param PrimaryKey<TEntity> $primaryKey
456
                 * @param string $prefix
457
                 */
458
                public function __construct(PrimaryKey $primaryKey, string $prefix)
459
                {
460
                    /** @var array<string, string> $columns */
461
                    $columns = [];
462
463
                    foreach ($primaryKey->columns() as $column) {
464
                        $columns[$column] = $prefix . '_' . $column;
465
                    }
466
                    parent::__construct($columns);
467
                }
468
            };
469
        }
470
471
        return $this->foreignKey;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->foreignKey could return the type null which is incompatible with the type-hinted return Platine\Orm\Relation\ForeignKey. Consider adding an additional type-check to rule them out.
Loading history...
472
    }
473
474
    /**
475
     *
476
     * @return callable|null
477
     */
478
    public function getPrimaryKeyGenerator()
479
    {
480
        return $this->primaryKeyGenerator;
481
    }
482
483
    /**
484
     *
485
     * @return array<string, callable>
486
     */
487
    public function getGetters(): array
488
    {
489
        return $this->getters;
490
    }
491
492
    /**
493
     *
494
     * @return array<string, callable>
495
     */
496
    public function getSetters(): array
497
    {
498
        return $this->setters;
499
    }
500
501
    /**
502
     *
503
     * @return array<string, string>
504
     */
505
    public function getCasts(): array
506
    {
507
        return $this->casts;
508
    }
509
510
    /**
511
     *
512
     * @return array<string, Relation<TEntity>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, Relation<TEntity>> at position 4 could not be parsed: Expected '>' at position 4, but found 'Relation'.
Loading history...
513
     */
514
    public function getRelations(): array
515
    {
516
        return $this->relations;
517
    }
518
519
    /**
520
     *
521
     * @return array<int, string>
522
     */
523
    public function getFillable(): array
524
    {
525
        return $this->fillable;
526
    }
527
528
    /**
529
     *
530
     * @return array<int, string>
531
     */
532
    public function getGuarded(): array
533
    {
534
        return $this->guarded;
535
    }
536
537
    /**
538
     *
539
     * @return array<string, callable>
540
     */
541
    public function getFilters(): array
542
    {
543
        return $this->filters;
544
    }
545
546
    /**
547
     *
548
     * @return bool
549
     */
550
    public function hasSoftDelete(): bool
551
    {
552
        $column = $this->softDeleteColumn;
553
554
        return $this->useSoftDelete
555
               && isset($this->casts[$column])
556
               && $this->casts[$column] === '?date';
557
    }
558
559
    /**
560
     *
561
     * @return string
562
     */
563
    public function getSoftDeleteColumn(): string
564
    {
565
        return $this->softDeleteColumn;
566
    }
567
568
    /**
569
     *
570
     * @return bool
571
     */
572
    public function hasTimestamp(): bool
573
    {
574
        list($createdAt, $updateAt) = $this->timestampColumns;
575
        return $this->useTimestamp
576
               && isset($this->casts[$createdAt])
577
               && $this->casts[$createdAt] === 'date'
578
               && isset($this->casts[$updateAt])
579
               && $this->casts[$updateAt] === '?date';
580
    }
581
582
    /**
583
     *
584
     * @return array<int, string>
585
     */
586
    public function getTimestampColumns(): array
587
    {
588
        return $this->timestampColumns;
589
    }
590
591
    /**
592
     *
593
     * @return array<string, array<callable>>
594
     */
595
    public function getEventHandlers(): array
596
    {
597
        return $this->eventHandlers;
598
    }
599
}
600