Passed
Push — master ( 52ecda...402e55 )
by Juan
09:00
created

NativeQueryBuilderHelper::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 6
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 11
rs 10
1
<?php
2
3
namespace Micayael\NativeQueryFromFileBuilderBundle\Helper;
4
5
use Adbar\Dot;
6
use Micayael\NativeQueryFromFileBuilderBundle\Event\NativeQueryFromFileBuilderEvents;
7
use Micayael\NativeQueryFromFileBuilderBundle\Event\ProcessQueryParamsEvent;
8
use Micayael\NativeQueryFromFileBuilderBundle\Exception\NonExistentQueryDirectoryException;
9
use Micayael\NativeQueryFromFileBuilderBundle\Exception\NonExistentQueryFileException;
10
use Micayael\NativeQueryFromFileBuilderBundle\Exception\NonExistentQueryKeyException;
11
use Symfony\Component\Cache\Adapter\AdapterInterface;
12
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
13
use Symfony\Component\Filesystem\Filesystem;
14
use Symfony\Component\Yaml\Yaml;
15
16
class NativeQueryBuilderHelper
17
{
18
    const REQUIRED_ID_PATTERN = '/@{.+?}/';
19
20
    const OPTIONAL_ID_PATTERN = "/@\[.+?\]/";
21
22
    const KEY_PATTERN = '/[a-z0-9._]+/';
23
24
    /**
25
     * @var null|EventDispatcherInterface
26
     */
27
    private $eventDispatcher;
28
29
    /**
30
     * @var null|AdapterInterface
31
     */
32
    private $cache;
33
34
    private $queryDir;
35
36
    private $fileExtension;
37
38
    public function __construct(?EventDispatcherInterface $eventDispatcher, ?AdapterInterface $cache, array $config)
39
    {
40
        $this->eventDispatcher = $eventDispatcher;
41
        $this->cache = null;
42
43
        if (!$config['debug']) {
44
            $this->cache = $cache;
45
        }
46
47
        $this->queryDir = $config['sql_queries_dir'];
48
        $this->fileExtension = $config['file_extension'];
49
    }
50
51
    /**
52
     * Get a SQL query from a query key in a yaml file.
53
     *
54
     * @param string $queryFullKey
55
     * @param array  $params
56
     *
57
     * @return string
58
     *
59
     * @throws NonExistentQueryDirectoryException
60
     * @throws NonExistentQueryFileException
61
     * @throws NonExistentQueryKeyException
62
     */
63
    public function getSqlFromYamlKey(string $queryFullKey, array &$params = []): string
64
    {
65
        $queryFullKey = explode(':', $queryFullKey);
66
67
        $fileKey = $queryFullKey[0];
68
        $queryKey = $queryFullKey[1];
69
70
        if ($this->cache) {
71
            $cacheItem = $this->cache->getItem('nqbff_'.$fileKey);
72
73
            if (!$cacheItem->isHit()) {
74
                $dot = $this->getQueryFileContent($fileKey);
75
76
                $cacheItem->set($dot);
77
                $this->cache->save($cacheItem);
78
            }
79
80
            $dot = $cacheItem->get();
81
        } else {
82
            $dot = $this->getQueryFileContent($fileKey);
83
        }
84
85
        if (!$dot->has($queryKey)) {
86
            throw new NonExistentQueryKeyException($queryKey);
87
        }
88
89
        $sql = $dot->get($queryKey);
90
91
        $sqlParts = $this->resolveRequiredKeys($dot, $sql);
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type null; however, parameter $sql of Micayael\NativeQueryFrom...::resolveRequiredKeys() 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

91
        $sqlParts = $this->resolveRequiredKeys($dot, /** @scrutinizer ignore-type */ $sql);
Loading history...
92
93
        array_push($sqlParts, $sql);
94
95
        $sql = $this->resolveOptionalKeys($dot, $sqlParts, $params);
96
97
        // Reemplaza espacios adicionales
98
        $sql = trim(preg_replace('/\s+/', ' ', $sql));
99
100
        if (isset($params['orderby'])) {
101
            $sql = preg_replace('/:\w+:/', $params['orderby'], $sql);
102
            $sql = str_replace(':orderby', $params['orderby'], $sql);
103
        }
104
105
        return $sql;
106
    }
107
108
    /**
109
     * @param string $fileKey
110
     *
111
     * @return Dot
112
     *
113
     * @throws NonExistentQueryDirectoryException
114
     * @throws NonExistentQueryFileException
115
     */
116
    private function getQueryFileContent(string $fileKey): Dot
117
    {
118
        $fileSystem = new Filesystem();
119
120
        if (!$fileSystem->exists($this->queryDir)) {
121
            throw new NonExistentQueryDirectoryException($this->queryDir);
122
        }
123
124
        $filename = sprintf('%s/%s.%s', $this->queryDir, $fileKey, $this->fileExtension);
125
126
        if (!$fileSystem->exists($filename)) {
127
            throw new NonExistentQueryFileException($filename);
128
        }
129
130
        $data = Yaml::parseFile($filename);
131
132
        $dot = new Dot($data);
133
134
        return $dot;
135
    }
136
137
    /**
138
     * @param Dot    $dot
139
     * @param string $sql
140
     *
141
     * @return array An array of query parts depending on required keys @{file:query}
142
     *
143
     * @throws NonExistentQueryKeyException
144
     */
145
    private function resolveRequiredKeys(Dot $dot, string $sql): array
146
    {
147
        $queryParts = [];
148
149
        $snippetIds = [];
150
151
        preg_match_all(self::REQUIRED_ID_PATTERN, $sql, $snippetIds);
152
153
        foreach ($snippetIds[0] as $snippetId) {
154
            preg_match(self::KEY_PATTERN, $snippetId, $matches);
155
156
            $snippetKey = $matches[0];
157
158
            if (empty($snippetKey)) {
159
                continue;
160
            }
161
162
            if (!$dot->has($snippetKey)) {
163
                throw new NonExistentQueryKeyException($snippetKey);
164
            }
165
166
            $snippetSql = $dot->get($snippetKey);
167
168
            $queryParts[$snippetId] = trim(preg_replace('/\s+/', ' ', $snippetSql));
169
        }
170
171
        return $queryParts;
172
    }
173
174
    /**
175
     * @param Dot   $dot
176
     * @param array $sqlParts
177
     * @param array $params
178
     *
179
     * @return string Processed SQL with parameters defined by @[file:query:params]
180
     *
181
     * @throws NonExistentQueryKeyException
182
     */
183
    private function resolveOptionalKeys(Dot $dot, array $sqlParts, array &$params = []): string
184
    {
185
        $paramKeys = array_keys($params);
186
187
        // Se recorren los sqlParts y por cada uno se van agregando los filtros opcionales.
188
        // El query principal se evalúa al final
189
        foreach ($sqlParts as $sqlPartKey => $sql) {
190
            $snippetIds = [];
191
192
            $whereConnector = (false === strpos(strtoupper($sql), 'WHERE')) ? 'WHERE ' : 'AND ';
193
194
            preg_match_all(self::OPTIONAL_ID_PATTERN, $sql, $snippetIds);
195
196
            foreach ($snippetIds[0] as $snippetId) {
197
                $requestedSnippets = [];
198
199
                preg_match(self::KEY_PATTERN, $snippetId, $matches);
200
201
                $snippetKey = $matches[0];
202
203
                if (empty($snippetKey)) {
204
                    continue;
205
                }
206
207
                if (!$dot->has($snippetKey)) {
208
                    throw new NonExistentQueryKeyException($snippetKey);
209
                }
210
211
                $snippets = $dot->get($snippetKey);
212
213
                foreach ($snippets as $filter) {
214
                    $filterType = null;
215
216
                    if (is_array($filter)) {
217
                        $filterType = key($filter);
218
                        $filter = $filter[$filterType];
219
                    }
220
221
                    foreach ($paramKeys as $paramKey) {
222
                        if (false !== strpos($filter, ':'.$paramKey)) {
223
                            $event = new ProcessQueryParamsEvent($snippetKey, $filterType, $paramKey, $params, $filter);
224
225
                            if ($this->eventDispatcher) {
226
                                $this->eventDispatcher->dispatch(NativeQueryFromFileBuilderEvents::PROCESS_QUERY_PARAMS, $event);
0 ignored issues
show
Bug introduced by
Micayael\NativeQueryFrom...s::PROCESS_QUERY_PARAMS of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

226
                                $this->eventDispatcher->dispatch(/** @scrutinizer ignore-type */ NativeQueryFromFileBuilderEvents::PROCESS_QUERY_PARAMS, $event);
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $event. ( Ignorable by Annotation )

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

226
                                $this->eventDispatcher->/** @scrutinizer ignore-call */ 
227
                                                        dispatch(NativeQueryFromFileBuilderEvents::PROCESS_QUERY_PARAMS, $event);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
227
                            }
228
229
                            $params = $event->getProcessedParams();
230
                            $filter = $event->getProcessedFilter();
231
232
                            // Si no existe en la lista de filtros a usar, se agrega
233
                            if (!in_array($filter, array_keys($requestedSnippets))) {
234
                                $requestedSnippets[$filter] = $whereConnector.'('.$filter.')';
235
                            }
236
237
                            $whereConnector = 'AND ';
238
                        }
239
                    }
240
                }
241
242
                if (!empty($requestedSnippets)) {
243
                    $snippetSql = implode(' ', $requestedSnippets);
244
245
                    $sqlParts[$sqlPartKey] = str_replace($snippetId, $snippetSql, $sql);
246
                } else {
247
                    $sqlParts[$sqlPartKey] = str_replace($snippetId, '', $sql);
248
                }
249
            }
250
        }
251
252
        // Obtiene la última posición del array que corresponde al SQL base
253
        $sql = array_pop($sqlParts);
254
255
        // Recorre las partes y las va reemplazando en el SQL base
256
        foreach ($sqlParts as $sqlPartKey => $sqlPart) {
257
            $sql = str_replace($sqlPartKey, $sqlPart, $sql);
258
        }
259
260
        return $sql;
261
    }
262
}
263