Completed
Push — master ( 4ebd03...d1fdcc )
by Kévin
03:00
created

AutoJoin::analizeEmbeddables()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 2
Metric Value
dl 0
loc 26
rs 8.439
cc 5
eloc 14
c 2
b 0
f 2
nc 7
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
        // otherwise the table should have automatically been joined, so we use our table prefix
81
        if (isset($this->knownAliases[self::ALIAS_PREFIX.$table])) {
82
            return self::ALIAS_PREFIX . $table;
83
        }
84
85
        throw new \RuntimeException(sprintf('Could not automatically join table "%s"', $table));
86
    }
87
88
    private function getEmbeddableAlias($fullPropertyPath)
89
    {
90
        $embeddableDimensions = explode('.', $fullPropertyPath);
91
92
        $embeddableName = array_pop($embeddableDimensions);
93
        $embeddableTable = array_pop($embeddableDimensions);
94
95
        if ($embeddableTable === null) {
96
            // the embeddable is not inside an association, so we use the root alias prefix.
97
            $alias = $this->queryBuilder->getRootAliases()[0];
98
        } elseif (array_key_exists($embeddableTable, $this->knownAliases)) {
99
            // the table name is a known alias (already join for instance) so we
100
            // don't need to do anything.
101
            $alias = $embeddableTable;
102
        } elseif (array_key_exists(self::ALIAS_PREFIX . $embeddableTable, $this->knownAliases)) {
103
            // otherwise the table should have automatically been joined, so we use our table prefix.
104
            $alias = self::ALIAS_PREFIX . $embeddableTable;
105
        }
106
107
        if (!isset($alias)) {
108
            throw new \RuntimeException(sprintf('Could find embeddable "%s"', $fullPropertyPath));
109
        }
110
111
        return $alias . '.' . $embeddableName;
112
    }
113
114
    private function traverseAssociationsForEmbeddables(EntityManager $entityManager, array $associations, $fieldNamePrefix = false)
115
    {
116
        $associationsEmbeddables = array();
117
118
        foreach ($associations as $association) {
119
            $classMetaData = $entityManager->getClassMetadata($association['targetEntity']);
120
            $this->analyzedTargetEntities[] = $association['targetEntity'];
121
122
            foreach ($classMetaData->embeddedClasses as $embeddedClassKey => $embeddedClass) {
123
                $associationsEmbeddables[] = implode('.', array_filter(array($fieldNamePrefix, $association['fieldName'], $embeddedClassKey)));
124
            }
125
126
            $associationMappings = $classMetaData->getAssociationMappings();
127
            $associationMappings = array_filter($associationMappings, function ($associationMapping) {
128
                return !in_array($associationMapping['targetEntity'], $this->analyzedTargetEntities);
129
            });
130
131
            if (count($associationMappings) !== 0) {
132
                $traversedAssociationsEmbeddables = $this->traverseAssociationsForEmbeddables($entityManager, $associationMappings, $association['fieldName']);
133
                $associationsEmbeddables = array_merge($associationsEmbeddables, $traversedAssociationsEmbeddables);
134
            }
135
        }
136
137
        return $associationsEmbeddables;
138
    }
139
140
    private function analizeEmbeddables(QueryBuilder $queryBuilder)
141
    {
142
        $embeddables = array();
143
        $entityManager = $queryBuilder->getEntityManager();
144
        $rootEntities = $queryBuilder->getRootEntities();
145
146
        foreach ($rootEntities as $rootEntity) {
147
            $classMetaData = $entityManager->getClassMetadata($rootEntity);
148
149
            foreach ($classMetaData->embeddedClasses as $embeddedClassKey => $embeddedClass) {
150
                $embeddables[] = $embeddedClassKey;
151
            }
152
153
            // Since this is a root entity embeddable, there is no need to join.
154
            foreach ($this->expectedJoinChains as $tablesToJoinKey => $tablesToJoin) {
155
                if (in_array(implode('.', $tablesToJoin), $embeddables)) {
156
                    unset($this->expectedJoinChains[$tablesToJoinKey]);
157
                }
158
            }
159
160
            $traversedAssociationsEmbeddables = $this->traverseAssociationsForEmbeddables($entityManager, $classMetaData->getAssociationMappings());
161
            $embeddables = array_merge($embeddables, $traversedAssociationsEmbeddables);
162
        }
163
164
        return $embeddables;
165
    }
166
167
    /**
168
     * Builds an associative array of already joined tables and their alias.
169
     *
170
     * @param QueryBuilder $queryBuilder
171
     *
172
     * @return array
173
     */
174
    private function analizeJoinedTables(QueryBuilder $queryBuilder)
175
    {
176
        $joinMap = [];
177
        $joins   = $queryBuilder->getDQLPart('join');
178
        foreach (array_keys($joins) as $fromTable) {
179
            foreach ($joins[$fromTable] as $join) {
180
                $joinMap[$join->getJoin()] = $join->getAlias();
181
            }
182
        }
183
        return $joinMap;
184
    }
185
186
    private function autoJoin(QueryBuilder $queryBuilder)
187
    {
188
        foreach ($this->expectedJoinChains as $tablesToJoin) {
189
            // if the table is an embeddable, the property needs to be removed
190
            if (array_search(implode('.', $tablesToJoin), $this->embeddables) !== false) {
191
                array_pop($tablesToJoin);
192
            }
193
194
            // check if the first dimension is a known alias
195
            if (isset($this->knownAliases[$tablesToJoin[0]])) {
196
                $joinTo = $tablesToJoin[0];
197
                array_shift($tablesToJoin);
198
            } else { // if not, it's the root table
199
                $joinTo = $queryBuilder->getRootAliases()[0];
200
            }
201
202
            foreach ($tablesToJoin as $table) {
203
                $joinAlias = self::ALIAS_PREFIX . $table;
204
                $join      = sprintf('%s.%s', $joinTo, $table);
205
206
                if (!isset($this->joinMap[$join])) {
207
                    $this->joinMap[$join]           = $joinAlias;
208
                    $this->knownAliases[$joinAlias] = true;
209
210
                    $queryBuilder->join(sprintf('%s.%s', $joinTo, $table), $joinAlias);
211
                } else {
212
                    $joinAlias = $this->joinMap[$join];
213
                }
214
215
                $joinTo = $joinAlias;
216
            }
217
        }
218
    }
219
}
220