ManyToMany   A
last analyzed

Complexity

Total Complexity 14

Size/Duplication

Total Lines 189
Duplicated Lines 0 %

Test Coverage

Coverage 98.44%

Importance

Changes 5
Bugs 1 Features 0
Metric Value
eloc 96
dl 0
loc 189
ccs 63
cts 64
cp 0.9844
rs 10
c 5
b 1
f 0
wmc 14

4 Methods

Rating   Name   Duplication   Size   Complexity  
A compute() 0 49 4
A inverseRelation() 0 20 4
A render() 0 38 5
A inverseTargets() 0 4 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\Schema\Relation;
6
7
use Cycle\ORM\Relation;
8
use Cycle\Schema\Definition\Entity;
9
use Cycle\Schema\Exception\RelationException;
10
use Cycle\Schema\InversableInterface;
11
use Cycle\Schema\Registry;
12
use Cycle\Schema\Relation\Traits\FieldTrait;
13
use Cycle\Schema\Relation\Traits\ForeignKeyTrait;
14
use Cycle\Schema\RelationInterface;
15
16
final class ManyToMany extends RelationSchema implements InversableInterface
17
{
18
    use FieldTrait;
19
    use ForeignKeyTrait;
20
21
    // internal relation type
22
    protected const RELATION_TYPE = Relation::MANY_TO_MANY;
23
24
    // relation schema options
25
    protected const RELATION_SCHEMA = [
26
        // save with parent
27
        Relation::CASCADE => true,
28
29
        // do not pre-load relation by default
30
        Relation::LOAD => Relation::LOAD_PROMISE,
31
32
        // nullable by default
33
        Relation::NULLABLE => false,
34
35
        // custom where condition
36
        Relation::WHERE => [],
37
38
        // custom orderBy rules
39
        Relation::ORDER_BY => [],
40
41
        // inner key of parent record will be used to fill "THROUGH_INNER_KEY" in pivot table
42
        Relation::INNER_KEY => '{source:primaryKey}',
43
44
        // we are going to use primary key of outer table to fill "THROUGH_OUTER_KEY" in pivot table
45
        // this is technically "inner" key of outer record, we will name it "outer key" for simplicity
46
        Relation::OUTER_KEY => '{target:primaryKey}',
47
48
        // through entity role name
49
        Relation::THROUGH_ENTITY => null,
50
51
        // name field where parent record inner key will be stored in pivot table, role + innerKey
52
        // by default
53
        Relation::THROUGH_INNER_KEY => '{source:role}_{innerKey}',
54
55
        // name field where inner key of outer record (outer key) will be stored in pivot table,
56
        // role + outerKey by default
57
        Relation::THROUGH_OUTER_KEY => '{target:role}_{outerKey}',
58
59
        // custom pivot where
60
        Relation::THROUGH_WHERE => [],
61
62
        // default collection.
63
        Relation::COLLECTION_TYPE => null,
64
65
        // rendering options
66
        RelationSchema::INDEX_CREATE => true,
67
        RelationSchema::FK_CREATE => true,
68
        RelationSchema::FK_ACTION => 'CASCADE',
69
        RelationSchema::FK_ON_DELETE => null,
70
    ];
71
72
    /**
73
     * @psalm-suppress PossiblyNullArgument
74 184
     */
75
    public function compute(Registry $registry): void
76 184
    {
77
        parent::compute($registry);
78 168
79 168
        $source = $registry->getEntity($this->source);
80 168
        $target = $registry->getEntity($this->target);
81
        $throughEntity = $this->options->get(Relation::THROUGH_ENTITY);
82 168
83 8
        if ($throughEntity === null) {
84 8
            throw new RelationException(sprintf(
85 8
                'Relation ManyToMany must have the throughEntity declaration (%s => ? => %s).',
86 8
                $source->getRole(),
87
                $target->getRole(),
88
            ));
89
        }
90 160
91
        $through = $registry->getEntity($throughEntity);
92 160
93 16
        if ($registry->getDatabase($source) !== $registry->getDatabase($target)) {
94 16
            throw new RelationException(sprintf(
95 16
                'Relation ManyToMany can only link entities from same database (%s, %s).',
96 16
                $source->getRole(),
97
                $target->getRole(),
98
            ));
99
        }
100 144
101 16
        if ($registry->getDatabase($source) !== $registry->getDatabase($through)) {
102 16
            throw new RelationException(sprintf(
103 16
                'Relation ManyToMany can only link entities from same database (%s, %s)',
104 16
                $source->getRole(),
105
                $through->getRole(),
106
            ));
107
        }
108 128
109 128
        $this->normalizeContextFields($source, $target);
110
        $this->normalizeContextFields($source, $through, ['innerKey', 'outerKey', 'throughInnerKey', 'throughOuterKey']);
111 128
112
        $this->createRelatedFields(
113 128
            $source,
114
            Relation::INNER_KEY,
115 128
            $through,
116
            Relation::THROUGH_INNER_KEY,
117
        );
118 128
119
        $this->createRelatedFields(
120 128
            $target,
121
            Relation::OUTER_KEY,
122 128
            $through,
123
            Relation::THROUGH_OUTER_KEY,
124 128
        );
125
    }
126
127
    public function render(Registry $registry): void
128
    {
129 24
        $source = $registry->getEntity($this->source);
130
        $target = $registry->getEntity($this->target);
131 24
132 24
        $through = $registry->getEntity($this->options->get(Relation::THROUGH_ENTITY));
133
134 24
        $sourceFields = $this->getFields($source, Relation::INNER_KEY);
135
        $targetFields = $this->getFields($target, Relation::OUTER_KEY);
136 24
137 24
        $throughSourceFields = $this->getFields($through, Relation::THROUGH_INNER_KEY);
138
        $throughTargetFields = $this->getFields($through, Relation::THROUGH_OUTER_KEY);
139 24
140 24
        $table = $registry->getTableSchema($through);
141
142 24
        if ($this->options->get(self::INDEX_CREATE)) {
143
            $index = array_merge($throughSourceFields->getColumnNames(), $throughTargetFields->getColumnNames());
144 24
            if (count($index) > 0 && !$this->hasIndex($table, $index)) {
145 24
                $table->index($index)->unique(!$this->hasIndex($table, $index, strictOrder: false, unique: true));
146 24
            }
147 24
        }
148
149
        if ($this->options->get(self::FK_CREATE)) {
150
            $this->createForeignCompositeKey(
151 24
                $registry,
152 24
                $source,
153 24
                $through,
154
                $sourceFields,
155 24
                $throughSourceFields,
156
                $this->options->get(self::INDEX_CREATE),
157
            );
158
            $this->createForeignCompositeKey(
159
                $registry,
160
                $target,
161
                $through,
162 32
                $targetFields,
163
                $throughTargetFields,
164
                $this->options->get(self::INDEX_CREATE),
165 32
            );
166
        }
167
    }
168
169
    /**
170
     *
171
     * @return Entity[]
172
     */
173
    public function inverseTargets(Registry $registry): array
174
    {
175
        return [
176
            $registry->getEntity($this->target),
177
        ];
178 32
    }
179
180 32
    /**
181 16
     *
182
     * @throws RelationException
183
     *
184 16
     */
185
    public function inverseRelation(RelationInterface $relation, string $into, ?int $load = null): RelationInterface
186
    {
187
        if (!$relation instanceof self) {
188 16
            throw new RelationException('ManyToMany relation can only be inversed into ManyToMany');
189 16
        }
190 16
191 16
        if (!empty($this->options->get(Relation::THROUGH_WHERE)) || !empty($this->options->get(Relation::WHERE))) {
192 16
            throw new RelationException('Unable to inverse ManyToMany relation with where scope.');
193 16
        }
194 16
195 16
        return $relation->withContext(
196 16
            $into,
197 16
            $this->target,
198
            $this->source,
199
            $this->options->withOptions([
200
                Relation::LOAD => $load,
201
                Relation::INNER_KEY => $this->options->get(Relation::OUTER_KEY),
202
                Relation::OUTER_KEY => $this->options->get(Relation::INNER_KEY),
203
                Relation::THROUGH_INNER_KEY => $this->options->get(Relation::THROUGH_OUTER_KEY),
204
                Relation::THROUGH_OUTER_KEY => $this->options->get(Relation::THROUGH_INNER_KEY),
205
            ]),
206
        );
207
    }
208
}
209