ManyToMorphedSchema::declareTables()   B
last analyzed

Complexity

Conditions 6
Paths 7

Size

Total Lines 78

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 78
rs 7.8577
c 0
b 0
f 0
cc 6
nc 7
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Spiral, Core Components
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\ORM\Schemas\Relations;
9
10
use Doctrine\Common\Inflector\Inflector;
11
use Spiral\ORM\Exceptions\DefinitionException;
12
use Spiral\ORM\Exceptions\RelationSchemaException;
13
use Spiral\ORM\Helpers\ColumnRenderer;
14
use Spiral\ORM\ORMInterface;
15
use Spiral\ORM\Record;
16
use Spiral\ORM\Schemas\Definitions\RelationContext;
17
use Spiral\ORM\Schemas\Definitions\RelationDefinition;
18
use Spiral\ORM\Schemas\InversableRelationInterface;
19
use Spiral\ORM\Schemas\Relations\Traits\ForeignsTrait;
20
use Spiral\ORM\Schemas\Relations\Traits\MorphedTrait;
21
use Spiral\ORM\Schemas\Relations\Traits\TypecastTrait;
22
use Spiral\ORM\Schemas\SchemaBuilder;
23
24
/**
25
 * ManyToMorphed relation declares relation between parent record and set of outer records joined by
26
 * common interface. Relation allow to specify inner key (key in parent record), outer key (key in
27
 * outer records), morph key, pivot table name, names of pivot columns to store inner and outer key
28
 * values and set of additional columns. Relation DOES NOT to specify WHERE statement for outer
29
 * records. However you can specify where conditions for PIVOT table.
30
 *
31
 * You can declare this relation using same syntax as for ManyToMany except your target class
32
 * must be an interface.
33
 *
34
 * Attention, be very careful using morphing relations, you must know what you doing!
35
 * Attention #2, relation like that can not be preloaded!
36
 *
37
 * Example [Tag related to many TaggableInterface], relation name "tagged", relation requested to be
38
 * inversed using name "tags":
39
 * - relation will walk should every record implementing TaggableInterface to collect name and
40
 *   type of outer keys, if outer key is not consistent across records implementing this interface
41
 *   an exception will be raised, let's say that outer key is "id" in every record
42
 * - relation will create pivot table named "tagged_map" (if allowed), where table name generated
43
 *   based on relation name (you can change name)
44
 * - relation will create pivot key named "tag_ud" related to Tag primary key
45
 * - relation will create pivot key named "tagged_id" related to primary key of outer records,
46
 *   singular relation name used to generate key like that
47
 * - relation will create pivot key named "tagged_type" to store role of outer record
48
 * - relation will create unique index on "tag_id", "tagged_id" and "tagged_type" columns if allowed
49
 * - relation will create additional columns in pivot table if any requested
50
 *
51
 * Using in records:
52
 * You can use inversed relation as usual ManyToMany, however in Tag record relation access will be
53
 * little bit more complex - every linked record will create inner ManyToMany relation:
54
 * $tag->tagged->users->count(); //Where "users" is plural form of one outer records
55
 *
56
 * You can defined your own inner relation names by using MORPHED_ALIASES option when defining
57
 * relation.
58
 *
59
 * Attention, relation do not support WHERE statement on outer records.
60
 *
61
 * @see BelongsToMorhedSchema
62
 * @see ManyToManySchema
63
 */
64
class ManyToMorphedSchema extends AbstractSchema implements InversableRelationInterface
65
{
66
    use TypecastTrait, ForeignsTrait, MorphedTrait;
67
68
    /**
69
     * Relation type.
70
     */
71
    const RELATION_TYPE = Record::MANY_TO_MORPHED;
72
73
    /**
74
     * Size of string column dedicated to store outer role name. Used in polymorphic relations.
75
     * Even simple relations might include morph key (usually such relations created via inversion
76
     * of polymorphic relation).
77
     *
78
     * @see RecordSchema::getRole()
79
     */
80
    const MORPH_COLUMN_SIZE = 32;
81
82
    /**
83
     * Options to be packed.
84
     */
85
    const PACK_OPTIONS = [
86
        Record::PIVOT_TABLE,
87
        Record::OUTER_KEY,
88
        Record::INNER_KEY,
89
        Record::THOUGHT_INNER_KEY,
90
        Record::THOUGHT_OUTER_KEY,
91
        Record::RELATION_COLUMNS,
92
        Record::PIVOT_COLUMNS,
93
        Record::WHERE_PIVOT,
94
        Record::MORPH_KEY,
95
        Record::ORDER_BY
96
    ];
97
98
    /**
99
     * Default postfix for pivot tables.
100
     */
101
    const PIVOT_POSTFIX = '_map';
102
103
    /**
104
     * {@inheritdoc}
105
     *
106
     * @invisible
107
     */
108
    const OPTIONS_TEMPLATE = [
109
        //Inner key of parent record will be used to fill "THOUGHT_INNER_KEY" in pivot table
110
        Record::INNER_KEY         => '{source:primaryKey}',
111
112
        //We are going to use primary key of outer table to fill "THOUGHT_OUTER_KEY" in pivot table
113
        //This is technically "inner" key of outer record, we will name it "outer key" for simplicity
114
        Record::OUTER_KEY         => ORMInterface::R_PRIMARY_KEY,
115
116
        //Name field where parent record inner key will be stored in pivot table, role + innerKey
117
        //by default
118
        Record::THOUGHT_INNER_KEY => '{source:role}_{option:innerKey}',
119
120
        //Name field where inner key of outer record (outer key) will be stored in pivot table,
121
        //role + outerKey by default
122
        Record::THOUGHT_OUTER_KEY => '{relation:name}_id',
123
124
        //Declares what specific record pivot record linking to
125
        Record::MORPH_KEY         => '{relation:name}_type',
126
127
        //Set constraints (foreign keys) by default, attention only set for source table
128
        Record::CREATE_CONSTRAINT => true,
129
130
        //@link https://en.wikipedia.org/wiki/Foreign_key
131
        Record::CONSTRAINT_ACTION => 'CASCADE',
132
133
        //Relation allowed to create indexes in pivot table
134
        Record::CREATE_INDEXES    => true,
135
136
        //Name of pivot table to be declared, default value is not stated as it will be generated
137
        //based on roles of inner and outer records
138
        Record::PIVOT_TABLE       => '{relation:name}_map',
139
140
        //Relation allowed to create pivot table
141
        Record::CREATE_PIVOT      => true,
142
143
        //Additional set of columns to be added into pivot table, you can use same column definition
144
        //type as you using for your records
145
        Record::PIVOT_COLUMNS     => [],
146
147
        //Set of default values to be used for pivot table
148
        Record::PIVOT_DEFAULTS    => [],
149
150
        //WHERE statement in a form of simplified array definition to be applied to pivot table
151
        //data.
152
        Record::WHERE_PIVOT       => [],
153
154
        //Order
155
        Record::ORDER_BY          => []
156
    ];
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function inverseDefinition(SchemaBuilder $builder, $inverseTo): \Generator
162
    {
163
        if (!is_string($inverseTo)) {
164
            throw new DefinitionException("Inversed relation must be specified as string");
165
        }
166
167
        foreach ($this->findTargets($builder) as $schema) {
168
            /**
169
             * We are going to simply replace outer key with inner key and keep the rest of options intact.
170
             */
171
            $inversed = new RelationDefinition(
172
                $inverseTo,
173
                Record::MANY_TO_MANY,
174
                $this->definition->sourceContext()->getClass(),
175
                [
176
                    Record::PIVOT_TABLE       => $this->option(Record::PIVOT_TABLE),
177
                    Record::OUTER_KEY         => $this->option(Record::INNER_KEY),
178
                    Record::INNER_KEY         => $this->findOuter($builder)->getName(),
179
                    Record::THOUGHT_INNER_KEY => $this->option(Record::THOUGHT_OUTER_KEY),
180
                    Record::THOUGHT_OUTER_KEY => $this->option(Record::THOUGHT_INNER_KEY),
181
                    Record::CREATE_CONSTRAINT => false,
182
                    Record::CREATE_INDEXES    => $this->option(Record::CREATE_INDEXES),
183
                    Record::CREATE_PIVOT      => false, //Table creation hes been already handled
184
                    //We have to include morphed key in here
185
                    Record::PIVOT_COLUMNS     => $this->option(Record::PIVOT_COLUMNS) + [
186
                            $this->option(Record::MORPH_KEY) => 'string'
187
                        ],
188
                    Record::WHERE_PIVOT       => $this->option(Record::WHERE_PIVOT),
189
                    Record::MORPH_KEY         => $this->option(Record::MORPH_KEY)
190
                ]
191
            );
192
193
            //In back order :)
194
            yield $inversed->withContext(
195
                RelationContext::createContent(
196
                    $schema,
197
                    $builder->requestTable($schema->getTable(), $schema->getDatabase())
198
                ),
199
                $this->definition->sourceContext()
200
            );
201
        }
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     *
207
     * Note: pivot table will be build from direction of source, please do not attempt to create
208
     * many to many relations between databases without specifying proper database.
209
     */
210
    public function declareTables(SchemaBuilder $builder): array
211
    {
212
        $sourceContext = $this->definition->sourceContext();
213
214
        if (!interface_exists($target = $this->definition->getTarget())) {
215
            throw new RelationSchemaException("Morphed relations can only be pointed to an interface");
216
        }
217
218
        if (!$this->option(Record::CREATE_PIVOT)) {
219
            //No pivot table creation were requested, noting really to do
220
            return [];
221
        }
222
223
        $outerKey = $this->findOuter($builder);
224
        if (empty($outerKey)) {
225
            throw new RelationSchemaException("Unable to build morphed relation, no outer record found");
226
        }
227
228
        //Make sure all tables has same outer
229
        $this->verifyOuter($builder, $outerKey);
230
231
        $pivotTable = $builder->requestTable(
232
            $this->option(Record::PIVOT_TABLE),
233
            $sourceContext->getDatabase(),
234
            false,
235
            true
236
        );
237
238
        /*
239
         * Declare columns in map/pivot table.
240
         */
241
        $thoughtInnerKey = $pivotTable->column($this->option(Record::THOUGHT_INNER_KEY));
242
        $thoughtInnerKey->nullable(false);
243
        $thoughtInnerKey->setType($this->resolveType(
244
            $sourceContext->getColumn($this->option(Record::INNER_KEY))
245
        ));
246
247
        $thoughtOuterKey = $pivotTable->column($this->option(Record::THOUGHT_OUTER_KEY));
248
        $thoughtOuterKey->nullable(false);
249
        $thoughtOuterKey->setType($this->resolveType($outerKey));
250
251
        //Morph key
252
        $thoughtMorphKey = $pivotTable->column($this->option(Record::MORPH_KEY));
253
        $thoughtMorphKey->nullable(false);
254
        $thoughtMorphKey->string(static::MORPH_COLUMN_SIZE);
255
256
        /*
257
         * Declare user columns in pivot table.
258
         */
259
        $rendered = new ColumnRenderer();
260
        $rendered->renderColumns(
261
            $this->option(Record::PIVOT_COLUMNS),
262
            $this->option(Record::PIVOT_DEFAULTS),
263
            $pivotTable
264
        );
265
266
        //Map might only contain unique link between source and target
267
        if ($this->option(Record::CREATE_INDEXES)) {
268
            $pivotTable->index([
269
                $thoughtInnerKey->getName(),
270
                $thoughtOuterKey->getName(),
271
                $thoughtMorphKey->getName()
272
            ])->unique();
273
        }
274
275
        //There is only 1 constrain
276
        if ($this->isConstrained()) {
277
            $this->createForeign(
278
                $pivotTable,
279
                $thoughtInnerKey,
280
                $sourceContext->getColumn($this->option(Record::INNER_KEY)),
281
                $this->option(Record::CONSTRAINT_ACTION),
282
                $this->option(Record::CONSTRAINT_ACTION)
283
            );
284
        }
285
286
        return [$pivotTable];
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     */
292
    public function packRelation(SchemaBuilder $builder): array
293
    {
294
        $packed = parent::packRelation($builder);
295
        $schema = &$packed[ORMInterface::R_SCHEMA];
296
297
        //Must be resolved thought builder (can't be defined manually)
298
        $schema[Record::OUTER_KEY] = $this->findOuter($builder)->getName();
299
300
        //Clarifying location
301
        $schema[Record::PIVOT_DATABASE] = $this->definition->sourceContext()->getDatabase();
302
        $schema[Record::PIVOT_COLUMNS] = array_keys($schema[Record::PIVOT_COLUMNS]);
303
304
        //Ensure that inner keys are always presented
305
        $schema[Record::PIVOT_COLUMNS] = array_merge(
306
            [
307
                $this->option(Record::THOUGHT_INNER_KEY),
308
                $this->option(Record::THOUGHT_OUTER_KEY),
309
                $this->option(Record::MORPH_KEY)
310
            ],
311
            $schema[Record::PIVOT_COLUMNS]
312
        );
313
314
        //Model-role mapping
315
        foreach ($this->findTargets($builder) as $outer) {
316
            /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
317
             * //Must be pluralized
318
             * $tag->tagged->posts->count();
319
             */
320
            $role = Inflector::pluralize($outer->getRole());
321
322
            //Role => model mapping
323
            $schema[Record::MORPHED_ALIASES][$role] = $outer->getClass();
324
        }
325
326
        return $packed;
327
    }
328
}