GridField   F
last analyzed

Complexity

Total Complexity 145

Size/Duplication

Total Lines 1208
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 1
Metric Value
eloc 428
c 3
b 1
f 1
dl 0
loc 1208
rs 2
wmc 145

39 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 3
A getModelClass() 0 18 5
A setModelClass() 0 5 1
A getReadonlyComponents() 0 3 1
A setReadonlyComponents() 0 3 1
A index() 0 3 1
A getConfig() 0 3 1
A setList() 0 5 1
A getManipulatedList() 0 11 3
A getList() 0 3 1
A performDisabledTransformation() 0 5 1
A getState() 0 7 2
A getComponents() 0 3 1
A getCastedValue() 0 22 3
A setConfig() 0 9 2
A performReadonlyTransformation() 0 22 5
A setReadonly() 0 5 1
A saveInto() 0 5 3
A handleAlterAction() 0 17 4
F FieldHolder() 0 222 27
A newRow() 0 6 1
A getRowAttributes() 0 8 1
A getOptionalTableBody() 0 11 2
A getDataFieldValue() 0 17 4
A getOptionalTableFooter() 0 11 2
A getAttributes() 0 6 1
A newCell() 0 6 1
A addDataFields() 0 6 2
A getColumnMetadata() 0 32 5
A getColumns() 0 11 3
B gridFieldAlterAction() 0 63 10
A getColumnContent() 0 21 4
A newRowClasses() 0 21 4
A Field() 0 4 1
A buildColumnDispatch() 0 10 4
A getOptionalTableHeader() 0 11 2
A getColumnCount() 0 7 2
A getColumnAttributes() 0 32 5
F handleRequest() 0 94 24

How to fix   Complexity   

Complex Class

Complex classes like GridField 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.

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 GridField, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Forms\GridField;
4
5
use InvalidArgumentException;
6
use LogicException;
7
use SilverStripe\Control\HasRequestHandler;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Control\HTTPResponse;
10
use SilverStripe\Control\HTTPResponse_Exception;
11
use SilverStripe\Control\RequestHandler;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Forms\Form;
14
use SilverStripe\Forms\FormField;
15
use SilverStripe\Forms\GridField\FormAction\SessionStore;
16
use SilverStripe\Forms\GridField\FormAction\StateStore;
17
use SilverStripe\ORM\ArrayList;
18
use SilverStripe\ORM\DataList;
19
use SilverStripe\ORM\DataObject;
20
use SilverStripe\ORM\DataObjectInterface;
21
use SilverStripe\ORM\FieldType\DBField;
22
use SilverStripe\ORM\SS_List;
23
use SilverStripe\View\HTML;
24
25
/**
26
 * Displays a {@link SS_List} in a grid format.
27
 *
28
 * GridField is a field that takes an SS_List and displays it in an table with rows and columns.
29
 * It reminds of the old TableFields but works with SS_List types and only loads the necessary
30
 * rows from the list.
31
 *
32
 * The minimum configuration is to pass in name and title of the field and a SS_List.
33
 *
34
 * <code>
35
 * $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page'));
36
 * </code>
37
 *
38
 * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
39
 * since the required frontend dependencies are included through CMS bundling.
40
 *
41
 * @see SS_List
42
 *
43
 * @property GridState_Data $State The gridstate of this object
44
 */
45
class GridField extends FormField
46
{
47
    /**
48
     * @var array
49
     */
50
    private static $allowed_actions = array(
51
        'index',
52
        'gridFieldAlterAction',
53
    );
54
55
    /**
56
     * Data source.
57
     *
58
     * @var SS_List
59
     */
60
    protected $list = null;
61
62
    /**
63
     * Class name of the DataObject that the GridField will display.
64
     *
65
     * Defaults to the value of $this->list->dataClass.
66
     *
67
     * @var string
68
     */
69
    protected $modelClassName = '';
70
71
    /**
72
     * Current state of the GridField.
73
     *
74
     * @var GridState
75
     */
76
    protected $state = null;
77
78
    /**
79
     * @var GridFieldConfig
80
     */
81
    protected $config = null;
82
83
    /**
84
     * Components list.
85
     *
86
     * @var array
87
     */
88
    protected $components = array();
89
90
    /**
91
     * Internal dispatcher for column handlers.
92
     *
93
     * Keys are column names and values are GridField_ColumnProvider objects.
94
     *
95
     * @var array
96
     */
97
    protected $columnDispatch = null;
98
99
    /**
100
     * Map of callbacks for custom data fields.
101
     *
102
     * @var array
103
     */
104
    protected $customDataFields = array();
105
106
    /**
107
     * @var string
108
     */
109
    protected $name = '';
110
111
    /**
112
     * A whitelist of readonly component classes allowed if performReadonlyTransform is called.
113
     *
114
     * @var array
115
     */
116
    protected $readonlyComponents = [
117
        GridField_ActionMenu::class,
118
        GridFieldConfig_RecordViewer::class,
119
        GridFieldButtonRow::class,
120
        GridFieldDataColumns::class,
121
        GridFieldDetailForm::class,
122
        GridFieldLazyLoader::class,
123
        GridFieldPageCount::class,
124
        GridFieldPaginator::class,
125
        GridFieldFilterHeader::class,
126
        GridFieldSortableHeader::class,
127
        GridFieldToolbarHeader::class,
128
        GridFieldViewButton::class,
129
        GridState_Component::class,
130
    ];
131
132
    /**
133
     * Pattern used for looking up
134
     */
135
    const FRAGMENT_REGEX = '/\$DefineFragment\(([a-z0-9\-_]+)\)/i';
136
137
    /**
138
     * @param string $name
139
     * @param string $title
140
     * @param SS_List $dataList
141
     * @param GridFieldConfig $config
142
     */
143
    public function __construct($name, $title = null, SS_List $dataList = null, GridFieldConfig $config = null)
144
    {
145
        parent::__construct($name, $title, null);
146
147
        $this->name = $name;
148
149
        if ($dataList) {
150
            $this->setList($dataList);
151
        }
152
153
        if (!$config) {
154
            $config = GridFieldConfig_Base::create();
155
        }
156
157
        $this->setConfig($config);
158
159
        $this->state = new GridState($this);
160
161
        $this->addExtraClass('grid-field');
162
    }
163
164
    /**
165
     * @param HTTPRequest $request
166
     *
167
     * @return string
168
     */
169
    public function index($request)
170
    {
171
        return $this->gridFieldAlterAction(array(), $this->getForm(), $request);
172
    }
173
174
    /**
175
     * Set the modelClass (data object) that this field will get it column headers from.
176
     *
177
     * If no $displayFields has been set, the display fields will be $summary_fields.
178
     *
179
     * @see GridFieldDataColumns::getDisplayFields()
180
     *
181
     * @param string $modelClassName
182
     *
183
     * @return $this
184
     */
185
    public function setModelClass($modelClassName)
186
    {
187
        $this->modelClassName = $modelClassName;
188
189
        return $this;
190
    }
191
192
    /**
193
     * Returns a data class that is a DataObject type that this GridField should look like.
194
     *
195
     * @return string
196
     *
197
     * @throws LogicException
198
     */
199
    public function getModelClass()
200
    {
201
        if ($this->modelClassName) {
202
            return $this->modelClassName;
203
        }
204
205
        /** @var DataList|ArrayList $list */
206
        $list = $this->list;
207
        if ($list && $list->hasMethod('dataClass')) {
208
            $class = $list->dataClass();
209
210
            if ($class) {
211
                return $class;
212
            }
213
        }
214
215
        throw new LogicException(
216
            'GridField doesn\'t have a modelClassName, so it doesn\'t know the columns of this grid.'
217
        );
218
    }
219
220
    /**
221
     * Overload the readonly components for this gridfield.
222
     *
223
     * @param array $components an array map of component class references to whitelist for a readonly version.
224
     */
225
    public function setReadonlyComponents(array $components)
226
    {
227
        $this->readonlyComponents = $components;
228
    }
229
230
    /**
231
     * Return the readonly components
232
     *
233
     * @return array a map of component classes.
234
     */
235
    public function getReadonlyComponents()
236
    {
237
        return $this->readonlyComponents;
238
    }
239
240
    /**
241
     * Custom Readonly transformation to remove actions which shouldn't be present for a readonly state.
242
     *
243
     * @return GridField
244
     */
245
    public function performReadonlyTransformation()
246
    {
247
        $copy = clone $this;
248
        $copy->setReadonly(true);
249
        $copyConfig = $copy->getConfig();
250
        $hadEditButton = $copyConfig->getComponentByType(GridFieldEditButton::class) !== null;
251
252
        // get the whitelist for allowable readonly components
253
        $allowedComponents = $this->getReadonlyComponents();
254
        foreach ($this->getConfig()->getComponents() as $component) {
255
            // if a component doesn't exist, remove it from the readonly version.
256
            if (!in_array(get_class($component), $allowedComponents)) {
257
                $copyConfig->removeComponent($component);
0 ignored issues
show
Bug introduced by
It seems like $component can also be of type SilverStripe\View\ArrayData; however, parameter $component of SilverStripe\Forms\GridF...nfig::removeComponent() does only seem to accept SilverStripe\Forms\GridField\GridFieldComponent, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

257
                $copyConfig->removeComponent(/** @scrutinizer ignore-type */ $component);
Loading history...
258
            }
259
        }
260
261
        // If the edit button has been removed, replace it with a view button
262
        if ($hadEditButton && !$copyConfig->getComponentByType(GridFieldViewButton::class)) {
263
            $copyConfig->addComponent(new GridFieldViewButton);
264
        }
265
266
        return $copy;
267
    }
268
269
    /**
270
     * Disabling the gridfield should have the same affect as making it readonly (removing all action items).
271
     *
272
     * @return GridField
273
     */
274
    public function performDisabledTransformation()
275
    {
276
        parent::performDisabledTransformation();
277
278
        return $this->performReadonlyTransformation();
279
    }
280
281
    /**
282
     * @return GridFieldConfig
283
     */
284
    public function getConfig()
285
    {
286
        return $this->config;
287
    }
288
289
    /**
290
     * @param GridFieldConfig $config
291
     *
292
     * @return $this
293
     */
294
    public function setConfig(GridFieldConfig $config)
295
    {
296
        $this->config = $config;
297
298
        if (!$this->config->getComponentByType(GridState_Component::class)) {
299
            $this->config->addComponent(new GridState_Component());
300
        }
301
302
        return $this;
303
    }
304
305
    /**
306
     * @param bool $readonly
307
     *
308
     * @return $this
309
     */
310
    public function setReadonly($readonly)
311
    {
312
        parent::setReadonly($readonly);
313
        $this->getState()->Readonly = $readonly;
0 ignored issues
show
Bug Best Practice introduced by
The property Readonly does not exist on SilverStripe\Forms\GridField\GridState_Data. Since you implemented __set, consider adding a @property annotation.
Loading history...
314
        return $this;
315
    }
316
317
    /**
318
     * @return ArrayList
319
     */
320
    public function getComponents()
321
    {
322
        return $this->config->getComponents();
323
    }
324
325
    /**
326
     * Cast an arbitrary value with the help of a $castingDefinition.
327
     *
328
     * @todo refactor this into GridFieldComponent
329
     *
330
     * @param mixed $value
331
     * @param string|array $castingDefinition
332
     *
333
     * @return mixed
334
     */
335
    public function getCastedValue($value, $castingDefinition)
336
    {
337
        $castingParams = array();
338
339
        if (is_array($castingDefinition)) {
340
            $castingParams = $castingDefinition;
341
            array_shift($castingParams);
342
            $castingDefinition = array_shift($castingDefinition);
343
        }
344
345
        if (strpos($castingDefinition, '->') === false) {
346
            $castingFieldType = $castingDefinition;
347
            $castingField = DBField::create_field($castingFieldType, $value);
348
349
            return call_user_func_array(array($castingField, 'XML'), $castingParams);
350
        }
351
352
        list($castingFieldType, $castingMethod) = explode('->', $castingDefinition);
353
354
        $castingField = DBField::create_field($castingFieldType, $value);
355
356
        return call_user_func_array(array($castingField, $castingMethod), $castingParams);
357
    }
358
359
    /**
360
     * Set the data source.
361
     *
362
     * @param SS_List $list
363
     *
364
     * @return $this
365
     */
366
    public function setList(SS_List $list)
367
    {
368
        $this->list = $list;
369
370
        return $this;
371
    }
372
373
    /**
374
     * Get the data source.
375
     *
376
     * @return SS_List
377
     */
378
    public function getList()
379
    {
380
        return $this->list;
381
    }
382
383
    /**
384
     * Get the data source after applying every {@link GridField_DataManipulator} to it.
385
     *
386
     * @return SS_List
387
     */
388
    public function getManipulatedList()
389
    {
390
        $list = $this->getList();
391
392
        foreach ($this->getComponents() as $item) {
393
            if ($item instanceof GridField_DataManipulator) {
394
                $list = $item->getManipulatedData($this, $list);
0 ignored issues
show
Bug introduced by
The method getManipulatedData() does not exist on SilverStripe\View\ArrayData. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

394
                /** @scrutinizer ignore-call */ 
395
                $list = $item->getManipulatedData($this, $list);
Loading history...
395
            }
396
        }
397
398
        return $list;
399
    }
400
401
    /**
402
     * Get the current GridState_Data or the GridState.
403
     *
404
     * @param bool $getData
405
     *
406
     * @return GridState_Data|GridState
407
     */
408
    public function getState($getData = true)
409
    {
410
        if ($getData) {
411
            return $this->state->getData();
412
        }
413
414
        return $this->state;
415
    }
416
417
    /**
418
     * Returns the whole gridfield rendered with all the attached components.
419
     *
420
     * @param array $properties
421
     * @return string
422
     */
423
    public function FieldHolder($properties = array())
424
    {
425
        $columns = $this->getColumns();
426
427
        $list = $this->getManipulatedList();
428
429
        $content = array(
430
            'before' => '',
431
            'after' => '',
432
            'header' => '',
433
            'footer' => '',
434
        );
435
436
        foreach ($this->getComponents() as $item) {
437
            if ($item instanceof GridField_HTMLProvider) {
438
                $fragments = $item->getHTMLFragments($this);
0 ignored issues
show
Bug introduced by
The method getHTMLFragments() does not exist on SilverStripe\View\ArrayData. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

438
                /** @scrutinizer ignore-call */ 
439
                $fragments = $item->getHTMLFragments($this);
Loading history...
439
440
                if ($fragments) {
441
                    foreach ($fragments as $fragmentKey => $fragmentValue) {
442
                        $fragmentKey = strtolower($fragmentKey);
443
444
                        if (!isset($content[$fragmentKey])) {
445
                            $content[$fragmentKey] = '';
446
                        }
447
448
                        $content[$fragmentKey] .= $fragmentValue . "\n";
449
                    }
450
                }
451
            }
452
        }
453
454
        foreach ($content as $contentKey => $contentValue) {
455
            $content[$contentKey] = trim($contentValue);
456
        }
457
458
        // Replace custom fragments and check which fragments are defined. Circular dependencies
459
        // are detected by disallowing any item to be deferred more than 5 times.
460
461
        $fragmentDefined = array(
462
            'header' => true,
463
            'footer' => true,
464
            'before' => true,
465
            'after' => true,
466
        );
467
        $fragmentDeferred = [];
468
469
        // TODO: Break the below into separate reducer methods
470
471
        // Continue looping if any placeholders exist
472
        while (array_filter($content, function ($value) {
473
            return preg_match(self::FRAGMENT_REGEX, $value);
474
        })) {
475
            foreach ($content as $contentKey => $contentValue) {
476
                // Skip if this specific content has no placeholders
477
                if (!preg_match_all(self::FRAGMENT_REGEX, $contentValue, $matches)) {
478
                    continue;
479
                }
480
                foreach ($matches[1] as $match) {
481
                    $fragmentName = strtolower($match);
482
                    $fragmentDefined[$fragmentName] = true;
483
484
                    $fragment = '';
485
486
                    if (isset($content[$fragmentName])) {
487
                        $fragment = $content[$fragmentName];
488
                    }
489
490
                    // If the fragment still has a fragment definition in it, when we should defer
491
                    // this item until later.
492
493
                    if (preg_match(self::FRAGMENT_REGEX, $fragment, $matches)) {
494
                        if (isset($fragmentDeferred[$contentKey]) && $fragmentDeferred[$contentKey] > 5) {
495
                            throw new LogicException(sprintf(
496
                                'GridField HTML fragment "%s" and "%s" appear to have a circular dependency.',
497
                                $fragmentName,
498
                                $matches[1]
499
                            ));
500
                        }
501
502
                        unset($content[$contentKey]);
503
504
                        $content[$contentKey] = $contentValue;
505
506
                        if (!isset($fragmentDeferred[$contentKey])) {
507
                            $fragmentDeferred[$contentKey] = 0;
508
                        }
509
510
                        $fragmentDeferred[$contentKey]++;
511
512
                        break;
513
                    } else {
514
                        $content[$contentKey] = preg_replace(
515
                            sprintf('/\$DefineFragment\(%s\)/i', $fragmentName),
516
                            $fragment,
517
                            $content[$contentKey]
518
                        );
519
                    }
520
                }
521
            }
522
        }
523
524
        // Check for any undefined fragments, and if so throw an exception.
525
        // While we're at it, trim whitespace off the elements.
526
527
        foreach ($content as $contentKey => $contentValue) {
528
            if (empty($fragmentDefined[$contentKey])) {
529
                throw new LogicException(sprintf(
530
                    'GridField HTML fragment "%s" was given content, but not defined. Perhaps there is a '
531
                    . 'supporting GridField component you need to add?',
532
                    $contentKey
533
                ));
534
            }
535
        }
536
537
        $total = count($list);
538
539
        if ($total > 0) {
540
            $rows = array();
541
542
            foreach ($list as $index => $record) {
543
                if ($record->hasMethod('canView') && !$record->canView()) {
544
                    continue;
545
                }
546
547
                $rowContent = '';
548
549
                foreach ($this->getColumns() as $column) {
550
                    $colContent = $this->getColumnContent($record, $column);
551
552
                    // Null means this columns should be skipped altogether.
553
554
                    if ($colContent === null) {
555
                        continue;
556
                    }
557
558
                    $colAttributes = $this->getColumnAttributes($record, $column);
559
560
                    $rowContent .= $this->newCell(
561
                        $total,
562
                        $index,
563
                        $record,
564
                        $colAttributes,
565
                        $colContent
566
                    );
567
                }
568
569
                $rowAttributes = $this->getRowAttributes($total, $index, $record);
570
571
                $rows[] = $this->newRow($total, $index, $record, $rowAttributes, $rowContent);
572
            }
573
            $content['body'] = implode("\n", $rows);
574
        }
575
576
        // Display a message when the grid field is empty.
577
        if (empty($content['body'])) {
578
            $cell = HTML::createTag(
579
                'td',
580
                array(
581
                    'colspan' => count($columns),
582
                ),
583
                _t('SilverStripe\\Forms\\GridField\\GridField.NoItemsFound', 'No items found')
584
            );
585
586
            $row = HTML::createTag(
587
                'tr',
588
                array(
589
                    'class' => 'ss-gridfield-item ss-gridfield-no-items',
590
                ),
591
                $cell
592
            );
593
594
            $content['body'] = $row;
595
        }
596
597
        $header = $this->getOptionalTableHeader($content);
598
        $body = $this->getOptionalTableBody($content);
599
        $footer = $this->getOptionalTableFooter($content);
600
601
        $this->addExtraClass('ss-gridfield grid-field field');
602
603
        $fieldsetAttributes = array_diff_key(
604
            $this->getAttributes(),
605
            array(
606
                'value' => false,
607
                'type' => false,
608
                'name' => false,
609
            )
610
        );
611
612
        $fieldsetAttributes['data-name'] = $this->getName();
613
614
        $tableId = null;
615
616
        if ($this->id) {
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on SilverStripe\Forms\GridField\GridField. Since you implemented __get, consider adding a @property annotation.
Loading history...
617
            $tableId = $this->id;
618
        }
619
620
        $tableAttributes = array(
621
            'id' => $tableId,
622
            'class' => 'table grid-field__table',
623
            'cellpadding' => '0',
624
            'cellspacing' => '0'
625
        );
626
627
        if ($this->getDescription()) {
628
            $content['after'] .= HTML::createTag(
629
                'span',
630
                array('class' => 'description'),
631
                $this->getDescription()
632
            );
633
        }
634
635
        $table = HTML::createTag(
636
            'table',
637
            $tableAttributes,
638
            $header . "\n" . $footer . "\n" . $body
639
        );
640
641
        return HTML::createTag(
642
            'fieldset',
643
            $fieldsetAttributes,
644
            $content['before'] . $table . $content['after']
645
        );
646
    }
647
648
    /**
649
     * @param int $total
650
     * @param int $index
651
     * @param DataObject $record
652
     * @param array $attributes
653
     * @param string $content
654
     *
655
     * @return string
656
     */
657
    protected function newCell($total, $index, $record, $attributes, $content)
658
    {
659
        return HTML::createTag(
660
            'td',
661
            $attributes,
662
            $content
663
        );
664
    }
665
666
    /**
667
     * @param int $total
668
     * @param int $index
669
     * @param DataObject $record
670
     * @param array $attributes
671
     * @param string $content
672
     *
673
     * @return string
674
     */
675
    protected function newRow($total, $index, $record, $attributes, $content)
676
    {
677
        return HTML::createTag(
678
            'tr',
679
            $attributes,
680
            $content
681
        );
682
    }
683
684
    /**
685
     * @param int $total
686
     * @param int $index
687
     * @param DataObject $record
688
     *
689
     * @return array
690
     */
691
    protected function getRowAttributes($total, $index, $record)
692
    {
693
        $rowClasses = $this->newRowClasses($total, $index, $record);
694
695
        return array(
696
            'class' => implode(' ', $rowClasses),
697
            'data-id' => $record->ID,
698
            'data-class' => $record->ClassName,
699
        );
700
    }
701
702
    /**
703
     * @param int $total
704
     * @param int $index
705
     * @param DataObject $record
706
     *
707
     * @return array
708
     */
709
    protected function newRowClasses($total, $index, $record)
710
    {
711
        $classes = array('ss-gridfield-item');
712
713
        if ($index == 0) {
714
            $classes[] = 'first';
715
        }
716
717
        if ($index == $total - 1) {
718
            $classes[] = 'last';
719
        }
720
721
        if ($index % 2) {
722
            $classes[] = 'even';
723
        } else {
724
            $classes[] = 'odd';
725
        }
726
727
        $this->extend('updateNewRowClasses', $classes, $total, $index, $record);
728
729
        return $classes;
730
    }
731
732
    /**
733
     * @param array $properties
734
     * @return string
735
     */
736
    public function Field($properties = array())
737
    {
738
        $this->extend('onBeforeRender', $this);
739
        return $this->FieldHolder($properties);
740
    }
741
742
    /**
743
     * {@inheritdoc}
744
     */
745
    public function getAttributes()
746
    {
747
        return array_merge(
748
            parent::getAttributes(),
749
            array(
750
                'data-url' => $this->Link(),
751
            )
752
        );
753
    }
754
755
    /**
756
     * Get the columns of this GridField, they are provided by attached GridField_ColumnProvider.
757
     *
758
     * @return array
759
     */
760
    public function getColumns()
761
    {
762
        $columns = array();
763
764
        foreach ($this->getComponents() as $item) {
765
            if ($item instanceof GridField_ColumnProvider) {
766
                $item->augmentColumns($this, $columns);
0 ignored issues
show
Bug introduced by
The method augmentColumns() does not exist on SilverStripe\View\ArrayData. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

766
                $item->/** @scrutinizer ignore-call */ 
767
                       augmentColumns($this, $columns);
Loading history...
767
            }
768
        }
769
770
        return $columns;
771
    }
772
773
    /**
774
     * Get the value from a column.
775
     *
776
     * @param DataObject $record
777
     * @param string $column
778
     *
779
     * @return string
780
     *
781
     * @throws InvalidArgumentException
782
     */
783
    public function getColumnContent($record, $column)
784
    {
785
        if (!$this->columnDispatch) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->columnDispatch 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...
786
            $this->buildColumnDispatch();
787
        }
788
789
        if (!empty($this->columnDispatch[$column])) {
790
            $content = '';
791
792
            foreach ($this->columnDispatch[$column] as $handler) {
793
                /**
794
                 * @var GridField_ColumnProvider $handler
795
                 */
796
                $content .= $handler->getColumnContent($this, $record, $column);
797
            }
798
799
            return $content;
800
        } else {
801
            throw new InvalidArgumentException(sprintf(
802
                'Bad column "%s"',
803
                $column
804
            ));
805
        }
806
    }
807
808
    /**
809
     * Add additional calculated data fields to be used on this GridField
810
     *
811
     * @param array $fields a map of fieldname to callback. The callback will
812
     *                      be passed the record as an argument.
813
     */
814
    public function addDataFields($fields)
815
    {
816
        if ($this->customDataFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customDataFields 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...
817
            $this->customDataFields = array_merge($this->customDataFields, $fields);
818
        } else {
819
            $this->customDataFields = $fields;
820
        }
821
    }
822
823
    /**
824
     * Get the value of a named field  on the given record.
825
     *
826
     * Use of this method ensures that any special rules around the data for this gridfield are
827
     * followed.
828
     *
829
     * @param DataObject $record
830
     * @param string $fieldName
831
     *
832
     * @return mixed
833
     */
834
    public function getDataFieldValue($record, $fieldName)
835
    {
836
        if (isset($this->customDataFields[$fieldName])) {
837
            $callback = $this->customDataFields[$fieldName];
838
839
            return $callback($record);
840
        }
841
842
        if ($record->hasMethod('relField')) {
843
            return $record->relField($fieldName);
844
        }
845
846
        if ($record->hasMethod($fieldName)) {
847
            return $record->$fieldName();
848
        }
849
850
        return $record->$fieldName;
851
    }
852
853
    /**
854
     * Get extra columns attributes used as HTML attributes.
855
     *
856
     * @param DataObject $record
857
     * @param string $column
858
     *
859
     * @return array
860
     *
861
     * @throws LogicException
862
     * @throws InvalidArgumentException
863
     */
864
    public function getColumnAttributes($record, $column)
865
    {
866
        if (!$this->columnDispatch) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->columnDispatch 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...
867
            $this->buildColumnDispatch();
868
        }
869
870
        if (!empty($this->columnDispatch[$column])) {
871
            $attributes = array();
872
873
            foreach ($this->columnDispatch[$column] as $handler) {
874
                /**
875
                 * @var GridField_ColumnProvider $handler
876
                 */
877
                $columnAttributes = $handler->getColumnAttributes($this, $record, $column);
878
879
                if (is_array($columnAttributes)) {
880
                    $attributes = array_merge($attributes, $columnAttributes);
881
                    continue;
882
                }
883
884
                throw new LogicException(sprintf(
885
                    'Non-array response from %s::getColumnAttributes().',
886
                    get_class($handler)
887
                ));
888
            }
889
890
            return $attributes;
891
        }
892
893
        throw new InvalidArgumentException(sprintf(
894
            'Bad column "%s"',
895
            $column
896
        ));
897
    }
898
899
    /**
900
     * Get metadata for a column.
901
     *
902
     * @example "array('Title'=>'Email address')"
903
     *
904
     * @param string $column
905
     *
906
     * @return array
907
     *
908
     * @throws LogicException
909
     * @throws InvalidArgumentException
910
     */
911
    public function getColumnMetadata($column)
912
    {
913
        if (!$this->columnDispatch) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->columnDispatch 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...
914
            $this->buildColumnDispatch();
915
        }
916
917
        if (!empty($this->columnDispatch[$column])) {
918
            $metaData = array();
919
920
            foreach ($this->columnDispatch[$column] as $handler) {
921
                /**
922
                 * @var GridField_ColumnProvider $handler
923
                 */
924
                $columnMetaData = $handler->getColumnMetadata($this, $column);
925
926
                if (is_array($columnMetaData)) {
927
                    $metaData = array_merge($metaData, $columnMetaData);
928
                    continue;
929
                }
930
931
                throw new LogicException(sprintf(
932
                    'Non-array response from %s::getColumnMetadata().',
933
                    get_class($handler)
934
                ));
935
            }
936
937
            return $metaData;
938
        }
939
940
        throw new InvalidArgumentException(sprintf(
941
            'Bad column "%s"',
942
            $column
943
        ));
944
    }
945
946
    /**
947
     * Return how many columns the grid will have.
948
     *
949
     * @return int
950
     */
951
    public function getColumnCount()
952
    {
953
        if (!$this->columnDispatch) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->columnDispatch 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...
954
            $this->buildColumnDispatch();
955
        }
956
957
        return count($this->columnDispatch);
958
    }
959
960
    /**
961
     * Build an columnDispatch that maps a GridField_ColumnProvider to a column for reference later.
962
     */
963
    protected function buildColumnDispatch()
964
    {
965
        $this->columnDispatch = array();
966
967
        foreach ($this->getComponents() as $item) {
968
            if ($item instanceof GridField_ColumnProvider) {
969
                $columns = $item->getColumnsHandled($this);
0 ignored issues
show
Bug introduced by
The method getColumnsHandled() does not exist on SilverStripe\View\ArrayData. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

969
                /** @scrutinizer ignore-call */ 
970
                $columns = $item->getColumnsHandled($this);
Loading history...
970
971
                foreach ($columns as $column) {
972
                    $this->columnDispatch[$column][] = $item;
973
                }
974
            }
975
        }
976
    }
977
978
    /**
979
     * This is the action that gets executed when a GridField_AlterAction gets clicked.
980
     *
981
     * @param array $data
982
     * @param Form $form
983
     * @param HTTPRequest $request
984
     *
985
     * @return string
986
     */
987
    public function gridFieldAlterAction($data, $form, HTTPRequest $request)
988
    {
989
        $data = $request->requestVars();
990
991
        // Protection against CSRF attacks
992
        $token = $this
993
            ->getForm()
994
            ->getSecurityToken();
995
        if (!$token->checkRequest($request)) {
996
            $this->httpError(400, _t(
997
                "SilverStripe\\Forms\\Form.CSRF_FAILED_MESSAGE",
998
                "There seems to have been a technical problem. Please click the back button, "
999
                . "refresh your browser, and try again."
1000
            ));
1001
        }
1002
1003
        $name = $this->getName();
1004
1005
        $fieldData = null;
1006
1007
        if (isset($data[$name])) {
1008
            $fieldData = $data[$name];
1009
        }
1010
1011
        $state = $this->getState(false);
1012
1013
        /** @skipUpgrade */
1014
        if (isset($fieldData['GridState'])) {
1015
            $state->setValue($fieldData['GridState']);
1016
        }
1017
1018
        // Fetch the store for the "state" of actions (not the GridField)
1019
        /** @var StateStore $store */
1020
        $store = Injector::inst()->create(StateStore::class . '.' . $this->getName());
1021
1022
        foreach ($data as $dataKey => $dataValue) {
1023
            if (preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) {
1024
                $stateChange = $store->load($matches[1]);
1025
1026
                $actionName = $stateChange['actionName'];
1027
1028
                $arguments = array();
1029
1030
                if (isset($stateChange['args'])) {
1031
                    $arguments = $stateChange['args'];
1032
                };
1033
1034
                $html = $this->handleAlterAction($actionName, $arguments, $data);
1035
1036
                if ($html) {
1037
                    return $html;
1038
                }
1039
            }
1040
        }
1041
1042
        if ($request->getHeader('X-Pjax') === 'CurrentField') {
1043
            if ($this->getState()->Readonly === true) {
0 ignored issues
show
Bug Best Practice introduced by
The property Readonly does not exist on SilverStripe\Forms\GridField\GridState_Data. Since you implemented __get, consider adding a @property annotation.
Loading history...
1044
                $this->performDisabledTransformation();
1045
            }
1046
            return $this->FieldHolder();
1047
        }
1048
1049
        return $form->forTemplate();
1050
    }
1051
1052
    /**
1053
     * Pass an action on the first GridField_ActionProvider that matches the $actionName.
1054
     *
1055
     * @param string $actionName
1056
     * @param mixed $arguments
1057
     * @param array $data
1058
     *
1059
     * @return mixed
1060
     *
1061
     * @throws InvalidArgumentException
1062
     */
1063
    public function handleAlterAction($actionName, $arguments, $data)
1064
    {
1065
        $actionName = strtolower($actionName);
1066
1067
        foreach ($this->getComponents() as $component) {
1068
            if ($component instanceof GridField_ActionProvider) {
1069
                $actions = array_map('strtolower', (array) $component->getActions($this));
0 ignored issues
show
Bug introduced by
The method getActions() does not exist on SilverStripe\View\ArrayData. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1069
                $actions = array_map('strtolower', (array) $component->/** @scrutinizer ignore-call */ getActions($this));
Loading history...
1070
1071
                if (in_array($actionName, $actions)) {
1072
                    return $component->handleAction($this, $actionName, $arguments, $data);
0 ignored issues
show
Bug introduced by
The method handleAction() does not exist on SilverStripe\View\ArrayData. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1072
                    return $component->/** @scrutinizer ignore-call */ handleAction($this, $actionName, $arguments, $data);
Loading history...
1073
                }
1074
            }
1075
        }
1076
1077
        throw new InvalidArgumentException(sprintf(
1078
            'Can\'t handle action "%s"',
1079
            $actionName
1080
        ));
1081
    }
1082
1083
    /**
1084
     * Custom request handler that will check component handlers before proceeding to the default
1085
     * implementation.
1086
     *
1087
     * @todo copy less code from RequestHandler.
1088
     *
1089
     * @param HTTPRequest $request
1090
     * @return array|RequestHandler|HTTPResponse|string
1091
     * @throws HTTPResponse_Exception
1092
     */
1093
    public function handleRequest(HTTPRequest $request)
1094
    {
1095
        if ($this->brokenOnConstruct) {
1096
            user_error(
1097
                sprintf(
1098
                    "parent::__construct() needs to be called on %s::__construct()",
1099
                    __CLASS__
1100
                ),
1101
                E_USER_WARNING
1102
            );
1103
        }
1104
1105
        $this->setRequest($request);
1106
1107
        $fieldData = $this->getRequest()->requestVar($this->getName());
1108
1109
        /** @skipUpgrade */
1110
        if ($fieldData && isset($fieldData['GridState'])) {
1111
            $this->getState(false)->setValue($fieldData['GridState']);
1112
        }
1113
1114
        foreach ($this->getComponents() as $component) {
1115
            if ($component instanceof GridField_URLHandler && $urlHandlers = $component->getURLHandlers($this)) {
0 ignored issues
show
Bug introduced by
The method getURLHandlers() does not exist on SilverStripe\View\ArrayData. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1115
            if ($component instanceof GridField_URLHandler && $urlHandlers = $component->/** @scrutinizer ignore-call */ getURLHandlers($this)) {
Loading history...
1116
                foreach ($urlHandlers as $rule => $action) {
1117
                    if ($params = $request->match($rule, true)) {
1118
                        // Actions can reference URL parameters.
1119
                        // e.g. '$Action/$ID/$OtherID' → '$Action'
1120
1121
                        if ($action[0] == '$') {
1122
                            $action = $params[substr($action, 1)];
1123
                        }
1124
1125
                        if (!method_exists($component, 'checkAccessAction') || $component->checkAccessAction($action)) {
0 ignored issues
show
Bug introduced by
The method checkAccessAction() does not exist on SilverStripe\View\ArrayData. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1125
                        if (!method_exists($component, 'checkAccessAction') || $component->/** @scrutinizer ignore-call */ checkAccessAction($action)) {
Loading history...
1126
                            if (!$action) {
1127
                                $action = "index";
1128
                            }
1129
1130
                            if (!is_string($action)) {
1131
                                throw new LogicException(sprintf(
1132
                                    'Non-string method name: %s',
1133
                                    var_export($action, true)
1134
                                ));
1135
                            }
1136
1137
                            try {
1138
                                $this->extend('beforeCallActionURLHandler', $request, $action);
1139
1140
                                $result = $component->$action($this, $request);
1141
1142
                                $this->extend('afterCallActionURLHandler', $request, $action, $result);
1143
                            } catch (HTTPResponse_Exception $responseException) {
1144
                                $result = $responseException->getResponse();
1145
                            }
1146
1147
                            if ($result instanceof HTTPResponse && $result->isError()) {
1148
                                return $result;
1149
                            }
1150
1151
                            if ($this !== $result &&
1152
                                !$request->isEmptyPattern($rule) &&
1153
                                ($result instanceof RequestHandler || $result instanceof HasRequestHandler)
1154
                            ) {
1155
                                if ($result instanceof HasRequestHandler) {
1156
                                    $result = $result->getRequestHandler();
1157
                                }
1158
                                $returnValue = $result->handleRequest($request);
1159
1160
                                if (is_array($returnValue)) {
1161
                                    throw new LogicException(
1162
                                        'GridField_URLHandler handlers can\'t return arrays'
1163
                                    );
1164
                                }
1165
1166
                                return $returnValue;
1167
                            }
1168
1169
                            if ($request->allParsed()) {
1170
                                return $result;
1171
                            }
1172
1173
                            return $this->httpError(
1174
                                404,
1175
                                sprintf(
1176
                                    'I can\'t handle sub-URLs of a %s object.',
1177
                                    get_class($result)
1178
                                )
1179
                            );
1180
                        }
1181
                    }
1182
                }
1183
            }
1184
        }
1185
1186
        return parent::handleRequest($request);
1187
    }
1188
1189
    /**
1190
     * {@inheritdoc}
1191
     */
1192
    public function saveInto(DataObjectInterface $record)
1193
    {
1194
        foreach ($this->getComponents() as $component) {
1195
            if ($component instanceof GridField_SaveHandler) {
1196
                $component->handleSave($this, $record);
0 ignored issues
show
Bug introduced by
The method handleSave() does not exist on SilverStripe\View\ArrayData. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1196
                $component->/** @scrutinizer ignore-call */ 
1197
                            handleSave($this, $record);
Loading history...
1197
            }
1198
        }
1199
    }
1200
1201
    /**
1202
     * @param array $content
1203
     *
1204
     * @return string
1205
     */
1206
    protected function getOptionalTableHeader(array $content)
1207
    {
1208
        if ($content['header']) {
1209
            return HTML::createTag(
1210
                'thead',
1211
                array(),
1212
                $content['header']
1213
            );
1214
        }
1215
1216
        return '';
1217
    }
1218
1219
    /**
1220
     * @param array $content
1221
     *
1222
     * @return string
1223
     */
1224
    protected function getOptionalTableBody(array $content)
1225
    {
1226
        if ($content['body']) {
1227
            return HTML::createTag(
1228
                'tbody',
1229
                array('class' => 'ss-gridfield-items'),
1230
                $content['body']
1231
            );
1232
        }
1233
1234
        return '';
1235
    }
1236
1237
    /**
1238
     * @param $content
1239
     *
1240
     * @return string
1241
     */
1242
    protected function getOptionalTableFooter($content)
1243
    {
1244
        if ($content['footer']) {
1245
            return HTML::createTag(
1246
                'tfoot',
1247
                array(),
1248
                $content['footer']
1249
            );
1250
        }
1251
1252
        return '';
1253
    }
1254
}
1255