Lines 94.11% 16 / 17
Functions and Methods 80.00% 4 / 5
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
MorphOneOrMany 94.11% 16 / 17 80.00% 4 / 5 6.01 0.00% 0 / 1
 __construct 100.00% 2 / 2 100.00% 1 / 1 1
 getTypeColumn 0.00% 0 / 1 0.00% 0 / 1 2
 parentMorphAlias 100.00% 1 / 1 100.00% 1 / 1 1
 addConstraints 100.00% 12 / 12 100.00% 1 / 1 2
 applyEagerOrdering 100.00% 1 / 1 100.00% 1 / 1 1
1<?php
2
3declare(strict_types=1);
4
5namespace BlueprintAU\Radiant\Relations;
6
7use BlueprintAU\Radiant\Database\Query\Enums\WhereOperator;
8use BlueprintAU\Radiant\Database\Query\WhereBuilder;
9use BlueprintAU\Radiant\Model;
10
11/**
12 * The shared base of the polymorphic one-to-one/one-to-many relations.
13 *
14 * A morph relation is a HasMany whose FK match carries a second
15 * constraint: the related rows must also declare this parent's morph
16 * alias in the `{name}_type` column. The alias is the parent model's full
17 * class-string — the same value the inverse {@see MorphTo} dispatches on;
18 * renaming a class changes the stored alias, a data-migration concern
19 * documented in docs/relations.md.
20 *
21 * @template TRelated of Model
22 * @extends Relation<TRelated>
23 */
24abstract class MorphOneOrMany extends Relation
25{
26    /**
27     * The type-discriminator column on the related table.
28     *
29     * @var string
30     */
31    protected readonly string $typeColumn;
32
33    /**
34     * Create a polymorphic relation.
35     *
36     * @param  Model  $parent
37     * @param  class-string<TRelated>  $related
38     * @param  string  $foreignKey
39     * @param  string  $localKey
40     * @param  string  $typeColumn
41     */
42    public function __construct(
43        Model $parent,
44        string $related,
45        string $foreignKey,
46        string $localKey,
47        string $typeColumn,
48    ) {
49        $this->typeColumn = $typeColumn;
50
51        parent::__construct($parent, $related, $foreignKey, $localKey);
52    }
53
54    /**
55     * The type-discriminator column on the related table.
56     *
57     * @return string
58     */
59    final public function getTypeColumn(): string
60    {
61        return $this->typeColumn;
62    }
63
64    /**
65     * THIS parent's morph alias — the value the related table's type
66     * column must hold to point back here.
67     *
68     * @return string
69     */
70    final protected function parentMorphAlias(): string
71    {
72        return $this->parent::class;
73    }
74
75    /**
76     * Apply the relation's constraint: the FK match PLUS the type filter.
77     *
78     * @return void
79     */
80    #[\Override]
81    protected function addConstraints(): void
82    {
83        $parentKey = $this->parent->attribute($this->getLocalKey());
84
85        if ($parentKey === null) {
86            // Null parent key → no results, without compiling a meaningless
87            // query (BelongsTo's convention).
88            $this->query = $this->query->whereRaw('1 = 0', []);
89            return;
90        }
91
92        $alias = $this->parentMorphAlias();
93        $foreignKey = $this->getForeignKey();
94        $typeColumn = $this->typeColumn;
95
96        $this->query = $this->query->whereNested(
97            fn (WhereBuilder $nested): WhereBuilder => $nested
98                ->where($foreignKey, WhereOperator::Eq, $parentKey)
99                ->where($typeColumn, WhereOperator::Eq, $alias)
100        );
101    }
102
103    /**
104     * Apply the eager-path ordering AND the type filter to the chunk query.
105     *
106     * @param  \BlueprintAU\Radiant\ModelQueryBuilder<TRelated>  $query
107     * @return \BlueprintAU\Radiant\ModelQueryBuilder<TRelated>
108     */
109    #[\Override]
110    protected function applyEagerOrdering(\BlueprintAU\Radiant\ModelQueryBuilder $query): \BlueprintAU\Radiant\ModelQueryBuilder
111    {
112        // The chunk query is immutable — the type filter returns a new
113        // instance, which the eager loader must receive.
114        return $query->where($this->typeColumn, WhereOperator::Eq, $this->parentMorphAlias());
115    }
116}