Test Failed
Push — master ( aa8ed5...6e1e15 )
by Matthew
05:35
created

ColumnExtractionTrait   D

Complexity

Total Complexity 81

Size/Duplication

Total Lines 452
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 81
lcom 1
cbo 5
dl 0
loc 452
rs 4.8717
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A setDebug() 0 4 1
A setAnnotationReader() 0 4 1
A setCacheDir() 0 4 1
getClassMetadata() 0 1 ?
A autoDiscoverColumns() 0 11 2
A getDefaultSort() 0 8 2
B populateAnnotationCacheFilename() 0 31 4
D getAnnotationColumns() 0 31 10
C tryIncludeAnnotationCache() 0 26 8
A includeAnnotationCache() 0 9 2
C populateAndCacheAnnotationColumns() 0 38 7
C readGridAnnotations() 0 72 13
C validateSortInfo() 0 26 8
A extractSortInfo() 0 12 2
C sortGridColumns() 0 33 7
C getReflectionColumns() 0 26 8
A getIdColumn() 0 13 3
A hasIdColumn() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like ColumnExtractionTrait 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 ColumnExtractionTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Dtc\GridBundle\Grid\Source;
4
5
use Doctrine\Common\Annotations\Reader;
6
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
7
use Dtc\GridBundle\Annotation\Action;
8
use Dtc\GridBundle\Annotation\DeleteAction;
9
use Dtc\GridBundle\Annotation\Grid;
10
use Dtc\GridBundle\Annotation\ShowAction;
11
use Dtc\GridBundle\Annotation\Sort;
12
use Dtc\GridBundle\Grid\Column\GridColumn;
13
use Dtc\GridBundle\Util\CamelCaseTrait;
14
15
trait ColumnExtractionTrait
16
{
17
    use CamelCaseTrait;
18
19
    /** @var Reader|null */
20
    protected $reader;
21
22
    /** @var string|null */
23
    protected $cacheDir;
24
25
    /** @var bool */
26
    protected $debug = false;
27
28
    /** @var string */
29
    protected $annotationCacheFilename;
30
31
    /** @var array|null */
32
    protected $annotationColumns;
33
34
    /**
35
     * @var array|null
36
     */
37
    protected $annotationSort;
38
39
    public function setDebug($flag)
40
    {
41
        $this->debug = $flag;
42
    }
43
44
    /**
45
     * @param Reader $reader
46
     */
47
    public function setAnnotationReader(Reader $reader)
48
    {
49
        $this->reader = $reader;
50
    }
51
52
    /**
53
     * @param string|null $cacheDir
54
     */
55
    public function setCacheDir($cacheDir)
56
    {
57
        $this->cacheDir = $cacheDir;
58
    }
59
60
    /**
61
     * @return ClassMetadataInfo|ClassMetadataInfo
62
     */
63
    abstract public function getClassMetadata();
64
65
    public function autoDiscoverColumns()
66
    {
67
        $annotationColumns = $this->getAnnotationColumns();
68
        if ($annotationColumns) {
69
            $this->setColumns($annotationColumns);
0 ignored issues
show
Bug introduced by
It seems like setColumns() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
70
71
            return;
72
        }
73
74
        $this->setColumns($this->getReflectionColumns());
0 ignored issues
show
Bug introduced by
It seems like setColumns() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
75
    }
76
77
    /**
78
     * @return array|null
79
     */
80
    public function getDefaultSort()
81
    {
82
        if (null !== $this->getAnnotationColumns()) {
83
            return $this->annotationSort;
84
        }
85
86
        return null;
87
    }
88
89
    /**
90
     * Populates the filename for the annotationCache.
91
     *
92
     * @return string
93
     *
94
     * @throws \Exception
95
     */
96
    protected function populateAnnotationCacheFilename()
97
    {
98
        if (isset($this->annotationCacheFilename)) {
99
            return $this->annotationCacheFilename;
100
        }
101
        $directory = $this->cacheDir.'/DtcGridBundle';
102
        $metadata = $this->getClassMetadata();
103
        $reflectionClass = $metadata->getReflectionClass();
104
        $name = $reflectionClass->getName();
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
105
        $namespace = $reflectionClass->getNamespaceName();
106
        $namespace = str_replace('\\', DIRECTORY_SEPARATOR, $namespace);
107
        $namespaceDir = $directory.DIRECTORY_SEPARATOR.$namespace;
108
109
        $umask = decoct(umask());
110
        $umask = str_pad($umask, 4, '0', STR_PAD_LEFT);
111
112
        // Is there a better way to do this?
113
        $permissions = '0777';
114
        $permissions[1] = intval($permissions[1]) - intval($umask[1]);
115
        $permissions[2] = intval($permissions[2]) - intval($umask[2]);
116
        $permissions[3] = intval($permissions[3]) - intval($umask[3]);
117
118
        if (!is_dir($namespaceDir) && !mkdir($namespaceDir, octdec($permissions), true)) {
119
            throw new \Exception("Can't create: ".$namespaceDir);
120
        }
121
122
        $name = str_replace('\\', DIRECTORY_SEPARATOR, $name);
123
        $this->annotationCacheFilename = $directory.DIRECTORY_SEPARATOR.$name.'.php';
124
125
        return $this->annotationCacheFilename;
126
    }
127
128
    /**
129
     * Attempt to discover columns using the GridColumn annotation.
130
     *
131
     * @throws \Exception
132
     */
133
    protected function getAnnotationColumns()
134
    {
135
        if (!isset($this->reader)) {
136
            return null;
137
        }
138
139
        if (!isset($this->cacheDir)) {
140
            return null;
141
        }
142
143
        if (!isset($this->annotationCacheFilename)) {
144
            $this->populateAnnotationCacheFilename();
145
        }
146
147
        if (!$this->debug && null !== $this->annotationColumns) {
148
            return $this->annotationColumns ?: null;
149
        }
150
151
        // Check mtime of class
152
        if (is_file($this->annotationCacheFilename)) {
153
            $result = $this->tryIncludeAnnotationCache();
154
            if ($result) {
155
                return $this->annotationColumns;
156
            }
157
        }
158
159
        // cache annotation
160
        $this->populateAndCacheAnnotationColumns();
161
162
        return $this->annotationColumns ?: null;
163
    }
164
165
    /**
166
     * Cached annotation info from the file, if the mtime of the file has not changed (or if not in debug).
167
     *
168
     * @return bool
169
     */
170
    protected function tryIncludeAnnotationCache()
171
    {
172
        if (!$this->debug) {
173
            $this->includeAnnotationCache();
174
175
            return true;
176
        }
177
178
        $metadata = $this->getClassMetadata();
179
        $reflectionClass = $metadata->getReflectionClass();
180
        $filename = $reflectionClass->getFileName();
181
        if ($filename && is_file($filename)) {
182
            $mtime = filemtime($filename);
183
            if (($currentfileMtime = filemtime(__FILE__)) > $mtime) {
184
                $mtime = $currentfileMtime;
185
            }
186
            $mtimeAnnotation = filemtime($this->annotationCacheFilename);
187
            if ($mtime && $mtimeAnnotation && $mtime <= $mtimeAnnotation) {
188
                $this->includeAnnotationCache();
189
190
                return true;
191
            }
192
        }
193
194
        return false;
195
    }
196
197
    /**
198
     * Retrieves the cached annotations from the cache file.
199
     */
200
    protected function includeAnnotationCache()
201
    {
202
        $annotationInfo = include $this->annotationCacheFilename;
203
        $this->annotationColumns = $annotationInfo['columns'];
204
        $this->annotationSort = $annotationInfo['sort'];
205
        if ($this->annotationSort) {
206
            $this->validateSortInfo($this->annotationSort, $this->annotationColumns);
207
        }
208
    }
209
210
    /**
211
     * Caches the annotation columns result into a file.
212
     */
213
    protected function populateAndCacheAnnotationColumns()
214
    {
215
        $gridAnnotations = $this->readGridAnnotations();
216
        $annotationColumns = $gridAnnotations['columns'];
217
218
        $sort = $gridAnnotations['sort'];
219
        if ($annotationColumns) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $annotationColumns of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
220
            $output = "<?php\nreturn array('columns' => array(\n";
221
            foreach ($annotationColumns as $field => $info) {
222
                $class = $info['class'];
223
                $output .= "'$field' => new $class(";
224
                $first = true;
225
                foreach ($info['arguments'] as $argument) {
226
                    if ($first) {
227
                        $first = false;
228
                    } else {
229
                        $output .= ',';
230
                    }
231
                    $output .= var_export($argument, true);
232
                }
233
                $output .= '),';
234
            }
235
            $output .= "), 'sort' => array(";
236
            foreach ($sort as $key => $value) {
237
                $output .= "'$key'".' => ';
238
                if (null === $value) {
239
                    $output .= 'null,';
240
                } else {
241
                    $output .= "'$value',";
242
                }
243
            }
244
            $output .= "));\n";
245
        } else {
246
            $output = "<?php\nreturn false;\n";
247
        }
248
        file_put_contents($this->annotationCacheFilename, $output);
249
        $this->includeAnnotationCache();
250
    }
251
252
    /**
253
     * Generates a list of property name and labels based on finding the GridColumn annotation.
254
     *
255
     * @return array Hash of grid annotation results: ['columns' => array, 'sort' => string]
256
     */
257
    protected function readGridAnnotations()
258
    {
259
        $metadata = $this->getClassMetadata();
260
        $reflectionClass = $metadata->getReflectionClass();
261
        $properties = $reflectionClass->getProperties();
262
263
        /** @var Grid $gridAnnotation */
264
        $sort = null;
265
        if ($gridAnnotation = $this->reader->getClassAnnotation($reflectionClass, 'Dtc\GridBundle\Annotation\Grid')) {
266
            $actions = $gridAnnotation->actions;
267
            $sort = $gridAnnotation->sort;
268
        }
269
270
        $gridColumns = [];
271
        foreach ($properties as $property) {
272
            /** @var \Dtc\GridBundle\Annotation\Column $annotation */
273
            $annotation = $this->reader->getPropertyAnnotation($property, 'Dtc\GridBundle\Annotation\Column');
274
            if ($annotation) {
275
                $name = $property->getName();
276
                $label = $annotation->label ?: $this->fromCamelCase($name);
277
                $gridColumns[$name] = ['class' => '\Dtc\GridBundle\Grid\Column\GridColumn', 'arguments' => [$name, $label]];
278
                $gridColumns[$name]['arguments'][] = null;
279
                if ($annotation->sortable) {
280
                    $gridColumns[$name]['arguments'][] = ['sortable' => true];
281
                } else {
282
                    $gridColumns[$name]['arguments'][] = [];
283
                }
284
                $gridColumns[$name]['arguments'][] = $annotation->searchable;
285
                $gridColumns[$name]['arguments'][] = $annotation->order;
286
            }
287
        }
288
289
        // Fall back to default column list if list is not specified
290
        if (!$gridColumns) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $gridColumns of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
291
            $gridColumnList = $this->getReflectionColumns();
292
            /** @var GridColumn $gridColumn */
293
            foreach ($gridColumnList as $field => $gridColumn) {
294
                $gridColumns[$field] = ['class' => '\Dtc\GridBundle\Grid\Column\GridColumn', 'arguments' => [$field, $gridColumn->getLabel(), null, ['sortable' => true], true, null]];
295
            }
296
        }
297
298
        if (isset($actions)) {
299
            $field = '\$-action';
300
            $actionArgs = [$field];
301
            $actionDefs = [];
302
            /* @var Action $action */
303
            foreach ($actions as $action) {
304
                $actionDef = ['label' => $action->label, 'route' => $action->route];
305
                if ($action instanceof ShowAction) {
306
                    $actionDef['action'] = 'show';
307
                }
308
                if ($action instanceof DeleteAction) {
309
                    $actionDef['action'] = 'delete';
310
                }
311
                $actionDefs[] = $actionDef;
312
            }
313
            $actionArgs[] = $actionDefs;
314
315
            $gridColumns[$field] = ['class' => '\Dtc\GridBundle\Grid\Column\ActionGridColumn',
316
                'arguments' => $actionArgs, ];
317
        }
318
319
        $this->sortGridColumns($gridColumns);
320
        try {
321
            $sortInfo = $this->extractSortInfo($sort);
322
            $this->validateSortInfo($sortInfo, $gridColumns);
323
        } catch (\InvalidArgumentException $exception) {
324
            throw new \InvalidArgumentException($reflectionClass->getName().' - '.$exception->getMessage(), $exception->getCode(), $exception);
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
325
        }
326
327
        return ['columns' => $gridColumns, 'sort' => $sortInfo];
328
    }
329
330
    /**
331
     * @param array $sortInfo
332
     * @param array $gridColumns
333
     *
334
     * @throws \InvalidArgumentException
335
     */
336
    protected function validateSortInfo(array $sortInfo, array $gridColumns)
337
    {
338
        if ($sortInfo['direction']) {
339
            switch ($sortInfo['direction']) {
340
                case 'ASC':
341
                case 'DESC':
342
                    break;
343
                default:
344
                    throw new \InvalidArgumentException("Grid's sort annotation direction '{$sortInfo['direction']}' is invalid");
345
            }
346
        }
347
348
        if (isset($sortInfo['column'])) {
349
            $column = $sortInfo['column'];
350
351
            if (!isset($sortInfo['direction'])) {
352
                throw new \InvalidArgumentException("Grid's sort annotation column '$column' specified but a sort direction was not");
353
            }
354
            foreach (array_keys($gridColumns) as $name) {
355
                if ($name === $column) {
356
                    return;
357
                }
358
            }
359
            throw new \InvalidArgumentException("Grid's sort annotation column '$column' not in list of columns (".implode(', ', array_keys($gridColumns)).')');
360
        }
361
    }
362
363
    /**
364
     * @param Sort|null $sortAnnotation
365
     *
366
     * @return array
367
     */
368
    protected function extractSortInfo($sortAnnotation)
369
    {
370
        $sortInfo = ['direction' => null, 'column' => null];
371
        if ($sortAnnotation) {
372
            $direction = $sortAnnotation->direction;
373
            $sortInfo['direction'] = $direction;
374
            $column = $sortAnnotation->column;
375
            $sortInfo['column'] = $column;
376
        }
377
378
        return $sortInfo;
379
    }
380
381
    protected function sortGridColumns(array &$columnDefs)
382
    {
383
        $unordered = [];
384
        $ordered = [];
385
        foreach ($columnDefs as $name => $columnDef) {
386
            $columnParts = $columnDef['arguments'];
387
            if (!isset($columnParts[5]) || null === $columnParts[5]) {
388
                $unordered[$name] = $columnDef;
389
                continue;
390
            }
391
            $ordered[$name] = $columnDef;
392
        }
393
394
        if (empty($ordered)) {
395
            return;
396
        }
397
398
        uasort($ordered, function ($columnDef1, $columnDef2) {
399
            $columnParts1 = $columnDef1['arguments'];
400
            $columnParts2 = $columnDef2['arguments'];
401
            $order1 = $columnParts1[5];
402
            $order2 = $columnParts2[5];
403
404
            return $order1 > $order2;
405
        });
406
407
        if ($unordered) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $unordered of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
408
            foreach ($unordered as $name => $columnDef) {
409
                $ordered[$name] = $columnDef;
410
            }
411
        }
412
        $columnDefs = $ordered;
413
    }
414
415
    /**
416
     * Generate Columns based on document's Metadata.
417
     */
418
    protected function getReflectionColumns()
419
    {
420
        $metadata = $this->getClassMetadata();
421
        $fields = $metadata->getFieldNames();
422
        $identifier = $metadata->getIdentifier();
423
        $identifier = isset($identifier[0]) ? $identifier[0] : null;
424
425
        $columns = array();
426
        foreach ($fields as $field) {
427
            $mapping = $metadata->getFieldMapping($field);
428
            if (isset($mapping['options']) && isset($mapping['options']['label'])) {
429
                $label = $mapping['options']['label'];
430
            } else {
431
                $label = $this->fromCamelCase($field);
432
            }
433
434
            if ($identifier === $field) {
435
                if (isset($mapping['strategy']) && 'auto' == $mapping['strategy']) {
436
                    continue;
437
                }
438
            }
439
            $columns[$field] = new GridColumn($field, $label);
440
        }
441
442
        return $columns;
443
    }
444
445
    /**
446
     * @return string|null
447
     */
448
    protected function getIdColumn()
449
    {
450
        static $identifier = false;
451
        if (false !== $identifier) {
452
            return $identifier;
453
        }
454
455
        $metadata = $this->getClassMetadata();
456
        $identifier = $metadata->getIdentifier();
457
        $identifier = isset($identifier[0]) ? $identifier[0] : null;
458
459
        return $identifier;
460
    }
461
462
    public function hasIdColumn()
463
    {
464
        return $this->getIdColumn() ? true : false;
465
    }
466
}
467