Passed
Pull Request — 2.x (#66)
by Maxim
15:09
created

GenerateRelations::inverse()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 7.0119

Importance

Changes 0
Metric Value
cc 7
eloc 21
nc 7
nop 2
dl 0
loc 30
ccs 15
cts 16
cp 0.9375
crap 7.0119
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
    /**
61
     * @param array|null $relations
62
     * @param OptionSchema|null $optionSchema
63
     */
64 1192
    public function __construct(array $relations = null, OptionSchema $optionSchema = null)
65
    {
66 1192
        $relations = $relations ?? self::getDefaultRelations();
67 1192
        $this->options = $optionSchema ?? new OptionSchema(self::OPTION_MAP);
68
69 1192
        foreach ($relations as $id => $relation) {
70 1192
            if (!$relation instanceof RelationInterface) {
71
                throw new \InvalidArgumentException(
72
                    sprintf(
73
                        'Invalid relation type, RelationInterface excepted, `%s` given',
74
                        is_object($relation) ? get_class($relation) : gettype($relation)
75
                    )
76
                );
77
            }
78
79 1192
            $this->relations[$id] = $relation;
80
        }
81 1192
    }
82
83
    /**
84
     * @param Registry $registry
85
     *
86
     * @return Registry
87
     */
88 1168
    public function run(Registry $registry): Registry
89
    {
90 1168
        foreach ($registry as $entity) {
91 1168
            $this->register($registry, $entity);
92
        }
93
94 984
        foreach ($registry as $entity) {
95 984
            $this->inverse($registry, $entity);
96
        }
97
98 888
        return $registry;
99
    }
100
101
    /**
102
     * @param Registry $registry
103
     * @param Entity $entity
104
     */
105 1168
    protected function register(Registry $registry, Entity $entity): void
106
    {
107 1168
        $role = $entity->getRole();
108 1168
        \assert($role !== null);
109
110 1168
        foreach ($entity->getRelations() as $name => $r) {
111 1168
            $schema = $this->initRelation($r->getType())->withContext(
112 1168
                $name,
113
                $role,
114
                $r->getTarget(),
115
                $this->options->withOptions($r->getOptions())
116
            );
117 1168
118 184
            // compute relation values (field names, related entities and etc)
119 56
            try {
120 56
                $schema->compute($registry);
121 56
            } catch (RelationException $e) {
122
                throw new SchemaException(
123
                    "Unable to compute relation `{$role}`.`{$name}`",
124
                    $e->getCode(),
125
                    $e
126 984
                );
127
            }
128 1032
129
            $registry->registerRelation($entity, $name, $schema);
130
        }
131
    }
132
133
    /**
134 984
     * @param Registry $registry
135
     * @param Entity $entity
136 984
     */
137 984
    protected function inverse(Registry $registry, Entity $entity): void
138 728
    {
139
        foreach ($entity->getRelations() as $name => $r) {
140
            if (!$r->isInversed()) {
141 256
                continue;
142 256
            }
143
144
            $inverseName = $r->getInverseName();
145
            $inverseType = $r->getInverseType();
146 256
            \assert(!empty($inverseName) && !empty($inverseType));
147
148 256
            $schema = $registry->getRelation($entity, $name);
149 256
            if (!$schema instanceof InversableInterface) {
150 240
                throw new SchemaException('Unable to inverse relation of type ' . get_class($schema));
151 240
            }
152
153
            foreach ($schema->inverseTargets($registry) as $target) {
154 160
                try {
155 96
                    $inversed = $schema->inverseRelation(
156 80
                        $this->initRelation($inverseType),
157 80
                        $inverseName,
158 80
                        $r->getInverseLoad()
159
                    );
160
161
                    $registry->registerRelation($target, $inverseName, $inversed);
162
                } catch (RelationException $e) {
163
                    throw new SchemaException(
164 920
                        "Unable to inverse relation `{$entity->getRole()}`.`{$name}`",
165
                        $e->getCode(),
166
                        $e
167
                    );
168
                }
169
            }
170
        }
171 1168
    }
172
173 1168
    /**
174 16
     * @param string $type
175
     *
176
     * @return RelationInterface
177 1168
     */
178
    protected function initRelation(string $type): RelationInterface
179
    {
180
        if (!isset($this->relations[$type])) {
181
            throw new RegistryException("Undefined relation type `{$type}`");
182
        }
183 120
184
        return $this->relations[$type];
185
    }
186 120
187 120
    /**
188 120
     * @return array
189 120
     */
190 120
    protected static function getDefaultRelations(): array
191 120
    {
192 120
        return [
193 120
            'embedded' => new Definition\Embedded(),
194 120
            'belongsTo' => new Definition\BelongsTo(),
195
            'hasOne' => new Definition\HasOne(),
196
            'hasMany' => new Definition\HasMany(),
197
            'refersTo' => new Definition\RefersTo(),
198
            'manyToMany' => new Definition\ManyToMany(),
199
            'belongsToMorphed' => new Definition\Morphed\BelongsToMorphed(),
200
            'morphedHasOne' => new Definition\Morphed\MorphedHasOne(),
201
            'morphedHasMany' => new Definition\Morphed\MorphedHasMany(),
202
        ];
203
    }
204
}
205