DoctrineExtension::composeMiniQuery()   B
last analyzed

Complexity

Conditions 5
Paths 10

Size

Total Lines 44
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 44
rs 8.439
c 0
b 0
f 0
cc 5
eloc 21
nc 10
nop 3
1
<?php
2
3
/*
4
 * This file is part of the Doctrine Bundle
5
 *
6
 * The code was originally distributed inside the Symfony framework.
7
 *
8
 * (c) Fabien Potencier <[email protected]>
9
 * (c) Doctrine Project, Benjamin Eberlei <[email protected]>
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
namespace Saxulum\SaxulumWebProfiler\Twig;
16
17
/**
18
 * This class contains the needed functions in order to do the query highlighting
19
 *
20
 * @author Florin Patan <[email protected]>
21
 * @author Christophe Coevoet <[email protected]>
22
 */
23
class DoctrineExtension extends \Twig_Extension
24
{
25
    /**
26
     * Number of maximum characters that one single line can hold in the interface
27
     *
28
     * @var int
29
     */
30
    private $maxCharWidth = 100;
31
32
    /**
33
     * Define our functions
34
     *
35
     * @return array
36
     */
37
    public function getFilters()
38
    {
39
        return array(
40
            new \Twig_SimpleFilter('doctrine_minify_query', array($this, 'minifyQuery')),
41
            new \Twig_SimpleFilter('doctrine_pretty_query', 'SqlFormatter::format'),
42
            new \Twig_SimpleFilter('doctrine_replace_query_parameters', array($this, 'replaceQueryParameters')),
43
        );
44
    }
45
46
    /**
47
     * Get the possible combinations of elements from the given array
48
     *
49
     * @param array   $elements
50
     * @param integer $combinationsLevel
51
     *
52
     * @return array
53
     */
54
    private function getPossibleCombinations($elements, $combinationsLevel)
55
    {
56
        $baseCount = count($elements);
57
        $result = array();
58
59
        if ($combinationsLevel == 1) {
60
            foreach ($elements as $element) {
61
                $result[] = array($element);
62
            }
63
64
            return $result;
65
        }
66
67
        $nextLevelElements = $this->getPossibleCombinations($elements, $combinationsLevel - 1);
68
69
        foreach ($nextLevelElements as $nextLevelElement) {
70
            $lastElement = $nextLevelElement[$combinationsLevel - 2];
71
            $found = false;
72
73
            foreach ($elements as $key => $element) {
74
                if ($element == $lastElement) {
75
                    $found = true;
76
                    continue;
77
                }
78
79
                if ($found == true && $key < $baseCount) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
80
                    $tmp = $nextLevelElement;
81
                    $newCombination = array_slice($tmp, 0);
82
                    $newCombination[] = $element;
83
                    $result[] = array_slice($newCombination, 0);
84
                }
85
            }
86
        }
87
88
        return $result;
89
    }
90
91
    /**
92
     * Shrink the values of parameters from a combination
93
     *
94
     * @param array $parameters
95
     * @param array $combination
96
     *
97
     * @return string
98
     */
99
    private function shrinkParameters($parameters, $combination)
100
    {
101
        array_shift($parameters);
102
        $result = '';
103
104
        $maxLength = $this->maxCharWidth;
105
        $maxLength -= count($parameters) * 5;
106
        $maxLength = $maxLength / count($parameters);
107
108
        foreach ($parameters as $key => $value) {
109
            $isLarger = false;
110
111
            if (strlen($value) > $maxLength) {
112
                $value = wordwrap($value, $maxLength, "\n", true);
113
                $value = explode("\n", $value);
114
                $value = $value[0];
115
116
                $isLarger = true;
117
            }
118
            $value = self::escapeFunction($value);
119
120
            if (!is_numeric($value)) {
121
                $value = substr($value, 1, -1);
122
            }
123
124
            if ($isLarger) {
125
                $value .= ' [...]';
126
            }
127
128
            $result .= ' ' . $combination[$key] . ' ' . $value;
129
        }
130
131
        return trim($result);
132
    }
133
134
    /**
135
     * Attempt to compose the best scenario minified query so that a user could find it without expanding it
136
     *
137
     * @param string  $query
138
     * @param array   $keywords
139
     * @param integer $required
140
     *
141
     * @return string
142
     */
143
    private function composeMiniQuery($query, $keywords = array(), $required = 1)
144
    {
145
        // Extract the mandatory keywords and consider the rest as optional keywords
146
        $mandatoryKeywords = array_splice($keywords, 0, $required);
147
148
        $combinations = array();
149
        $combinationsCount = count($keywords);
150
151
        // Compute all the possible combinations of keywords to match the query for
152
        while ($combinationsCount > 0) {
153
            $combinations = array_merge($combinations, $this->getPossibleCombinations($keywords, $combinationsCount));
154
            $combinationsCount--;
155
        }
156
157
        // Try and match the best case query pattern
158
        foreach ($combinations as $combination) {
159
            $combination = array_merge($mandatoryKeywords, $combination);
160
161
            $regexp = implode('(.*) ', $combination) . ' (.*)';
162
            $regexp = '/^' . $regexp . '/is';
163
164
            if (preg_match($regexp, $query, $matches)) {
165
166
                $result = $this->shrinkParameters($matches, $combination);
167
168
                return $result;
169
            }
170
        }
171
172
        // Try and match the simplest query form that contains only the mandatory keywords
173
        $regexp = implode(' (.*)', $mandatoryKeywords) . ' (.*)';
174
        $regexp = '/^' . $regexp . '/is';
175
176
        if (preg_match($regexp, $query, $matches)) {
177
            $result = $this->shrinkParameters($matches, $mandatoryKeywords);
178
179
            return $result;
180
        }
181
182
        // Fallback in case we didn't managed to find any good match (can we actually have that happen?!)
183
        $result = substr($query, 0, $this->maxCharWidth);
184
185
        return $result;
186
    }
187
188
    /**
189
     * Minify the query
190
     *
191
     * @param string $query
192
     *
193
     * @return string
194
     */
195
    public function minifyQuery($query)
196
    {
197
        $result = '';
198
        $keywords = array();
199
        $required = 1;
200
201
        // Check if we can match the query against any of the major types
202
        switch (true) {
203 View Code Duplication
            case stripos($query, 'SELECT') !== false:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
204
                $keywords = array('SELECT', 'FROM', 'WHERE', 'HAVING', 'ORDER BY', 'LIMIT');
205
                $required = 2;
206
                break;
207
208 View Code Duplication
            case stripos($query, 'DELETE') !== false :
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
209
                $keywords = array('DELETE', 'FROM', 'WHERE', 'ORDER BY', 'LIMIT');
210
                $required = 2;
211
                break;
212
213 View Code Duplication
            case stripos($query, 'UPDATE') !== false :
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
214
                $keywords = array('UPDATE', 'SET', 'WHERE', 'ORDER BY', 'LIMIT');
215
                $required = 2;
216
                break;
217
218 View Code Duplication
            case stripos($query, 'INSERT') !== false :
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
219
                $keywords = array('INSERT', 'INTO', 'VALUE', 'VALUES');
220
                $required = 2;
221
                break;
222
223
            // If there's no match so far just truncate it to the maximum allowed by the interface
224
            default:
225
                $result = substr($query, 0, $this->maxCharWidth);
226
        }
227
228
        // If we had a match then we should minify it
229
        if ($result == '') {
230
            $result = $this->composeMiniQuery($query, $keywords, $required);
231
        }
232
233
        // Remove unneeded boilerplate HTML
234
        $result = str_replace(array("<pre style='background:white;'", "</pre>"), array("<span", "</span>"), $result);
235
236
        return $result;
237
    }
238
239
    /**
240
     * Escape parameters of a SQL query
241
     * DON'T USE THIS FUNCTION OUTSIDE ITS INTEDED SCOPE
242
     *
243
     * @internal
244
     *
245
     * @param mixed $parameter
246
     *
247
     * @return string
248
     */
249
    public static function escapeFunction($parameter)
250
    {
251
        $result = $parameter;
252
253
        switch (true) {
254
            case is_string($result) :
255
                $result = "'" . addslashes($result) . "'";
256
                break;
257
258
            case is_array($result) :
259
                foreach ($result as &$value) {
260
                    $value = static::escapeFunction($value);
261
                }
262
263
                $result = implode(', ', $result);
264
                break;
265
266
            case is_object($result) :
267
                $result = addslashes((string) $result);
268
                break;
269
        }
270
271
        return $result;
272
    }
273
274
    /**
275
     * Return a query with the parameters replaced
276
     *
277
     * @param string $query
278
     * @param array  $parameters
279
     *
280
     * @return string
281
     */
282
    public function replaceQueryParameters($query, $parameters)
283
    {
284
        $i = 0;
285
286
        $result = preg_replace_callback(
287
            '/\?|(:[a-z0-9_]+)/i',
288
            function ($matches) use ($parameters, &$i) {
289
                $key = substr($matches[0], 1);
290
                if (!isset($parameters[$i]) && !isset($parameters[$key])) {
291
                    return $matches[0];
292
                }
293
294
                $value = isset($parameters[$i]) ? $parameters[$i] : $parameters[$key];
295
                $result = DoctrineExtension::escapeFunction($value);
296
                $i++;
297
298
                return $result;
299
            },
300
            $query
301
        );
302
303
        $result = \SqlFormatter::highlight($result);
304
        $result = str_replace(array("<pre ", "</pre>"), array("<span ", "</span>"), $result);
305
306
        return $result;
307
    }
308
309
    /**
310
     * Get the name of the extension
311
     *
312
     * @return string
313
     */
314
    public function getName()
315
    {
316
        return 'doctrine_extension';
317
    }
318
319
}
320