Passed
Push — master ( 1c101f...c673c5 )
by Matthew
11:42
created

ColumnExtractionTrait   C

Complexity

Total Complexity 79

Size/Duplication

Total Lines 443
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 79
lcom 1
cbo 5
dl 0
loc 443
c 0
b 0
f 0
rs 5.442

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 63 11
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 ($value === null) {
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 proeprty 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
        /* @var Action $action */
290
        if (isset($actions)) {
291
            $field = '\$-action';
292
            $actionArgs = [$field];
293
            $actionDefs = [];
294
            foreach ($actions as $action) {
295
                $actionDef = ['label' => $action->label, 'route' => $action->route];
296
                if ($action instanceof ShowAction) {
297
                    $actionDef['action'] = 'show';
298
                }
299
                if ($action instanceof DeleteAction) {
300
                    $actionDef['action'] = 'delete';
301
                }
302
                $actionDefs[] = $actionDef;
303
            }
304
            $actionArgs[] = $actionDefs;
305
306
            $gridColumns[$field] = ['class' => '\Dtc\GridBundle\Grid\Column\ActionGridColumn',
307
                'arguments' => $actionArgs, ];
308
        }
309
310
        $this->sortGridColumns($gridColumns);
311
        try {
312
            $sortInfo = $this->extractSortInfo($sort);
313
            $this->validateSortInfo($sortInfo, $gridColumns);
314
        } catch (\InvalidArgumentException $exception) {
315
            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...
316
        }
317
318
        return ['columns' => $gridColumns, 'sort' => $sortInfo];
319
    }
320
321
    /**
322
     * @param array $sortInfo
323
     * @param array $gridColumns
324
     *
325
     * @throws \InvalidArgumentException
326
     */
327
    protected function validateSortInfo(array $sortInfo, array $gridColumns)
328
    {
329
        if ($sortInfo['direction']) {
330
            switch ($sortInfo['direction']) {
331
                case 'ASC':
332
                case 'DESC':
333
                    break;
334
                default:
335
                    throw new \InvalidArgumentException("Grid's sort annotation direction '{$sortInfo['direction']}' is invalid");
336
            }
337
        }
338
339
        if (isset($sortInfo['column'])) {
340
            $column = $sortInfo['column'];
341
342
            if (!isset($sortInfo['direction'])) {
343
                throw new \InvalidArgumentException("Grid's sort annotation column '$column' specified but a sort direction was not");
344
            }
345
            foreach (array_keys($gridColumns) as $name) {
346
                if ($name === $column) {
347
                    return;
348
                }
349
            }
350
            throw new \InvalidArgumentException("Grid's sort annotation column '$column' not in list of columns (".implode(', ', $gridColumns).')');
351
        }
352
    }
353
354
    /**
355
     * @param Sort|null $sortAnnotation
356
     *
357
     * @return array
358
     */
359
    protected function extractSortInfo($sortAnnotation)
360
    {
361
        $sortInfo = ['direction' => null, 'column' => null];
362
        if ($sortAnnotation) {
363
            $direction = $sortAnnotation->direction;
364
            $sortInfo['direction'] = $direction;
365
            $column = $sortAnnotation->column;
366
            $sortInfo['column'] = $column;
367
        }
368
369
        return $sortInfo;
370
    }
371
372
    protected function sortGridColumns(array &$columnDefs)
373
    {
374
        $unordered = [];
375
        $ordered = [];
376
        foreach ($columnDefs as $name => $columnDef) {
377
            $columnParts = $columnDef['arguments'];
378
            if (!isset($columnParts[5]) || $columnParts[5] === null) {
379
                $unordered[$name] = $columnDef;
380
                continue;
381
            }
382
            $ordered[$name] = $columnDef;
383
        }
384
385
        if (empty($ordered)) {
386
            return;
387
        }
388
389
        uasort($ordered, function ($columnDef1, $columnDef2) {
390
            $columnParts1 = $columnDef1['arguments'];
391
            $columnParts2 = $columnDef2['arguments'];
392
            $order1 = $columnParts1[5];
393
            $order2 = $columnParts2[5];
394
395
            return $order1 > $order2;
396
        });
397
398
        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...
399
            foreach ($unordered as $name => $columnDef) {
400
                $ordered[$name] = $columnDef;
401
            }
402
        }
403
        $columnDefs = $ordered;
404
    }
405
406
    /**
407
     * Generate Columns based on document's Metadata.
408
     */
409
    protected function getReflectionColumns()
410
    {
411
        $metadata = $this->getClassMetadata();
412
        $fields = $metadata->getFieldNames();
413
        $identifier = $metadata->getIdentifier();
414
        $identifier = isset($identifier[0]) ? $identifier[0] : null;
415
416
        $columns = array();
417
        foreach ($fields as $field) {
418
            $mapping = $metadata->getFieldMapping($field);
419
            if (isset($mapping['options']) && isset($mapping['options']['label'])) {
420
                $label = $mapping['options']['label'];
421
            } else {
422
                $label = $this->fromCamelCase($field);
423
            }
424
425
            if ($identifier === $field) {
426
                if (isset($mapping['strategy']) && 'auto' == $mapping['strategy']) {
427
                    continue;
428
                }
429
            }
430
            $columns[$field] = new GridColumn($field, $label);
431
        }
432
433
        return $columns;
434
    }
435
436
    /**
437
     * @return string|null
438
     */
439
    protected function getIdColumn()
440
    {
441
        static $identifier = false;
442
        if (false !== $identifier) {
443
            return $identifier;
444
        }
445
446
        $metadata = $this->getClassMetadata();
447
        $identifier = $metadata->getIdentifier();
448
        $identifier = isset($identifier[0]) ? $identifier[0] : null;
449
450
        return $identifier;
451
    }
452
453
    public function hasIdColumn()
454
    {
455
        return $this->getIdColumn() ? true : false;
456
    }
457
}
458