Issues (20)

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
        // do not pre-load relation by default
33
        Relation::LOAD               => Relation::LOAD_PROMISE,
34
35
        // nullable by default
36
        Relation::NULLABLE           => false,
37
38
        // custom where condition
39
        Relation::WHERE              => [],
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
        // rendering options
63
        RelationSchema::INDEX_CREATE => true,
64
        RelationSchema::FK_CREATE    => true,
65
        RelationSchema::FK_ACTION    => 'CASCADE'
66
    ];
67
68
    /**
69
     * @param Registry $registry
70
     */
71
    public function compute(Registry $registry)
72
    {
73
        parent::compute($registry);
74
75
        $source = $registry->getEntity($this->source);
76
        $target = $registry->getEntity($this->target);
77
78
        $through = $registry->getEntity($this->options->get(Relation::THROUGH_ENTITY));
79
80
        if ($registry->getDatabase($source) !== $registry->getDatabase($target)) {
81
            throw new RelationException(sprintf(
82
                "Relation ManyToMany can only link entities from same database (%s, %s)",
83
                $source->getRole(),
84
                $target->getRole()
85
            ));
86
        }
87
88
        if ($registry->getDatabase($source) !== $registry->getDatabase($through)) {
89
            throw new RelationException(sprintf(
90
                "Relation ManyToMany can only link entities from same database (%s, %s)",
91
                $source->getRole(),
92
                $through->getRole()
93
            ));
94
        }
95
96
        $this->ensureField(
97
            $through,
98
            $this->options->get(Relation::THROUGH_INNER_KEY),
99
            $this->getField($source, Relation::INNER_KEY),
100
            $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

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