Test Failed
Push — master ( e630f8...594d62 )
by Kirill
06:52
created

DatabaseProcessor::apply()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 11
nc 5
nop 3
1
<?php
2
/**
3
 * This file is part of Hydrogen package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
declare(strict_types=1);
9
10
namespace RDS\Hydrogen\Processor;
11
12
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
13
use Doctrine\ORM\EntityManagerInterface;
14
use Doctrine\ORM\NoResultException;
15
use Doctrine\ORM\QueryBuilder;
16
use Illuminate\Support\Str;
17
use RDS\Hydrogen\Criteria\Common\Field;
18
use RDS\Hydrogen\Criteria\CriterionInterface;
19
use RDS\Hydrogen\Criteria\Group;
20
use RDS\Hydrogen\Criteria\GroupBy;
21
use RDS\Hydrogen\Criteria\Limit;
22
use RDS\Hydrogen\Criteria\Offset;
23
use RDS\Hydrogen\Criteria\Relation;
24
use RDS\Hydrogen\Criteria\Selection;
25
use RDS\Hydrogen\Criteria\Where;
26
use RDS\Hydrogen\Processor\DatabaseProcessor\DatabaseCriterionProcessor;
27
use RDS\Hydrogen\Processor\DatabaseProcessor\GroupByProcessor;
28
use RDS\Hydrogen\Processor\DatabaseProcessor\GroupProcessor;
29
use RDS\Hydrogen\Processor\DatabaseProcessor\LimitProcessor;
30
use RDS\Hydrogen\Processor\DatabaseProcessor\OffsetProcessor;
31
use RDS\Hydrogen\Processor\DatabaseProcessor\RelationProcessor;
32
use RDS\Hydrogen\Processor\DatabaseProcessor\SelectProcessor;
33
use RDS\Hydrogen\Processor\DatabaseProcessor\WhereProcessor;
34
use RDS\Hydrogen\Query;
35
36
/**
37
 * Class DatabaseProcessor
38
 */
39
class DatabaseProcessor implements ProcessorInterface
40
{
41
    /**
42
     * @var array|DatabaseCriterionProcessor[]
43
     */
44
    private const MAPPINGS = [
45
        Where::class     => WhereProcessor::class,
46
        Limit::class     => LimitProcessor::class,
47
        Offset::class    => OffsetProcessor::class,
48
        GroupBy::class   => GroupByProcessor::class,
49
        Selection::class => SelectProcessor::class,
50
        Group::class     => GroupProcessor::class,
51
        Relation::class  => RelationProcessor::class,
52
    ];
53
54
    /**
55
     * @var int
56
     */
57
    private static $lastSelectionId = 0;
58
59
    /**
60
     * @var EntityManagerInterface
61
     */
62
    private $em;
63
64
    /**
65
     * @var ClassMetadata
66
     */
67
    private $meta;
68
69
    /**
70
     * @var string
71
     */
72
    private $alias;
73
74
    /**
75
     * DatabaseProcessor constructor.
76
     * @param EntityManagerInterface $em
77
     * @param ClassMetadata $meta
78
     */
79
    public function __construct(EntityManagerInterface $em, ClassMetadata $meta)
80
    {
81
        $this->em    = $em;
82
        $this->meta  = $meta;
83
        $this->alias = $this->createAlias($meta);
84
    }
85
86
    /**
87
     * @param ClassMetadata $meta
88
     * @return string
89
     */
90
    private function createAlias(ClassMetadata $meta): string
91
    {
92
        return \vsprintf('%s_%s', [
93
            Str::snake(\class_basename($meta->getName())),
94
            ++self::$lastSelectionId,
95
        ]);
96
    }
97
98
    /**
99
     * @param Query $query
100
     * @return iterable|object[]
101
     */
102
    public function get(Query $query): iterable
103
    {
104
        return $this->toBuilder($query)->getQuery()->getResult();
105
    }
106
107
    /**
108
     * @param Query $query
109
     * @return QueryBuilder
110
     */
111
    public function toBuilder(Query $query): QueryBuilder
112
    {
113
        $builder = $this->em->createQueryBuilder()
114
            ->select($this->alias)
115
            ->from($this->meta->getName(), $this->alias);
116
117
        $this->apply($builder, $query, $this->alias);
118
119
        return $builder;
120
    }
121
122
    /**
123
     * @param QueryBuilder $builder
124
     * @param Query $query
125
     * @param string $alias
126
     * @return QueryBuilder
127
     */
128
    public function apply(QueryBuilder $builder, Query $query, string $alias): QueryBuilder
129
    {
130
        /** @var DatabaseCriterionProcessor[] $criteria */
131
        $criteria = [];
132
133
        foreach ($query->getCriteria() as $criterion) {
134
            $identifier = \get_class($criterion);
135
136
            if (! \array_key_exists($identifier, $criteria)) {
137
                $criteria[$identifier] = $this->criterion($criterion, $alias);
138
            }
139
140
            /** @var DatabaseCriterionProcessor $applicator */
141
            $applicator = $criteria[$identifier];
142
143
            $builder = $applicator->apply($builder, $criterion);
144
145
            foreach ($applicator->getParameters() as $field => $value) {
146
                $builder->setParameter($field, $value);
147
            }
148
        }
149
150
        return $builder;
151
    }
152
153
    /**
154
     * @param CriterionInterface $criterion
155
     * @param string $alias
156
     * @return DatabaseCriterionProcessor
157
     */
158
    private function criterion(CriterionInterface $criterion, string $alias): DatabaseCriterionProcessor
159
    {
160
        $processor = self::MAPPINGS[\get_class($criterion)] ?? null;
161
162
        if ($processor === null) {
163
            $error = \vsprintf('%s is not support the %s criterion', [
164
                \class_basename($this),
165
                \class_basename($criterion),
166
            ]);
167
168
            throw new \InvalidArgumentException($error);
169
        }
170
171
        return new $processor($alias, $this);
172
    }
173
174
    /**
175
     * @return EntityManagerInterface
176
     */
177
    public function getEntityManager(): EntityManagerInterface
178
    {
179
        return $this->em;
180
    }
181
182
    /**
183
     * @param string|null $entity
184
     * @return ClassMetadata
185
     */
186
    public function getClassMetadata(string $entity = null): ClassMetadata
187
    {
188
        return $this->em->getClassMetadata($entity ?? $this->meta->getName());
189
    }
190
191
    /**
192
     * @param Query $query
193
     * @return mixed|null|object
194
     * @throws \Doctrine\ORM\NonUniqueResultException
195
     */
196
    public function first(Query $query)
197
    {
198
        try {
199
            $builder = $this->toBuilder($query);
200
            $builder->setMaxResults(1);
201
202
            return $builder->getQuery()->getOneOrNullResult();
203
        } catch (\InvalidArgumentException $e) {
204
            return null;
205
        }
206
    }
207
208
    /**
209
     * @param Query $query
210
     * @return int
211
     * @throws \Doctrine\ORM\NonUniqueResultException
212
     */
213
    public function count(Query $query): int
214
    {
215
        return $this->scalar($query, function (QueryBuilder $builder, string $field) {
216
            return $builder->expr()->count($field);
217
        });
218
    }
219
220
    /**
221
     * @param Query $query
222
     * @param \Closure $expr
223
     * @return int
224
     * @throws \Doctrine\ORM\NonUniqueResultException
225
     */
226
    private function scalar(Query $query, \Closure $expr): int
227
    {
228
        $builder = $this->toBuilder($query);
229
230
        try {
231
            return (int)$builder->select($expr($builder, $this->getPrimary()->withAlias($this->alias)))
232
                ->getQuery()
233
                ->getSingleScalarResult();
234
        } catch (NoResultException | \InvalidArgumentException $e) {
235
            return 0;
236
        }
237
    }
238
239
    /**
240
     * @return Field
241
     */
242
    private function getPrimary(): Field
243
    {
244
        return new Field(\array_first($this->meta->getIdentifierFieldNames()));
245
    }
246
}
247