Schema::normalize()   F
last analyzed

Complexity

Conditions 18
Paths 780

Size

Total Lines 69
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 18

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 18
eloc 38
nc 780
nop 1
dl 0
loc 69
ccs 37
cts 37
cp 1
crap 18
rs 1.0055
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\ORM;
6
7
use Cycle\ORM\Exception\SchemaException;
8
9
/**
10
 * Static schema with automatic class name => role aliasing.
11
 */
12
final class Schema implements SchemaInterface
13
{
14
    private array $aliases;
15
16
    /**
17
     * @var string[]
18
     *
19
     * @psalm-var class-string[]
20
     */
21
    private array $classes = [];
22
23
    /** @var array<string, array> */
24
    private array $subclasses = [];
25
26 7428
    private array $schema;
27
28
    public function __construct(array $schema)
29 7428
    {
30
        // split into two?
31
        [$this->schema, $this->aliases] = $this->normalize($schema);
32
    }
33
34
    public function getRoles(): array
35
    {
36
        return \array_keys($this->schema);
37
    }
38
39
    public function getRelations(string $role): array
40
    {
41 42
        return \array_keys($this->define($role, self::RELATIONS));
42
    }
43 42
44
    /**
45
     * Return all defined roles with the schema.
46 16
     */
47
    public function toArray(): array
48 16
    {
49
        return $this->schema;
50
    }
51
52
    /**
53
     * @return array [role => [relation name => relation schema]]
54 2
     */
55
    public function getOuterRelations(string $role): array
56 2
    {
57
        // return null;
58
        $result = [];
59
        foreach ($this->schema as $roleName => $entitySchema) {
60
            foreach ($entitySchema[SchemaInterface::RELATIONS] ?? [] as $relName => $item) {
61
                if ($item[Relation::TARGET] === $role) {
62 6878
                    $result[$roleName][$relName] = $item;
63
                } elseif ($item[Relation::TYPE] === Relation::MANY_TO_MANY) {
64
                    $through = $this->resolveAlias($item[Relation::SCHEMA][Relation::THROUGH_ENTITY]);
65 6878
                    if ($through !== $role) {
66 6878
                        continue;
67 6878
                    }
68 5412
                    $handshake = $item[Relation::SCHEMA][Relation::INVERSION] ?? null;
69 4534
                    $target = $item[Relation::TARGET];
70 5236
                    $result[$roleName][$relName] = [
71 936
                        Relation::TYPE => Relation::HAS_MANY,
72 936
                        Relation::TARGET => $role,
73 902
                        Relation::SCHEMA => [
74
                            Relation::CASCADE => $item[Relation::SCHEMA][Relation::CASCADE] ?? null,
75 866
                            Relation::INNER_KEY => $item[Relation::SCHEMA][Relation::INNER_KEY],
76 866
                            Relation::OUTER_KEY => $item[Relation::SCHEMA][Relation::THROUGH_INNER_KEY],
77 866
                        ],
78
                    ];
79
                    $result[$target][$handshake ?? ($roleName . '.' . $relName . ':' . $target)] = [
80
                        Relation::TYPE => Relation::HAS_MANY,
81
                        Relation::TARGET => $role,
82
                        Relation::SCHEMA => [
83
                            Relation::CASCADE => $item[Relation::SCHEMA][Relation::CASCADE] ?? null,
84
                            Relation::INNER_KEY => $item[Relation::SCHEMA][Relation::OUTER_KEY],
85
                            Relation::OUTER_KEY => $item[Relation::SCHEMA][Relation::THROUGH_OUTER_KEY],
86 866
                        ],
87
                    ];
88
                }
89
            }
90
        }
91
        return $result;
92
    }
93
94
    /**
95
     * @return array [relation name => relation schema]
96
     */
97
    public function getInnerRelations(string $role): array
98 6878
    {
99
        return $this->schema[$role][SchemaInterface::RELATIONS] ?? [];
100
    }
101
102
    public function defines(string $role): bool
103
    {
104 6878
        return isset($this->schema[$role]) || isset($this->aliases[$role]);
105
    }
106 6878
107
    public function define(string $role, int $property): mixed
108
    {
109 1582
        if ($property === SchemaInterface::ENTITY) {
110
            return $this->defineEntityClass($role);
111 1582
        }
112
        $role = $this->resolveAlias($role) ?? $role;
113
114 7232
        if (!isset($this->schema[$role])) {
115
            throw new SchemaException("Undefined schema `{$role}`, not found.");
116 7232
        }
117 6088
118
        return $this->schema[$role][$property] ?? null;
119 7226
    }
120
121 7226
    public function defineRelation(string $role, string $relation): array
122
    {
123
        $relations = $this->define($role, self::RELATIONS);
124
125 7226
        if (!isset($relations[$relation])) {
126
            throw new SchemaException("Undefined relation `{$role}`.`{$relation}`.");
127
        }
128 5524
129
        return $relations[$relation];
130 5524
    }
131
132 5524
    public function resolveAlias(string $role): ?string
133 64
    {
134
        // walk through all children until parent entity found
135
        $found = $this->aliases[$role] ?? null;
136 5524
        while ($found !== null && $found !== $role) {
137
            $role = $found;
138
            $found = $this->aliases[$found] ?? null;
139 7232
        }
140
141
        return $role;
142 7232
    }
143 7232
144 5786
    public function getInheritedRoles(string $parent): array
145 5786
    {
146
        return $this->subclasses[$parent] ?? [];
147
    }
148 7232
149
    public static function __set_state(array $an_array): self
150
    {
151 6834
        $schema = new self([]);
152
        $schema->schema = $an_array['schema'];
153 6834
        $schema->aliases = $an_array['aliases'];
154
155
        return $schema;
156
    }
157
158
    /**
159
     * Automatically replace class names with their aliases.
160
     *
161 7428
     * @return array Pair of [schema, aliases]
162
     */
163 7428
    private function normalize(array $schema): array
164
    {
165 7428
        $result = $aliases = [];
166 7258
167 7258
        foreach ($schema as $key => $item) {
168
            $role = $key;
169 6802
            if (!isset($item[self::ENTITY])) {
170
                // legacy type of declaration (class => schema)
171
                $item[self::ENTITY] = $key;
172 7258
            }
173 6026
174 6026
            if (\class_exists($key)) {
175 6010
                $role = $item[self::ROLE] ?? $key;
176
                if ($role !== $key) {
177
                    $aliases[$key] = $role;
178
                }
179 7258
            }
180 6478
181 6478
            if ($item[self::ENTITY] !== $role && \class_exists($item[self::ENTITY])) {
182
                $aliases[$item[self::ENTITY]] = $role;
183
                $this->classes[$role] = $item[self::ENTITY];
184 7258
            }
185 7258
186
            unset($item[self::ROLE]);
187
            $result[$role] = $item;
188
        }
189 7428
190 7258
        // Normalize PARENT option
191 1076
        foreach ($result as $role => &$item) {
192 312
            if (isset($item[self::PARENT])) {
193 312
                if (\class_exists($item[self::PARENT])) {
194 312
                    $parent = $item[self::PARENT];
195
                    while (isset($aliases[$parent])) {
196 312
                        $parent = $aliases[$parent];
197
                    }
198 1076
                    $item[self::PARENT] = $parent;
199 1076
                }
200
                $this->subclasses[$role] ??= [];
201
                $this->subclasses[$item[self::PARENT]][$role] = &$this->subclasses[$role];
202 7428
            }
203
        }
204
        unset($item);
205 7428
206 7258
        // Extract aliases from CHILDREN options
207 416
        foreach ($result as $role => $item) {
208 416
            if (isset($item[self::CHILDREN])) {
209 416
                foreach ($item[self::CHILDREN] as $child) {
210
                    if (isset($aliases[$child]) && \class_exists($child)) {
211 416
                        $aliases[$aliases[$child]] = $role;
212
                    }
213
                    $aliases[$child] = $role;
214
                }
215
            }
216
        }
217 7428
218 7258
        // Normalize relation associations
219 7248
        foreach ($result as &$item) {
220 7248
            if (isset($item[self::RELATIONS])) {
221
                $item[self::RELATIONS] = \iterator_to_array($this->normalizeRelations(
222
                    $item[self::RELATIONS],
223
                    $aliases,
224
                ));
225 7428
            }
226
        }
227 7428
        unset($item);
228
229 7428
        $result = $this->linkRelations($result, $aliases);
230
231
        return [$result, $aliases];
232 7248
    }
233
234 7248
    private function normalizeRelations(array $relations, array $aliases): \Generator
235 5628
    {
236 5628
        foreach ($relations as $name => &$rel) {
237 4306
            $target = $rel[Relation::TARGET];
238
            while (isset($aliases[$target])) {
239
                $target = $aliases[$target];
240 5628
            }
241
242 5628
            $rel[Relation::TARGET] = $target;
243
244 5628
            $nullable = $rel[Relation::SCHEMA][Relation::NULLABLE] ?? null;
245 16
            // Transform not nullable RefersTo to BelongsTo
246
            if ($rel[Relation::TYPE] === Relation::REFERS_TO && $nullable === false) {
247
                $rel[Relation::TYPE] = Relation::BELONGS_TO;
248
            }
249 5628
250 1008
            // Normalize THROUGH_ENTITY value
251 1008
            if ($rel[Relation::TYPE] === Relation::MANY_TO_MANY) {
252 784
                $through = $rel[Relation::SCHEMA][Relation::THROUGH_ENTITY];
253
                while (isset($aliases[$through])) {
254 1008
                    $through = $aliases[$through];
255
                }
256
                $rel[Relation::SCHEMA][Relation::THROUGH_ENTITY] = $through;
257 5628
            }
258
259
            yield $name => $rel;
260
        }
261 7428
    }
262
263 7428
    private function linkRelations(array $schemaArray, array $aliases): array
264 7428
    {
265 7258
        $result = $schemaArray;
266 5628
        foreach ($result as $role => $item) {
267 5628
            foreach ($item[self::RELATIONS] ?? [] as $container => $relation) {
268 98
                $target = $relation[Relation::TARGET];
269
                if (!\array_key_exists($target, $result)) {
270 5626
                    continue;
271 5626
                }
272 5626
                $targetSchema = $result[$target];
273 5626
                $targetRelations = $targetSchema[self::RELATIONS] ?? [];
274 40
                $inversion = $relation[Relation::SCHEMA][Relation::INVERSION] ?? null;
275
                if ($inversion !== null) {
276
                    if (!\array_key_exists($inversion, $targetRelations)) {
277
                        throw new SchemaException(
278
                            \sprintf(
279
                                'Relation `%s` as inversion of `%s.%s` not found in the `%s` role.',
280
                                $inversion,
281
                                $role,
282
                                $container,
283
                                $target,
284
                            ),
285 40
                        );
286 40
                    }
287
                    $targetHandshake = $targetRelations[$inversion][Relation::SCHEMA][Relation::INVERSION] ?? null;
288
                    if ($targetHandshake !== null && $container !== $targetHandshake) {
289
                        throw new SchemaException(
290
                            \sprintf(
291
                                'Relation `%s.%s` can\'t be inversion of `%s.%s` because they have different relation values.',
292
                                $role,
293
                                $container,
294
                                $target,
295
                                $inversion,
296
                            ),
297 40
                        );
298 40
                    }
299
                    $result[$target][self::RELATIONS][$inversion][Relation::SCHEMA][Relation::INVERSION] = $container;
300
                    continue;
301 5586
                }
302 5586
                // find inverted relation
303 5586
                $inversion = $this->findInvertedRelation($role, $container, $relation, $targetRelations);
304
                if ($inversion === null) {
305 520
                    continue;
306 520
                }
307
                $result[$role][self::RELATIONS][$container][Relation::SCHEMA][Relation::INVERSION] = $inversion;
308
                $result[$target][self::RELATIONS][$inversion][Relation::SCHEMA][Relation::INVERSION] = $container;
309
            }
310 7428
        }
311
312
        return $result;
313 5586
    }
314
315
    private function findInvertedRelation(
316
        string $role,
317
        string $container,
318
        array $relation,
319 5586
        array $targetRelations,
320
    ): ?string {
321 5586
        $nullable = $relation[Relation::SCHEMA][Relation::NULLABLE] ?? null;
0 ignored issues
show
Unused Code introduced by
The assignment to $nullable is dead and can be removed.
Loading history...
322 968
        /** @var callable $compareCallback */
323 496
        $compareCallback = match ($relation[Relation::TYPE]) {
324
            Relation::MANY_TO_MANY => [$this, 'compareManyToMany'],
325 2205
            Relation::BELONGS_TO => [$this, 'checkBelongsToInversion'],
326
            // Relation::HAS_ONE, Relation::HAS_MANY => $nullable === true ? Relation::REFERS_TO : Relation::BELONGS_TO,
327 5586
            default => null,
328 4410
        };
329
        if ($compareCallback === null) {
0 ignored issues
show
introduced by
The condition $compareCallback === null is always false.
Loading history...
330 1760
            return null;
331 904
        }
332 904
        foreach ($targetRelations as $targetContainer => $targetRelation) {
333 704
            $targetSchema = $targetRelation[Relation::SCHEMA];
334
            if ($role !== $targetRelation[Relation::TARGET]) {
335 520
                continue;
336 216
            }
337
            if (isset($targetSchema[Relation::INVERSION])) {
338 216
                if ($targetSchema[Relation::INVERSION] === $container) {
339
                    // This target relation will be checked in the linkRelations() method
340 48
                    return null;
341
                }
342 520
                continue;
343 520
            }
344
            if ($compareCallback($relation, $targetRelation)) {
345
                return $targetContainer;
346 1328
            }
347
        }
348
        return null;
349 216
    }
350
351 216
    private function compareManyToMany(array $relation, array $targetRelation): bool
352 216
    {
353
        $schema = $relation[Relation::SCHEMA];
354 216
        $targetSchema = $targetRelation[Relation::SCHEMA];
355 48
        // MTM connects with MTM only
356
        if ($targetRelation[Relation::TYPE] !== Relation::MANY_TO_MANY) {
357
            return false;
358 216
        }
359
        // Pivot entity should be same
360
        if ($schema[Relation::THROUGH_ENTITY] !== $targetSchema[Relation::THROUGH_ENTITY]) {
361
            return false;
362 216
        }
363 216
        // Same keys
364
        if ((array) $schema[Relation::INNER_KEY] !== (array) $targetSchema[Relation::OUTER_KEY]
365
            || (array) $schema[Relation::OUTER_KEY] !== (array) $targetSchema[Relation::INNER_KEY]) {
366
            return false;
367 216
        }
368 216
        // Optional fields
369
        return !(($schema[Relation::WHERE] ?? []) !== ($targetSchema[Relation::WHERE] ?? [])
370
            || ($schema[Relation::THROUGH_WHERE] ?? []) !== ($targetSchema[Relation::THROUGH_WHERE] ?? []));
371 456
    }
372
373 456
    private function checkBelongsToInversion(array $relation, array $targetRelation): bool
374 456
    {
375
        $schema = $relation[Relation::SCHEMA];
376 456
        $targetSchema = $targetRelation[Relation::SCHEMA];
377 56
        // MTM connects with MTM only
378
        if (!\in_array($targetRelation[Relation::TYPE], [Relation::HAS_MANY, Relation::HAS_ONE], true)) {
379
            return false;
380 456
        }
381 456
        // Same keys
382
        if ((array) $schema[Relation::INNER_KEY] !== (array) $targetSchema[Relation::OUTER_KEY]
383
            || (array) $schema[Relation::OUTER_KEY] !== (array) $targetSchema[Relation::INNER_KEY]) {
384
            return false;
385 456
        }
386
        // Optional fields
387
        return empty($schema[Relation::WHERE]) && empty($targetSchema[Relation::WHERE]);
388
    }
389
390
    /**
391 6088
     * @psalm-return null|class-string
392
     */
393 6088
    private function defineEntityClass(string $role): ?string
394 6070
    {
395
        if (\array_key_exists($role, $this->classes)) {
396 28
            return $this->classes[$role];
397 28
        }
398 26
        $rr = $this->resolveAlias($role) ?? $role;
399 28
        return $this->classes[$rr]
400
            ?? $this->schema[$rr][self::ENTITY]
401
            ?? throw new SchemaException("Undefined schema `{$role}`, not found.");
402
    }
403
}
404