Completed
Push — master ( a8cb34...ce5009 )
by Anton
03:37
created

RelationBuilder::locateOuter()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 27
nc 8
nop 2
dl 0
loc 43
rs 8.439
c 0
b 0
f 0
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
        if (!empty($definition->targetContext())) {
188
            //Nothing to do, already have outer parent
189
            return $definition;
190
        }
191
192
        $found = null;
193
        foreach ($builder->getSchemas() as $schema) {
194
            if ($this->matchBinded($definition->getTarget(), $schema)) {
195
                if (!empty($found)) {
196
                    //Multiple records found
197
                    throw new DefinitionException(sprintf(
198
                        "Ambiguous target of '%s' for late binded relation %s.%s",
199
                        $definition->getTarget(),
200
                        $definition->sourceContext()->getClass(),
201
                        $definition->getName()
202
                    ));
203
                }
204
205
                $found = $schema;
206
            }
207
        }
208
209
        if (empty($found)) {
210
            throw new DefinitionException(sprintf(
211
                "Unable to locate outer record of '%s' for late binded relation %s.%s",
212
                $definition->getTarget(),
213
                $definition->sourceContext()->getClass(),
214
                $definition->getName()
215
            ));
216
        }
217
218
        return $definition->withContext(
219
            $definition->sourceContext(),
220
            RelationContext::createContent(
221
                $found,
222
                $builder->requestTable($found->getTable(), $found->getDatabase())
223
            )
224
        );
225
    }
226
227
    /**
228
     * Check if schema matches relation target.
229
     *
230
     * @param string                              $target
231
     * @param \Spiral\ORM\Schemas\SchemaInterface $schema
232
     *
233
     * @return bool
234
     */
235
    private function matchBinded(string $target, SchemaInterface $schema): bool
236
    {
237
        if ($schema->getRole() == $target) {
238
            return true;
239
        }
240
241
        if (interface_exists($target) && is_a($schema->getClass(), $target, true)) {
242
            //Match by interface
243
            return true;
244
        }
245
246
        return false;
247
    }
248
}