Passed
Push — master ( 6410ef...806142 )
by Robbie
48:34 queued 35:53
created

GridField::setReadonly()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

390
                /** @scrutinizer ignore-call */ 
391
                $list = $item->getManipulatedData($this, $list);
Loading history...
391
            }
392
        }
393
394
        return $list;
395
    }
396
397
    /**
398
     * Get the current GridState_Data or the GridState.
399
     *
400
     * @param bool $getData
401
     *
402
     * @return GridState_Data|GridState
403
     */
404
    public function getState($getData = true)
405
    {
406
        if ($getData) {
407
            return $this->state->getData();
408
        }
409
410
        return $this->state;
411
    }
412
413
    /**
414
     * Returns the whole gridfield rendered with all the attached components.
415
     *
416
     * @param array $properties
417
     * @return string
418
     */
419
    public function FieldHolder($properties = array())
420
    {
421
        $columns = $this->getColumns();
422
423
        $list = $this->getManipulatedList();
424
425
        $content = array(
426
            'before' => '',
427
            'after' => '',
428
            'header' => '',
429
            'footer' => '',
430
        );
431
432
        foreach ($this->getComponents() as $item) {
433
            if ($item instanceof GridField_HTMLProvider) {
434
                $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

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

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

965
                /** @scrutinizer ignore-call */ 
966
                $columns = $item->getColumnsHandled($this);
Loading history...
966
967
                foreach ($columns as $column) {
968
                    $this->columnDispatch[$column][] = $item;
969
                }
970
            }
971
        }
972
    }
973
974
    /**
975
     * This is the action that gets executed when a GridField_AlterAction gets clicked.
976
     *
977
     * @param array $data
978
     * @param Form $form
979
     * @param HTTPRequest $request
980
     *
981
     * @return string
982
     */
983
    public function gridFieldAlterAction($data, $form, HTTPRequest $request)
984
    {
985
        $data = $request->requestVars();
986
987
        // Protection against CSRF attacks
988
        $token = $this
989
            ->getForm()
990
            ->getSecurityToken();
991
        if (!$token->checkRequest($request)) {
992
            $this->httpError(400, _t(
993
                "SilverStripe\\Forms\\Form.CSRF_FAILED_MESSAGE",
994
                "There seems to have been a technical problem. Please click the back button, "
995
                . "refresh your browser, and try again."
996
            ));
997
        }
998
999
        $name = $this->getName();
1000
1001
        $fieldData = null;
1002
1003
        if (isset($data[$name])) {
1004
            $fieldData = $data[$name];
1005
        }
1006
1007
        $state = $this->getState(false);
1008
1009
        /** @skipUpgrade */
1010
        if (isset($fieldData['GridState'])) {
1011
            $state->setValue($fieldData['GridState']);
1012
        }
1013
1014
        foreach ($data as $dataKey => $dataValue) {
1015
            if (preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) {
1016
                $stateChange = $request->getSession()->get($matches[1]);
1017
                $actionName = $stateChange['actionName'];
1018
1019
                $arguments = array();
1020
1021
                if (isset($stateChange['args'])) {
1022
                    $arguments = $stateChange['args'];
1023
                };
1024
1025
                $html = $this->handleAlterAction($actionName, $arguments, $data);
1026
1027
                if ($html) {
1028
                    return $html;
1029
                }
1030
            }
1031
        }
1032
1033
        if ($request->getHeader('X-Pjax') === 'CurrentField') {
1034
            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...
1035
                $this->performDisabledTransformation();
1036
            }
1037
            return $this->FieldHolder();
1038
        }
1039
1040
        return $form->forTemplate();
1041
    }
1042
1043
    /**
1044
     * Pass an action on the first GridField_ActionProvider that matches the $actionName.
1045
     *
1046
     * @param string $actionName
1047
     * @param mixed $arguments
1048
     * @param array $data
1049
     *
1050
     * @return mixed
1051
     *
1052
     * @throws InvalidArgumentException
1053
     */
1054
    public function handleAlterAction($actionName, $arguments, $data)
1055
    {
1056
        $actionName = strtolower($actionName);
1057
1058
        foreach ($this->getComponents() as $component) {
1059
            if ($component instanceof GridField_ActionProvider) {
1060
                $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

1060
                $actions = array_map('strtolower', (array) $component->/** @scrutinizer ignore-call */ getActions($this));
Loading history...
1061
1062
                if (in_array($actionName, $actions)) {
1063
                    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

1063
                    return $component->/** @scrutinizer ignore-call */ handleAction($this, $actionName, $arguments, $data);
Loading history...
1064
                }
1065
            }
1066
        }
1067
1068
        throw new InvalidArgumentException(sprintf(
1069
            'Can\'t handle action "%s"',
1070
            $actionName
1071
        ));
1072
    }
1073
1074
    /**
1075
     * Custom request handler that will check component handlers before proceeding to the default
1076
     * implementation.
1077
     *
1078
     * @todo copy less code from RequestHandler.
1079
     *
1080
     * @param HTTPRequest $request
1081
     * @return array|RequestHandler|HTTPResponse|string
1082
     * @throws HTTPResponse_Exception
1083
     */
1084
    public function handleRequest(HTTPRequest $request)
1085
    {
1086
        if ($this->brokenOnConstruct) {
1087
            user_error(
1088
                sprintf(
1089
                    "parent::__construct() needs to be called on %s::__construct()",
1090
                    __CLASS__
1091
                ),
1092
                E_USER_WARNING
1093
            );
1094
        }
1095
1096
        $this->setRequest($request);
1097
1098
        $fieldData = $this->getRequest()->requestVar($this->getName());
1099
1100
        /** @skipUpgrade */
1101
        if ($fieldData && isset($fieldData['GridState'])) {
1102
            $this->getState(false)->setValue($fieldData['GridState']);
1103
        }
1104
1105
        foreach ($this->getComponents() as $component) {
1106
            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

1106
            if ($component instanceof GridField_URLHandler && $urlHandlers = $component->/** @scrutinizer ignore-call */ getURLHandlers($this)) {
Loading history...
1107
                foreach ($urlHandlers as $rule => $action) {
1108
                    if ($params = $request->match($rule, true)) {
1109
                        // Actions can reference URL parameters.
1110
                        // e.g. '$Action/$ID/$OtherID' → '$Action'
1111
1112
                        if ($action[0] == '$') {
1113
                            $action = $params[substr($action, 1)];
1114
                        }
1115
1116
                        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

1116
                        if (!method_exists($component, 'checkAccessAction') || $component->/** @scrutinizer ignore-call */ checkAccessAction($action)) {
Loading history...
1117
                            if (!$action) {
1118
                                $action = "index";
1119
                            }
1120
1121
                            if (!is_string($action)) {
1122
                                throw new LogicException(sprintf(
1123
                                    'Non-string method name: %s',
1124
                                    var_export($action, true)
1125
                                ));
1126
                            }
1127
1128
                            try {
1129
                                $result = $component->$action($this, $request);
1130
                            } catch (HTTPResponse_Exception $responseException) {
1131
                                $result = $responseException->getResponse();
1132
                            }
1133
1134
                            if ($result instanceof HTTPResponse && $result->isError()) {
1135
                                return $result;
1136
                            }
1137
1138
                            if ($this !== $result &&
1139
                                !$request->isEmptyPattern($rule) &&
1140
                                ($result instanceof RequestHandler || $result instanceof HasRequestHandler)
1141
                            ) {
1142
                                if ($result instanceof HasRequestHandler) {
1143
                                    $result = $result->getRequestHandler();
1144
                                }
1145
                                $returnValue = $result->handleRequest($request);
1146
1147
                                if (is_array($returnValue)) {
1148
                                    throw new LogicException(
1149
                                        'GridField_URLHandler handlers can\'t return arrays'
1150
                                    );
1151
                                }
1152
1153
                                return $returnValue;
1154
                            }
1155
1156
                            if ($request->allParsed()) {
1157
                                return $result;
1158
                            }
1159
1160
                            return $this->httpError(
1161
                                404,
1162
                                sprintf(
1163
                                    'I can\'t handle sub-URLs of a %s object.',
1164
                                    get_class($result)
1165
                                )
1166
                            );
1167
                        }
1168
                    }
1169
                }
1170
            }
1171
        }
1172
1173
        return parent::handleRequest($request);
1174
    }
1175
1176
    /**
1177
     * {@inheritdoc}
1178
     */
1179
    public function saveInto(DataObjectInterface $record)
1180
    {
1181
        foreach ($this->getComponents() as $component) {
1182
            if ($component instanceof GridField_SaveHandler) {
1183
                $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

1183
                $component->/** @scrutinizer ignore-call */ 
1184
                            handleSave($this, $record);
Loading history...
1184
            }
1185
        }
1186
    }
1187
1188
    /**
1189
     * @param array $content
1190
     *
1191
     * @return string
1192
     */
1193
    protected function getOptionalTableHeader(array $content)
1194
    {
1195
        if ($content['header']) {
1196
            return HTML::createTag(
1197
                'thead',
1198
                array(),
1199
                $content['header']
1200
            );
1201
        }
1202
1203
        return '';
1204
    }
1205
1206
    /**
1207
     * @param array $content
1208
     *
1209
     * @return string
1210
     */
1211
    protected function getOptionalTableBody(array $content)
1212
    {
1213
        if ($content['body']) {
1214
            return HTML::createTag(
1215
                'tbody',
1216
                array('class' => 'ss-gridfield-items'),
1217
                $content['body']
1218
            );
1219
        }
1220
1221
        return '';
1222
    }
1223
1224
    /**
1225
     * @param $content
1226
     *
1227
     * @return string
1228
     */
1229
    protected function getOptionalTableFooter($content)
1230
    {
1231
        if ($content['footer']) {
1232
            return HTML::createTag(
1233
                'tfoot',
1234
                array(),
1235
                $content['footer']
1236
            );
1237
        }
1238
1239
        return '';
1240
    }
1241
}
1242