Issues (20)

src/Relation/ManyToMany.php (1 issue)

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

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