Completed
Pull Request — master (#71)
by
unknown
02:09
created

DoctrineAutoJoin::addDetectedJoin()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 3
nop 3
1
<?php
2
3
namespace RulerZ\Target\DoctrineORM;
4
5
use Doctrine\ORM\EntityManagerInterface as EntityManager;
6
use Hoa\Ruler\Model as AST;
7
8
class DoctrineAutoJoin
9
{
10
    /**
11
     * @var EntityManager
12
     */
13
    private $em;
14
15
    /**
16
     * @var array
17
     */
18
    private $detectedJoins = [];
19
20
    /**
21
     * @var array<Entity,array<dimmension,alias>
22
     */
23
    private $knownEntities = [];
24
25
    /**
26
     * @var array<alias,Entity>
27
     */
28
    private $aliasMap = [];
29
30
    /**
31
     * @var array
32
     */
33
    private $rootEntities = [];
34
35
    public function __construct(EntityManager $em, array $rootEntities, array $rootAliases, array $existingJoins)
36
    {
37
        $this->em = $em;
38
        $this->rootEntities = array_combine($rootAliases, $rootEntities);
39
40
        $this->aliasMap = array_combine($rootAliases, $rootEntities);
41
42
        foreach ($existingJoins as $joins) {
43
            /** @var \Doctrine\ORM\Query\Expr\Join $join */
44
            foreach ($joins as $join) {
45
                list($fromAlias, $attribute) = explode('.', $join->getJoin());
46
                $relation = $this->getRelation($attribute, $this->getEntity($fromAlias));
47
48
                $this->saveAlias($relation['targetEntity'], $relation['fieldName'], $join->getAlias());
49
            }
50
        }
51
    }
52
53
    public function getDetectedJoins()
54
    {
55
        return $this->detectedJoins;
56
    }
57
58
    /**
59
     * @param string $root
60
     * @param string $column
61
     * @param string $alias
62
     */
63
    private function addDetectedJoin($root, $column, $alias)
64
    {
65
        foreach ($this->detectedJoins as &$join) {
66
            if ($join['root'] === (string) $root && $join['column'] === (string) $column &&
67
                $join['as'] === (string) $alias
68
            ) {
69
                return;
70
            }
71
        }
72
        $this->detectedJoins[] = [
73
            'root' => (string) $root,
74
            'column' => (string) $column,
75
            'as' => (string) $alias,
76
        ];
77
    }
78
79
    public function buildAccessPath(AST\Bag\Context $element)
80
    {
81
        $dimensionNames = array_map(function ($dimension) {
82
            return $dimension[1];
83
        }, $element->getDimensions());
84
        array_unshift($dimensionNames, $element->getId());
85
86
        $currentEntity = current($this->rootEntities);
87
        $lastAlias = key($this->aliasMap);
88
89
        foreach ($dimensionNames as $i => $dimension) {
90
            if (!$this->em->getClassMetadata($currentEntity)) {
91
                throw new \Exception(sprintf('Metadata not found for entity "%s"', $currentEntity));
92
            }
93
94
            // the current dimension is an alias (a table already joined for instance)
95
            if (isset($this->aliasMap[$dimension])) {
96
                $currentEntity = $this->getEntity($dimension);
97
                $lastAlias = $dimension;
98
                continue;
99
            }
100
101
            // the current dimension is a relation of the current entity
102
            if ($this->relationExists($dimension, $currentEntity)) {
103
                /** @var \Doctrine\ORM\Mapping\ClassMetadataInfo $classMetadata */
104
                $classMetadata = $this->em->getClassMetadata($currentEntity);
105
                $association = $classMetadata->getAssociationMapping($dimension);
106
107
                if (!isset($this->knownEntities[$currentEntity], $this->knownEntities[$currentEntity][$association['fieldName']])) {
108
                    $alias = sprintf('_%d_%s', count($this->knownEntities), $dimension);
109
110
                    $this->saveAlias($currentEntity, $association['fieldName'], $alias);
111
                }
112
113
                $this->addDetectedJoin(
114
                    $lastAlias,
115
                    $dimension,
116
                    $alias = $this->getAlias($currentEntity, $association['fieldName'])
117
                );
118
119
                $currentEntity = $association['targetEntity'];
120
                $lastAlias = $alias;
121
                continue;
122
            }
123
124
            // the current dimension is an embedded class
125
            if ($this->embeddedExists($dimension, $currentEntity)) {
126
                /** @var \Doctrine\ORM\Mapping\ClassMetadataInfo $classMetadata */
127
                $classMetadata = $this->em->getClassMetadata($currentEntity);
128
                $embeddableMetadata = $classMetadata->embeddedClasses[$dimension];
129
130
                $currentEntity = $embeddableMetadata['class'];
131
                $lastAlias = $lastAlias.'.'.$dimension;
132
                continue;
133
            }
134
135
            // or, at last, it's a column access.
136
            if ($this->columnExists($dimension, $currentEntity)) {
137
                if (($i + 1) === count($dimensionNames)) {
138
                    return sprintf('%s.%s', $lastAlias, $dimension);
139
                }
140
141
                throw new \RuntimeException('Found scalar attribute in the middle of an access path.');
142
            }
143
144
            throw new \Exception(sprintf('"%s" not found for entity "%s"', $dimension, $currentEntity));
145
        }
146
147
        return $lastAlias;
148
    }
149
150
    private function columnExists($name, $rootEntity)
151
    {
152
        /** @var \Doctrine\ORM\Mapping\ClassMetadataInfo $classMetadata */
153
        $classMetadata = $this->em->getClassMetadata($rootEntity);
154
155
        return $classMetadata->hasField($name);
156
    }
157
158
    private function relationExists($name, $rootEntity)
159
    {
160
        /** @var \Doctrine\ORM\Mapping\ClassMetadataInfo $classMetadata */
161
        $classMetadata = $this->em->getClassMetadata($rootEntity);
162
163
        return $classMetadata->hasAssociation($name);
164
    }
165
166
    private function getRelation($name, $rootEntity)
167
    {
168
        /** @var \Doctrine\ORM\Mapping\ClassMetadataInfo $classMetadata */
169
        $classMetadata = $this->em->getClassMetadata($rootEntity);
170
171
        return $classMetadata->getAssociationMapping($name);
172
    }
173
174
    private function embeddedExists($name, $rootEntity)
175
    {
176
        /** @var \Doctrine\ORM\Mapping\ClassMetadataInfo $classMetadata */
177
        $classMetadata = $this->em->getClassMetadata($rootEntity);
178
179
        return isset($classMetadata->embeddedClasses) && isset($classMetadata->embeddedClasses[$name]);
180
    }
181
182
    private function saveAlias($entity, $dimension, $alias)
183
    {
184
        if (!isset($this->knownEntities[$entity])) {
185
            $this->knownEntities[$entity] = [];
186
        }
187
188
        $this->knownEntities[$entity][$dimension] = $alias;
189
        $this->aliasMap[$alias] = $entity;
190
    }
191
192
    private function getAlias($entity, $dimension)
193
    {
194
        return $this->knownEntities[$entity][$dimension];
195
    }
196
197
    private function getEntity($entity)
198
    {
199
        return $this->aliasMap[$entity];
200
    }
201
}
202