Passed
Push — master ( ff3639...8a879c )
by Anton
01:54
created

ManyToMany::inverseRelation()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 3
nop 2
dl 0
loc 19
rs 9.8333
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Cycle\Schema\Relation;
10
11
use Cycle\ORM\Relation;
12
use Cycle\Schema\Exception\RelationException;
13
use Cycle\Schema\InversableInterface;
14
use Cycle\Schema\Registry;
15
use Cycle\Schema\Relation\Traits\FieldTrait;
16
use Cycle\Schema\Relation\Traits\ForeignKeyTrait;
17
use Cycle\Schema\RelationInterface;
18
19
class ManyToMany extends RelationSchema implements InversableInterface
20
{
21
    use FieldTrait, ForeignKeyTrait;
22
23
    // internal relation type
24
    protected const RELATION_TYPE = Relation::MANY_TO_MANY;
25
26
    // relation schema options
27
    protected const RELATION_SCHEMA = [
28
        // save with parent
29
        Relation::CASCADE            => true,
30
31
        // use outer entity constrain by default
32
        Relation::CONSTRAIN          => true,
33
34
        // nullable by default
35
        Relation::NULLABLE           => true,
36
37
        // custom where condition
38
        Relation::WHERE              => [],
39
40
        // inner key of parent record will be used to fill "THOUGHT_INNER_KEY" in pivot table
41
        Relation::INNER_KEY          => '{source:primaryKey}',
42
43
        // we are going to use primary key of outer table to fill "THOUGHT_OUTER_KEY" in pivot table
44
        // this is technically "inner" key of outer record, we will name it "outer key" for simplicity
45
        Relation::OUTER_KEY          => '{target:primaryKey}',
46
47
        // thought entity role name
48
        Relation::THOUGH_ENTITY      => null,
49
50
        // name field where parent record inner key will be stored in pivot table, role + innerKey
51
        // by default
52
        Relation::THOUGH_INNER_KEY   => '{source:role}_{innerKey}',
53
54
        // name field where inner key of outer record (outer key) will be stored in pivot table,
55
        // role + outerKey by default
56
        Relation::THOUGH_OUTER_KEY   => '{target:role}_{outerKey}',
57
58
        // apply pivot constrain
59
        Relation::THOUGH_CONSTRAIN   => true,
60
61
        // custom pivot where
62
        Relation::THOUGH_WHERE       => [],
63
64
        // rendering options
65
        RelationSchema::INDEX_CREATE => true,
66
        RelationSchema::FK_CREATE    => true,
67
        RelationSchema::FK_ACTION    => 'CASCADE'
68
    ];
69
70
    /**
71
     * @param Registry $registry
72
     */
73
    public function compute(Registry $registry)
74
    {
75
        parent::compute($registry);
76
77
        $source = $registry->getEntity($this->source);
78
        $target = $registry->getEntity($this->target);
79
80
        $thought = $registry->getEntity($this->options->get(Relation::THOUGH_ENTITY));
81
82
        if ($registry->getDatabase($source) !== $registry->getDatabase($target)) {
83
            throw new RelationException(sprintf(
84
                "Relation ManyToMany can only link entities from same database (%s, %s)",
85
                $source->getRole(),
86
                $target->getRole()
87
            ));
88
        }
89
90
        if ($registry->getDatabase($source) !== $registry->getDatabase($thought)) {
91
            throw new RelationException(sprintf(
92
                "Relation ManyToMany can only link entities from same database (%s, %s)",
93
                $source->getRole(),
94
                $thought->getRole()
95
            ));
96
        }
97
98
        $this->ensureField(
99
            $thought,
100
            $this->options->get(Relation::THOUGH_INNER_KEY),
101
            $this->getField($source, Relation::INNER_KEY),
102
            $this->options->get(Relation::NULLABLE)
0 ignored issues
show
Bug introduced by
It seems like $this->options->get(Cycle\ORM\Relation::NULLABLE) can also be of type string; however, parameter $nullable of Cycle\Schema\Relation\ManyToMany::ensureField() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

102
            /** @scrutinizer ignore-type */ $this->options->get(Relation::NULLABLE)
Loading history...
103
        );
104
105
        $this->ensureField(
106
            $thought,
107
            $this->options->get(Relation::THOUGH_OUTER_KEY),
108
            $this->getField($target, Relation::OUTER_KEY),
109
            $this->options->get(Relation::NULLABLE)
110
        );
111
    }
112
113
    /**
114
     * @param Registry $registry
115
     */
116
    public function render(Registry $registry)
117
    {
118
        $source = $registry->getEntity($this->source);
119
        $target = $registry->getEntity($this->target);
120
121
        $thought = $registry->getEntity($this->options->get(Relation::THOUGH_ENTITY));
122
123
        $sourceField = $this->getField($source, Relation::INNER_KEY);
124
        $targetField = $this->getField($target, Relation::OUTER_KEY);
125
126
        $thoughtSourceField = $this->getField($thought, Relation::THOUGH_INNER_KEY);
127
        $thoughtTargetField = $this->getField($thought, Relation::THOUGH_OUTER_KEY);
128
129
        $table = $registry->getTableSchema($thought);
130
131
        if ($this->options->get(self::INDEX_CREATE)) {
132
            $table->index([
133
                $thoughtSourceField->getColumn(),
134
                $thoughtTargetField->getColumn()
135
            ])->unique(true);
136
        }
137
138
        if ($this->options->get(self::FK_CREATE)) {
139
            $this->createForeignKey($registry, $source, $thought, $sourceField, $thoughtSourceField);
140
            $this->createForeignKey($registry, $target, $thought, $targetField, $thoughtTargetField);
141
        }
142
    }
143
144
    /**
145
     * @param Registry $registry
146
     * @return array
147
     */
148
    public function inverseTargets(Registry $registry): array
149
    {
150
        return [
151
            $registry->getEntity($this->target)
152
        ];
153
    }
154
155
    /**
156
     * @param RelationInterface $relation
157
     * @param string            $into
158
     * @return RelationInterface
159
     *
160
     * @throws RelationException
161
     */
162
    public function inverseRelation(RelationInterface $relation, string $into): RelationInterface
163
    {
164
        if (!$relation instanceof self) {
165
            throw new RelationException("ManyToMany relation can only be inversed into ManyToMany");
166
        }
167
168
        if (!empty($this->options->get(Relation::THOUGH_WHERE)) || !empty($this->options->get(Relation::WHERE))) {
169
            throw new RelationException("Unable to inverse ManyToMany relation with where constrain");
170
        }
171
172
        return $relation->withContext(
173
            $into,
174
            $this->target,
175
            $this->source,
176
            $this->options->withOptions([
177
                Relation::INNER_KEY        => $this->options->get(Relation::OUTER_KEY),
178
                Relation::OUTER_KEY        => $this->options->get(Relation::INNER_KEY),
179
                Relation::THOUGH_INNER_KEY => $this->options->get(Relation::THOUGH_OUTER_KEY),
180
                Relation::THOUGH_OUTER_KEY => $this->options->get(Relation::THOUGH_INNER_KEY),
181
            ])
182
        );
183
    }
184
}