QuerySorting::addSorting()   A
last analyzed

Complexity

Conditions 5
Paths 1

Size

Total Lines 36
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 21
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 36
rs 9.2728
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Bugloos\QuerySortingBundle\Service;
6
7
use Bugloos\QuerySortingBundle\SortingHandler\Contract\WithRelationInterface;
8
use Bugloos\QuerySortingBundle\SortingHandler\Factory\SortingFactory;
9
use Closure;
10
use Doctrine\ORM\EntityManagerInterface;
11
use Doctrine\ORM\Mapping\ClassMetadata;
12
use Doctrine\ORM\QueryBuilder;
13
use JsonException;
14
use Psr\Cache\CacheItemInterface;
15
use Psr\Cache\InvalidArgumentException;
16
use Symfony\Contracts\Cache\CacheInterface;
17
18
/**
19
 * @author Milad Ghofrani <[email protected]>
20
 */
21
class QuerySorting
22
{
23
    private const DEFAULT_CACHE_TIME = 3600;
24
25
    private const SEPARATOR = '.';
26
27
    private EntityManagerInterface $entityManager;
28
29
    private SortingFactory $sortingFactory;
30
31
    private CacheInterface $cache;
32
33
    private string $rootAlias;
34
35
    private string $rootEntity;
36
37
    private ClassMetadata $rootClass;
38
39
    private QueryBuilder $query;
40
41
    private string $cacheKey;
42
43
    private array $orders = [];
44
45
    private array $mapper = [];
46
47
    private ?int $cacheTime = null;
48
49
    private $defaultCacheTime;
50
51
    private $separator;
52
53
    public function __construct(
54
        EntityManagerInterface $entityManager,
55
        CacheInterface $cache,
56
        SortingFactory $sortingFactory,
57
        $defaultCacheTime = self::DEFAULT_CACHE_TIME,
58
        $separator = self::SEPARATOR
59
    ) {
60
        $this->entityManager = $entityManager;
61
        $this->cache = $cache;
62
        $this->sortingFactory = $sortingFactory;
63
        $this->defaultCacheTime = $defaultCacheTime;
64
        $this->separator = $separator;
65
    }
66
67
    public function for(QueryBuilder $query): self
68
    {
69
        $this->initializeRootQueryConfig($query);
70
71
        return $this;
72
    }
73
74
    /**
75
     * @param mixed $orders
76
     *
77
     * @throws JsonException
78
     */
79
    public function parameters($orders): self
80
    {
81
        if (empty($orders)) {
82
            return $this;
83
        }
84
85
        if (!\is_array($orders)) {
86
            throw new \InvalidArgumentException(
87
                'Order parameters should be an array type'
88
            );
89
        }
90
91
        // Remove empty value from array
92
        $this->orders = array_filter($orders);
93
94
        // Create cache key by request
95
        $this->createCacheKey($this->orders);
96
97
        return $this;
98
    }
99
100
    public function mappers(array $mappers): self
101
    {
102
        if (empty($mappers)) {
103
            return $this;
104
        }
105
106
        foreach ($mappers as $parameter => $mapper) {
107
            $this->addMapper($parameter, $mapper);
108
        }
109
110
        return $this;
111
    }
112
113
    public function addMapper(string $parameter, string $mapper): self
114
    {
115
        if (empty($parameter) || empty($mapper)) {
116
            return $this;
117
        }
118
119
        $this->mapper[$parameter] = $mapper;
120
121
        return $this;
122
    }
123
124
    public function cacheTime(int $cacheTime): self
125
    {
126
        $this->cacheTime = $cacheTime;
127
128
        return $this;
129
    }
130
131
    /**
132
     * @throws InvalidArgumentException
133
     */
134
    public function sort(): QueryBuilder
135
    {
136
        // Early return if array is empty
137
        if (empty($this->orders)) {
138
            return $this->query;
139
        }
140
141
        // Calculate and cache fields.
142
        [$sortItems, $relationJoins] = $this->cache->get(
143
            $this->cacheKey,
144
            $this->addSorting()
145
        );
146
147
        $this->applyRelationJoinToQuery($relationJoins);
148
149
        $this->applyOrdersToQuery($sortItems);
150
151
        return $this->query;
152
    }
153
154
    private function applyOrdersToQuery($orderItems): void
155
    {
156
        foreach ($orderItems as $order => $type) {
157
            if ($order === array_key_first($orderItems)) {
158
                $this->query->orderBy($order, $type);
159
            } else {
160
                $this->query->addOrderBy($order, $type);
161
            }
162
        }
163
    }
164
165
    private function applyRelationJoinToQuery($relationJoins): void
166
    {
167
        // Remove exist joined from a list
168
        $filteredJoins = array_diff($relationJoins, $this->query->getAllAliases());
169
170
        // Add a left join to query which does not exist in the query
171
        if (!empty($filteredJoins)) {
172
            foreach ($filteredJoins as $property => $column) {
173
                $this->query->addSelect($column);
174
                $this->query->leftJoin($property, $column);
175
            }
176
        }
177
    }
178
179
    /**
180
     * @param mixed $array
181
     *
182
     * @throws JsonException
183
     */
184
    private function createCacheKey($array): void
185
    {
186
        $this->cacheKey = md5($this->rootEntity . json_encode($array, \JSON_THROW_ON_ERROR));
187
    }
188
189
    private function addSorting(): Closure
190
    {
191
        return function (CacheItemInterface $item) {
192
            $item->expiresAfter($this->cacheTime ?: $this->defaultCacheTime);
193
194
            $sortItems = [];
195
            $relationJoins = [];
196
197
            foreach ($this->orders as $parameter => $sortType) {
198
                // Check $parameter exists in mapper
199
                $parameter = (\array_key_exists($parameter, $this->mapper))
200
                    ? $this->mapper[$parameter] : $parameter;
201
202
                $relationsAndFieldName = explode($this->separator, $parameter);
203
204
                $sortingHandler = $this->sortingFactory->createSortingHandler($relationsAndFieldName);
205
206
                $sortProperty = $sortingHandler->getSortProperty(
207
                    $this->rootAlias,
208
                    $this->rootClass,
209
                    $relationsAndFieldName
210
                );
211
212
                $sortItems[$sortProperty] = $sortType;
213
214
                if ($sortingHandler instanceof WithRelationInterface) {
215
                    $relationJoins = $sortingHandler->getRelationJoin(
216
                        $relationJoins,
217
                        $this->rootAlias,
218
                        $this->rootClass,
219
                        $relationsAndFieldName
220
                    );
221
                }
222
            }
223
224
            return [$sortItems, $relationJoins];
225
        };
226
    }
227
228
    private function initializeRootQueryConfig($query): void
229
    {
230
        $rootEntities = $query->getRootEntities();
231
        $rootAliasArray = $query->getRootAliases();
232
233
        if (!isset($rootEntities[0], $rootAliasArray[0])) {
234
            throw new \InvalidArgumentException('Root Alias not defined correctly.');
235
        }
236
237
        $this->query = $query;
238
        $this->rootAlias = $rootAliasArray[0];
239
        $this->rootEntity = $rootEntities[0];
240
        $this->rootClass = $this->entityManager->getClassMetadata($this->rootEntity);
241
    }
242
}
243