Passed
Push — master ( 6ac325...8081f8 )
by Anton
01:32
created

src/Relation/ManyToMany.php (1 issue)

Labels
Severity
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
        // nullable by default
32
        Relation::NULLABLE           => true,
33
34
        // custom where condition
35
        Relation::WHERE              => [],
36
37
        // inner key of parent record will be used to fill "THOUGHT_INNER_KEY" in pivot table
38
        Relation::INNER_KEY          => '{source:primaryKey}',
39
40
        // we are going to use primary key of outer table to fill "THOUGHT_OUTER_KEY" in pivot table
41
        // this is technically "inner" key of outer record, we will name it "outer key" for simplicity
42
        Relation::OUTER_KEY          => '{target:primaryKey}',
43
44
        // thought entity role name
45
        Relation::THOUGH_ENTITY      => null,
46
47
        // name field where parent record inner key will be stored in pivot table, role + innerKey
48
        // by default
49
        Relation::THOUGH_INNER_KEY   => '{source:role}_{innerKey}',
50
51
        // name field where inner key of outer record (outer key) will be stored in pivot table,
52
        // role + outerKey by default
53
        Relation::THOUGH_OUTER_KEY   => '{target:role}_{outerKey}',
54
55
        // custom pivot where
56
        Relation::THOUGH_WHERE       => [],
57
58
        // rendering options
59
        RelationSchema::INDEX_CREATE => true,
60
        RelationSchema::FK_CREATE    => true,
61
        RelationSchema::FK_ACTION    => 'CASCADE'
62
    ];
63
64
    /**
65
     * @param Registry $registry
66
     */
67
    public function compute(Registry $registry)
68
    {
69
        parent::compute($registry);
70
71
        $source = $registry->getEntity($this->source);
72
        $target = $registry->getEntity($this->target);
73
74
        $thought = $registry->getEntity($this->options->get(Relation::THOUGH_ENTITY));
75
76
        if ($registry->getDatabase($source) !== $registry->getDatabase($target)) {
77
            throw new RelationException(sprintf(
78
                "Relation ManyToMany can only link entities from same database (%s, %s)",
79
                $source->getRole(),
80
                $target->getRole()
81
            ));
82
        }
83
84
        if ($registry->getDatabase($source) !== $registry->getDatabase($thought)) {
85
            throw new RelationException(sprintf(
86
                "Relation ManyToMany can only link entities from same database (%s, %s)",
87
                $source->getRole(),
88
                $thought->getRole()
89
            ));
90
        }
91
92
        $this->ensureField(
93
            $thought,
94
            $this->options->get(Relation::THOUGH_INNER_KEY),
95
            $this->getField($source, Relation::INNER_KEY),
96
            $this->options->get(Relation::NULLABLE)
0 ignored issues
show
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

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