Completed
Push — master ( 81250e...eb6e4f )
by Mike
02:26
created

DoctrineExtension::minifyQuery()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 40
Code Lines 26

Duplication

Lines 16
Ratio 40 %

Importance

Changes 0
Metric Value
dl 16
loc 40
rs 8.439
c 0
b 0
f 0
cc 6
eloc 26
nc 10
nop 1
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 Doctrine\Bundle\DoctrineBundle\Twig;
16
17
use Symfony\Component\VarDumper\Cloner\Data;
18
19
/**
20
 * This class contains the needed functions in order to do the query highlighting
21
 *
22
 * @author Florin Patan <[email protected]>
23
 * @author Christophe Coevoet <[email protected]>
24
 */
25
class DoctrineExtension extends \Twig_Extension
26
{
27
    /**
28
     * Number of maximum characters that one single line can hold in the interface
29
     *
30
     * @var int
31
     */
32
    private $maxCharWidth = 100;
33
34
    /**
35
     * Define our functions
36
     *
37
     * @return \Twig_SimpleFilter[]
38
     */
39
    public function getFilters()
40
    {
41
        return array(
42
            new \Twig_SimpleFilter('doctrine_minify_query', array($this, 'minifyQuery'), array('deprecated' => true)),
43
            new \Twig_SimpleFilter('doctrine_pretty_query', array($this, 'formatQuery'), array('is_safe' => array('html'))),
44
            new \Twig_SimpleFilter('doctrine_replace_query_parameters', array($this, 'replaceQueryParameters')),
45
        );
46
    }
47
48
    /**
49
     * Get the possible combinations of elements from the given array
50
     *
51
     * @param array   $elements
52
     * @param integer $combinationsLevel
53
     *
54
     * @return array
55
     */
56
    private function getPossibleCombinations(array $elements, $combinationsLevel)
57
    {
58
        $baseCount = count($elements);
59
        $result = array();
60
61
        if (1 === $combinationsLevel) {
62
            foreach ($elements as $element) {
63
                $result[] = array($element);
64
            }
65
66
            return $result;
67
        }
68
69
        $nextLevelElements = $this->getPossibleCombinations($elements, $combinationsLevel - 1);
70
71
        foreach ($nextLevelElements as $nextLevelElement) {
72
            $lastElement = $nextLevelElement[$combinationsLevel - 2];
73
            $found = false;
74
75
            foreach ($elements as $key => $element) {
76
                if ($element == $lastElement) {
77
                    $found = true;
78
                    continue;
79
                }
80
81
                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...
82
                    $tmp = $nextLevelElement;
83
                    $newCombination = array_slice($tmp, 0);
84
                    $newCombination[] = $element;
85
                    $result[] = array_slice($newCombination, 0);
86
                }
87
            }
88
        }
89
90
        return $result;
91
    }
92
93
    /**
94
     * Shrink the values of parameters from a combination
95
     *
96
     * @param array $parameters
97
     * @param array $combination
98
     *
99
     * @return string
100
     */
101
    private function shrinkParameters(array $parameters, array $combination)
102
    {
103
        array_shift($parameters);
104
        $result = '';
105
106
        $maxLength = $this->maxCharWidth;
107
        $maxLength -= count($parameters) * 5;
108
        $maxLength = $maxLength / count($parameters);
109
110
        foreach ($parameters as $key => $value) {
111
            $isLarger = false;
112
113
            if (strlen($value) > $maxLength) {
114
                $value = wordwrap($value, $maxLength, "\n", true);
115
                $value = explode("\n", $value);
116
                $value = $value[0];
117
118
                $isLarger = true;
119
            }
120
            $value = self::escapeFunction($value);
121
122
            if (!is_numeric($value)) {
123
                $value = substr($value, 1, -1);
124
            }
125
126
            if ($isLarger) {
127
                $value .= ' [...]';
128
            }
129
130
            $result .= ' '.$combination[$key].' '.$value;
131
        }
132
133
        return trim($result);
134
    }
135
136
    /**
137
     * Attempt to compose the best scenario minified query so that a user could find it without expanding it
138
     *
139
     * @param string  $query
140
     * @param array   $keywords
141
     * @param integer $required
142
     *
143
     * @return string
144
     */
145
    private function composeMiniQuery($query, array $keywords, $required)
146
    {
147
        // Extract the mandatory keywords and consider the rest as optional keywords
148
        $mandatoryKeywords = array_splice($keywords, 0, $required);
149
150
        $combinations = array();
151
        $combinationsCount = count($keywords);
152
153
        // Compute all the possible combinations of keywords to match the query for
154
        while ($combinationsCount > 0) {
155
            $combinations = array_merge($combinations, $this->getPossibleCombinations($keywords, $combinationsCount));
156
            $combinationsCount--;
157
        }
158
159
        // Try and match the best case query pattern
160
        foreach ($combinations as $combination) {
161
            $combination = array_merge($mandatoryKeywords, $combination);
162
163
            $regexp = implode('(.*) ', $combination).' (.*)';
164
            $regexp = '/^'.$regexp.'/is';
165
166
            if (preg_match($regexp, $query, $matches)) {
167
                $result = $this->shrinkParameters($matches, $combination);
168
169
                return $result;
170
            }
171
        }
172
173
        // Try and match the simplest query form that contains only the mandatory keywords
174
        $regexp = implode(' (.*)', $mandatoryKeywords).' (.*)';
175
        $regexp = '/^'.$regexp.'/is';
176
177
        if (preg_match($regexp, $query, $matches)) {
178
            $result = $this->shrinkParameters($matches, $mandatoryKeywords);
179
180
            return $result;
181
        }
182
183
        // Fallback in case we didn't managed to find any good match (can we actually have that happen?!)
184
        $result = substr($query, 0, $this->maxCharWidth);
185
186
        return $result;
187
    }
188
189
    /**
190
     * Minify the query
191
     *
192
     * @param string $query
193
     *
194
     * @return string
195
     */
196
    public function minifyQuery($query)
197
    {
198
        $result = '';
199
        $keywords = array();
200
        $required = 1;
201
202
        // Check if we can match the query against any of the major types
203
        switch (true) {
204 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...
205
                $keywords = array('SELECT', 'FROM', 'WHERE', 'HAVING', 'ORDER BY', 'LIMIT');
206
                $required = 2;
207
                break;
208
209 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...
210
                $keywords = array('DELETE', 'FROM', 'WHERE', 'ORDER BY', 'LIMIT');
211
                $required = 2;
212
                break;
213
214 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...
215
                $keywords = array('UPDATE', 'SET', 'WHERE', 'ORDER BY', 'LIMIT');
216
                $required = 2;
217
                break;
218
219 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...
220
                $keywords = array('INSERT', 'INTO', 'VALUE', 'VALUES');
221
                $required = 2;
222
                break;
223
224
            // If there's no match so far just truncate it to the maximum allowed by the interface
225
            default:
226
                $result = substr($query, 0, $this->maxCharWidth);
227
        }
228
229
        // If we had a match then we should minify it
230
        if ($result == '') {
231
            $result = $this->composeMiniQuery($query, $keywords, $required);
232
        }
233
234
        return $result;
235
    }
236
237
    /**
238
     * Escape parameters of a SQL query
239
     * DON'T USE THIS FUNCTION OUTSIDE ITS INTENDED SCOPE
240
     *
241
     * @internal
242
     *
243
     * @param mixed $parameter
244
     *
245
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|double|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
246
     */
247
    public static function escapeFunction($parameter)
248
    {
249
        $result = $parameter;
250
251
        switch (true) {
252
            // Check if result is non-unicode string using PCRE_UTF8 modifier
253
            case is_string($result) && !preg_match('//u', $result):
254
                $result = '0x'. strtoupper(bin2hex($result));
255
                break;
256
257
            case is_string($result):
258
                $result = "'".addslashes($result)."'";
259
                break;
260
261
            case is_array($result):
262
                foreach ($result as &$value) {
263
                    $value = static::escapeFunction($value);
264
                }
265
266
                $result = implode(', ', $result);
267
                break;
268
269
            case is_object($result):
270
                $result = addslashes((string) $result);
271
                break;
272
273
            case null === $result:
274
                $result = 'NULL';
275
                break;
276
277
            case is_bool($result):
278
                $result = $result ? '1' : '0';
279
                break;
280
        }
281
282
        return $result;
283
    }
284
285
    /**
286
     * Return a query with the parameters replaced
287
     *
288
     * @param string      $query
289
     * @param array|Data  $parameters
290
     *
291
     * @return string
292
     */
293
    public function replaceQueryParameters($query, $parameters)
294
    {
295
        if ($parameters instanceof Data) {
296
            // VarDumper < 3.3 compatibility layer
297
            $parameters = method_exists($parameters, 'getValue') ? $parameters->getValue(true) : $parameters->getRawData();
0 ignored issues
show
Deprecated Code introduced by
The method Symfony\Component\VarDum...oner\Data::getRawData() has been deprecated with message: since version 3.3. Use array or object access instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
298
        }
299
300
        $i = 0;
301
302
        if (!array_key_exists(0, $parameters) && array_key_exists(1, $parameters)) {
303
            $i = 1;
304
        }
305
306
        $result = preg_replace_callback(
307
            '/\?|((?<!:):[a-z0-9_]+)/i',
308
            function ($matches) use ($parameters, &$i) {
309
                $key = substr($matches[0], 1);
310
                if (!array_key_exists($i, $parameters) && (false === $key || !array_key_exists($key, $parameters))) {
311
                    return $matches[0];
312
                }
313
314
                $value = array_key_exists($i, $parameters) ? $parameters[$i] : $parameters[$key];
315
                $result = DoctrineExtension::escapeFunction($value);
316
                $i++;
317
318
                return $result;
319
            },
320
            $query
321
        );
322
323
        return $result;
324
    }
325
326
    /**
327
     * Formats and/or highlights the given SQL statement.
328
     *
329
     * @param  string $sql
330
     * @param  bool   $highlightOnly If true the query is not formatted, just highlighted
331
     *
332
     * @return string
333
     */
334
    public function formatQuery($sql, $highlightOnly = false)
335
    {
336
        \SqlFormatter::$pre_attributes = 'class="highlight highlight-sql"';
337
        \SqlFormatter::$quote_attributes = 'class="string"';
338
        \SqlFormatter::$backtick_quote_attributes = 'class="string"';
339
        \SqlFormatter::$reserved_attributes = 'class="keyword"';
340
        \SqlFormatter::$boundary_attributes = 'class="symbol"';
341
        \SqlFormatter::$number_attributes = 'class="number"';
342
        \SqlFormatter::$word_attributes = 'class="word"';
343
        \SqlFormatter::$error_attributes = 'class="error"';
344
        \SqlFormatter::$comment_attributes = 'class="comment"';
345
        \SqlFormatter::$variable_attributes = 'class="variable"';
346
347
        if ($highlightOnly) {
348
            $html = \SqlFormatter::highlight($sql);
349
            $html = preg_replace('/<pre class=".*">([^"]*+)<\/pre>/Us', '\1', $html);
350
        } else {
351
            $html = \SqlFormatter::format($sql);
352
            $html = preg_replace('/<pre class="(.*)">([^"]*+)<\/pre>/Us', '<div class="\1"><pre>\2</pre></div>', $html);
353
        }
354
355
        return $html;
356
    }
357
358
    /**
359
     * Get the name of the extension
360
     *
361
     * @return string
362
     */
363
    public function getName()
364
    {
365
        return 'doctrine_extension';
366
    }
367
}
368