Completed
Pull Request — master (#28)
by
unknown
03:32 queued 10s
created

AutoJoin   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 218
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 8
Bugs 1 Features 5
Metric Value
wmc 30
c 8
b 1
f 5
lcom 1
cbo 3
dl 0
loc 218
rs 10

7 Methods

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