Completed
Push — master ( a7b476...58257b )
by Adrian
01:51
created

ManyToMany::joinSubselect()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 17
rs 9.9332
1
<?php
2
3
namespace Sirius\Orm\Relation;
4
5
use Sirius\Orm\Action\BaseAction;
6
use Sirius\Orm\Collection\Collection;
7
use Sirius\Orm\Entity\EntityInterface;
8
use Sirius\Orm\Entity\StateEnum;
9
use Sirius\Orm\Entity\Tracker;
10
use Sirius\Orm\Helpers\Inflector;
11
use Sirius\Orm\Helpers\QueryHelper;
12
use Sirius\Orm\Query;
13
14
class ManyToMany extends Relation
15
{
16
    protected function applyDefaults(): void
17
    {
18
        parent::applyDefaults();
19
20
        $this->setOptionIfMissing(RelationConfig::THROUGH_COLUMNS_PREFIX, 'pivot_');
21
22
        $foreignKey = $this->foreignMapper->getPrimaryKey();
23
        if (! isset($this->options[RelationConfig::FOREIGN_KEY])) {
24
            $this->options[RelationConfig::FOREIGN_KEY] = $foreignKey;
25
        }
26
27
        $nativeKey = $this->foreignMapper->getPrimaryKey();
28
        if (! isset($this->options[RelationConfig::NATIVE_KEY])) {
29
            $this->options[RelationConfig::NATIVE_KEY] = $nativeKey;
30
        }
31
32
        if (! isset($this->options[RelationConfig::THROUGH_TABLE])) {
33
            $tables = [$this->foreignMapper->getTable(), $this->nativeMapper->getTable()];
34
            sort($tables);
35
            $this->options[RelationConfig::THROUGH_TABLE] = implode('_', $tables);
36
        }
37
38
        if (! isset($this->options[RelationConfig::THROUGH_NATIVE_COLUMN])) {
39
            $prefix = Inflector::singularize($this->nativeMapper->getTableAlias(true));
40
41
            $this->options[RelationConfig::THROUGH_NATIVE_COLUMN] = $this->getKeyColumn($prefix, $nativeKey);
42
        }
43
44
        if (! isset($this->options[RelationConfig::THROUGH_FOREIGN_COLUMN])) {
45
            $prefix = Inflector::singularize($this->foreignMapper->getTableAlias(true));
46
47
            $this->options[RelationConfig::THROUGH_FOREIGN_COLUMN] = $this->getKeyColumn($prefix, $foreignKey);
48
        }
49
    }
50
51
    public function getQuery(Tracker $tracker)
52
    {
53
        $nativeKey = $this->options[RelationConfig::NATIVE_KEY];
54
        $nativePks = $tracker->pluck($nativeKey);
55
56
        $query = $this->foreignMapper
57
            ->newQuery();
58
59
        $query = $this->joinWithThroughTable($query)
60
                      ->where($this->options[RelationConfig::THROUGH_NATIVE_COLUMN], $nativePks);
61
62
        $query = $this->applyQueryCallback($query);
63
64
        $query = $this->applyForeignGuards($query);
65
66
        $query = $this->addPivotColumns($query);
67
68
        return $query;
69
    }
70
71
    protected function joinWithThroughTable($query)
72
    {
73
        $through          = $this->getOption(RelationConfig::THROUGH_TABLE);
74
        $throughAlias     = $this->getOption(RelationConfig::THROUGH_TABLE_ALIAS);
75
        $throughReference = QueryHelper::reference($through, $throughAlias);
76
        $throughName      = $throughAlias ?? $through;
77
78
        $throughCols      = $this->options[RelationConfig::THROUGH_FOREIGN_COLUMN];
79
        $foreignTableName = $this->foreignMapper->getTableAlias(true);
80
        $foreignKeys      = $this->options[RelationConfig::FOREIGN_KEY];
81
82
        $joinCondition = QueryHelper::joinCondition($foreignTableName, $foreignKeys, $throughName, $throughCols);
83
84
        return $query->join('INNER', $throughReference, $joinCondition);
85
    }
86
87
    private function addPivotColumns($query)
88
    {
89
        $throughColumns = $this->getOption(RelationConfig::THROUGH_COLUMNS);
90
91
        $through      = $this->getOption(RelationConfig::THROUGH_TABLE);
92
        $throughAlias = $this->getOption(RelationConfig::THROUGH_TABLE_ALIAS);
93
        $throughName  = $throughAlias ?? $through;
94
95
        if (! empty($throughColumns)) {
96
            $prefix = $this->getOption(RelationConfig::THROUGH_COLUMNS_PREFIX);
97
            foreach ($throughColumns as $col) {
98
                $query->columns("{$throughName}.{$col} AS {$prefix}{$col}");
99
            }
100
        }
101
102
        foreach ((array)$this->options[RelationConfig::THROUGH_NATIVE_COLUMN] as $col) {
103
            $query->columns("{$throughName}.{$col}");
104
        }
105
106
        return $query;
107
    }
108
109
    public function joinSubselect(Query $query, string $reference)
110
    {
111
        $tableRef  = $this->foreignMapper->getTableAlias(true);
0 ignored issues
show
Unused Code introduced by
The assignment to $tableRef is dead and can be removed.
Loading history...
112
        $subselect = $query->subSelectForJoinWith()
113
                           ->from($this->foreignMapper->getTable())
114
                           ->columns($this->foreignMapper->getTable() . '.*')
115
                           ->as($reference);
116
117
        $subselect = $this->joinWithThroughTable($subselect);
118
119
        $subselect = $this->addPivotColumns($subselect);
120
121
        $subselect = $this->applyQueryCallback($subselect);
122
123
        $subselect = $this->applyForeignGuards($subselect);
124
125
        return $query->join('INNER', $subselect->getStatement(), $this->getJoinOnForSubselect());
126
    }
127
128
    protected function computeKeyPairs()
129
    {
130
        $pairs      = [];
131
        $nativeKey  = (array)$this->options[RelationConfig::NATIVE_KEY];
132
        $foreignKey = (array)$this->options[RelationConfig::THROUGH_NATIVE_COLUMN];
133
        foreach ($nativeKey as $k => $v) {
134
            $pairs[$v] = $foreignKey[$k];
135
        }
136
137
        return $pairs;
138
    }
139
140
    public function attachMatchesToEntity(EntityInterface $nativeEntity, array $result)
141
    {
142
        $found = [];
143
        foreach ($result as $foreignEntity) {
144
            if ($this->entitiesBelongTogether($nativeEntity, $foreignEntity)) {
145
                $found[] = $foreignEntity;
146
                $this->attachEntities($nativeEntity, $foreignEntity);
147
            }
148
        }
149
150
        if ($this->entityHasRelationLoaded($nativeEntity)) {
151
            /** @var Collection $collection */
152
            $collection = $this->nativeMapper->getEntityAttribute($nativeEntity, $this->name);
153
            if (! $collection->contains($foreignEntity)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $foreignEntity seems to be defined by a foreach iteration on line 143. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
154
                $collection->add($foreignEntity);
155
            }
156
        } else {
157
            $found = new Collection($found);
158
            $this->nativeMapper->setEntityAttribute($nativeEntity, $this->name, $found);
159
        }
160
    }
161
162
    protected function entityHasRelationLoaded(EntityInterface $entity)
163
    {
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
        $nativeEntityKey    = $nativeEntity->getPk();
0 ignored issues
show
Unused Code introduced by
The assignment to $nativeEntityKey is dead and can be removed.
Loading history...
197
        $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
198
199
        // no cascade delete? treat as save so we can process the changes
200
        if (! $this->isCascade()) {
201
            $this->addActionOnSave($action);
202
        } else {
203
            // retrieve them again from the DB since the related collection might not have everything
204
            // for example due to a relation query callback
205
            $foreignEntities = $this->getQuery(new Tracker($this->nativeMapper, [$nativeEntity->getArrayCopy()]))
206
                                    ->get();
207
208
            foreach ($foreignEntities as $entity) {
209
                $deleteAction = $this->foreignMapper
210
                    ->newDeleteAction($entity, ['relations' => $remainingRelations]);
211
                $action->append($deleteAction);
212
            }
213
        }
214
    }
215
216
    protected function addActionOnSave(BaseAction $action)
217
    {
218
        $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
219
220
        /** @var Collection $foreignEntities */
221
        $foreignEntities = $this->nativeMapper->getEntityAttribute($action->getEntity(), $this->name);
222
        if (! $foreignEntities) {
0 ignored issues
show
introduced by
$foreignEntities is of type Sirius\Orm\Collection\Collection, thus it always evaluated to true.
Loading history...
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());
0 ignored issues
show
Bug introduced by
The method addColumns() does not exist on Sirius\Orm\Action\BaseAction. It seems like you code against a sub-type of Sirius\Orm\Action\BaseAction such as Sirius\Orm\Action\Update. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

235
                $saveAction->/** @scrutinizer ignore-call */ 
236
                             addColumns($this->getExtraColumnsForAction());
Loading history...
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