Completed
Push — master ( eacc73...69d932 )
by Adrian
01:58
created

ManyToMany::entityHasRelationLoaded()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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
13
class ManyToMany extends Relation
14
{
15
    protected function applyDefaults(): void
16
    {
17
        parent::applyDefaults();
18
19
        $this->setOptionIfMissing(RelationConfig::THROUGH_COLUMNS_PREFIX, 'pivot_');
20
21
        $foreignKey = $this->foreignMapper->getPrimaryKey();
22
        if (! isset($this->options[RelationConfig::FOREIGN_KEY])) {
23
            $this->options[RelationConfig::FOREIGN_KEY] = $foreignKey;
24
        }
25
26
        $nativeKey = $this->foreignMapper->getPrimaryKey();
27
        if (! isset($this->options[RelationConfig::NATIVE_KEY])) {
28
            $this->options[RelationConfig::NATIVE_KEY] = $nativeKey;
29
        }
30
31
        if (! isset($this->options[RelationConfig::THROUGH_TABLE])) {
32
            $tables = [$this->foreignMapper->getTable(), $this->nativeMapper->getTable()];
33
            sort($tables);
34
            $this->options[RelationConfig::THROUGH_TABLE] = implode('_', $tables);
35
        }
36
37
        if (! isset($this->options[RelationConfig::THROUGH_NATIVE_COLUMN])) {
38
            $prefix = Inflector::singularize($this->nativeMapper->getTableAlias(true));
39
40
            $this->options[RelationConfig::THROUGH_NATIVE_COLUMN] = $this->getKeyColumn($prefix, $nativeKey);
41
        }
42
43
        if (! isset($this->options[RelationConfig::THROUGH_FOREIGN_COLUMN])) {
44
            $prefix = Inflector::singularize($this->foreignMapper->getTableAlias(true));
45
46
            $this->options[RelationConfig::THROUGH_FOREIGN_COLUMN] = $this->getKeyColumn($prefix, $foreignKey);
47
        }
48
    }
49
50
    public function getQuery(Tracker $tracker)
51
    {
52
        $nativeKey = $this->options[RelationConfig::NATIVE_KEY];
53
        $nativePks = $tracker->pluck($nativeKey);
54
55
        $query = $this->foreignMapper
56
            ->newQuery();
57
58
        $query = $this->joinWithThroughTable($query)
59
                      ->where($this->options[RelationConfig::THROUGH_NATIVE_COLUMN], $nativePks);
60
61
        if ($this->getOption(RelationConfig::QUERY_CALLBACK) &&
62
            is_callable($this->getOption(RelationConfig::QUERY_CALLBACK))) {
63
            $callback = $this->options[RelationConfig::QUERY_CALLBACK];
64
            $query    = $callback($query);
65
        }
66
67
        if ($this->getOption(RelationConfig::FOREIGN_GUARDS)) {
68
            $query->setGuards($this->options[RelationConfig::FOREIGN_GUARDS]);
69
        }
70
71
        $query = $this->addPivotColumns($query);
72
73
        return $query;
74
    }
75
76
    protected function joinWithThroughTable($query)
77
    {
78
        $through          = $this->getOption(RelationConfig::THROUGH_TABLE);
79
        $throughAlias     = $this->getOption(RelationConfig::THROUGH_TABLE_ALIAS);
80
        $throughReference = QueryHelper::reference($through, $throughAlias);
81
        $throughName      = $throughAlias ?? $through;
82
83
        $foreignTableName       = $this->foreignMapper->getTableAlias(true);
84
        $throughTableConditions = [];
85
86
        foreach ((array)$this->options[RelationConfig::FOREIGN_KEY] as $k => $col) {
87
            $throughCols              = (array)$this->options[RelationConfig::THROUGH_FOREIGN_COLUMN];
88
            $throughCol               = $throughCols[$k];
89
            $throughTableConditions[] = "{$foreignTableName}.{$col} = {$throughName}.{$throughCol}";
90
        }
91
92
        return $query->join('INNER', $throughReference, implode(' AND ', $throughTableConditions));
93
    }
94
95
    private function addPivotColumns($query)
96
    {
97
        $throughColumns = $this->getOption(RelationConfig::THROUGH_COLUMNS);
98
99
        $through      = $this->getOption(RelationConfig::THROUGH_TABLE);
100
        $throughAlias = $this->getOption(RelationConfig::THROUGH_TABLE_ALIAS);
101
        $throughName  = $throughAlias ?? $through;
102
103
        if (! empty($throughColumns)) {
104
            $prefix = $this->getOption(RelationConfig::THROUGH_COLUMNS_PREFIX);
105
            foreach ($throughColumns as $col) {
106
                $query->columns("{$throughName}.{$col} AS {$prefix}{$col}");
107
            }
108
        }
109
110
        foreach ((array)$this->options[RelationConfig::THROUGH_NATIVE_COLUMN] as $col) {
111
            $query->columns("{$throughName}.{$col}");
112
        }
113
114
        return $query;
115
    }
116
117
    protected function computeKeyPairs()
118
    {
119
        $pairs      = [];
120
        $nativeKey  = (array)$this->options[RelationConfig::NATIVE_KEY];
121
        $foreignKey = (array)$this->options[RelationConfig::THROUGH_NATIVE_COLUMN];
122
        foreach ($nativeKey as $k => $v) {
123
            $pairs[$v] = $foreignKey[$k];
124
        }
125
126
        return $pairs;
127
    }
128
129
    public function attachMatchesToEntity(EntityInterface $nativeEntity, array $result)
130
    {
131
        $found = [];
132
        foreach ($result as $foreignEntity) {
133
            if ($this->entitiesBelongTogether($nativeEntity, $foreignEntity)) {
134
                $found[] = $foreignEntity;
135
                $this->attachEntities($nativeEntity, $foreignEntity);
136
            }
137
        }
138
139
        if ($this->entityHasRelationLoaded($nativeEntity)) {
140
            /** @var Collection $collection */
141
            $collection = $this->nativeMapper->getEntityAttribute($nativeEntity, $this->name);
142
            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 132. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
143
                $collection->add($foreignEntity);
144
            }
145
        } else {
146
            $found = new Collection($found);
147
            $this->nativeMapper->setEntityAttribute($nativeEntity, $this->name, $found);
148
        }
149
    }
150
151
    protected function entityHasRelationLoaded(EntityInterface $entity)
152
    {
153
        return array_key_exists($this->name, $entity->getArrayCopy());
154
    }
155
156
    public function attachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity)
157
    {
158
        foreach ($this->keyPairs as $nativeCol => $foreignCol) {
159
            $nativeKeyValue = $this->nativeMapper->getEntityAttribute($nativeEntity, $nativeCol);
160
            $this->foreignMapper->setEntityAttribute($foreignEntity, $foreignCol, $nativeKeyValue);
161
        }
162
    }
163
164
    public function detachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity)
165
    {
166
        $state = $foreignEntity->getPersistenceState();
167
168
        $foreignEntity->setPersistenceState(StateEnum::SYNCHRONIZED);
169
        foreach ($this->keyPairs as $nativeCol => $foreignCol) {
170
            $this->foreignMapper->setEntityAttribute($foreignEntity, $foreignCol, null);
171
        }
172
        $this->foreignMapper->setEntityAttribute($foreignEntity, $this->name, null);
173
174
        $collection = $this->nativeMapper->getEntityAttribute($nativeEntity, $this->name);
175
        if ($collection instanceof Collection) {
176
            $collection->removeElement($foreignEntity);
177
        }
178
179
        $foreignEntity->setPersistenceState($state);
180
    }
181
182
    protected function addActionOnDelete(BaseAction $action)
183
    {
184
        $nativeEntity       = $action->getEntity();
185
        $nativeEntityKey    = $nativeEntity->getPk();
0 ignored issues
show
Unused Code introduced by
The assignment to $nativeEntityKey is dead and can be removed.
Loading history...
186
        $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
187
188
        // no cascade delete? treat as save so we can process the changes
189
        if (! $this->isCascade()) {
190
            $this->addActionOnSave($action);
191
        } else {
192
            // retrieve them again from the DB since the related collection might not have everything
193
            // for example due to a relation query callback
194
            $foreignEntities = $this->getQuery(new Tracker($this->nativeMapper, [$nativeEntity->getArrayCopy()]))
195
                                    ->get();
196
197
            foreach ($foreignEntities as $entity) {
198
                $deleteAction = $this->foreignMapper
199
                    ->newDeleteAction($entity, ['relations' => $remainingRelations]);
200
                $action->append($deleteAction);
201
            }
202
        }
203
    }
204
205
    protected function addActionOnSave(BaseAction $action)
206
    {
207
        $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
208
209
        /** @var Collection $foreignEntities */
210
        $foreignEntities = $this->nativeMapper->getEntityAttribute($action->getEntity(), $this->name);
211
        if (! $foreignEntities) {
0 ignored issues
show
introduced by
$foreignEntities is of type Sirius\Orm\Collection\Collection, thus it always evaluated to true.
Loading history...
212
            return;
213
        }
214
215
        $changes = $foreignEntities->getChanges();
216
217
        // save the entities still in the collection
218
        foreach ($foreignEntities as $foreignEntity) {
219
            if (! empty($foreignEntity->getChanges())) {
220
                $saveAction = $this->foreignMapper
221
                    ->newSaveAction($foreignEntity, [
222
                        'relations' => $remainingRelations
223
                    ]);
224
                $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

224
                $saveAction->/** @scrutinizer ignore-call */ 
225
                             addColumns($this->getExtraColumnsForAction());
Loading history...
225
                $action->prepend($saveAction);
226
                $action->append($this->newSyncAction(
227
                    $action->getEntity(),
228
                    $foreignEntity,
229
                    'save'
230
                ));
231
            }
232
        }
233
234
        // save entities that were removed but NOT deleted
235
        foreach ($changes['removed'] as $foreignEntity) {
236
            $saveAction = $this->foreignMapper
237
                ->newSaveAction($foreignEntity, [
238
                    'relations' => $remainingRelations
239
                ])
240
                ->addColumns($this->getExtraColumnsForAction());
241
            $action->prepend($saveAction);
242
            $action->append($this->newSyncAction(
243
                $action->getEntity(),
244
                $foreignEntity,
245
                'delete'
246
            ));
247
        }
248
    }
249
}
250