Completed
Branch develop (c2aa4c)
by Anton
05:17
created

ManyToMorphedSchema::getPivotTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\ORM\Entities\Schemas\Relations;
9
10
use Doctrine\Common\Inflector\Inflector;
11
use Spiral\Database\Entities\Schemas\AbstractTable;
12
use Spiral\ORM\Entities\Schemas\MorphedSchema;
13
use Spiral\ORM\Entities\Schemas\Relations\Traits\ColumnsTrait;
14
use Spiral\ORM\Exceptions\RelationSchemaException;
15
use Spiral\ORM\ORM;
16
use Spiral\ORM\RecordEntity;
17
18
/**
19
 * ManyToMorphed relation declares relation between parent record and set of outer records joined by
20
 * common interface. Relation allow to specify inner key (key in parent record), outer key (key in
21
 * outer records), morph key, pivot table name, names of pivot columns to store inner and outer key
22
 * values and set of additional columns. Relation DOES NOT to specify WHERE statement for outer
23
 * records. However you can specify where conditions for PIVOT table.
24
 *
25
 * You can declare this relation using same syntax as for ManyToMany except your target class
26
 * must be an interface.
27
 *
28
 * Attention, be very careful using morphing relations, you must know what you doing!
29
 * Attention #2, relation like that can not be preloaded!
30
 *
31
 * Example [Tag related to many TaggableInterface], relation name "tagged", relation requested to be
32
 * inversed using name "tags":
33
 * - relation will walk should every record implementing TaggableInterface to collect name and
34
 *   type of outer keys, if outer key is not consistent across records implementing this interface
35
 *   an exception will be raised, let's say that outer key is "id" in every record
36
 * - relation will create pivot table named "tagged_map" (if allowed), where table name generated
37
 *   based on relation name (you can change name)
38
 * - relation will create pivot key named "tag_ud" related to Tag primary key
39
 * - relation will create pivot key named "tagged_id" related to primary key of outer records,
40
 *   singular relation name used to generate key like that
41
 * - relation will create pivot key named "tagged_type" to store role of outer record
42
 * - relation will create unique index on "tag_id", "tagged_id" and "tagged_type" columns if allowed
43
 * - relation will create additional columns in pivot table if any requested
44
 *
45
 * Using in records:
46
 * You can use inversed relation as usual ManyToMany, however in Tag record relation access will be
47
 * little bit more complex - every linked record will create inner ManyToMany relation:
48
 * $tag->tagged->users->count(); //Where "users" is plural form of one outer records
49
 *
50
 * You can defined your own inner relation names by using MORPHED_ALIASES option when defining
51
 * relation.
52
 *
53
 * @see BelongsToMorhedSchema
54
 * @see ManyToManySchema
55
 */
56
class ManyToMorphedSchema extends MorphedSchema
57
{
58
    /**
59
     * Relation may create custom columns in pivot table using Record schema format.
60
     */
61
    use ColumnsTrait;
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    const RELATION_TYPE = RecordEntity::MANY_TO_MORPHED;
67
68
    /**
69
     * Relation represent multiple records.
70
     */
71
    const MULTIPLE = true;
72
73
    /**
74
     * {@inheritdoc}
75
     *
76
     * @invisible
77
     */
78
    protected $defaultDefinition = [
79
        //Association list between tables and roles, internal
80
        RecordEntity::MORPHED_ALIASES   => [],
81
        //Pivot table name will be generated based on singular relation name and _map postfix
82
        RecordEntity::PIVOT_TABLE       => '{name:singular}_map',
83
        //Inner key points to primary key of parent record by default
84
        RecordEntity::INNER_KEY         => '{record:primaryKey}',
85
        //By default, we are looking for primary key in our outer records, outer key must present
86
        //in every outer record and be consistent
87
        RecordEntity::OUTER_KEY         => '{outer:primaryKey}',
88
        //Linking pivot table and parent record
89
        RecordEntity::THOUGHT_INNER_KEY => '{record:role}_{definition:innerKey}',
90
        //Linking pivot table and outer records
91
        RecordEntity::THOUGHT_OUTER_KEY => '{name:singular}_{definition:outerKey}',
92
        //Declares what specific record pivot record linking to
93
        RecordEntity::MORPH_KEY         => '{name:singular}_type',
94
        //Set constraints in pivot table (foreign keys)
95
        RecordEntity::CONSTRAINT        => true,
96
        //@link https://en.wikipedia.org/wiki/Foreign_key
97
        RecordEntity::CONSTRAINT_ACTION => 'CASCADE',
98
        //Relation allowed to create indexes in pivot table
99
        RecordEntity::CREATE_INDEXES    => true,
100
        //Relation allowed to create pivot table
101
        RecordEntity::CREATE_PIVOT      => true,
102
        //Additional set of columns to be added into pivot table, you can use same column definition
103
        //type as you using for your records
104
        RecordEntity::PIVOT_COLUMNS     => [],
105
        //Set of default values to be used for pivot table
106
        RecordEntity::PIVOT_DEFAULTS    => [],
107
        //WHERE statement in a form of simplified array definition to be applied to pivot table
108
        //data
109
        RecordEntity::WHERE_PIVOT       => []
110
    ];
111
112
    /**
113
     * {@inheritdoc}
114
     *
115
     * Relation will be inversed to every associated record.
116
     */
117
    public function inverseRelation()
118
    {
119
        //WHERE conditions can not be inversed
120
        foreach ($this->outerRecords() as $record) {
121
            if (!$record->hasRelation($this->definition[RecordEntity::INVERSE])) {
122
                $record->addRelation($this->definition[RecordEntity::INVERSE], [
123
                    RecordEntity::MANY_TO_MANY      => $this->record->getName(),
124
                    RecordEntity::PIVOT_TABLE       => $this->definition[RecordEntity::PIVOT_TABLE],
125
                    RecordEntity::OUTER_KEY         => $this->definition[RecordEntity::INNER_KEY],
126
                    RecordEntity::INNER_KEY         => $this->definition[RecordEntity::OUTER_KEY],
127
                    RecordEntity::THOUGHT_INNER_KEY => $this->definition[RecordEntity::THOUGHT_OUTER_KEY],
128
                    RecordEntity::THOUGHT_OUTER_KEY => $this->definition[RecordEntity::THOUGHT_INNER_KEY],
129
                    RecordEntity::MORPH_KEY         => $this->definition[RecordEntity::MORPH_KEY],
130
                    RecordEntity::CREATE_INDEXES    => $this->definition[RecordEntity::CREATE_INDEXES],
131
                    RecordEntity::CREATE_PIVOT      => $this->definition[RecordEntity::CREATE_PIVOT],
132
                    RecordEntity::PIVOT_COLUMNS     => $this->definition[RecordEntity::PIVOT_COLUMNS],
133
                    RecordEntity::WHERE_PIVOT       => $this->definition[RecordEntity::WHERE_PIVOT]
134
                ]);
135
            }
136
        }
137
    }
138
139
    /**
140
     * Generate name of pivot table or fetch if from schema.
141
     *
142
     * @return string
143
     */
144
    public function getPivotTable()
145
    {
146
        return $this->definition[RecordEntity::PIVOT_TABLE];
147
    }
148
149
    /**
150
     * Instance of AbstractTable associated with relation pivot table.
151
     *
152
     * @return AbstractTable
153
     */
154
    public function pivotSchema()
155
    {
156
        return $this->builder->declareTable(
157
            $this->record->getDatabase(),
158
            $this->getPivotTable()
159
        );
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function buildSchema()
166
    {
167
        if (!$this->definition[RecordEntity::CREATE_PIVOT]) {
168
            //No pivot table creation were requested, noting really to do
169
            return;
170
        }
171
172
        $pivotTable = $this->pivotSchema();
173
174
        //Inner key points to our parent record
175
        $innerKey = $pivotTable->column($this->definition[RecordEntity::THOUGHT_INNER_KEY]);
176
        $innerKey->setType($this->getInnerKeyType());
177
178
        if ($this->isIndexed()) {
179
            $innerKey->index();
180
        }
181
182
        //Morph key will store role name of outer records
183
        $morphKey = $pivotTable->column($this->getMorphKey());
184
        $morphKey->string(static::MORPH_COLUMN_SIZE);
185
186
        //Points to inner key of our outer records (outer key)
187
        $outerKey = $pivotTable->column($this->definition[RecordEntity::THOUGHT_OUTER_KEY]);
188
        $outerKey->setType($this->getOuterKeyType());
189
190
        //Casting pivot table columns
191
        $this->castTable(
192
            $this->pivotSchema(),
193
            $this->definition[RecordEntity::PIVOT_COLUMNS],
194
            $this->definition[RecordEntity::PIVOT_DEFAULTS]
195
        );
196
197
        //Complex index
198
        if ($this->isIndexed()) {
199
            //Complex index including 3 columns from pivot table
200
            $pivotTable->unique(
201
                $this->definition[RecordEntity::THOUGHT_INNER_KEY],
202
                $this->definition[RecordEntity::MORPH_KEY],
203
                $this->definition[RecordEntity::THOUGHT_OUTER_KEY]
204
            );
205
        }
206
207
        if ($this->isConstrained()) {
208
            $foreignKey = $innerKey->references(
209
                $this->record->getTable(),
210
                $this->record->getPrimaryKey()
211
            );
212
213
            $foreignKey->onDelete($this->getConstraintAction());
214
            $foreignKey->onUpdate($this->getConstraintAction());
215
        }
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    protected function normalizeDefinition()
222
    {
223
        $definition = parent::normalizeDefinition();
224
225
        foreach ($this->outerRecords() as $record) {
226
            if (!in_array($record->getRole(), $definition[RecordEntity::MORPHED_ALIASES])) {
227
                //Let's remember associations between tables and roles
228
                $plural = Inflector::pluralize($record->getRole());
229
                $definition[RecordEntity::MORPHED_ALIASES][$plural] = $record->getRole();
230
            }
231
232
            //We must include pivot table database into data for easier access
233
            $definition[ORM::R_DATABASE] = $record->getDatabase();
234
        }
235
236
        //Let's include pivot table columns
237
        $definition[RecordEntity::PIVOT_COLUMNS] = [];
238
        foreach ($this->pivotSchema()->getColumns() as $column) {
239
            $definition[RecordEntity::PIVOT_COLUMNS][] = $column->getName();
240
        }
241
242
        return $definition;
243
    }
244
245
    /**
246
     * {@inheritdoc}
247
     */
248
    protected function clarifyDefinition()
249
    {
250
        parent::clarifyDefinition();
251
        if (!$this->isSameDatabase()) {
252
            throw new RelationSchemaException(
253
                "Many-to-Many morphed relation can create relations ({$this}) "
254
                . "only to entities from same database."
255
            );
256
        }
257
    }
258
}