Passed
Push — dev_2x ( fedccd...fb9ebe )
by Adrian
02:16
created

ManyToMany::entityHasRelationLoaded()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 1
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\Contract\EntityInterface;
9
use Sirius\Orm\Contract\Relation\ToManyInterface;
10
use Sirius\Orm\Entity\StateEnum;
11
use Sirius\Orm\Entity\Tracker;
12
use Sirius\Orm\Helpers\Inflector;
13
use Sirius\Orm\Helpers\QueryHelper;
14
use Sirius\Orm\Query;
15
16
class ManyToMany extends Relation implements ToManyInterface
17
{
18
    use HasAggregates;
19
20
    protected function applyDefaults(): void
21
    {
22
        parent::applyDefaults();
23
24
        $foreignKey = $this->foreignMapper->getConfig()->getPrimaryKey();
25
        if (! isset($this->options[RelationConfig::FOREIGN_KEY])) {
26
            $this->options[RelationConfig::FOREIGN_KEY] = $foreignKey;
27
        }
28
29
        $nativeKey = $this->foreignMapper->getConfig()->getPrimaryKey();
30
        if (! isset($this->options[RelationConfig::NATIVE_KEY])) {
31
            $this->options[RelationConfig::NATIVE_KEY] = $nativeKey;
32
        }
33
34
        if (! isset($this->options[RelationConfig::THROUGH_TABLE])) {
35
            $tables = [$this->foreignMapper->getConfig()->getTable(), $this->nativeMapper->getConfig()->getTable()];
36
            sort($tables);
37
            $this->options[RelationConfig::THROUGH_TABLE] = implode('_', $tables);
38
        }
39
40
        if (! isset($this->options[RelationConfig::THROUGH_NATIVE_COLUMN])) {
41
            $prefix = Inflector::singularize($this->nativeMapper->getConfig()->getTableAlias(true));
42
43
            $this->options[RelationConfig::THROUGH_NATIVE_COLUMN] = $this->getKeyColumn($prefix, $nativeKey);
44
        }
45
46
        if (! isset($this->options[RelationConfig::THROUGH_FOREIGN_COLUMN])) {
47
            $prefix = Inflector::singularize($this->foreignMapper->getConfig()->getTableAlias(true));
48
49
            $this->options[RelationConfig::THROUGH_FOREIGN_COLUMN] = $this->getKeyColumn($prefix, $foreignKey);
50
        }
51
    }
52
53
    public function getQuery(Tracker $tracker)
54
    {
55
        $nativeKey = $this->options[RelationConfig::NATIVE_KEY];
56
        $nativePks = $tracker->pluck($nativeKey, $this->nativeEntityHydrator);
57
58
        $query = $this->foreignMapper
59
            ->newQuery();
60
61
        $query = $this->joinWithThroughTable($query)
62
                      ->where($this->options[RelationConfig::THROUGH_NATIVE_COLUMN], $nativePks);
63
64
        $query = $this->applyQueryCallback($query);
65
66
        $query = $this->applyForeignGuards($query);
67
68
        $query = $this->applyThroughGuards($query);
69
70
        $query = $this->addPivotColumns($query);
71
72
        return $query;
73
    }
74
75
    protected function joinWithThroughTable($query)
76
    {
77
        $through          = $this->getOption(RelationConfig::THROUGH_TABLE);
78
        $throughAlias     = $this->getOption(RelationConfig::THROUGH_TABLE_ALIAS);
79
        $throughReference = QueryHelper::reference($through, $throughAlias);
80
        $throughName      = $throughAlias ?? $through;
81
82
        $throughCols      = $this->options[RelationConfig::THROUGH_FOREIGN_COLUMN];
83
        $foreignTableName = $this->foreignMapper->getConfig()->getTableAlias(true);
84
        $foreignKeys      = $this->options[RelationConfig::FOREIGN_KEY];
85
86
        $joinCondition = QueryHelper::joinCondition($foreignTableName, $foreignKeys, $throughName, $throughCols);
87
88
        return $query->join('INNER', $throughReference, $joinCondition);
89
    }
90
91
    private function addPivotColumns($query)
92
    {
93
        $throughColumns = $this->getOption(RelationConfig::THROUGH_COLUMNS);
94
95
        $through      = $this->getOption(RelationConfig::THROUGH_TABLE);
96
        $throughAlias = $this->getOption(RelationConfig::THROUGH_TABLE_ALIAS);
97
        $throughName  = $throughAlias ?? $through;
98
99
        if (! empty($throughColumns)) {
100
            foreach ($throughColumns as $col => $alias) {
101
                $query->columns("{$throughName}.{$col} AS {$alias}");
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 = $this->foreignMapper->newQuery();
0 ignored issues
show
Unused Code introduced by
The assignment to $subselect is dead and can be removed.
Loading history...
115
        $subselect = $query->subSelectForJoinWith($this->foreignMapper)
116
                           ->as($reference);
117
        #$subselect->resetGuards();
118
        #$subselect->setGuards($this->foreignMapper->getConfig()->getGuards());
119
120
        $subselect = $this->joinWithThroughTable($subselect);
121
122
        $subselect = $this->addPivotColumns($subselect);
123
124
        $subselect = $this->applyQueryCallback($subselect);
125
126
        $subselect = $this->applyForeignGuards($subselect);
127
128
        return $query->join('INNER', $subselect->getStatement(), $this->getJoinOnForSubselect());
129
    }
130
131
    protected function getJoinOnForSubselect()
132
    {
133
        return QueryHelper::joinCondition(
134
            $this->nativeMapper->getConfig()->getTableAlias(true),
135
            $this->getOption(RelationConfig::NATIVE_KEY),
136
            $this->name,
137
            $this->getOption(RelationConfig::THROUGH_NATIVE_COLUMN)
138
        );
139
    }
140
141
    protected function computeKeyPairs()
142
    {
143
        $pairs      = [];
144
        $nativeKey  = (array)$this->options[RelationConfig::NATIVE_KEY];
145
        $foreignKey = (array)$this->options[RelationConfig::THROUGH_NATIVE_COLUMN];
146
        foreach ($nativeKey as $k => $v) {
147
            $pairs[$v] = $foreignKey[$k];
148
        }
149
150
        return $pairs;
151
    }
152
153
    public function attachMatchesToEntity(EntityInterface $nativeEntity, array $result)
154
    {
155
        $nativeId = $this->getEntityId($this->nativeMapper, $nativeEntity, array_keys($this->keyPairs));
156
157
        $found = $result[$nativeId] ?? [];
158
159
        $collection = $this->foreignMapper->newCollection($found);
160
        $this->nativeEntityHydrator->set($nativeEntity, $this->name, $collection);
161
    }
162
163
    public function attachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity)
164
    {
165
        foreach ($this->keyPairs as $nativeCol => $foreignCol) {
166
            $nativeKeyValue = $this->nativeEntityHydrator->get($nativeEntity, $nativeCol);
167
            $this->foreignEntityHydrator->set($foreignEntity, $foreignCol, $nativeKeyValue);
168
        }
169
    }
170
171
    public function detachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity)
172
    {
173
        foreach ($this->keyPairs as $nativeCol => $foreignCol) {
174
            $this->foreignEntityHydrator->set($foreignEntity, $foreignCol, null);
175
        }
176
    }
177
178
    protected function addActionOnDelete(BaseAction $action)
179
    {
180
        $nativeEntity       = $action->getEntity();
181
182
        // retrieve them again from the DB since the related collection might not have everything
183
        // for example due to a relation query callback
184
        $foreignEntities = $this->getQuery(new Tracker([$nativeEntity->toArray()]))
185
                                ->get();
186
187
        foreach ($foreignEntities as $entity) {
188
            $deletePivotAction = new DeletePivotRows($this, $nativeEntity, $entity);
189
            $action->append($deletePivotAction);
190
        }
191
    }
192
193
    protected function addActionOnSave(BaseAction $action)
194
    {
195
        if (! $action->includesRelation($this->name)) {
196
            return;
197
        }
198
199
        $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
200
201
        /** @var Collection $foreignEntities */
202
        $foreignEntities = $this->nativeEntityHydrator->get($action->getEntity(), $this->name);
203
        if (! $foreignEntities || !$foreignEntities instanceof Collection || $foreignEntities->isEmpty()) {
0 ignored issues
show
introduced by
$foreignEntities is always a sub-type of Sirius\Orm\Collection\Collection.
Loading history...
introduced by
$foreignEntities is of type Sirius\Orm\Collection\Collection, thus it always evaluated to true.
Loading history...
204
            return;
205
        }
206
207
        $changes = $foreignEntities->getChanges();
208
209
        // save the entities still in the collection
210
        foreach ($foreignEntities as $foreignEntity) {
211
            if (! empty($foreignEntity->getChanges())) {
212
                $saveAction = $this->foreignMapper
213
                    ->newSaveAction($foreignEntity, [
0 ignored issues
show
Bug introduced by
The method newSaveAction() does not exist on Sirius\Orm\Mapper. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

213
                /** @scrutinizer ignore-call */ 
214
                $saveAction = $this->foreignMapper
Loading history...
214
                        'relations' => $remainingRelations
215
                    ]);
216
                $saveAction->addColumns($this->getExtraColumnsForAction());
217
                $action->prepend($saveAction);
218
                $action->append($this->newSyncAction(
219
                    $action->getEntity(),
220
                    $foreignEntity,
221
                    'save'
222
                ));
223
            }
224
        }
225
226
        // save entities that were removed but NOT deleted
227
        foreach ($changes['removed'] as $foreignEntity) {
228
            $deletePivotAction = new DeletePivotRows($this, $action->getEntity(), $foreignEntity);
229
            $action->append($deletePivotAction);
230
        }
231
    }
232
233
    private function applyThroughGuards(Query $query)
234
    {
235
        $guards = $this->getOption(RelationConfig::THROUGH_GUARDS);
236
        if ($guards) {
237
            $query->setGuards($guards);
238
        }
239
240
        return $query;
241
    }
242
}
243