Completed
Push — master ( 99165b...3b1bd1 )
by Mike
02:14
created

DoctrineExtension::escapeFunction()   D

Complexity

Conditions 10
Paths 8

Size

Total Lines 37
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 37
rs 4.8196
cc 10
eloc 24
nc 8
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
/**
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 \Twig_SimpleFilter[]
36
     */
37
    public function getFilters()
38
    {
39
        return array(
40
            new \Twig_SimpleFilter('doctrine_minify_query', array($this, 'minifyQuery'), array('deprecated' => true)),
41
            new \Twig_SimpleFilter('doctrine_pretty_query', array($this, 'formatQuery'), array('is_safe' => array('html'))),
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(array $elements, $combinationsLevel)
55
    {
56
        $baseCount = count($elements);
57
        $result = array();
58
59
        if (1 === $combinationsLevel) {
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(array $parameters, array $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, array $keywords, $required)
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
                $result = $this->shrinkParameters($matches, $combination);
166
167
                return $result;
168
            }
169
        }
170
171
        // Try and match the simplest query form that contains only the mandatory keywords
172
        $regexp = implode(' (.*)', $mandatoryKeywords).' (.*)';
173
        $regexp = '/^'.$regexp.'/is';
174
175
        if (preg_match($regexp, $query, $matches)) {
176
            $result = $this->shrinkParameters($matches, $mandatoryKeywords);
177
178
            return $result;
179
        }
180
181
        // Fallback in case we didn't managed to find any good match (can we actually have that happen?!)
182
        $result = substr($query, 0, $this->maxCharWidth);
183
184
        return $result;
185
    }
186
187
    /**
188
     * Minify the query
189
     *
190
     * @param string $query
191
     *
192
     * @return string
193
     */
194
    public function minifyQuery($query)
195
    {
196
        $result = '';
197
        $keywords = array();
198
        $required = 1;
199
200
        // Check if we can match the query against any of the major types
201
        switch (true) {
202 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...
203
                $keywords = array('SELECT', 'FROM', 'WHERE', 'HAVING', 'ORDER BY', 'LIMIT');
204
                $required = 2;
205
                break;
206
207 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...
208
                $keywords = array('DELETE', 'FROM', 'WHERE', 'ORDER BY', 'LIMIT');
209
                $required = 2;
210
                break;
211
212 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...
213
                $keywords = array('UPDATE', 'SET', 'WHERE', 'ORDER BY', 'LIMIT');
214
                $required = 2;
215
                break;
216
217 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...
218
                $keywords = array('INSERT', 'INTO', 'VALUE', 'VALUES');
219
                $required = 2;
220
                break;
221
222
            // If there's no match so far just truncate it to the maximum allowed by the interface
223
            default:
224
                $result = substr($query, 0, $this->maxCharWidth);
225
        }
226
227
        // If we had a match then we should minify it
228
        if ($result == '') {
229
            $result = $this->composeMiniQuery($query, $keywords, $required);
230
        }
231
232
        return $result;
233
    }
234
235
    /**
236
     * Escape parameters of a SQL query
237
     * DON'T USE THIS FUNCTION OUTSIDE ITS INTENDED SCOPE
238
     *
239
     * @internal
240
     *
241
     * @param mixed $parameter
242
     *
243
     * @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...
244
     */
245
    public static function escapeFunction($parameter)
246
    {
247
        $result = $parameter;
248
249
        switch (true) {
250
            // Check if result is non-unicode string using PCRE_UTF8 modifier
251
            case is_string($result) && !preg_match('//u', $result):
252
                $result = '0x'. strtoupper(bin2hex($result));
253
                break;
254
255
            case is_string($result):
256
                $result = "'".addslashes($result)."'";
257
                break;
258
259
            case is_array($result):
260
                foreach ($result as &$value) {
261
                    $value = static::escapeFunction($value);
262
                }
263
264
                $result = implode(', ', $result);
265
                break;
266
267
            case is_object($result):
268
                $result = addslashes((string) $result);
269
                break;
270
271
            case null === $result:
272
                $result = 'NULL';
273
                break;
274
275
            case is_bool($result):
276
                $result = $result ? '1' : '0';
277
                break;
278
        }
279
280
        return $result;
281
    }
282
283
    /**
284
     * Return a query with the parameters replaced
285
     *
286
     * @param string $query
287
     * @param array  $parameters
288
     *
289
     * @return string
290
     */
291
    public function replaceQueryParameters($query, array $parameters)
292
    {
293
        $i = 0;
294
295
        if (!array_key_exists(0, $parameters) && array_key_exists(1, $parameters)) {
296
            $i = 1;
297
        }
298
299
        $result = preg_replace_callback(
300
            '/\?|((?<!:):[a-z0-9_]+)/i',
301
            function ($matches) use ($parameters, &$i) {
302
                $key = substr($matches[0], 1);
303
                if (!array_key_exists($i, $parameters) && (false === $key || !array_key_exists($key, $parameters))) {
304
                    return $matches[0];
305
                }
306
307
                $value = array_key_exists($i, $parameters) ? $parameters[$i] : $parameters[$key];
308
                $result = DoctrineExtension::escapeFunction($value);
309
                $i++;
310
311
                return $result;
312
            },
313
            $query
314
        );
315
316
        return $result;
317
    }
318
319
    /**
320
     * Formats and/or highlights the given SQL statement.
321
     *
322
     * @param  string $sql
323
     * @param  bool   $highlightOnly If true the query is not formatted, just highlighted
324
     *
325
     * @return string
326
     */
327
    public function formatQuery($sql, $highlightOnly = false)
328
    {
329
        \SqlFormatter::$pre_attributes = 'class="highlight highlight-sql"';
330
        \SqlFormatter::$quote_attributes = 'class="string"';
331
        \SqlFormatter::$backtick_quote_attributes = 'class="string"';
332
        \SqlFormatter::$reserved_attributes = 'class="keyword"';
333
        \SqlFormatter::$boundary_attributes = 'class="symbol"';
334
        \SqlFormatter::$number_attributes = 'class="number"';
335
        \SqlFormatter::$word_attributes = 'class="word"';
336
        \SqlFormatter::$error_attributes = 'class="error"';
337
        \SqlFormatter::$comment_attributes = 'class="comment"';
338
        \SqlFormatter::$variable_attributes = 'class="variable"';
339
340
        if ($highlightOnly) {
341
            $html = \SqlFormatter::highlight($sql);
342
            $html = preg_replace('/<pre class=".*">([^"]*+)<\/pre>/Us', '\1', $html);
343
        } else {
344
            $html = \SqlFormatter::format($sql);
345
            $html = preg_replace('/<pre class="(.*)">([^"]*+)<\/pre>/Us', '<div class="\1"><pre>\2</pre></div>', $html);
346
        }
347
348
        return $html;
349
    }
350
351
    /**
352
     * Get the name of the extension
353
     *
354
     * @return string
355
     */
356
    public function getName()
357
    {
358
        return 'doctrine_extension';
359
    }
360
}
361