Test Failed
Pull Request — master (#4)
by
unknown
11:18
created

Grammar   C

Complexity

Total Complexity 62

Size/Duplication

Total Lines 466
Duplicated Lines 3.86 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
dl 18
loc 466
rs 5.9493
c 0
b 0
f 0
wmc 62
lcom 1
cbo 3

29 Methods

Rating   Name   Duplication   Size   Complexity  
A compileInsertGetId() 0 4 1
B compileInsert() 0 32 5
B wrapColumn() 0 24 6
A wrapTable() 0 4 1
A columnize() 0 18 4
B compileUpdate() 0 30 2
A compileJoins() 0 9 1
A compileDelete() 0 10 2
B compileSelect() 0 32 4
B compileComponents() 0 21 5
A compileFrom() 0 4 1
A compileColumns() 0 9 3
A compileAggregateExtended() 0 4 1
A compileWheres() 0 18 3
A concatenateWhereClauses() 0 4 1
A compileWheresToArray() 0 6 1
A whereBasic() 0 4 1
A whereIn() 9 9 2
A whereNotIn() 9 9 2
A whereNull() 0 4 1
A whereNotNull() 0 4 1
A whereColumn() 0 6 1
A compileOrders() 0 8 2
A compileOrdersToArray() 0 8 2
A compileLimit() 0 11 2
A compileOffset() 0 7 2
A wrapCollection() 0 3 1
A getClearColumnName() 0 9 2
A getAliasNameFromColumn() 0 8 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Grammar often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Grammar, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: admin
5
 * Date: 21.01.2018
6
 * Time: 14:35
7
 */
8
9
namespace sonrac\Arango\Query\Grammars;
10
11
use Illuminate\Database\Query\Builder;
12
use \Illuminate\Database\Query\Grammars\Grammar as IlluminateGrammar;
13
use function sonrac\Arango\Helpers\getEntityName;
14
use function sonrac\Arango\Helpers\getEntityNameFromColumn;
15
16
class Grammar extends IlluminateGrammar
17
{
18
    /**
19
     * The components that make up a select clause.
20
     *
21
     * @var array
22
     */
23
    protected $selectComponents = [
24
        'aggregate',
25
        'from',
26
        'joins',
27
        'wheres',
28
        'groups',
29
        'havings',
30
        'orders',
31
        'limit',
32
        'offset',
33
        'columns',
34
        'unions',
35
        'lock',
36
    ];
37
38
    /**
39
     * @inheritdoc
40
     */
41
    public function compileInsertGetId(Builder $query, $values, $sequence)
42
    {
43
        return $this->compileInsert($query, $values) . ' RETURN NEW';
44
    }
45
46
    /**
47
     * @inheritdoc
48
     */
49
    public function compileInsert(Builder $query, array $values)
50
    {
51
        // Essentially we will force every insert to be treated as a batch insert which
52
        // simply makes creating the SQL easier for us since we can utilize the same
53
        // basic routine regardless of an amount of records given to us to insert.
54
        $collection = $this->wrapTable($query->from);
55
56
        $entityName = getEntityName($query->from);
57
58
        if (! is_array(reset($values))) {
59
            $values = [$values];
60
        }
61
62
        $columns = array_keys(reset($values));
63
64
        $parameters = [];
65
66
        foreach ($values as $record){
67
            $bindValuesTmp = [];
68
            foreach ($columns as $column){
69
                if(!isset($record[$column])) continue;
70
                $bindValuesTmp[$column] = $record[$column];
71
            }
72
            $parameters[] =  $bindValuesTmp;
73
        }
74
        $parameters = json_encode($parameters);
75
        $parameters = preg_replace('/"(\@B\w+)"/', '$1', $parameters);
76
77
        $aql =  "FOR ".$entityName." IN ".$parameters." INSERT ".$entityName." INTO ".$collection;
78
        //var_dump($aql);
79
        return $aql;
80
    }
81
82
    /**
83
     * Prepare column before use in AQL request
84
     * Add entityName and wrap it if needed.
85
     * Add alias for string like (column as other_name)
86
     *
87
     * @param $collection
88
     * @param $column
89
     * @param bool $withCollection
90
     * @return string
91
     */
92
    public function wrapColumn($column, $collection = null, $withCollection = true){
93
94
        $entityName = getEntityNameFromColumn($column);
95
96
        $clearColumn = $this->getClearColumnName($column);
97
98
        $alias = $this->getAliasNameFromColumn($column);
99
100
        if(is_null($entityName) && !is_null($collection)){
101
            $entityName =  getEntityName($collection);
102
        }
103
104
        if($clearColumn !== '_key'){
105
            $clearColumn = trim($clearColumn, '`');
106
            $clearColumn = '`'.$clearColumn.'`';
107
        }
108
        if($withCollection){
109
            $column = $entityName.'.'.$clearColumn;
110
        }
111
        if($alias){
0 ignored issues
show
Bug Best Practice introduced by
The expression $alias of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
112
            $column = $alias.':'.$column;
113
        }
114
        return $column;
115
    }
116
117
    /**
118
     * Return collection name
119
     * @return string
120
     */
121
    public function wrapTable($table)
122
    {
123
        return $this->wrapCollection($table);
124
    }
125
126
    /**
127
     * @inheritdoc
128
     */
129
    function columnize(array $columns)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
130
    {
131
        $resultColumns = [];
132
        foreach ($columns as $column){
133
            if(strpos($column, ':') !== false){
134
                $resultColumns[] = $column;
135
                continue;
136
            }
137
138
            list($entityName, $column) = explode(".", $column);
139
            if($column === '`*`'){
140
                $resultColumns[] = $entityName . ': '.$entityName;
141
                continue;
142
            }
143
            $resultColumns[] =  $column . ': ' . $entityName.'.'.$column;
144
        }
145
        return implode(',', $resultColumns);
146
    }
147
148
    /**
149
     * @inheritdoc
150
     */
151
    public function compileUpdate(Builder $query, $values)
152
    {
153
        $table = $this->wrapTable($query->from);
154
155
        // Each one of the columns in the update statements needs to be wrapped in the
156
        // keyword identifiers, also a place-holder needs to be created for each of
157
        // the values in the list of bindings so we can make the sets statements.
158
        $columns = collect($values)->map(function ($value, $key) {
159
            return $key.' : '.$value;
160
        })->implode(', ');
161
162
        $joins = '';
163
164
        if (isset($query->joins)) {
165
            $joins = $this->compileJoins($query, $query->joins).' ';
166
        }
167
168
        // Of course, update queries may also be constrained by where clauses so we'll
169
        // need to compile the where clauses and attach it to the query so only the
170
        // intended records are updated by the SQL statements we generate to run.
171
        $wheres = $this->compileWheres($query);
172
173
        $entityName = getEntityName($table);
174
175
        $aql = $joins."FOR ".$entityName." IN ".$table." ".$wheres.
176
            " UPDATE ".$entityName." WITH { ".$columns." } IN ".$table;
177
178
        var_dump($aql);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($aql); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
179
        return $aql;
180
    }
181
182
    /**
183
     * @inheritdoc
184
     */
185
    protected function compileJoins(Builder $query, $joins)
186
    {
187
188
        return collect($joins)->map(function ($join) use(&$aql) {
189
            $table = $this->wrapTable($join->table);
190
            $entityName = getEntityName($join->table);
191
            return 'FOR '.$entityName.' IN '.$table;
192
        })->implode(' ');
193
    }
194
195
    /**
196
     * @inheritdoc
197
     */
198
    public function compileDelete(Builder $query)
199
    {
200
        $wheres = is_array($query->wheres) ? $this->compileWheres($query) : '';
201
202
        $collection = $this->wrapTable($query->from);
203
        $entityName = getEntityName($collection);
204
        $aql = "FOR {$entityName} in {$collection} {$wheres} REMOVE {$entityName} IN {$collection}";
205
        var_dump($aql);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($aql); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
206
        return $aql;
207
    }
208
209
    /**
210
     * @inheritdoc
211
     */
212
    public function compileSelect(Builder $query)
213
    {
214
        // If the query does not have any columns set, we'll set the columns to the
215
        // * character to just get all of the columns from the database. Then we
216
        // can build the query and concatenate all the pieces together as one.
217
        $original = $query->columns;
218
219
        if (is_null($query->columns)) {
220
            $query->columns = ['*'];
221
        }
222
223
        // To compile the query, we'll spin through each component of the query and
224
        // see if that component exists. If it does we'll just call the compiler
225
        // function for the component which is responsible for making the AQL.
226
        $aql = trim($this->concatenate(
227
            $this->compileComponents($query))
228
        );
229
230
        if (isset($query->joins)) {
231
            $aql = $this->compileJoins($query, $query->joins).' '.$aql;
232
        }
233
234
235
236
237
        if(!is_null($query->aggregate)){
238
            $aql = $this->compileAggregateExtended($query, $query->aggregate, $aql);
239
        }
240
        $query->columns = $original;
241
242
        return $aql;
243
    }
244
245
    /**
246
     * @inheritdoc
247
     */
248
    protected function compileComponents(Builder $query)
249
    {
250
        $aql = [];
251
252
        foreach ($this->selectComponents as $component) {
253
            // To compile the query, we'll spin through each component of the query and
254
            // see if that component exists. If it does we'll just call the compiler
255
            // function for the component which is responsible for making the SQL.
256
            if (! is_null($query->$component)) {
257
                if($component === 'aggregate' ||
258
                   $component === 'joins'){
259
                    continue;
260
                }
261
                $method = 'compile'.ucfirst($component);
262
263
                $aql[$component] = $this->$method($query, $query->$component);
264
            }
265
        }
266
267
        return $aql;
268
    }
269
270
    /**
271
     * @inheritdoc
272
     */
273
    protected function compileFrom(Builder $query, $collection)
274
    {
275
        return 'FOR '.getEntityName($collection).' IN '.$this->wrapCollection($collection);
276
    }
277
278
    /**
279
     * @inheritdoc
280
     */
281
    protected function compileColumns(Builder $query, $columns)
282
    {
283
        if(count($columns) === 1 && $columns[0] === "*"){
284
            return 'RETURN '.getEntityName($query->from);
285
        }
286
287
288
        return "RETURN { " . $this->columnize($columns) . " }";
289
    }
290
291
    /**
292
     * Return string for aggregate some column from AQL request
293
     *
294
     * @param Builder $query
295
     * @param $aggregate
296
     * @param $aql
297
     * @return string
298
     */
299
    protected function compileAggregateExtended(Builder $query, $aggregate, $aql)
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
300
    {
301
        return "RETURN {\"aggregate\":".$aggregate['function']."(".$aql.")}";
302
    }
303
304
    /**
305
     * @inheritdoc
306
     */
307
    protected function compileWheres(Builder $query)
308
    {
309
        // Each type of where clauses has its own compiler function which is responsible
310
        // for actually creating the where clauses SQL. This helps keep the code nice
311
        // and maintainable since each clause has a very small method that it uses.
312
        if (is_null($query->wheres)) {
313
            return '';
314
        }
315
316
        // If we actually have some where clauses, we will strip off the first boolean
317
        // operator, which is added by the query builders for convenience so we can
318
        // avoid checking for the first clauses in each of the compilers methods.
319
        if (count($sql = $this->compileWheresToArray($query)) > 0) {
320
            return $this->concatenateWhereClauses($query, $sql);
321
        }
322
323
        return '';
324
    }
325
326
    /**
327
     * @inheritdoc
328
     */
329
    protected function concatenateWhereClauses($query, $sql)
330
    {
331
        return 'FILTER '.$this->removeLeadingBoolean(implode(' ', $sql));
332
    }
333
334
    /**
335
     * @inheritdoc
336
     */
337
    protected function compileWheresToArray($query)
338
    {
339
        return collect($query->wheres)->map(function ($where) use ($query) {
340
            return $where['boolean'].' '.$this->{"where{$where['type']}"}($query, $where);
341
        })->all();
342
    }
343
344
    /**
345
     * @inheritdoc
346
     */
347
    protected function whereBasic(Builder $query, $where)
348
    {
349
        return $where['column'].' '.$where['operator'].' '.$where['value'];
350
    }
351
352
    /**
353
     * @inheritdoc
354
     */
355 View Code Duplication
    protected function whereIn(Builder $query, $where)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
356
    {
357
        if (! empty($where['values'])) {
358
            $column = $this->wrapColumn($where['column'], $query->from);
359
            return '['.implode(",", $where['values']).'] ANY == '.$column;
360
        }
361
362
        return '0 = 1';
363
    }
364
365
    /**
366
     * @inheritdoc
367
     */
368 View Code Duplication
    protected function whereNotIn(Builder $query, $where)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
369
    {
370
        if (! empty($where['values'])) {
371
            $column = $this->wrapColumn($where['table'],$where['column']);
372
            return '['.implode(",", $where['values']).'] NONE == '.$column;
373
        }
374
375
        return '0 = 1';
376
    }
377
378
    /**
379
     * @inheritdoc
380
     */
381
    protected function whereNull(Builder $query, $where)
382
    {
383
        return $this->wrapColumn($where['column'], $query->from).' == NULL';
384
    }
385
386
    /**
387
     * @inheritdoc
388
     */
389
    protected function whereNotNull(Builder $query, $where)
390
    {
391
        return $this->wrapColumn($where['column'], $query->from).' != NULL';
392
    }
393
394
    /**
395
     * @inheritdoc
396
     */
397
    protected function whereColumn(Builder $query, $where)
398
    {
399
        $firstWrapColumn = $this->wrapColumn($where['first'], $query->from);
400
        $secondWrapColumn = $this->wrapColumn($where['second'], $query->from);
401
        return $firstWrapColumn.' '.$where['operator'].' '.$secondWrapColumn;
402
    }
403
404
    /**
405
     * @inheritdoc
406
     */
407
    protected function compileOrders(Builder $query, $orders)
408
    {
409
        if (! empty($orders)) {
410
            return 'SORT '.implode(', ', $this->compileOrdersToArray($query, $orders));
411
        }
412
413
        return '';
414
    }
415
416
    /**
417
     * @inheritdoc
418
     */
419
    protected function compileOrdersToArray(Builder $query, $orders)
420
    {
421
        return array_map(function ($order) {
422
            return ! isset($order['sql'])
423
                ? $order['column'].' '.$order['direction']
424
                : $order['sql'];
425
        }, $orders);
426
    }
427
428
    /**
429
     * @inheritdoc
430
     */
431
    protected function compileLimit(Builder $query, $limit)
432
    {
433
        $result = 'LIMIT ';
434
435
        if(isset($query->offset)){
436
            $result .= (int)$query->offset.', ';
437
        }
438
        $result .= (int)$limit;
439
440
        return $result;
441
    }
442
443
    /**
444
     * @inheritdoc
445
     */
446
    protected function compileOffset(Builder $query, $offset)
447
    {
448
        if(!isset($query->limit)){
449
            throw new \Exception("You can't set offset without limit for arangodb");
450
        }
451
        return '';
452
    }
453
454
    /**
455
     * Wrap collection name
456
     * @param string $collection
457
     * @return string
458
     */
459
    protected function wrapCollection($collection){
460
        return "`".trim($collection,'`')."`";
461
    }
462
463
    protected function getClearColumnName($column){
464
        $parts = explode('.', $column);
465
        if(count($parts) > 1){
466
            $column = $parts[1];
467
        }
468
        $column = explode('as', $column)[0];
469
470
        return trim($column, '` ');
471
    }
472
473
    protected function getAliasNameFromColumn($column){
474
        $parts = explode('as', $column);
475
        if(count($parts) < 2){
476
            return null;
477
        }
478
479
        return '`'.trim($parts[1], '` ').'`';
480
    }
481
}