Schema   F
last analyzed

Complexity

Total Complexity 77

Size/Duplication

Total Lines 389
Duplicated Lines 0 %

Test Coverage

Coverage 92.7%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 187
dl 0
loc 389
ccs 165
cts 178
cp 0.927
rs 2.24
c 3
b 0
f 0
wmc 77

19 Methods

Rating   Name   Duplication   Size   Complexity  
A defines() 0 3 2
B findInvertedRelation() 0 34 7
A __construct() 0 4 1
A toArray() 0 3 1
B normalizeRelations() 0 26 7
A getInnerRelations() 0 3 1
A getInheritedRoles() 0 3 1
B getOuterRelations() 0 37 6
A resolveAlias() 0 10 3
B linkRelations() 0 50 9
A defineRelation() 0 9 2
A compareManyToMany() 0 20 6
A getRoles() 0 3 1
A define() 0 12 3
A defineEntityClass() 0 9 2
A getRelations() 0 3 1
A checkBelongsToInversion() 0 15 5
A __set_state() 0 7 1
F normalize() 0 69 18

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

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