Completed
Pull Request — master (#45)
by
unknown
06:02
created

AutoJoin::autoJoin()   B

Complexity

Conditions 6
Paths 13

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 2
Metric Value
dl 0
loc 33
rs 8.439
cc 6
eloc 19
c 2
b 0
f 2
nc 13
nop 1
1
<?php
2
3
namespace RulerZ\Executor\DoctrineQueryBuilder;
4
5
use Doctrine\ORM\QueryBuilder;
6
use Doctrine\ORM\EntityManager;
7
8
class AutoJoin
9
{
10
    const ALIAS_PREFIX = 'rulerz_';
11
12
    /**
13
     * List of root and association entity embeddables
14
     *
15
     * @var array
16
     */
17
    private $embeddables = null;
18
19
    /**
20
     * List of entities that have been analyzed by the embeddable traverser
21
     *
22
     * @var array
23
     */
24
    private $analyzedTargetEntities = [];
25
26
    /**
27
     * Associative list of known aliases (selected or joined tables).
28
     *
29
     * @var array
30
     */
31
    private $knownAliases = [];
32
33
    /**
34
     * Associative list of joined tables and their alias.
35
     *
36
     * @var array
37
     */
38
    private $joinMap = null;
39
40
    /**
41
     * @var QueryBuilder
42
     */
43
    private $queryBuilder;
44
45
    /**
46
     * @var array
47
     */
48
    private $expectedJoinChains = [];
49
50
    public function __construct(QueryBuilder $queryBuilder, array $expectedJoinChains)
51
    {
52
        $this->queryBuilder       = $queryBuilder;
53
        $this->expectedJoinChains = $expectedJoinChains;
54
    }
55
56
    public function getJoinAlias($table, $fullPropertyPath = null)
57
    {
58
        if ($this->embeddables === null) {
59
            $this->embeddables = $this->analizeEmbeddables($this->queryBuilder);
60
        }
61
62
        if ($this->joinMap === null) {
63
            $this->joinMap      = $this->analizeJoinedTables($this->queryBuilder);
64
            $this->knownAliases = array_flip($this->queryBuilder->getRootAliases()) + array_flip($this->joinMap);
65
66
            $this->autoJoin($this->queryBuilder);
67
        }
68
69
        // the table is identified as an embeddable
70
        if (in_array($fullPropertyPath, $this->embeddables)) {
71
            return $this->getEmbeddableAlias($fullPropertyPath);
72
        }
73
74
        // the table name is a known alias (already join for instance) so we
75
        // don't need to do anything.
76
        if (isset($this->knownAliases[$table])) {
77
            return $table;
78
        }
79
80
        // the table name is a known alias that was assigned an obscure alias in the query builder
81
        if (in_array($fullPropertyPath, $this->knownAliases, true)) {
82
            return array_search($fullPropertyPath, $this->knownAliases, true);
83
        }
84
85
        // otherwise the table should have automatically been joined, so we use our table prefix
86
        if (isset($this->knownAliases[self::ALIAS_PREFIX.$table])) {
87
            return self::ALIAS_PREFIX . $table;
88
        }
89
90
        throw new \RuntimeException(sprintf('Could not automatically join table "%s"', $table));
91
    }
92
93
    private function getEmbeddableAlias($fullPropertyPath)
94
    {
95
        $embeddableDimensions = explode('.', $fullPropertyPath);
96
97
        $embeddableName = array_pop($embeddableDimensions);
98
        $embeddableTable = array_pop($embeddableDimensions);
99
100
        if ($embeddableTable === null) {
101
            // the embeddable is not inside an association, so we use the root alias prefix.
102
            $alias = $this->queryBuilder->getRootAliases()[0];
103
        } elseif (array_key_exists($embeddableTable, $this->knownAliases)) {
104
            // the table name is a known alias (already join for instance) so we
105
            // don't need to do anything.
106
            $alias = $embeddableTable;
107
        } elseif (in_array($embeddableTable, $this->knownAliases, true)) {
108
            // the table name is a known alias that was assigned an obscure alias in the query builder
109
            $alias = array_search($embeddableTable, $this->knownAliases, true);
110
        } elseif (array_key_exists(self::ALIAS_PREFIX . $embeddableTable, $this->knownAliases)) {
111
            // otherwise the table should have automatically been joined, so we use our table prefix.
112
            $alias = self::ALIAS_PREFIX . $embeddableTable;
113
        }
114
115
        if (!isset($alias)) {
116
            throw new \RuntimeException(sprintf('Could find embeddable "%s"', $fullPropertyPath));
117
        }
118
119
        return $alias . '.' . $embeddableName;
120
    }
121
122
    private function traverseAssociationsForEmbeddables(EntityManager $entityManager, array $associations, $fieldNamePrefix = false)
123
    {
124
        $associationsEmbeddables = [];
125
126
        foreach ($associations as $association) {
127
            $classMetaData = $entityManager->getClassMetadata($association['targetEntity']);
128
            $this->analyzedTargetEntities[] = $association['targetEntity'];
129
130
            $embeddedClasses = isset($classMetaData->embeddedClasses) ? $classMetaData->embeddedClasses : [];
131
            foreach ($embeddedClasses as $embeddedClassKey => $embeddedClass) {
132
                $associationsEmbeddables[] = implode('.', array_filter([$fieldNamePrefix, $association['fieldName'], $embeddedClassKey]));
133
            }
134
135
            $associationMappings = $classMetaData->getAssociationMappings();
136
            $associationMappings = array_filter($associationMappings, function ($associationMapping) {
137
                return !in_array($associationMapping['targetEntity'], $this->analyzedTargetEntities);
138
            });
139
140
            if (count($associationMappings) !== 0) {
141
                $traversedAssociationsEmbeddables = $this->traverseAssociationsForEmbeddables($entityManager, $associationMappings, $association['fieldName']);
142
                $associationsEmbeddables = array_merge($associationsEmbeddables, $traversedAssociationsEmbeddables);
143
            }
144
        }
145
146
        return $associationsEmbeddables;
147
    }
148
149
    private function analizeEmbeddables(QueryBuilder $queryBuilder)
150
    {
151
        $embeddables = [];
152
        $entityManager = $queryBuilder->getEntityManager();
153
        $rootEntities = $queryBuilder->getRootEntities();
154
155
        foreach ($rootEntities as $rootEntity) {
156
            $classMetaData = $entityManager->getClassMetadata($rootEntity);
157
            $embeddedClasses = isset($classMetaData->embeddedClasses) ? $classMetaData->embeddedClasses : [];
158
159
            foreach ($embeddedClasses as $embeddedClassKey => $embeddedClass) {
160
                $embeddables[] = $embeddedClassKey;
161
            }
162
163
            // Since this is a root entity embeddable, there is no need to join.
164
            foreach ($this->expectedJoinChains as $tablesToJoinKey => $tablesToJoin) {
165
                if (in_array(implode('.', $tablesToJoin), $embeddables)) {
166
                    unset($this->expectedJoinChains[$tablesToJoinKey]);
167
                }
168
            }
169
170
            $traversedAssociationsEmbeddables = $this->traverseAssociationsForEmbeddables($entityManager, $classMetaData->getAssociationMappings());
171
            $embeddables = array_merge($embeddables, $traversedAssociationsEmbeddables);
172
        }
173
174
        return $embeddables;
175
    }
176
177
    /**
178
     * Builds an associative array of already joined tables and their alias.
179
     *
180
     * @param QueryBuilder $queryBuilder
181
     *
182
     * @return array
183
     */
184
    private function analizeJoinedTables(QueryBuilder $queryBuilder)
185
    {
186
        $joinMap = [];
187
        $joins   = $queryBuilder->getDQLPart('join');
188
        foreach (array_keys($joins) as $fromTable) {
189
            foreach ($joins[$fromTable] as $join) {
190
                $joinMap[$join->getJoin()] = $join->getAlias();
191
            }
192
        }
193
        return $joinMap;
194
    }
195
196
    private function autoJoin(QueryBuilder $queryBuilder)
197
    {
198
        foreach ($this->expectedJoinChains as $tablesToJoin) {
199
            // if the table is an embeddable, the property needs to be removed
200
            if (array_search(implode('.', $tablesToJoin), $this->embeddables) !== false) {
201
                array_pop($tablesToJoin);
202
            }
203
204
            // check if the first dimension is a known alias
205
            if (isset($this->knownAliases[$tablesToJoin[0]])) {
206
                $joinTo = $tablesToJoin[0];
207
                array_shift($tablesToJoin);
208
            } else { // if not, it's the root table
209
                $joinTo = $queryBuilder->getRootAliases()[0];
210
            }
211
212
            foreach ($tablesToJoin as $table) {
213
                $joinAlias = self::ALIAS_PREFIX . $table;
214
                $join      = sprintf('%s.%s', $joinTo, $table);
215
216
                if (!isset($this->joinMap[$join])) {
217
                    $this->joinMap[$join]           = $joinAlias;
218
                    $this->knownAliases[$joinAlias] = true;
219
220
                    $queryBuilder->join(sprintf('%s.%s', $joinTo, $table), $joinAlias);
221
                } else {
222
                    $joinAlias = $this->joinMap[$join];
223
                }
224
225
                $joinTo = $joinAlias;
226
            }
227
        }
228
    }
229
}
230