GenerateRelations::inverse()   B
last analyzed

Complexity

Conditions 7
Paths 7

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.0099

Importance

Changes 0
Metric Value
cc 7
eloc 21
nc 7
nop 2
dl 0
loc 30
ccs 16
cts 17
cp 0.9412
crap 7.0099
rs 8.6506
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\Schema\Generator;
6
7
use Cycle\ORM\Relation;
8
use Cycle\Schema\Definition\Entity;
9
use Cycle\Schema\Exception\RegistryException;
10
use Cycle\Schema\Exception\RelationException;
11
use Cycle\Schema\Exception\SchemaException;
12
use Cycle\Schema\GeneratorInterface;
13
use Cycle\Schema\InversableInterface;
14
use Cycle\Schema\Registry;
15
use Cycle\Schema\Relation as Definition;
16
use Cycle\Schema\Relation\OptionSchema;
17
use Cycle\Schema\Relation\RelationSchema;
18
use Cycle\Schema\RelationInterface;
19
20
/**
21
 * Generate relations based on their schematic definitions.
22
 */
23
final class GenerateRelations implements GeneratorInterface
24
{
25
    // aliases between option names and their internal IDs
26
    public const OPTION_MAP = [
27
        'nullable' => Relation::NULLABLE,
28
        'cascade' => Relation::CASCADE,
29
        'load' => Relation::LOAD,
30
        'innerKey' => Relation::INNER_KEY,
31
        'outerKey' => Relation::OUTER_KEY,
32
        'morphKey' => Relation::MORPH_KEY,
33
        'through' => Relation::THROUGH_ENTITY,
34
        'throughInnerKey' => Relation::THROUGH_INNER_KEY,
35
        'throughOuterKey' => Relation::THROUGH_OUTER_KEY,
36
        'throughWhere' => Relation::THROUGH_WHERE,
37
        'where' => Relation::WHERE,
38
        'collection' => Relation::COLLECTION_TYPE,
39
        'orderBy' => Relation::ORDER_BY,
40
        'fkCreate' => RelationSchema::FK_CREATE,
41
        'fkAction' => RelationSchema::FK_ACTION,
42
        'fkOnDelete' => RelationSchema::FK_ON_DELETE,
43
        'indexCreate' => RelationSchema::INDEX_CREATE,
44
        'morphKeyLength' => RelationSchema::MORPH_KEY_LENGTH,
45
        'embeddedPrefix' => RelationSchema::EMBEDDED_PREFIX,
46
47
        // deprecated
48
        'though' => Relation::THROUGH_ENTITY,
49
        'thoughInnerKey' => Relation::THROUGH_INNER_KEY,
50
        'thoughOuterKey' => Relation::THROUGH_OUTER_KEY,
51
        'thoughWhere' => Relation::THROUGH_WHERE,
52
    ];
53
54
    /** @var OptionSchema */
55
    private $options;
56
57
    /** @var RelationInterface[] */
58
    private $relations = [];
59
60
    public function __construct(?array $relations = null, ?OptionSchema $optionSchema = null)
61
    {
62
        $relations = $relations ?? self::getDefaultRelations();
63
        $this->options = $optionSchema ?? new OptionSchema(self::OPTION_MAP);
64 1192
65
        foreach ($relations as $id => $relation) {
66 1192
            if (!$relation instanceof RelationInterface) {
67 1192
                throw new \InvalidArgumentException(
68
                    sprintf(
69 1192
                        'Invalid relation type, RelationInterface excepted, `%s` given',
70 1192
                        is_object($relation) ? get_class($relation) : gettype($relation),
71
                    ),
72
                );
73
            }
74
75
            $this->relations[$id] = $relation;
76
        }
77
    }
78
79 1192
    public function run(Registry $registry): Registry
80
    {
81 1192
        foreach ($registry as $entity) {
82
            $this->register($registry, $entity);
83
        }
84
85
        foreach ($registry as $entity) {
86
            $this->inverse($registry, $entity);
87
        }
88 1168
89
        return $registry;
90 1168
    }
91 1168
92
    protected static function getDefaultRelations(): array
93
    {
94 984
        return [
95 984
            'embedded' => new Definition\Embedded(),
96
            'belongsTo' => new Definition\BelongsTo(),
97
            'hasOne' => new Definition\HasOne(),
98 888
            'hasMany' => new Definition\HasMany(),
99
            'refersTo' => new Definition\RefersTo(),
100
            'manyToMany' => new Definition\ManyToMany(),
101
            'belongsToMorphed' => new Definition\Morphed\BelongsToMorphed(),
102
            'morphedHasOne' => new Definition\Morphed\MorphedHasOne(),
103
            'morphedHasMany' => new Definition\Morphed\MorphedHasMany(),
104
        ];
105 1168
    }
106
107 1168
    protected function register(Registry $registry, Entity $entity): void
108 1168
    {
109
        $role = $entity->getRole();
110 1168
        \assert($role !== null);
111 1168
112 1168
        foreach ($entity->getRelations() as $name => $r) {
113
            $schema = $this->initRelation($r->getType())->withContext(
114
                $name,
115
                $role,
116
                $r->getTarget(),
117 1168
                $this->options->withOptions($r->getOptions()),
118 184
            );
119 56
120 56
            // compute relation values (field names, related entities and etc)
121 56
            try {
122
                $schema->compute($registry);
123
            } catch (RelationException $e) {
124
                throw new SchemaException(
125
                    "Unable to compute relation `{$role}`.`{$name}`",
126 984
                    $e->getCode(),
127
                    $e,
128 1032
                );
129
            }
130
131
            $registry->registerRelation($entity, $name, $schema);
132
        }
133
    }
134 984
135
    protected function inverse(Registry $registry, Entity $entity): void
136 984
    {
137 984
        foreach ($entity->getRelations() as $name => $r) {
138 728
            if (!$r->isInversed()) {
139
                continue;
140
            }
141 256
142 256
            $inverseName = $r->getInverseName();
143
            $inverseType = $r->getInverseType();
144
            \assert(!empty($inverseName) && !empty($inverseType));
145
146 256
            $schema = $registry->getRelation($entity, $name);
147
            if (!$schema instanceof InversableInterface) {
148 256
                throw new SchemaException('Unable to inverse relation of type ' . get_class($schema));
149 256
            }
150 240
151 240
            foreach ($schema->inverseTargets($registry) as $target) {
152
                try {
153
                    $inversed = $schema->inverseRelation(
154 160
                        $this->initRelation($inverseType),
155 96
                        $inverseName,
156 80
                        $r->getInverseLoad(),
157 80
                    );
158 80
159
                    $registry->registerRelation($target, $inverseName, $inversed);
160
                } catch (RelationException $e) {
161
                    throw new SchemaException(
162
                        "Unable to inverse relation `{$entity->getRole()}`.`{$name}`",
163
                        $e->getCode(),
164 920
                        $e,
165
                    );
166
                }
167
            }
168
        }
169
    }
170
171 1168
    protected function initRelation(string $type): RelationInterface
172
    {
173 1168
        if (!isset($this->relations[$type])) {
174 16
            throw new RegistryException("Undefined relation type `{$type}`");
175
        }
176
177 1168
        return $this->relations[$type];
178
    }
179
}
180