Issues (82)

src/Relation/RelationSchema.php (4 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\Schema\Relation;
6
7
use Cycle\Database\Schema\AbstractTable;
8
use Cycle\ORM\Relation;
9
use Cycle\ORM\SchemaInterface;
10
use Cycle\Schema\Definition\Entity;
11
use Cycle\Schema\Exception\RegistryException;
12
use Cycle\Schema\Registry;
13
use Cycle\Schema\RelationInterface;
14
15
/**
16
 * Defines relation options, renders needed columns and other options.
17
 */
18
abstract class RelationSchema implements RelationInterface
19
{
20
    // relation rendering options
21
    public const INDEX_CREATE = 1001;
22
    public const FK_CREATE = 1002;
23
    public const FK_ACTION = 1003;
24
    public const FK_ON_DELETE = 1004;
25
    public const INVERSE = 1005;
26
    public const MORPH_KEY_LENGTH = 1009;
27
    public const EMBEDDED_PREFIX = 1010;
28
29
    // options to be excluded from generated schema (helpers)
30
    protected const EXCLUDE = [
31
        self::FK_CREATE,
32
        self::FK_ACTION,
33
        self::FK_ON_DELETE,
34
        self::INDEX_CREATE,
35
        self::EMBEDDED_PREFIX,
36
    ];
37
38
    // exported relation type
39
    protected const RELATION_TYPE = null;
40
41
    // name of all required relation options
42
    protected const RELATION_SCHEMA = [];
43
44
    /**
45
     * Relation container name in the entity
46
     */
47
    protected string $name;
48
49
    /**
50
     * @var non-empty-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
51
     */
52
    protected string $source;
53
54
    /**
55
     * @var non-empty-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
56
     */
57
    protected string $target;
58
59
    protected OptionSchema $options;
60
61
    /**
62 720
     * @param non-empty-string $role
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
63
     */
64 720
    public function withRole(string $role): static
65 720
    {
66
        $relation = clone $this;
67
        $relation->source = $role;
68
        return $relation;
69
    }
70 1168
71
    public function modifySchema(array &$schema): void
72 1168
    {
73 1168
        $schema[SchemaInterface::RELATIONS][$this->name] = $this->packSchema();
74 1168
    }
75 1168
76
    /**
77 1168
     * @param non-empty-string $source
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
78 1168
     * @param non-empty-string $target
79 1168
     */
80 1168
    public function withContext(string $name, string $source, string $target, OptionSchema $options): RelationInterface
81
    {
82
        $relation = clone $this;
83 1168
        $relation->source = $source;
84
        $relation->target = $target;
85
        $relation->name = $name;
86 1048
87
        $relation->options = $options->withTemplate(static::RELATION_SCHEMA)->withContext([
88 1048
            'relation' => $name,
89 1048
            'source:role' => $source,
90
            'target:role' => $target,
91
        ]);
92 1000
93 1000
        return $relation;
94 1000
    }
95
96
    public function compute(Registry $registry): void
97 960
    {
98
        $this->options = $this->options->withContext([
99 720
            'source:primaryKey' => $this->getPrimaryColumns($registry->getEntity($this->source)),
100
        ]);
101 720
102
        if ($registry->hasEntity($this->target)) {
103
            $this->options = $this->options->withContext([
104
                'target:primaryKey' => $this->getPrimaryColumns($registry->getEntity($this->target)),
105 720
            ]);
106 720
        }
107
    }
108 48
109
    protected function getLoadMethod(): ?int
110 688
    {
111
        if (!$this->options->has(Relation::LOAD)) {
112
            return null;
113
        }
114 952
115
        switch ($this->options->get(Relation::LOAD)) {
116 952
            case 'eager':
117
            case Relation::LOAD_EAGER:
118
                return Relation::LOAD_EAGER;
119
            default:
120
                return Relation::LOAD_PROMISE;
121
        }
122 1160
    }
123
124 1160
    protected function getOptions(): OptionSchema
125
    {
126 1160
        return $this->options;
127 88
    }
128
129
    /**
130 1112
     * @throws RegistryException
131
     */
132
    protected function getPrimaryColumns(Entity $entity): array
133
    {
134
        $columns = $entity->getPrimaryFields()->getNames();
135
136
        if ($columns === []) {
137
            throw new RegistryException("Entity `{$entity->getRole()}` must have defined primary key");
138
        }
139 24
140
        return $columns;
141
    }
142
143
    /**
144
     * @param array<string> $columns
145
     * @param bool $strictOrder True means that fields order in the {@see $columns} argument is matter
146 24
     * @param bool $withSorting True means that fields will be compared taking into account the column values sorting
147 24
     * @param bool|null $unique Unique index or not. Null means both
148
     */
149 24
    protected function hasIndex(
150
        AbstractTable $table,
151 24
        array $columns,
152 24
        bool $strictOrder = true,
153 24
        bool $withSorting = true,
154
        ?bool $unique = null,
155 8
    ): bool {
156
        if ($strictOrder && $withSorting && $unique === null) {
157 8
            return $table->hasIndex($columns);
158
        }
159
        $indexes = $table->getIndexes();
160
161 8
        foreach ($indexes as $index) {
162 8
            if ($unique !== null && $index->isUnique() !== $unique) {
163
                continue;
164
            }
165 24
            $tableColumns = $withSorting ? $index->getColumnsWithSort() : $index->getColumns();
166
167
            if (count($columns) !== count($tableColumns)) {
168 720
                continue;
169
            }
170 720
171
            if ($strictOrder ? $columns === $tableColumns : array_diff($columns, $tableColumns) === []) {
172 720
                return true;
173 720
            }
174 720
        }
175
        return false;
176
    }
177 720
178
    private function packSchema(): array
179
    {
180
        $schema = [];
181 720
182
        foreach (static::RELATION_SCHEMA as $option => $template) {
183
            if (in_array($option, static::EXCLUDE, true)) {
184 720
                continue;
185 720
            }
186 720
187 720
            $schema[$option] = $this->options->get($option);
188
        }
189
190
        // load option is not required in schema
191
        unset($schema[Relation::LOAD]);
192
193
        return [
194
            Relation::TYPE => static::RELATION_TYPE,
195
            Relation::TARGET => $this->target,
196
            Relation::LOAD => $this->getLoadMethod(),
197
            Relation::SCHEMA => $schema,
198
        ];
199
    }
200
}
201