Passed
Push — master ( 288a96...964b09 )
by Daniel
10:30
created

src/Forms/GridField/GridField.php (1 issue)

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
     * Pattern used for looking up
110
     */
111
    const FRAGMENT_REGEX = '/\$DefineFragment\(([a-z0-9\-_]+)\)/i';
112
113
    /**
114
     * @param string $name
115
     * @param string $title
116
     * @param SS_List $dataList
117
     * @param GridFieldConfig $config
118
     */
119
    public function __construct($name, $title = null, SS_List $dataList = null, GridFieldConfig $config = null)
120
    {
121
        parent::__construct($name, $title, null);
122
123
        $this->name = $name;
124
125
        if ($dataList) {
126
            $this->setList($dataList);
127
        }
128
129
        if (!$config) {
130
            $config = GridFieldConfig_Base::create();
131
        }
132
133
        $this->setConfig($config);
134
135
        $this->state = new GridState($this);
136
137
        $this->addExtraClass('grid-field');
138
    }
139
140
    /**
141
     * @param HTTPRequest $request
142
     *
143
     * @return string
144
     */
145
    public function index($request)
146
    {
147
        return $this->gridFieldAlterAction(array(), $this->getForm(), $request);
148
    }
149
150
    /**
151
     * Set the modelClass (data object) that this field will get it column headers from.
152
     *
153
     * If no $displayFields has been set, the display fields will be $summary_fields.
154
     *
155
     * @see GridFieldDataColumns::getDisplayFields()
156
     *
157
     * @param string $modelClassName
158
     *
159
     * @return $this
160
     */
161
    public function setModelClass($modelClassName)
162
    {
163
        $this->modelClassName = $modelClassName;
164
165
        return $this;
166
    }
167
168
    /**
169
     * Returns a data class that is a DataObject type that this GridField should look like.
170
     *
171
     * @return string
172
     *
173
     * @throws LogicException
174
     */
175
    public function getModelClass()
176
    {
177
        if ($this->modelClassName) {
178
            return $this->modelClassName;
179
        }
180
181
        /** @var DataList|ArrayList $list */
182
        $list = $this->list;
183
        if ($list && $list->hasMethod('dataClass')) {
184
            $class = $list->dataClass();
185
186
            if ($class) {
187
                return $class;
188
            }
189
        }
190
191
        throw new LogicException(
192
            'GridField doesn\'t have a modelClassName, so it doesn\'t know the columns of this grid.'
193
        );
194
    }
195
196
    /**
197
     * @return GridFieldConfig
198
     */
199
    public function getConfig()
200
    {
201
        return $this->config;
202
    }
203
204
    /**
205
     * @param GridFieldConfig $config
206
     *
207
     * @return $this
208
     */
209
    public function setConfig(GridFieldConfig $config)
210
    {
211
        $this->config = $config;
212
213
        if (!$this->config->getComponentByType(GridState_Component::class)) {
214
            $this->config->addComponent(new GridState_Component());
215
        }
216
217
        return $this;
218
    }
219
220
    /**
221
     * @return ArrayList
222
     */
223
    public function getComponents()
224
    {
225
        return $this->config->getComponents();
226
    }
227
228
    /**
229
     * Cast an arbitrary value with the help of a $castingDefinition.
230
     *
231
     * @todo refactor this into GridFieldComponent
232
     *
233
     * @param mixed $value
234
     * @param string|array $castingDefinition
235
     *
236
     * @return mixed
237
     */
238
    public function getCastedValue($value, $castingDefinition)
239
    {
240
        $castingParams = array();
241
242
        if (is_array($castingDefinition)) {
243
            $castingParams = $castingDefinition;
244
            array_shift($castingParams);
245
            $castingDefinition = array_shift($castingDefinition);
246
        }
247
248
        if (strpos($castingDefinition, '->') === false) {
249
            $castingFieldType = $castingDefinition;
250
            $castingField = DBField::create_field($castingFieldType, $value);
251
252
            return call_user_func_array(array($castingField, 'XML'), $castingParams);
253
        }
254
255
        list($castingFieldType, $castingMethod) = explode('->', $castingDefinition);
256
257
        $castingField = DBField::create_field($castingFieldType, $value);
258
259
        return call_user_func_array(array($castingField, $castingMethod), $castingParams);
260
    }
261
262
    /**
263
     * Set the data source.
264
     *
265
     * @param SS_List $list
266
     *
267
     * @return $this
268
     */
269
    public function setList(SS_List $list)
270
    {
271
        $this->list = $list;
272
273
        return $this;
274
    }
275
276
    /**
277
     * Get the data source.
278
     *
279
     * @return SS_List
280
     */
281
    public function getList()
282
    {
283
        return $this->list;
284
    }
285
286
    /**
287
     * Get the data source after applying every {@link GridField_DataManipulator} to it.
288
     *
289
     * @return SS_List
290
     */
291
    public function getManipulatedList()
292
    {
293
        $list = $this->getList();
294
295
        foreach ($this->getComponents() as $item) {
296
            if ($item instanceof GridField_DataManipulator) {
297
                $list = $item->getManipulatedData($this, $list);
298
            }
299
        }
300
301
        return $list;
302
    }
303
304
    /**
305
     * Get the current GridState_Data or the GridState.
306
     *
307
     * @param bool $getData
308
     *
309
     * @return GridState_Data|GridState
310
     */
311
    public function getState($getData = true)
312
    {
313
        if ($getData) {
314
            return $this->state->getData();
315
        }
316
317
        return $this->state;
318
    }
319
320
    /**
321
     * Returns the whole gridfield rendered with all the attached components.
322
     *
323
     * @param array $properties
324
     * @return string
325
     */
326
    public function FieldHolder($properties = array())
327
    {
328
        $columns = $this->getColumns();
329
330
        $list = $this->getManipulatedList();
331
332
        $content = array(
333
            'before' => '',
334
            'after' => '',
335
            'header' => '',
336
            'footer' => '',
337
        );
338
339
        foreach ($this->getComponents() as $item) {
340
            if ($item instanceof GridField_HTMLProvider) {
341
                $fragments = $item->getHTMLFragments($this);
342
343
                if ($fragments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fragments 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...
344
                    foreach ($fragments as $fragmentKey => $fragmentValue) {
345
                        $fragmentKey = strtolower($fragmentKey);
346
347
                        if (!isset($content[$fragmentKey])) {
348
                            $content[$fragmentKey] = '';
349
                        }
350
351
                        $content[$fragmentKey] .= $fragmentValue . "\n";
352
                    }
353
                }
354
            }
355
        }
356
357
        foreach ($content as $contentKey => $contentValue) {
358
            $content[$contentKey] = trim($contentValue);
359
        }
360
361
        // Replace custom fragments and check which fragments are defined. Circular dependencies
362
        // are detected by disallowing any item to be deferred more than 5 times.
363
364
        $fragmentDefined = array(
365
            'header' => true,
366
            'footer' => true,
367
            'before' => true,
368
            'after' => true,
369
        );
370
        $fragmentDeferred = [];
371
372
        // TODO: Break the below into separate reducer methods
373
374
        // Continue looping if any placeholders exist
375
        while (array_filter($content, function ($value) {
376
            return preg_match(self::FRAGMENT_REGEX, $value);
377
        })) {
378
            foreach ($content as $contentKey => $contentValue) {
379
                // Skip if this specific content has no placeholders
380
                if (!preg_match_all(self::FRAGMENT_REGEX, $contentValue, $matches)) {
381
                    continue;
382
                }
383
                foreach ($matches[1] as $match) {
384
                    $fragmentName = strtolower($match);
385
                    $fragmentDefined[$fragmentName] = true;
386
387
                    $fragment = '';
388
389
                    if (isset($content[$fragmentName])) {
390
                        $fragment = $content[$fragmentName];
391
                    }
392
393
                    // If the fragment still has a fragment definition in it, when we should defer
394
                    // this item until later.
395
396
                    if (preg_match(self::FRAGMENT_REGEX, $fragment, $matches)) {
397
                        if (isset($fragmentDeferred[$contentKey]) && $fragmentDeferred[$contentKey] > 5) {
398
                            throw new LogicException(sprintf(
399
                                'GridField HTML fragment "%s" and "%s" appear to have a circular dependency.',
400
                                $fragmentName,
401
                                $matches[1]
402
                            ));
403
                        }
404
405
                        unset($content[$contentKey]);
406
407
                        $content[$contentKey] = $contentValue;
408
409
                        if (!isset($fragmentDeferred[$contentKey])) {
410
                            $fragmentDeferred[$contentKey] = 0;
411
                        }
412
413
                        $fragmentDeferred[$contentKey]++;
414
415
                        break;
416
                    } else {
417
                        $content[$contentKey] = preg_replace(
418
                            sprintf('/\$DefineFragment\(%s\)/i', $fragmentName),
419
                            $fragment,
420
                            $content[$contentKey]
421
                        );
422
                    }
423
                }
424
            }
425
        }
426
427
        // Check for any undefined fragments, and if so throw an exception.
428
        // While we're at it, trim whitespace off the elements.
429
430
        foreach ($content as $contentKey => $contentValue) {
431
            if (empty($fragmentDefined[$contentKey])) {
432
                throw new LogicException(sprintf(
433
                    'GridField HTML fragment "%s" was given content, but not defined. Perhaps there is a supporting GridField component you need to add?',
434
                    $contentKey
435
                ));
436
            }
437
        }
438
439
        $total = count($list);
440
441
        if ($total > 0) {
442
            $rows = array();
443
444
            foreach ($list as $index => $record) {
445
                if ($record->hasMethod('canView') && !$record->canView()) {
446
                    continue;
447
                }
448
449
                $rowContent = '';
450
451
                foreach ($this->getColumns() as $column) {
452
                    $colContent = $this->getColumnContent($record, $column);
453
454
                    // Null means this columns should be skipped altogether.
455
456
                    if ($colContent === null) {
457
                        continue;
458
                    }
459
460
                    $colAttributes = $this->getColumnAttributes($record, $column);
461
462
                    $rowContent .= $this->newCell(
463
                        $total,
464
                        $index,
465
                        $record,
466
                        $colAttributes,
467
                        $colContent
468
                    );
469
                }
470
471
                $rowAttributes = $this->getRowAttributes($total, $index, $record);
472
473
                $rows[] = $this->newRow($total, $index, $record, $rowAttributes, $rowContent);
474
            }
475
            $content['body'] = implode("\n", $rows);
476
        }
477
478
        // Display a message when the grid field is empty.
479
        if (empty($content['body'])) {
480
            $cell = HTML::createTag(
481
                'td',
482
                array(
483
                    'colspan' => count($columns),
484
                ),
485
                _t('SilverStripe\\Forms\\GridField\\GridField.NoItemsFound', 'No items found')
486
            );
487
488
            $row = HTML::createTag(
489
                'tr',
490
                array(
491
                    'class' => 'ss-gridfield-item ss-gridfield-no-items',
492
                ),
493
                $cell
494
            );
495
496
            $content['body'] = $row;
497
        }
498
499
        $header = $this->getOptionalTableHeader($content);
500
        $body = $this->getOptionalTableBody($content);
501
        $footer = $this->getOptionalTableFooter($content);
502
503
        $this->addExtraClass('ss-gridfield grid-field field');
504
505
        $fieldsetAttributes = array_diff_key(
506
            $this->getAttributes(),
507
            array(
508
                'value' => false,
509
                'type' => false,
510
                'name' => false,
511
            )
512
        );
513
514
        $fieldsetAttributes['data-name'] = $this->getName();
515
516
        $tableId = null;
517
518
        if ($this->id) {
519
            $tableId = $this->id;
520
        }
521
522
        $tableAttributes = array(
523
            'id' => $tableId,
524
            'class' => 'table grid-field__table',
525
            'cellpadding' => '0',
526
            'cellspacing' => '0'
527
        );
528
529
        if ($this->getDescription()) {
530
            $content['after'] .= HTML::createTag(
531
                'span',
532
                array('class' => 'description'),
533
                $this->getDescription()
534
            );
535
        }
536
537
        $table = HTML::createTag(
538
            'table',
539
            $tableAttributes,
540
            $header . "\n" . $footer . "\n" . $body
541
        );
542
543
        return HTML::createTag(
544
            'fieldset',
545
            $fieldsetAttributes,
546
            $content['before'] . $table . $content['after']
547
        );
548
    }
549
550
    /**
551
     * @param int $total
552
     * @param int $index
553
     * @param DataObject $record
554
     * @param array $attributes
555
     * @param string $content
556
     *
557
     * @return string
558
     */
559
    protected function newCell($total, $index, $record, $attributes, $content)
560
    {
561
        return HTML::createTag(
562
            'td',
563
            $attributes,
564
            $content
565
        );
566
    }
567
568
    /**
569
     * @param int $total
570
     * @param int $index
571
     * @param DataObject $record
572
     * @param array $attributes
573
     * @param string $content
574
     *
575
     * @return string
576
     */
577
    protected function newRow($total, $index, $record, $attributes, $content)
578
    {
579
        return HTML::createTag(
580
            'tr',
581
            $attributes,
582
            $content
583
        );
584
    }
585
586
    /**
587
     * @param int $total
588
     * @param int $index
589
     * @param DataObject $record
590
     *
591
     * @return array
592
     */
593
    protected function getRowAttributes($total, $index, $record)
594
    {
595
        $rowClasses = $this->newRowClasses($total, $index, $record);
596
597
        return array(
598
            'class' => implode(' ', $rowClasses),
599
            'data-id' => $record->ID,
600
            'data-class' => $record->ClassName,
601
        );
602
    }
603
604
    /**
605
     * @param int $total
606
     * @param int $index
607
     * @param DataObject $record
608
     *
609
     * @return array
610
     */
611
    protected function newRowClasses($total, $index, $record)
612
    {
613
        $classes = array('ss-gridfield-item');
614
615
        if ($index == 0) {
616
            $classes[] = 'first';
617
        }
618
619
        if ($index == $total - 1) {
620
            $classes[] = 'last';
621
        }
622
623
        if ($index % 2) {
624
            $classes[] = 'even';
625
        } else {
626
            $classes[] = 'odd';
627
        }
628
629
        $this->extend('updateNewRowClasses', $classes, $total, $index, $record);
630
631
        return $classes;
632
    }
633
634
    /**
635
     * @param array $properties
636
     * @return string
637
     */
638
    public function Field($properties = array())
639
    {
640
        $this->extend('onBeforeRender', $this);
641
        return $this->FieldHolder($properties);
642
    }
643
644
    /**
645
     * {@inheritdoc}
646
     */
647
    public function getAttributes()
648
    {
649
        return array_merge(
650
            parent::getAttributes(),
651
            array(
652
                'data-url' => $this->Link(),
653
            )
654
        );
655
    }
656
657
    /**
658
     * Get the columns of this GridField, they are provided by attached GridField_ColumnProvider.
659
     *
660
     * @return array
661
     */
662
    public function getColumns()
663
    {
664
        $columns = array();
665
666
        foreach ($this->getComponents() as $item) {
667
            if ($item instanceof GridField_ColumnProvider) {
668
                $item->augmentColumns($this, $columns);
669
            }
670
        }
671
672
        return $columns;
673
    }
674
675
    /**
676
     * Get the value from a column.
677
     *
678
     * @param DataObject $record
679
     * @param string $column
680
     *
681
     * @return string
682
     *
683
     * @throws InvalidArgumentException
684
     */
685
    public function getColumnContent($record, $column)
686
    {
687
        if (!$this->columnDispatch) {
688
            $this->buildColumnDispatch();
689
        }
690
691
        if (!empty($this->columnDispatch[$column])) {
692
            $content = '';
693
694
            foreach ($this->columnDispatch[$column] as $handler) {
695
                /**
696
                 * @var GridField_ColumnProvider $handler
697
                 */
698
                $content .= $handler->getColumnContent($this, $record, $column);
699
            }
700
701
            return $content;
702
        } else {
703
            throw new InvalidArgumentException(sprintf(
704
                'Bad column "%s"',
705
                $column
706
            ));
707
        }
708
    }
709
710
    /**
711
     * Add additional calculated data fields to be used on this GridField
712
     *
713
     * @param array $fields a map of fieldname to callback. The callback will
714
     *                      be passed the record as an argument.
715
     */
716
    public function addDataFields($fields)
717
    {
718
        if ($this->customDataFields) {
719
            $this->customDataFields = array_merge($this->customDataFields, $fields);
720
        } else {
721
            $this->customDataFields = $fields;
722
        }
723
    }
724
725
    /**
726
     * Get the value of a named field  on the given record.
727
     *
728
     * Use of this method ensures that any special rules around the data for this gridfield are
729
     * followed.
730
     *
731
     * @param DataObject $record
732
     * @param string $fieldName
733
     *
734
     * @return mixed
735
     */
736
    public function getDataFieldValue($record, $fieldName)
737
    {
738
        if (isset($this->customDataFields[$fieldName])) {
739
            $callback = $this->customDataFields[$fieldName];
740
741
            return $callback($record);
742
        }
743
744
        if ($record->hasMethod('relField')) {
745
            return $record->relField($fieldName);
746
        }
747
748
        if ($record->hasMethod($fieldName)) {
749
            return $record->$fieldName();
750
        }
751
752
        return $record->$fieldName;
753
    }
754
755
    /**
756
     * Get extra columns attributes used as HTML attributes.
757
     *
758
     * @param DataObject $record
759
     * @param string $column
760
     *
761
     * @return array
762
     *
763
     * @throws LogicException
764
     * @throws InvalidArgumentException
765
     */
766
    public function getColumnAttributes($record, $column)
767
    {
768
        if (!$this->columnDispatch) {
769
            $this->buildColumnDispatch();
770
        }
771
772
        if (!empty($this->columnDispatch[$column])) {
773
            $attributes = array();
774
775
            foreach ($this->columnDispatch[$column] as $handler) {
776
                /**
777
                 * @var GridField_ColumnProvider $handler
778
                 */
779
                $columnAttributes = $handler->getColumnAttributes($this, $record, $column);
780
781
                if (is_array($columnAttributes)) {
782
                    $attributes = array_merge($attributes, $columnAttributes);
783
                    continue;
784
                }
785
786
                throw new LogicException(sprintf(
787
                    'Non-array response from %s::getColumnAttributes().',
788
                    get_class($handler)
789
                ));
790
            }
791
792
            return $attributes;
793
        }
794
795
        throw new InvalidArgumentException(sprintf(
796
            'Bad column "%s"',
797
            $column
798
        ));
799
    }
800
801
    /**
802
     * Get metadata for a column.
803
     *
804
     * @example "array('Title'=>'Email address')"
805
     *
806
     * @param string $column
807
     *
808
     * @return array
809
     *
810
     * @throws LogicException
811
     * @throws InvalidArgumentException
812
     */
813
    public function getColumnMetadata($column)
814
    {
815
        if (!$this->columnDispatch) {
816
            $this->buildColumnDispatch();
817
        }
818
819
        if (!empty($this->columnDispatch[$column])) {
820
            $metaData = array();
821
822
            foreach ($this->columnDispatch[$column] as $handler) {
823
                /**
824
                 * @var GridField_ColumnProvider $handler
825
                 */
826
                $columnMetaData = $handler->getColumnMetadata($this, $column);
827
828
                if (is_array($columnMetaData)) {
829
                    $metaData = array_merge($metaData, $columnMetaData);
830
                    continue;
831
                }
832
833
                throw new LogicException(sprintf(
834
                    'Non-array response from %s::getColumnMetadata().',
835
                    get_class($handler)
836
                ));
837
            }
838
839
            return $metaData;
840
        }
841
842
        throw new InvalidArgumentException(sprintf(
843
            'Bad column "%s"',
844
            $column
845
        ));
846
    }
847
848
    /**
849
     * Return how many columns the grid will have.
850
     *
851
     * @return int
852
     */
853
    public function getColumnCount()
854
    {
855
        if (!$this->columnDispatch) {
856
            $this->buildColumnDispatch();
857
        }
858
859
        return count($this->columnDispatch);
860
    }
861
862
    /**
863
     * Build an columnDispatch that maps a GridField_ColumnProvider to a column for reference later.
864
     */
865
    protected function buildColumnDispatch()
866
    {
867
        $this->columnDispatch = array();
868
869
        foreach ($this->getComponents() as $item) {
870
            if ($item instanceof GridField_ColumnProvider) {
871
                $columns = $item->getColumnsHandled($this);
872
873
                foreach ($columns as $column) {
874
                    $this->columnDispatch[$column][] = $item;
875
                }
876
            }
877
        }
878
    }
879
880
    /**
881
     * This is the action that gets executed when a GridField_AlterAction gets clicked.
882
     *
883
     * @param array $data
884
     * @param Form $form
885
     * @param HTTPRequest $request
886
     *
887
     * @return string
888
     */
889
    public function gridFieldAlterAction($data, $form, HTTPRequest $request)
890
    {
891
        $data = $request->requestVars();
892
893
        // Protection against CSRF attacks
894
        $token = $this
895
            ->getForm()
896
            ->getSecurityToken();
897
        if (!$token->checkRequest($request)) {
898
            $this->httpError(400, _t(
899
                "SilverStripe\\Forms\\Form.CSRF_FAILED_MESSAGE",
900
                "There seems to have been a technical problem. Please click the back button, " . "refresh your browser, and try again."
901
            ));
902
        }
903
904
        $name = $this->getName();
905
906
        $fieldData = null;
907
908
        if (isset($data[$name])) {
909
            $fieldData = $data[$name];
910
        }
911
912
        $state = $this->getState(false);
913
914
        /** @skipUpgrade */
915
        if (isset($fieldData['GridState'])) {
916
            $state->setValue($fieldData['GridState']);
917
        }
918
919
        foreach ($data as $dataKey => $dataValue) {
920
            if (preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) {
921
                $stateChange = $request->getSession()->get($matches[1]);
922
                $actionName = $stateChange['actionName'];
923
924
                $arguments = array();
925
926
                if (isset($stateChange['args'])) {
927
                    $arguments = $stateChange['args'];
928
                };
929
930
                $html = $this->handleAlterAction($actionName, $arguments, $data);
931
932
                if ($html) {
933
                    return $html;
934
                }
935
            }
936
        }
937
938
        if ($request->getHeader('X-Pjax') === 'CurrentField') {
939
            return $this->FieldHolder();
940
        }
941
942
        return $form->forTemplate();
943
    }
944
945
    /**
946
     * Pass an action on the first GridField_ActionProvider that matches the $actionName.
947
     *
948
     * @param string $actionName
949
     * @param mixed $arguments
950
     * @param array $data
951
     *
952
     * @return mixed
953
     *
954
     * @throws InvalidArgumentException
955
     */
956
    public function handleAlterAction($actionName, $arguments, $data)
957
    {
958
        $actionName = strtolower($actionName);
959
960
        foreach ($this->getComponents() as $component) {
961
            if ($component instanceof GridField_ActionProvider) {
962
                $actions = array_map('strtolower', (array) $component->getActions($this));
963
964
                if (in_array($actionName, $actions)) {
965
                    return $component->handleAction($this, $actionName, $arguments, $data);
966
                }
967
            }
968
        }
969
970
        throw new InvalidArgumentException(sprintf(
971
            'Can\'t handle action "%s"',
972
            $actionName
973
        ));
974
    }
975
976
    /**
977
     * Custom request handler that will check component handlers before proceeding to the default
978
     * implementation.
979
     *
980
     * @todo copy less code from RequestHandler.
981
     *
982
     * @param HTTPRequest $request
983
     * @return array|RequestHandler|HTTPResponse|string
984
     * @throws HTTPResponse_Exception
985
     */
986
    public function handleRequest(HTTPRequest $request)
987
    {
988
        if ($this->brokenOnConstruct) {
989
            user_error(
990
                sprintf(
991
                    "parent::__construct() needs to be called on %s::__construct()",
992
                    __CLASS__
993
                ),
994
                E_USER_WARNING
995
            );
996
        }
997
998
        $this->setRequest($request);
999
1000
        $fieldData = $this->getRequest()->requestVar($this->getName());
1001
1002
        /** @skipUpgrade */
1003
        if ($fieldData && isset($fieldData['GridState'])) {
1004
            $this->getState(false)->setValue($fieldData['GridState']);
1005
        }
1006
1007
        foreach ($this->getComponents() as $component) {
1008
            if ($component instanceof GridField_URLHandler && $urlHandlers = $component->getURLHandlers($this)) {
1009
                foreach ($urlHandlers as $rule => $action) {
1010
                    if ($params = $request->match($rule, true)) {
1011
                        // Actions can reference URL parameters.
1012
                        // e.g. '$Action/$ID/$OtherID' → '$Action'
1013
1014
                        if ($action[0] == '$') {
1015
                            $action = $params[substr($action, 1)];
1016
                        }
1017
1018
                        if (!method_exists($component, 'checkAccessAction') || $component->checkAccessAction($action)) {
1019
                            if (!$action) {
1020
                                $action = "index";
1021
                            }
1022
1023
                            if (!is_string($action)) {
1024
                                throw new LogicException(sprintf(
1025
                                    'Non-string method name: %s',
1026
                                    var_export($action, true)
1027
                                ));
1028
                            }
1029
1030
                            try {
1031
                                $result = $component->$action($this, $request);
1032
                            } catch (HTTPResponse_Exception $responseException) {
1033
                                $result = $responseException->getResponse();
1034
                            }
1035
1036
                            if ($result instanceof HTTPResponse && $result->isError()) {
1037
                                return $result;
1038
                            }
1039
1040
                            if ($this !== $result &&
1041
                                !$request->isEmptyPattern($rule) &&
1042
                                ($result instanceof RequestHandler || $result instanceof HasRequestHandler)
1043
                            ) {
1044
                                if ($result instanceof HasRequestHandler) {
1045
                                    $result = $result->getRequestHandler();
1046
                                }
1047
                                $returnValue = $result->handleRequest($request);
1048
1049
                                if (is_array($returnValue)) {
1050
                                    throw new LogicException(
1051
                                        'GridField_URLHandler handlers can\'t return arrays'
1052
                                    );
1053
                                }
1054
1055
                                return $returnValue;
1056
                            }
1057
1058
                            if ($request->allParsed()) {
1059
                                return $result;
1060
                            }
1061
1062
                            return $this->httpError(
1063
                                404,
1064
                                sprintf(
1065
                                    'I can\'t handle sub-URLs of a %s object.',
1066
                                    get_class($result)
1067
                                )
1068
                            );
1069
                        }
1070
                    }
1071
                }
1072
            }
1073
        }
1074
1075
        return parent::handleRequest($request);
1076
    }
1077
1078
    /**
1079
     * {@inheritdoc}
1080
     */
1081
    public function saveInto(DataObjectInterface $record)
1082
    {
1083
        foreach ($this->getComponents() as $component) {
1084
            if ($component instanceof GridField_SaveHandler) {
1085
                $component->handleSave($this, $record);
1086
            }
1087
        }
1088
    }
1089
1090
    /**
1091
     * @param array $content
1092
     *
1093
     * @return string
1094
     */
1095
    protected function getOptionalTableHeader(array $content)
1096
    {
1097
        if ($content['header']) {
1098
            return HTML::createTag(
1099
                'thead',
1100
                array(),
1101
                $content['header']
1102
            );
1103
        }
1104
1105
        return '';
1106
    }
1107
1108
    /**
1109
     * @param array $content
1110
     *
1111
     * @return string
1112
     */
1113
    protected function getOptionalTableBody(array $content)
1114
    {
1115
        if ($content['body']) {
1116
            return HTML::createTag(
1117
                'tbody',
1118
                array('class' => 'ss-gridfield-items'),
1119
                $content['body']
1120
            );
1121
        }
1122
1123
        return '';
1124
    }
1125
1126
    /**
1127
     * @param $content
1128
     *
1129
     * @return string
1130
     */
1131
    protected function getOptionalTableFooter($content)
1132
    {
1133
        if ($content['footer']) {
1134
            return HTML::createTag(
1135
                'tfoot',
1136
                array(),
1137
                $content['footer']
1138
            );
1139
        }
1140
1141
        return '';
1142
    }
1143
}
1144