ManyToMany::attachMatchesToEntity()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 16
rs 9.6111
cc 5
nc 4
nop 2
1
<?php
2
3
namespace Sirius\Orm\Relation;
4
5
use Sirius\Orm\Action\BaseAction;
6
use Sirius\Orm\Action\DeletePivotRows;
7
use Sirius\Orm\Collection\Collection;
8
use Sirius\Orm\Entity\EntityInterface;
9
use Sirius\Orm\Entity\StateEnum;
10
use Sirius\Orm\Entity\Tracker;
11
use Sirius\Orm\Helpers\Inflector;
12
use Sirius\Orm\Helpers\QueryHelper;
13
use Sirius\Orm\Query;
14
15
class ManyToMany extends Relation
16
{
17
    use HasAggregates;
18
19
    protected function applyDefaults(): void
20
    {
21
        parent::applyDefaults();
22
23
        $this->setOptionIfMissing(RelationConfig::THROUGH_COLUMNS_PREFIX, 'pivot_');
24
25
        $foreignKey = $this->foreignMapper->getPrimaryKey();
26
        if (! isset($this->options[RelationConfig::FOREIGN_KEY])) {
27
            $this->options[RelationConfig::FOREIGN_KEY] = $foreignKey;
28
        }
29
30
        $nativeKey = $this->foreignMapper->getPrimaryKey();
31
        if (! isset($this->options[RelationConfig::NATIVE_KEY])) {
32
            $this->options[RelationConfig::NATIVE_KEY] = $nativeKey;
33
        }
34
35
        if (! isset($this->options[RelationConfig::THROUGH_TABLE])) {
36
            $tables = [$this->foreignMapper->getTable(), $this->nativeMapper->getTable()];
37
            sort($tables);
38
            $this->options[RelationConfig::THROUGH_TABLE] = implode('_', $tables);
39
        }
40
41
        if (! isset($this->options[RelationConfig::THROUGH_NATIVE_COLUMN])) {
42
            $prefix = Inflector::singularize($this->nativeMapper->getTableAlias(true));
43
44
            $this->options[RelationConfig::THROUGH_NATIVE_COLUMN] = $this->getKeyColumn($prefix, $nativeKey);
45
        }
46
47
        if (! isset($this->options[RelationConfig::THROUGH_FOREIGN_COLUMN])) {
48
            $prefix = Inflector::singularize($this->foreignMapper->getTableAlias(true));
49
50
            $this->options[RelationConfig::THROUGH_FOREIGN_COLUMN] = $this->getKeyColumn($prefix, $foreignKey);
51
        }
52
    }
53
54
    public function getQuery(Tracker $tracker)
55
    {
56
        $nativeKey = $this->options[RelationConfig::NATIVE_KEY];
57
        $nativePks = $tracker->pluck($nativeKey);
58
59
        $query = $this->foreignMapper
60
            ->newQuery();
61
62
        $query = $this->joinWithThroughTable($query)
63
                      ->where($this->options[RelationConfig::THROUGH_NATIVE_COLUMN], $nativePks);
64
65
        $query = $this->applyQueryCallback($query);
66
67
        $query = $this->applyForeignGuards($query);
68
69
        $query = $this->addPivotColumns($query);
70
71
        return $query;
72
    }
73
74
    protected function joinWithThroughTable($query)
75
    {
76
        $through          = $this->getOption(RelationConfig::THROUGH_TABLE);
77
        $throughAlias     = $this->getOption(RelationConfig::THROUGH_TABLE_ALIAS);
78
        $throughReference = QueryHelper::reference($through, $throughAlias);
79
        $throughName      = $throughAlias ?? $through;
80
81
        $throughCols      = $this->options[RelationConfig::THROUGH_FOREIGN_COLUMN];
82
        $foreignTableName = $this->foreignMapper->getTableAlias(true);
83
        $foreignKeys      = $this->options[RelationConfig::FOREIGN_KEY];
84
85
        $joinCondition = QueryHelper::joinCondition($foreignTableName, $foreignKeys, $throughName, $throughCols);
86
87
        return $query->join('INNER', $throughReference, $joinCondition);
88
    }
89
90
    private function addPivotColumns($query)
91
    {
92
        $throughColumns = $this->getOption(RelationConfig::THROUGH_COLUMNS);
93
94
        $through      = $this->getOption(RelationConfig::THROUGH_TABLE);
95
        $throughAlias = $this->getOption(RelationConfig::THROUGH_TABLE_ALIAS);
96
        $throughName  = $throughAlias ?? $through;
97
98
        if (! empty($throughColumns)) {
99
            $prefix = $this->getOption(RelationConfig::THROUGH_COLUMNS_PREFIX);
100
            foreach ($throughColumns as $col) {
101
                $query->columns("{$throughName}.{$col} AS {$prefix}{$col}");
102
            }
103
        }
104
105
        foreach ((array)$this->options[RelationConfig::THROUGH_NATIVE_COLUMN] as $col) {
106
            $query->columns("{$throughName}.{$col}");
107
        }
108
109
        return $query;
110
    }
111
112
    public function joinSubselect(Query $query, string $reference)
113
    {
114
        $subselect = $query->subSelectForJoinWith()
115
                           ->from($this->foreignMapper->getTable())
116
                           ->columns($this->foreignMapper->getTable() . '.*')
117
                           ->as($reference);
118
119
        $subselect = $this->joinWithThroughTable($subselect);
120
121
        $subselect = $this->addPivotColumns($subselect);
122
123
        $subselect = $this->applyQueryCallback($subselect);
124
125
        $subselect = $this->applyForeignGuards($subselect);
126
127
        return $query->join('INNER', $subselect->getStatement(), $this->getJoinOnForSubselect());
128
    }
129
130
    protected function computeKeyPairs()
131
    {
132
        $pairs      = [];
133
        $nativeKey  = (array)$this->options[RelationConfig::NATIVE_KEY];
134
        $foreignKey = (array)$this->options[RelationConfig::THROUGH_NATIVE_COLUMN];
135
        foreach ($nativeKey as $k => $v) {
136
            $pairs[$v] = $foreignKey[$k];
137
        }
138
139
        return $pairs;
140
    }
141
142
    public function attachMatchesToEntity(EntityInterface $nativeEntity, array $result)
143
    {
144
        $nativeId = $this->getEntityId($this->nativeMapper, $nativeEntity, array_keys($this->keyPairs));
145
146
        $found = $result[$nativeId] ?? [];
147
148
        if (!empty($found) && $this->entityHasRelationLoaded($nativeEntity)) {
149
            /** @var Collection $collection */
150
            $collection = $this->nativeMapper->getEntityAttribute($nativeEntity, $this->name);
151
            foreach ($found as $foreignEntity) {
152
                if (! $collection->contains($foreignEntity)) {
153
                    $collection->add($foreignEntity);
154
                }
155
            }
156
        } else {
157
            $this->nativeMapper->setEntityAttribute($nativeEntity, $this->name, new Collection($found));
158
        }
159
    }
160
161
    protected function entityHasRelationLoaded(EntityInterface $entity)
162
    {
163
        // lazy loaded relations are not included in `getArrayCopy()`
164
        return array_key_exists($this->name, $entity->getArrayCopy());
165
    }
166
167
    public function attachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity)
168
    {
169
        foreach ($this->keyPairs as $nativeCol => $foreignCol) {
170
            $nativeKeyValue = $this->nativeMapper->getEntityAttribute($nativeEntity, $nativeCol);
171
            $this->foreignMapper->setEntityAttribute($foreignEntity, $foreignCol, $nativeKeyValue);
172
        }
173
    }
174
175
    public function detachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity)
176
    {
177
        $state = $foreignEntity->getPersistenceState();
178
179
        $foreignEntity->setPersistenceState(StateEnum::SYNCHRONIZED);
180
        foreach ($this->keyPairs as $nativeCol => $foreignCol) {
181
            $this->foreignMapper->setEntityAttribute($foreignEntity, $foreignCol, null);
182
        }
183
        $this->foreignMapper->setEntityAttribute($foreignEntity, $this->name, null);
184
185
        $collection = $this->nativeMapper->getEntityAttribute($nativeEntity, $this->name);
186
        if ($collection instanceof Collection) {
187
            $collection->removeElement($foreignEntity);
188
        }
189
190
        $foreignEntity->setPersistenceState($state);
191
    }
192
193
    protected function addActionOnDelete(BaseAction $action)
194
    {
195
        $nativeEntity       = $action->getEntity();
196
        $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
197
198
        // no cascade delete? treat as save so we can process the changes
199
        if (! $this->isCascade()) {
200
            $this->addActionOnSave($action);
201
        } else {
202
            // retrieve them again from the DB since the related collection might not have everything
203
            // for example due to a relation query callback
204
            $foreignEntities = $this->getQuery(new Tracker([$nativeEntity->getArrayCopy()]))
205
                                    ->get();
206
207
            foreach ($foreignEntities as $entity) {
208
                $deleteAction = $this->foreignMapper
209
                    ->newDeleteAction($entity, ['relations' => $remainingRelations]);
210
                $action->append($deleteAction);
211
                $deletePivotAction = new DeletePivotRows($this, $nativeEntity, $entity);
212
                $action->append($deletePivotAction);
213
            }
214
        }
215
    }
216
217
    protected function addActionOnSave(BaseAction $action)
218
    {
219
        $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
220
221
        $foreignEntities = $this->nativeMapper->getEntityAttribute($action->getEntity(), $this->name);
222
        if (! $foreignEntities) {
223
            return;
224
        }
225
226
        $changes = $foreignEntities->getChanges();
227
228
        // save the entities still in the collection
229
        foreach ($foreignEntities as $foreignEntity) {
230
            if (! empty($foreignEntity->getChanges())) {
231
                $saveAction = $this->foreignMapper
232
                    ->newSaveAction($foreignEntity, [
233
                        'relations' => $remainingRelations
234
                    ]);
235
                $saveAction->addColumns($this->getExtraColumnsForAction());
236
                $action->prepend($saveAction);
237
                $action->append($this->newSyncAction(
238
                    $action->getEntity(),
239
                    $foreignEntity,
240
                    'save'
241
                ));
242
            }
243
        }
244
245
        // save entities that were removed but NOT deleted
246
        foreach ($changes['removed'] as $foreignEntity) {
247
            $saveAction = $this->foreignMapper
248
                ->newSaveAction($foreignEntity, [
249
                    'relations' => $remainingRelations
250
                ])
251
                ->addColumns($this->getExtraColumnsForAction());
252
            $action->prepend($saveAction);
253
            $action->append($this->newSyncAction(
254
                $action->getEntity(),
255
                $foreignEntity,
256
                'delete'
257
            ));
258
        }
259
    }
260
}
261