Passed
Push — master ( 1316c5...63993c )
by Matthew
05:18
created

ColumnSource::getColumnSourceInfo()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 21
c 1
b 0
f 0
dl 0
loc 34
rs 8.6506
cc 7
nc 16
nop 4
1
<?php
2
3
namespace Dtc\GridBundle\Grid\Source;
4
5
use Doctrine\Common\Annotations\Reader;
0 ignored issues
show
Bug introduced by
The type Doctrine\Common\Annotations\Reader was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
0 ignored issues
show
Bug introduced by
The type Doctrine\Common\Persistence\Mapping\ClassMetadata was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use Doctrine\Common\Persistence\ObjectManager;
0 ignored issues
show
Bug introduced by
The type Doctrine\Common\Persistence\ObjectManager was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use Dtc\GridBundle\Annotation\Action;
9
use Dtc\GridBundle\Annotation\Column;
10
use Dtc\GridBundle\Annotation\DeleteAction;
11
use Dtc\GridBundle\Annotation\Grid;
12
use Dtc\GridBundle\Annotation\ShowAction;
13
use Dtc\GridBundle\Annotation\Sort;
14
use Dtc\GridBundle\Grid\Column\GridColumn;
15
use Dtc\GridBundle\Util\CamelCase;
16
use Dtc\GridBundle\Util\ColumnUtil;
17
use Exception;
18
use InvalidArgumentException;
19
20
class ColumnSource
21
{
22
    /** @var string|null */
23
    private $cacheDir;
24
25
    /** @var bool */
26
    private $debug = false;
27
28
    public function __construct($cacheDir, $debug)
29
    {
30
        $this->debug = $debug;
31
        $this->cacheDir = $cacheDir;
32
    }
33
34
    /**
35
     * @var array|null
36
     */
37
    private $cachedSort;
0 ignored issues
show
introduced by
The private property $cachedSort is not used, and could be removed.
Loading history...
38
39
    public function setDebug($flag)
40
    {
41
        $this->debug = $flag;
42
    }
43
44
    /**
45
     * @param string|null $cacheDir
46
     */
47
    public function setCacheDir($cacheDir)
48
    {
49
        $this->cacheDir = $cacheDir;
50
    }
51
52
    public static function getIdColumn(ClassMetadata $classMetadata)
53
    {
54
        $identifier = $classMetadata->getIdentifier();
55
56
        return isset($identifier[0]) ? $identifier[0] : null;
57
    }
58
59
    /**
60
     * @param $cacheFilename
61
     * @param ClassMetadata $classMetadata
62
     * @param Reader|null   $reader
63
     *
64
     * @return array|null
65
     *
66
     * @throws \Exception
67
     */
68
    private function getCachedColumnInfo($cacheFilename, ClassMetadata $classMetadata, Reader $reader = null)
69
    {
70
        $params = [$classMetadata, $cacheFilename];
71
        if ($reader) {
72
            $params[] = $reader;
73
        }
74
        if (call_user_func_array([$this, 'shouldIncludeColumnCache'], $params)) {
75
            $columnInfo = include $cacheFilename;
76
            if (!isset($columnInfo['columns'])) {
77
                throw new \Exception("Bad column cache, missing columns: {$cacheFilename}");
78
            }
79
            if (!isset($columnInfo['sort'])) {
80
                throw new \Exception("Bad column cache, missing sort: {$cacheFilename}");
81
            }
82
            if ($columnInfo['sort']) {
83
                self::validateSortInfo($columnInfo['sort'], $columnInfo['columns']);
84
            }
85
86
            return $columnInfo;
87
        }
88
89
        return null;
90
    }
91
92
    /**
93
     * @return ColumnSourceInfo|null
94
     *
95
     * @throws Exception
96
     */
97
    public function getColumnSourceInfo(ObjectManager $objectManager, $objectName, $allowReflection, Reader $reader = null)
98
    {
99
        $metadataFactory = $objectManager->getMetadataFactory();
100
        $classMetadata = $metadataFactory->getMetadataFor($objectName);
101
        $reflectionClass = $classMetadata->getReflectionClass();
102
        $name = $reflectionClass->getName();
103
        $cacheFilename = ColumnUtil::createCacheFilename($this->cacheDir, $name);
104
105
        // Try to include them from the cached file if exists.
106
        $params = [$cacheFilename, $classMetadata];
107
        if ($reader) {
108
            $params[] = $reader;
109
        }
110
        $columnInfo = call_user_func_array([$this, 'getCachedColumnInfo'], $params);
111
112
        if (!$columnInfo && $reader) {
113
            $columnInfo = $this->readAndCacheGridAnnotations($cacheFilename, $reader, $classMetadata, $allowReflection);
114
        }
115
116
        if (!$columnInfo && $allowReflection) {
117
            $columns = self::getReflectionColumns($classMetadata);
118
            $columnInfo = ['columns' => $columns, 'sort' => []];
119
        }
120
121
        if (!$columnInfo) {
122
            return null;
123
        }
124
125
        $columnSourceInfo = new ColumnSourceInfo();
126
        $columnSourceInfo->columns = $columnInfo['columns'];
127
        $columnSourceInfo->sort = $columnInfo['sort'];
128
        $columnSourceInfo->idColumn = self::getIdColumn($classMetadata);
129
130
        return $columnSourceInfo;
131
    }
132
133
    /**
134
     * Cached annotation info from the file, if the mtime of the file has not changed (or if not in debug).
135
     *
136
     * @return bool
137
     *
138
     * @throws Exception
139
     */
140
    private function shouldIncludeColumnCache(ClassMetadata $metadata, $columnCacheFilename, Reader $reader = null)
141
    {
142
        // In production, or if we're sure there's no annotaitons, just include the cache.
143
        if (!$this->debug || !isset($reader)) {
144
            if (!is_file($columnCacheFilename) || !is_readable($columnCacheFilename)) {
145
                return false;
146
            }
147
148
            return true;
149
        }
150
151
        return self::checkTimestamps($metadata, $columnCacheFilename);
152
    }
153
154
    /**
155
     * Check timestamps of the file pointed to by the class metadata, and the columnCacheFilename and see if any
156
     * are newer (meaning we .
157
     *
158
     * @param ClassMetadata $metadata
159
     * @param $columnCacheFilename
160
     *
161
     * @return bool
162
     */
163
    public static function checkTimestamps(ClassMetadata $metadata, $columnCacheFilename)
164
    {
165
        $reflectionClass = $metadata->getReflectionClass();
166
        $filename = $reflectionClass->getFileName();
167
        if ($filename && is_file($filename)) {
168
            $mtime = filemtime($filename);
169
            if (($currentfileMtime = filemtime(__FILE__)) > $mtime) {
170
                $mtime = $currentfileMtime;
171
            }
172
            $mtimeAnnotation = file_exists($columnCacheFilename) ? filemtime($columnCacheFilename) : null;
173
            if ($mtime && $mtimeAnnotation && $mtime <= $mtimeAnnotation) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mtimeAnnotation of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
174
                return true;
175
            }
176
        }
177
178
        return false;
179
    }
180
181
    /**
182
     * Generates a list of property name and labels based on finding the GridColumn annotation.
183
     *
184
     * @throws \Exception
185
     *
186
     * @return array|null Hash of grid annotation results: ['columns' => array, 'sort' => string]
187
     */
188
    private function readAndCacheGridAnnotations($cacheFilename, Reader $reader, ClassMetadata $metadata, $allowReflection)
189
    {
190
        $reflectionClass = $metadata->getReflectionClass();
191
        $properties = $reflectionClass->getProperties();
192
193
        /** @var Grid $gridAnnotation */
194
        $sort = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $sort is dead and can be removed.
Loading history...
195
        $sortMulti = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $sortMulti is dead and can be removed.
Loading history...
196
        if (!($gridAnnotation = $reader->getClassAnnotation($reflectionClass, 'Dtc\GridBundle\Annotation\Grid'))) {
197
            return null;
198
        }
199
200
        $actions = $gridAnnotation->actions;
201
        $sort = $gridAnnotation->sort;
202
        $sortMulti = $gridAnnotation->sortMulti;
203
204
        $gridColumns = [];
205
        foreach ($properties as $property) {
206
            /** @var Column $annotation */
207
            $annotation = $reader->getPropertyAnnotation($property, 'Dtc\GridBundle\Annotation\Column');
208
            if ($annotation) {
209
                $name = $property->getName();
210
                $label = $annotation->label ?: CamelCase::fromCamelCase($name);
211
                $gridColumns[$name] = ['class' => '\Dtc\GridBundle\Grid\Column\GridColumn', 'arguments' => [$name, $label]];
212
                $gridColumns[$name]['arguments'][] = isset($annotation->formatter) ? $annotation->formatter : null;
213
                if ($annotation->sortable) {
214
                    $gridColumns[$name]['arguments'][] = ['sortable' => true];
215
                } else {
216
                    $gridColumns[$name]['arguments'][] = [];
217
                }
218
                $gridColumns[$name]['arguments'][] = $annotation->searchable;
219
                $gridColumns[$name]['arguments'][] = $annotation->order;
220
            }
221
        }
222
223
        // Fall back to default column list if list is not specified
224
        if (!$gridColumns && $allowReflection) {
225
            $gridColumnList = self::getReflectionColumns($metadata);
226
            /** @var GridColumn $gridColumn */
227
            foreach ($gridColumnList as $field => $gridColumn) {
228
                $gridColumns[$field] = ['class' => '\Dtc\GridBundle\Grid\Column\GridColumn', 'arguments' => [$field, $gridColumn->getLabel(), null, ['sortable' => true], true, null]];
229
            }
230
        }
231
232
        if (isset($actions)) {
233
            $field = '\$-action';
234
            $actionArgs = [$field];
235
            $actionDefs = [];
236
            /* @var Action $action */
237
            foreach ($actions as $action) {
238
                $actionDef = ['label' => $action->label, 'route' => $action->route];
239
                if ($action instanceof ShowAction) {
240
                    $actionDef['action'] = 'show';
241
                }
242
                if ($action instanceof DeleteAction) {
243
                    $actionDef['action'] = 'delete';
244
                }
245
                $actionDefs[] = $actionDef;
246
            }
247
            $actionArgs[] = $actionDefs;
248
249
            $gridColumns[$field] = ['class' => '\Dtc\GridBundle\Grid\Column\ActionGridColumn',
250
                'arguments' => $actionArgs, ];
251
        }
252
253
        $this->sortGridColumns($gridColumns);
254
255
        if ($sort) {
256
            if ($sortMulti) {
257
                throw new InvalidArgumentException($reflectionClass->getName().' - '."Can't have sort and sortMulti defined on Grid annotation");
258
            }
259
            $sortMulti = [$sort];
260
        }
261
262
        $sortList = [];
263
        try {
264
            foreach ($sortMulti as $sortDef) {
265
                $sortInfo = self::extractSortInfo($sortDef);
266
                self::validateSortInfo($sortInfo, $gridColumns);
267
                if (isset($sortInfo['column'])) {
268
                    $sortList[$sortInfo['column']] = $sortInfo['direction'];
269
                }
270
            }
271
        } catch (InvalidArgumentException $exception) {
272
            throw new InvalidArgumentException($reflectionClass->getName().' - '.$exception->getMessage(), $exception->getCode(), $exception);
273
        }
274
275
        $columnInfo = ['columns' => $gridColumns, 'sort' => $sortList];
276
277
        ColumnUtil::populateCacheFile($cacheFilename, $columnInfo);
278
279
        return $this->getCachedColumnInfo($cacheFilename, $metadata, $reader);
280
    }
281
282
    /**
283
     * @param array $sortInfo
284
     * @param array $gridColumns
285
     *
286
     * @throws InvalidArgumentException
287
     */
288
    private static function validateSortInfo(array $sortInfo, array $gridColumns)
289
    {
290
        if (isset($sortInfo['direction'])) {
291
            switch ($sortInfo['direction']) {
292
                case 'ASC':
293
                case 'DESC':
294
                    break;
295
                default:
296
                    throw new InvalidArgumentException("Grid's sort annotation direction '{$sortInfo['direction']}' is invalid");
297
            }
298
        }
299
300
        if (isset($sortInfo['column'])) {
301
            $column = $sortInfo['column'];
302
303
            if (!isset($sortInfo['direction'])) {
304
                throw new InvalidArgumentException("Grid's sort annotation column '$column' specified but a sort direction was not");
305
            }
306
            if (isset($gridColumns[$column])) {
307
                return;
308
            }
309
            throw new InvalidArgumentException("Grid's sort annotation column '$column' not in list of columns (".implode(', ', array_keys($gridColumns)).')');
310
        }
311
    }
312
313
    /**
314
     * @param Sort|null $sortAnnotation
315
     *
316
     * @return array
317
     */
318
    private static function extractSortInfo($sortAnnotation)
319
    {
320
        $sortInfo = ['direction' => null, 'column' => null];
321
        if ($sortAnnotation) {
322
            $direction = $sortAnnotation->direction;
323
            $sortInfo['direction'] = $direction;
324
            $column = $sortAnnotation->column;
325
            $sortInfo['column'] = $column;
326
        }
327
328
        return $sortInfo;
329
    }
330
331
    private function sortGridColumns(array &$columnDefs)
332
    {
333
        $unordered = [];
334
        $ordered = [];
335
        foreach ($columnDefs as $name => $columnDef) {
336
            $columnParts = $columnDef['arguments'];
337
            if (!isset($columnParts[5]) || null === $columnParts[5]) {
338
                $unordered[$name] = $columnDef;
339
                continue;
340
            }
341
            $ordered[$name] = $columnDef;
342
        }
343
344
        if (empty($ordered)) {
345
            return;
346
        }
347
348
        uasort($ordered, function ($columnDef1, $columnDef2) {
349
            $columnParts1 = $columnDef1['arguments'];
350
            $columnParts2 = $columnDef2['arguments'];
351
            $order1 = $columnParts1[5];
352
            $order2 = $columnParts2[5];
353
354
            return $order1 > $order2;
355
        });
356
357
        if ($unordered) {
358
            foreach ($unordered as $name => $columnDef) {
359
                $ordered[$name] = $columnDef;
360
            }
361
        }
362
        $columnDefs = $ordered;
363
    }
364
365
    /**
366
     * Generate Columns based on document's Metadata.
367
     */
368
    private static function getReflectionColumns(ClassMetadata $metadata)
369
    {
370
        $fields = $metadata->getFieldNames();
371
        $identifier = $metadata->getIdentifier();
372
        $identifier = isset($identifier[0]) ? $identifier[0] : null;
373
374
        if (!method_exists($metadata, 'getFieldMapping')) {
375
            return array();
376
        }
377
378
        $columns = array();
379
        foreach ($fields as $field) {
380
            $mapping = $metadata->getFieldMapping($field);
381
            if (isset($mapping['options']) && isset($mapping['options']['label'])) {
382
                $label = $mapping['options']['label'];
383
            } else {
384
                $label = CamelCase::fromCamelCase($field);
385
            }
386
387
            if ($identifier === $field) {
388
                if (isset($mapping['strategy']) && 'auto' == $mapping['strategy']) {
389
                    continue;
390
                }
391
            }
392
            $columns[$field] = new GridColumn($field, $label);
393
        }
394
395
        return $columns;
396
    }
397
}
398