Completed
Branch feature/pre-split (c41c6b)
by Anton
03:19
created

BelongsToMorphedSchema::inverseDefinition()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 36
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 20
nc 3
nop 2
dl 0
loc 36
rs 8.5806
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
8
namespace Spiral\ORM\Schemas\Relations;
9
10
use Spiral\ORM\Exceptions\DefinitionException;
11
use Spiral\ORM\Exceptions\RelationSchemaException;
12
use Spiral\ORM\ORMInterface;
13
use Spiral\ORM\Record;
14
use Spiral\ORM\Schemas\Definitions\RelationContext;
15
use Spiral\ORM\Schemas\Definitions\RelationDefinition;
16
use Spiral\ORM\Schemas\InversableRelationInterface;
17
use Spiral\ORM\Schemas\Relations\Traits\MorphedTrait;
18
use Spiral\ORM\Schemas\Relations\Traits\TypecastTrait;
19
use Spiral\ORM\Schemas\SchemaBuilder;
20
21
/**
22
 * BelongsToMorphed are almost identical to BelongsTo except it parent Record defined by role value
23
 * stored in [morph key] and parent key in [inner key].
24
 *
25
 * You can define BelongsToMorphed relation using syntax for BelongsTo but declaring outer class
26
 * as interface, meaning you should not only declare inversed relation name, but also it's type -
27
 * HAS_ONE or HAS_MANY.
28
 *
29
 * Example: 'parent' => [self::BELONGS_TO_MORPHED => 'Records\CommentableInterface']
30
 *
31
 * Attention, be very careful using morphing relations, you must know what you doing!
32
 * Attention #2, relation like that can not be preloaded!
33
 *
34
 * Attention #3, inverse morphed relation to use it efficiently (inversed into HAS_ONE relation).
35
 *
36
 * Example, [Comment can belong to any CommentableInterface record], relation name "parent",
37
 * relation requested to be inversed into HAS_MANY "comments":
38
 * - relation will walk should every record implementing CommentableInterface to collect name and
39
 *   type of outer keys, if outer key is not consistent across records implementing this interface
40
 *   an exception will be raised, let's say that outer key is "id" in every record
41
 * - relation will create inner key "parent_id" in "comments" table (or other table name), nullable
42
 *   by default
43
 * - relation will create "parent_type" morph key in "comments" table, nullable by default
44
 * - relation will create complex index index on columns "parent_id" and "parent_type" in
45
 *   "comments" table if allowed
46
 * - due relation is inversable every record implementing CommentableInterface will receive
47
 *   HAS_MANY relation "comments" pointing to Comment record using record role value
48
 *
49
 * @see BelongsToSchema
50
 */
51
class BelongsToMorphedSchema extends AbstractSchema implements InversableRelationInterface
52
{
53
    use MorphedTrait, TypecastTrait;
54
55
    /**
56
     * Relation type.
57
     */
58
    const RELATION_TYPE = Record::BELONGS_TO_MORPHED;
59
60
    /**
61
     * Size of string column dedicated to store outer role name. Used in polymorphic relations.
62
     * Even simple relations might include morph key (usually such relations created via inversion
63
     * of polymorphic relation).
64
     *
65
     * @see RecordSchema::getRole()
66
     */
67
    const MORPH_COLUMN_SIZE = 32;
68
69
    /**
70
     * Options needed in runtime.
71
     */
72
    const PACK_OPTIONS = [
73
        Record::INNER_KEY,
74
        Record::OUTER_KEY,
75
        Record::MORPH_KEY,
76
        Record::NULLABLE
77
    ];
78
79
    /**
80
     * {@inheritdoc}
81
     */
82
    const OPTIONS_TEMPLATE = [
83
        //By default morphed relations points to PRIMARY KEY
84
        Record::OUTER_KEY      => ORMInterface::R_PRIMARY_KEY,
85
86
        //Inner key will be based on singular name of relation and outer key name
87
        Record::INNER_KEY      => '{relation:singular}_id',
88
89
        //Inner key will be based on singular name of relation and outer key name
90
        Record::MORPH_KEY      => '{relation:singular}_type',
91
92
        //Relation allowed to create indexes in inner table
93
        Record::CREATE_INDEXES => true,
94
95
        //We are going to make all relations nullable by default, so we can add fields to existed
96
        //tables without raising an exceptions
97
        Record::NULLABLE       => true,
98
    ];
99
100
    /**
101
     * {@inheritdoc}
102
     *
103
     * Relation will be inversed into every associated parent.
104
     */
105
    public function inverseDefinition(SchemaBuilder $builder, $inverseTo): \Generator
106
    {
107
        if (!is_array($inverseTo) || count($inverseTo) != 2) {
108
            throw new DefinitionException(
109
                "BelongsToMorphed relation inverse must be defined as [type, outer relation name]"
110
            );
111
        }
112
113
        foreach ($this->findTargets($builder) as $schema) {
114
            /**
115
             * We are going to simply replace outer key with inner key and keep the rest of options intact.
116
             */
117
            $inversed = new RelationDefinition(
118
                $inverseTo[1],
119
                $inverseTo[0],
120
                $this->definition->sourceContext()->getClass(),
121
                [
122
                    Record::INNER_KEY         => $this->findOuter($builder)->getName(),
123
                    Record::OUTER_KEY         => $this->option(Record::INNER_KEY),
124
                    Record::CREATE_CONSTRAINT => false,
125
                    Record::CREATE_INDEXES    => $this->option(Record::CREATE_INDEXES),
126
                    Record::NULLABLE          => $this->option(Record::NULLABLE),
127
                    Record::MORPH_KEY         => $this->option(Record::MORPH_KEY)
128
                ]
129
            );
130
131
            //In back order :)
132
            yield $inversed->withContext(
133
                RelationContext::createContent(
134
                    $schema,
135
                    $builder->requestTable($schema->getTable(), $schema->getDatabase())
136
                ),
137
                $this->definition->sourceContext()
138
            );
139
        }
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function declareTables(SchemaBuilder $builder): array
146
    {
147
        $sourceTable = $this->sourceTable($builder);
148
149
        if (!interface_exists($target = $this->definition->getTarget())) {
150
            throw new RelationSchemaException("Morphed relations can only be pointed to an interface");
151
        }
152
153
        $outerKey = $this->findOuter($builder);
154
        if (empty($outerKey)) {
155
            throw new RelationSchemaException("Unable to build morphed relation, no outer record found");
156
        }
157
158
        //Make sure all tables has same outer
159
        $this->verifyOuter($builder, $outerKey);
160
161
        //Column to be used as inner key
162
        $innerKey = $sourceTable->column($this->option(Record::INNER_KEY));
163
164
        //Syncing types
165
        $innerKey->setType($this->resolveType($outerKey));
166
167
        //If nullable
168
        $innerKey->nullable($this->option(Record::NULLABLE));
169
170
        //Morph key is always string
171
        $morphKey = $sourceTable->column($this->option(Record::MORPH_KEY));
172
        $morphKey->string(self::MORPH_COLUMN_SIZE);
173
174
        //Do we need indexes?
175
        if ($this->option(Record::CREATE_INDEXES)) {
176
            //Compound outer key
177
            $sourceTable->index([$innerKey->getName(), $morphKey->getName()]);
178
        }
179
180
        //No constrains to create
181
        return [$sourceTable];
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187
    public function packRelation(SchemaBuilder $builder): array
188
    {
189
        $schema = parent::packRelation($builder);
190
        $schema[ORMInterface::R_SCHEMA][Record::OUTER_KEY] = $this->findOuter($builder)->getName();
191
192
        foreach ($this->findTargets($builder) as $outer) {
193
            //Role => model mapping
194
            $schema[ORMInterface::R_SCHEMA][ORMInterface::R_ROLE_NAME][$outer->getRole()] = $outer->getClass();
195
        }
196
197
        return $schema;
198
    }
199
}