RelationBuilder   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 233
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 9
dl 0
loc 233
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A registerRelation() 0 28 3
A inverseRelations() 0 25 5
A getRelations() 0 4 1
A declareTables() 0 8 3
A packRelations() 0 14 3
B locateOuter() 0 47 6
A matchBinded() 0 13 4
1
<?php
2
/**
3
 * Spiral, Core Components
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\ORM\Schemas;
9
10
use Spiral\Core\FactoryInterface;
11
use Spiral\ORM\Configs\RelationsConfig;
12
use Spiral\ORM\Exceptions\DefinitionException;
13
use Spiral\ORM\Schemas\Definitions\RelationContext;
14
use Spiral\ORM\Schemas\Definitions\RelationDefinition;
15
16
/**
17
 * Subsection of SchemaBuilder used to configure tables and columns defined by model to model
18
 * relations.
19
 */
20
class RelationBuilder
21
{
22
    /**
23
     * @invisible
24
     * @var RelationsConfig
25
     */
26
    protected $config;
27
28
    /**
29
     * @invisible
30
     * @var FactoryInterface
31
     */
32
    protected $factory;
33
34
    /**
35
     * Set of relation definitions.
36
     *
37
     * @var RelationInterface[]
38
     */
39
    private $relations = [];
40
41
    /**
42
     * @param RelationsConfig  $config
43
     * @param FactoryInterface $factory
44
     */
45
    public function __construct(RelationsConfig $config, FactoryInterface $factory)
46
    {
47
        $this->config = $config;
48
        $this->factory = $factory;
49
    }
50
51
    /**
52
     * Registering new relation definition. At this moment function would not check if relation is
53
     * unique and will redeclare it.
54
     *
55
     * @param SchemaBuilder      $builder
56
     * @param RelationDefinition $definition Relation options (definition).
57
     *
58
     * @throws DefinitionException
59
     */
60
    public function registerRelation(SchemaBuilder $builder, RelationDefinition $definition)
61
    {
62
        if (!$this->config->hasRelation($definition->getType())) {
63
            throw new DefinitionException(sprintf(
64
                "Undefined relation type '%s' in '%s'.'%s'",
65
                $definition->getType(),
66
                $definition->sourceContext()->getClass(),
67
                $definition->getName()
68
            ));
69
        }
70
71
        if ($definition->isLateBinded()) {
72
            /**
73
             * Late binded relations locate their parent based on all existed records.
74
             */
75
            $definition = $this->locateOuter($builder, $definition);
76
        }
77
78
        $class = $this->config->relationClass(
79
            $definition->getType(),
80
            RelationsConfig::SCHEMA_CLASS
81
        );
82
83
        //Creating relation schema
84
        $relation = $this->factory->make($class, compact('definition'));
85
86
        $this->relations[] = $relation;
87
    }
88
89
    /**
90
     * Create inverse relations where needed.
91
     *
92
     * @param SchemaBuilder $builder
93
     *
94
     * @throws DefinitionException
95
     */
96
    public function inverseRelations(SchemaBuilder $builder)
97
    {
98
        /**
99
         * Inverse process is relation specific.
100
         */
101
        foreach ($this->relations as $relation) {
102
            $definition = $relation->getDefinition();
103
104
            if ($definition->needInversion()) {
105
                if (!$relation instanceof InversableRelationInterface) {
106
                    throw new DefinitionException(sprintf(
107
                        "Unable to inverse relation '%s'.'%s', relation schema '%s' is non inversable",
108
                        $definition->sourceContext()->getClass(),
109
                        $definition->getName(),
110
                        get_class($relation)
111
                    ));
112
                }
113
114
                $inversed = $relation->inverseDefinition($builder, $definition->getInverse());
115
                foreach ($inversed as $definition) {
116
                    $this->registerRelation($builder, $definition);
117
                }
118
            }
119
        }
120
    }
121
122
    /**
123
     * All declared relations.
124
     *
125
     * @return RelationInterface[]
126
     */
127
    public function getRelations(): array
128
    {
129
        return $this->relations;
130
    }
131
132
    /**
133
     * Declare set of tables for each relation. Method must return Generator of AbstractTable
134
     * sequentially (attention, non sequential processing will cause collision issues between
135
     * tables).
136
     *
137
     * @param SchemaBuilder $builder
138
     *
139
     * @return \Generator
140
     */
141
    public function declareTables(SchemaBuilder $builder): \Generator
142
    {
143
        foreach ($this->relations as $relation) {
144
            foreach ($relation->declareTables($builder) as $table) {
145
                yield $table;
146
            }
147
        }
148
    }
149
150
    /**
151
     * Pack relation schemas for specific model class in order to be saved in memory.
152
     *
153
     * @param string        $class
154
     * @param SchemaBuilder $builder
155
     *
156
     * @return array
157
     */
158
    public function packRelations(string $class, SchemaBuilder $builder): array
159
    {
160
        $result = [];
161
        foreach ($this->relations as $relation) {
162
            $definition = $relation->getDefinition();
163
164
            if ($definition->sourceContext()->getClass() == $class) {
165
                //Packing relation, relation schema are given with associated table
166
                $result[$definition->getName()] = $relation->packRelation($builder);
167
            }
168
        }
169
170
        return $result;
171
    }
172
173
    /**
174
     * Populate entity target based on interface or role.
175
     *
176
     * @param SchemaBuilder                                      $builder
177
     * @param \Spiral\ORM\Schemas\Definitions\RelationDefinition $definition
178
     *
179
     * @return \Spiral\ORM\Schemas\Definitions\RelationDefinition
180
     *
181
     * @throws DefinitionException
182
     */
183
    protected function locateOuter(
184
        SchemaBuilder $builder,
185
        RelationDefinition $definition
186
    ): RelationDefinition {
187
        /**
188
         * todo: add functionality to resolve database alias
189
         */
190
191
        if (!empty($definition->targetContext())) {
192
            //Nothing to do, already have outer parent
193
            return $definition;
194
        }
195
196
        $found = null;
197
        foreach ($builder->getSchemas() as $schema) {
198
            if ($this->matchBinded($definition->getTarget(), $schema)) {
199
                if (!empty($found)) {
200
                    //Multiple records found
201
                    throw new DefinitionException(sprintf(
202
                        "Ambiguous target of '%s' for late binded relation %s.%s",
203
                        $definition->getTarget(),
204
                        $definition->sourceContext()->getClass(),
205
                        $definition->getName()
206
                    ));
207
                }
208
209
                $found = $schema;
210
            }
211
        }
212
213
        if (empty($found)) {
214
            throw new DefinitionException(sprintf(
215
                "Unable to locate outer record of '%s' for late binded relation %s.%s",
216
                $definition->getTarget(),
217
                $definition->sourceContext()->getClass(),
218
                $definition->getName()
219
            ));
220
        }
221
222
        return $definition->withContext(
223
            $definition->sourceContext(),
224
            RelationContext::createContent(
225
                $found,
226
                $builder->requestTable($found->getTable(), $found->getDatabase())
227
            )
228
        );
229
    }
230
231
    /**
232
     * Check if schema matches relation target.
233
     *
234
     * @param string                              $target
235
     * @param \Spiral\ORM\Schemas\SchemaInterface $schema
236
     *
237
     * @return bool
238
     */
239
    private function matchBinded(string $target, SchemaInterface $schema): bool
240
    {
241
        if ($schema->getRole() == $target) {
242
            return true;
243
        }
244
245
        if (interface_exists($target) && is_a($schema->getClass(), $target, true)) {
246
            //Match by interface
247
            return true;
248
        }
249
250
        return false;
251
    }
252
}