Passed
Push — master ( 384538...4f7dc7 )
by Anton
02:27
created

src/Relation/ManyToMany.php (1 issue)

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

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