Passed
Push — master ( bad1ac...1cdfe7 )
by Robbie
15:25 queued 05:45
created

GridField::performReadonlyTransformation()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 0
dl 0
loc 15
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 = array(
114
        GridField_ActionMenu::class,
115
        GridState_Component::class,
116
        GridFieldConfig_RecordViewer::class,
117
        GridFieldDetailForm::class,
118
        GridFieldDataColumns::class,
119
        GridFieldPageCount::class,
120
        GridFieldPaginator::class,
121
        GridFieldSortableHeader::class,
122
        GridFieldToolbarHeader::class,
123
        GridFieldViewButton::class,
124
    );
125
126
    /**
127
     * Pattern used for looking up
128
     */
129
    const FRAGMENT_REGEX = '/\$DefineFragment\(([a-z0-9\-_]+)\)/i';
130
131
    /**
132
     * @param string $name
133
     * @param string $title
134
     * @param SS_List $dataList
135
     * @param GridFieldConfig $config
136
     */
137
    public function __construct($name, $title = null, SS_List $dataList = null, GridFieldConfig $config = null)
138
    {
139
        parent::__construct($name, $title, null);
140
141
        $this->name = $name;
142
143
        if ($dataList) {
144
            $this->setList($dataList);
145
        }
146
147
        if (!$config) {
148
            $config = GridFieldConfig_Base::create();
149
        }
150
151
        $this->setConfig($config);
152
153
        $this->state = new GridState($this);
154
155
        $this->addExtraClass('grid-field');
156
    }
157
158
    /**
159
     * @param HTTPRequest $request
160
     *
161
     * @return string
162
     */
163
    public function index($request)
164
    {
165
        return $this->gridFieldAlterAction(array(), $this->getForm(), $request);
166
    }
167
168
    /**
169
     * Set the modelClass (data object) that this field will get it column headers from.
170
     *
171
     * If no $displayFields has been set, the display fields will be $summary_fields.
172
     *
173
     * @see GridFieldDataColumns::getDisplayFields()
174
     *
175
     * @param string $modelClassName
176
     *
177
     * @return $this
178
     */
179
    public function setModelClass($modelClassName)
180
    {
181
        $this->modelClassName = $modelClassName;
182
183
        return $this;
184
    }
185
186
    /**
187
     * Returns a data class that is a DataObject type that this GridField should look like.
188
     *
189
     * @return string
190
     *
191
     * @throws LogicException
192
     */
193
    public function getModelClass()
194
    {
195
        if ($this->modelClassName) {
196
            return $this->modelClassName;
197
        }
198
199
        /** @var DataList|ArrayList $list */
200
        $list = $this->list;
201
        if ($list && $list->hasMethod('dataClass')) {
202
            $class = $list->dataClass();
203
204
            if ($class) {
205
                return $class;
206
            }
207
        }
208
209
        throw new LogicException(
210
            'GridField doesn\'t have a modelClassName, so it doesn\'t know the columns of this grid.'
211
        );
212
    }
213
214
    /**
215
     * Overload the readonly components for this gridfield.
216
     *
217
     * @param array $components an array map of component class references to whitelist for a readonly version.
218
     */
219
    public function setReadonlyComponents(array $components)
220
    {
221
        $this->readonlyComponents = $components;
222
    }
223
224
    /**
225
     * Return the readonly components
226
     *
227
     * @return array a map of component classes.
228
     */
229
    public function getReadonlyComponents()
230
    {
231
        return $this->readonlyComponents;
232
    }
233
234
    /**
235
     * Custom Readonly transformation to remove actions which shouldn't be present for a readonly state.
236
     *
237
     * @return GridField
238
     */
239
    public function performReadonlyTransformation()
240
    {
241
        $copy = clone $this;
242
        $copy->setReadonly(true);
243
244
        // get the whitelist for allowable readonly components
245
        $allowedComponents = $this->getReadonlyComponents();
246
        foreach ($this->getConfig()->getComponents() as $component) {
247
            // if a component doesn't exist, remove it from the readonly version.
248
            if (!in_array(get_class($component), $allowedComponents)) {
249
                $copy->getConfig()->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

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

369
                /** @scrutinizer ignore-call */ 
370
                $list = $item->getManipulatedData($this, $list);
Loading history...
370
            }
371
        }
372
373
        return $list;
374
    }
375
376
    /**
377
     * Get the current GridState_Data or the GridState.
378
     *
379
     * @param bool $getData
380
     *
381
     * @return GridState_Data|GridState
382
     */
383
    public function getState($getData = true)
384
    {
385
        if ($getData) {
386
            return $this->state->getData();
387
        }
388
389
        return $this->state;
390
    }
391
392
    /**
393
     * Returns the whole gridfield rendered with all the attached components.
394
     *
395
     * @param array $properties
396
     * @return string
397
     */
398
    public function FieldHolder($properties = array())
399
    {
400
        $columns = $this->getColumns();
401
402
        $list = $this->getManipulatedList();
403
404
        $content = array(
405
            'before' => '',
406
            'after' => '',
407
            'header' => '',
408
            'footer' => '',
409
        );
410
411
        foreach ($this->getComponents() as $item) {
412
            if ($item instanceof GridField_HTMLProvider) {
413
                $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

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

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

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

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

1039
                    return $component->/** @scrutinizer ignore-call */ handleAction($this, $actionName, $arguments, $data);
Loading history...
1040
                }
1041
            }
1042
        }
1043
1044
        throw new InvalidArgumentException(sprintf(
1045
            'Can\'t handle action "%s"',
1046
            $actionName
1047
        ));
1048
    }
1049
1050
    /**
1051
     * Custom request handler that will check component handlers before proceeding to the default
1052
     * implementation.
1053
     *
1054
     * @todo copy less code from RequestHandler.
1055
     *
1056
     * @param HTTPRequest $request
1057
     * @return array|RequestHandler|HTTPResponse|string
1058
     * @throws HTTPResponse_Exception
1059
     */
1060
    public function handleRequest(HTTPRequest $request)
1061
    {
1062
        if ($this->brokenOnConstruct) {
1063
            user_error(
1064
                sprintf(
1065
                    "parent::__construct() needs to be called on %s::__construct()",
1066
                    __CLASS__
1067
                ),
1068
                E_USER_WARNING
1069
            );
1070
        }
1071
1072
        $this->setRequest($request);
1073
1074
        $fieldData = $this->getRequest()->requestVar($this->getName());
1075
1076
        /** @skipUpgrade */
1077
        if ($fieldData && isset($fieldData['GridState'])) {
1078
            $this->getState(false)->setValue($fieldData['GridState']);
1079
        }
1080
1081
        foreach ($this->getComponents() as $component) {
1082
            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

1082
            if ($component instanceof GridField_URLHandler && $urlHandlers = $component->/** @scrutinizer ignore-call */ getURLHandlers($this)) {
Loading history...
1083
                foreach ($urlHandlers as $rule => $action) {
1084
                    if ($params = $request->match($rule, true)) {
1085
                        // Actions can reference URL parameters.
1086
                        // e.g. '$Action/$ID/$OtherID' → '$Action'
1087
1088
                        if ($action[0] == '$') {
1089
                            $action = $params[substr($action, 1)];
1090
                        }
1091
1092
                        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

1092
                        if (!method_exists($component, 'checkAccessAction') || $component->/** @scrutinizer ignore-call */ checkAccessAction($action)) {
Loading history...
1093
                            if (!$action) {
1094
                                $action = "index";
1095
                            }
1096
1097
                            if (!is_string($action)) {
1098
                                throw new LogicException(sprintf(
1099
                                    'Non-string method name: %s',
1100
                                    var_export($action, true)
1101
                                ));
1102
                            }
1103
1104
                            try {
1105
                                $result = $component->$action($this, $request);
1106
                            } catch (HTTPResponse_Exception $responseException) {
1107
                                $result = $responseException->getResponse();
1108
                            }
1109
1110
                            if ($result instanceof HTTPResponse && $result->isError()) {
1111
                                return $result;
1112
                            }
1113
1114
                            if ($this !== $result &&
1115
                                !$request->isEmptyPattern($rule) &&
1116
                                ($result instanceof RequestHandler || $result instanceof HasRequestHandler)
1117
                            ) {
1118
                                if ($result instanceof HasRequestHandler) {
1119
                                    $result = $result->getRequestHandler();
1120
                                }
1121
                                $returnValue = $result->handleRequest($request);
1122
1123
                                if (is_array($returnValue)) {
1124
                                    throw new LogicException(
1125
                                        'GridField_URLHandler handlers can\'t return arrays'
1126
                                    );
1127
                                }
1128
1129
                                return $returnValue;
1130
                            }
1131
1132
                            if ($request->allParsed()) {
1133
                                return $result;
1134
                            }
1135
1136
                            return $this->httpError(
1137
                                404,
1138
                                sprintf(
1139
                                    'I can\'t handle sub-URLs of a %s object.',
1140
                                    get_class($result)
1141
                                )
1142
                            );
1143
                        }
1144
                    }
1145
                }
1146
            }
1147
        }
1148
1149
        return parent::handleRequest($request);
1150
    }
1151
1152
    /**
1153
     * {@inheritdoc}
1154
     */
1155
    public function saveInto(DataObjectInterface $record)
1156
    {
1157
        foreach ($this->getComponents() as $component) {
1158
            if ($component instanceof GridField_SaveHandler) {
1159
                $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

1159
                $component->/** @scrutinizer ignore-call */ 
1160
                            handleSave($this, $record);
Loading history...
1160
            }
1161
        }
1162
    }
1163
1164
    /**
1165
     * @param array $content
1166
     *
1167
     * @return string
1168
     */
1169
    protected function getOptionalTableHeader(array $content)
1170
    {
1171
        if ($content['header']) {
1172
            return HTML::createTag(
1173
                'thead',
1174
                array(),
1175
                $content['header']
1176
            );
1177
        }
1178
1179
        return '';
1180
    }
1181
1182
    /**
1183
     * @param array $content
1184
     *
1185
     * @return string
1186
     */
1187
    protected function getOptionalTableBody(array $content)
1188
    {
1189
        if ($content['body']) {
1190
            return HTML::createTag(
1191
                'tbody',
1192
                array('class' => 'ss-gridfield-items'),
1193
                $content['body']
1194
            );
1195
        }
1196
1197
        return '';
1198
    }
1199
1200
    /**
1201
     * @param $content
1202
     *
1203
     * @return string
1204
     */
1205
    protected function getOptionalTableFooter($content)
1206
    {
1207
        if ($content['footer']) {
1208
            return HTML::createTag(
1209
                'tfoot',
1210
                array(),
1211
                $content['footer']
1212
            );
1213
        }
1214
1215
        return '';
1216
    }
1217
}
1218