ListQueryBuilder::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 10
ccs 5
cts 5
cp 1
rs 10
cc 1
nc 1
nop 4
crap 1
1
<?php
2
3
namespace Povs\ListerBundle\Service;
4
5
use Doctrine\ORM\EntityManagerInterface;
6
use Doctrine\ORM\QueryBuilder;
7
use Povs\ListerBundle\Declaration\ListInterface;
8
use Povs\ListerBundle\Declaration\ListValueInterface;
9
use Povs\ListerBundle\DependencyInjection\Locator\QueryTypeLocator;
10
use Povs\ListerBundle\DependencyInjection\Locator\SelectorTypeLocator;
11
use Povs\ListerBundle\Exception\ListFieldException;
12
use Povs\ListerBundle\Mapper\FilterField;
13
use Povs\ListerBundle\Mapper\FilterMapper;
14
use Povs\ListerBundle\Mapper\JoinField;
15
use Povs\ListerBundle\Mapper\JoinMapper;
16
use Povs\ListerBundle\Mapper\ListField;
17
use Povs\ListerBundle\Mapper\ListMapper;
18
use Povs\ListerBundle\Type\QueryType\QueryTypeInterface;
19
use Povs\ListerBundle\Type\SelectorType\SelectorTypeInterface;
20
use Symfony\Component\OptionsResolver\OptionsResolver;
21
22
/**
23
 * @author Povilas Margaiatis <[email protected]>
24
 */
25
class ListQueryBuilder
26
{
27
    public const IDENTIFIER_ALIAS = 'list_identifier';
28
29
    /**
30
     * @var EntityManagerInterface
31
     */
32
    private $em;
33
34
    /**
35
     * @var QueryBuilder
36
     */
37
    private $queryBuilder;
38
39
    /**
40
     * @var QueryTypeLocator
41
     */
42
    private $queryTypeLocator;
43
44
    /**
45
     * @var SelectorTypeLocator
46
     */
47
    private $selectorTypeLocator;
48
49
    /**
50
     * @var ConfigurationResolver
51
     */
52
    private $configuration;
53
54
    /**
55
     * @var bool
56
     */
57
    private $hasAggregation = false;
58
59
    /**
60
     * ListQueryBuilder constructor.
61
     *
62
     * @param EntityManagerInterface $entityManager
63
     * @param QueryTypeLocator       $queryTypeLocator
64
     * @param SelectorTypeLocator    $selectorTypeLocator
65
     * @param ConfigurationResolver  $configuration
66
     */
67 8
    public function __construct(
68
        EntityManagerInterface $entityManager,
69
        QueryTypeLocator $queryTypeLocator,
70
        SelectorTypeLocator $selectorTypeLocator,
71
        ConfigurationResolver $configuration
72
    ) {
73 8
        $this->em = $entityManager;
74 8
        $this->queryTypeLocator = $queryTypeLocator;
75 8
        $this->selectorTypeLocator = $selectorTypeLocator;
76 8
        $this->configuration = $configuration;
77
    }
78
79
    /**
80
     * @param ListInterface   $list
81
     * @param JoinMapper      $joinMapper
82
     * @param ListMapper      $listMapper
83
     * @param FilterMapper    $filterMapper
84
     * @param ListValueInterface $listValue
85
     *
86
     * @return QueryBuilder
87
     */
88 6
    public function buildQuery(
89
        ListInterface $list,
90
        JoinMapper $joinMapper,
91
        ListMapper $listMapper,
92
        FilterMapper $filterMapper,
93
        ListValueInterface $listValue
94
    ): QueryBuilder {
95 6
        $this->hasAggregation = false;
96 6
        $this->queryBuilder = $this->em->createQueryBuilder()
97 6
            ->from($list->getDataClass(), $this->configuration->getAlias());
98
99 6
        $this->applyJoins($joinMapper, false);
100 6
        $this->applySelects($listMapper, $joinMapper, false);
101 4
        $this->applyFilter($filterMapper, $joinMapper);
102 3
        $this->applyGroup();
103 3
        $list->configureQuery($this->queryBuilder, $listValue);
104
105 3
        return $this->queryBuilder;
106
    }
107
108
    /**
109
     * @param ListInterface $list
110
     * @param JoinMapper    $joinMapper
111
     * @param ListMapper    $listMapper
112
     *
113
     * @return QueryBuilder|null
114
     */
115 2
    public function buildLazyQuery(
116
        ListInterface $list,
117
        JoinMapper $joinMapper,
118
        ListMapper $listMapper
119
    ): ?QueryBuilder {
120 2
        if ($listMapper->getFields(true)->isEmpty()) {
121 1
            return null;
122
        }
123
124 1
        $this->hasAggregation = false;
125 1
        $this->queryBuilder = $this->em->createQueryBuilder()
126 1
            ->from($list->getDataClass(), $this->configuration->getAlias());
127 1
        $this->applyJoins($joinMapper, true);
128 1
        $this->applySelects($listMapper, $joinMapper, true);
129 1
        $this->applyGroup();
130
131 1
        return $this->queryBuilder;
132
    }
133
134
    /**
135
     * Adds join dql parts.
136
     *
137
     * @param JoinMapper $joinMapper
138
     * @param bool       $lazy
139
     */
140 7
    private function applyJoins(JoinMapper $joinMapper, bool $lazy): void
141
    {
142 7
        foreach ($joinMapper->getFields(null, $lazy) as $field) {
143 1
            $joinPath = $field->getJoinPath($this->configuration->getAlias());
144 1
            $condition = null;
145 1
            $conditionType = null;
146
147 1
            if ($field->hasOption(JoinField::OPTION_CONDITION)) {
148 1
                $condition = $field->getOption(JoinField::OPTION_CONDITION);
149 1
                $conditionType = $field->getOption(JoinField::OPTION_CONDITION_TYPE);
150
            }
151
152 1
            if ($field->getOption(JoinField::OPTION_JOIN_TYPE) === JoinField::JOIN_INNER) {
153 1
                $this->queryBuilder->innerJoin($joinPath, $field->getAlias(), $conditionType, $condition);
154
            } else {
155 1
                $this->queryBuilder->leftJoin($joinPath, $field->getAlias(), $conditionType, $condition);
156
            }
157
158 1
            if ($field->hasOption(JoinField::OPTION_CONDITION_PARAMETERS)) {
159 1
                $this->queryBuilder->setParameters($field->getOption(JoinField::OPTION_CONDITION_PARAMETERS));
160
            }
161
        }
162
    }
163
164
    /**
165
     * Adds select and sort DQL parts
166
     *
167
     * @param ListMapper $listMapper
168
     * @param JoinMapper $joinMapper
169
     * @param bool       $lazy
170
     */
171 7
    private function applySelects(ListMapper $listMapper, JoinMapper $joinMapper, bool $lazy): void
172
    {
173 7
        if ($lazy === false) {
174 6
            $idSelector = sprintf(
175 6
                '%s.%s as %s',
176 6
                $this->configuration->getAlias(),
177 6
                $this->configuration->getIdentifier(),
178 6
                self::IDENTIFIER_ALIAS
179 6
            );
180 6
            $this->queryBuilder->addSelect($idSelector);
181
        }
182
183 7
        foreach ($listMapper->getFields($lazy) as $field) {
184 3
            $paths = $this->parsePaths($joinMapper, $field->getPaths(), $lazy);
185 2
            $selectorType = $field->getOption(ListField::OPTION_SELECTOR);
186
187 2
            if (!$this->selectorTypeLocator->has($selectorType)) {
188 1
                throw ListFieldException::invalidType($field->getId(), $selectorType, SelectorTypeInterface::class);
189
            }
190
191 1
            $selectorType = $this->selectorTypeLocator->get($selectorType);
192 1
            $selectorType->apply($this->queryBuilder, $paths, $field->getId());
193
194 1
            if ($selectorType->hasAggregation()) {
195 1
                $this->hasAggregation = true;
196
            }
197
198
            if (
199 1
                false === $lazy && $field->getOption(ListField::OPTION_SORTABLE) &&
200 1
                ($dir = $field->getOption(ListField::OPTION_SORT_VALUE))
201
            ) {
202 1
                if ($sortPath = $field->getOption(ListField::OPTION_SORT_PATH)) {
203 1
                    $select = $this->parsePaths($joinMapper, (array) $sortPath, false)[0];
204
                } else {
205 1
                    $select = $selectorType->getSortPath($field->getId());
206
                }
207
208 1
                $this->queryBuilder->addOrderBy($select, $dir);
209
            }
210
        }
211
    }
212
213
    /**
214
     * @param FilterMapper $filterMapper
215
     * @param JoinMapper   $joinMapper
216
     */
217 4
    private function applyFilter(FilterMapper $filterMapper, JoinMapper $joinMapper): void
218
    {
219 4
        foreach ($filterMapper->getFields() as $field) {
220 2
            if (!$field->hasValue() || false === $field->getOption(FilterField::OPTION_MAPPED)) {
221 1
                continue;
222
            }
223
224 2
            $queryType = $field->getOption(FilterField::OPTION_QUERY_TYPE);
225
226 2
            if (!$this->queryTypeLocator->has($queryType)) {
0 ignored issues
show
Bug introduced by
It seems like $queryType can also be of type null; however, parameter $id of Symfony\Component\Depend...n\ServiceLocator::has() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

226
            if (!$this->queryTypeLocator->has(/** @scrutinizer ignore-type */ $queryType)) {
Loading history...
227 1
                throw ListFieldException::invalidType($field->getId(), $queryType, QueryTypeInterface::class);
228
            }
229
230 1
            $paths = $this->parsePaths($joinMapper, $field->getPaths(), false);
231 1
            $queryType = $this->queryTypeLocator->get($queryType);
232 1
            $resolver = new OptionsResolver();
233 1
            $queryType->configureOptions($resolver);
234 1
            $queryType->setOptions($resolver->resolve($field->getOption(FilterField::OPTION_QUERY_OPTIONS)));
0 ignored issues
show
Bug introduced by
$field->getOption(Povs\L...::OPTION_QUERY_OPTIONS) of type Doctrine\Common\Collections\T|null is incompatible with the type array expected by parameter $options of Symfony\Component\Option...ionsResolver::resolve(). ( Ignorable by Annotation )

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

234
            $queryType->setOptions($resolver->resolve(/** @scrutinizer ignore-type */ $field->getOption(FilterField::OPTION_QUERY_OPTIONS)));
Loading history...
235 1
            $queryType->filter($this->queryBuilder, $paths, $field->getId(), $field->getValue());
236
237 1
            if ($queryType->hasAggregation()) {
238 1
                $this->hasAggregation = true;
239
            }
240
        }
241
    }
242
243
    /**
244
     * Applies group by identifier if query has aggregations
245
     */
246 4
    private function applyGroup(): void
247
    {
248 4
        if ($this->hasAggregation) {
249 2
            $statement = sprintf('%s.%s', $this->configuration->getAlias(), $this->configuration->getIdentifier());
250 2
            $this->queryBuilder->groupBy($statement);
251
        } else {
252 2
            $this->queryBuilder->distinct();
253
        }
254
    }
255
256
    /**
257
     * @param JoinMapper $joinMapper
258
     * @param array      $paths
259
     * @param bool       $lazy
260
     *
261
     * @return array
262
     */
263 4
    private function parsePaths(JoinMapper $joinMapper, array $paths, bool $lazy): array
264
    {
265 4
        $parsedPaths = [];
266
267 4
        foreach ($paths as $path) {
268 3
            $pathElements = explode('.', $path);
269
270 3
            if (count($pathElements) === 1) {
271 2
                $prop = $pathElements[0];
272 2
                $path = null;
273
            } else {
274 3
                $prop = array_pop($pathElements);
275 3
                $path = implode('.', $pathElements);
276
            }
277
278 3
            if (null !== $path) {
279 3
                if (!$joinField = $joinMapper->getByPath($path, $lazy)) {
280 1
                    throw ListFieldException::invalidPath($path);
281
                }
282
283 2
                $alias = $joinField->getAlias();
284
            } else {
285 2
                $alias = $this->configuration->getAlias();
286
            }
287
288 2
            $parsedPaths[] = sprintf('%s.%s', $alias, $prop);
289
        }
290
291 3
        return $parsedPaths;
292
    }
293
}
294