Completed
Push — 4.2 ( 270aba...4ac4cd )
by Luke
45s queued 18s
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);
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);
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);
414
415
                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...
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 supporting GridField component you need to add?',
506
                    $contentKey
507
                ));
508
            }
509
        }
510
511
        $total = count($list);
512
513
        if ($total > 0) {
514
            $rows = array();
515
516
            foreach ($list as $index => $record) {
517
                if ($record->hasMethod('canView') && !$record->canView()) {
518
                    continue;
519
                }
520
521
                $rowContent = '';
522
523
                foreach ($this->getColumns() as $column) {
524
                    $colContent = $this->getColumnContent($record, $column);
525
526
                    // Null means this columns should be skipped altogether.
527
528
                    if ($colContent === null) {
529
                        continue;
530
                    }
531
532
                    $colAttributes = $this->getColumnAttributes($record, $column);
533
534
                    $rowContent .= $this->newCell(
535
                        $total,
536
                        $index,
537
                        $record,
538
                        $colAttributes,
539
                        $colContent
540
                    );
541
                }
542
543
                $rowAttributes = $this->getRowAttributes($total, $index, $record);
544
545
                $rows[] = $this->newRow($total, $index, $record, $rowAttributes, $rowContent);
546
            }
547
            $content['body'] = implode("\n", $rows);
548
        }
549
550
        // Display a message when the grid field is empty.
551
        if (empty($content['body'])) {
552
            $cell = HTML::createTag(
553
                'td',
554
                array(
555
                    'colspan' => count($columns),
556
                ),
557
                _t('SilverStripe\\Forms\\GridField\\GridField.NoItemsFound', 'No items found')
558
            );
559
560
            $row = HTML::createTag(
561
                'tr',
562
                array(
563
                    'class' => 'ss-gridfield-item ss-gridfield-no-items',
564
                ),
565
                $cell
566
            );
567
568
            $content['body'] = $row;
569
        }
570
571
        $header = $this->getOptionalTableHeader($content);
572
        $body = $this->getOptionalTableBody($content);
573
        $footer = $this->getOptionalTableFooter($content);
574
575
        $this->addExtraClass('ss-gridfield grid-field field');
576
577
        $fieldsetAttributes = array_diff_key(
578
            $this->getAttributes(),
579
            array(
580
                'value' => false,
581
                'type' => false,
582
                'name' => false,
583
            )
584
        );
585
586
        $fieldsetAttributes['data-name'] = $this->getName();
587
588
        $tableId = null;
589
590
        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...
591
            $tableId = $this->id;
592
        }
593
594
        $tableAttributes = array(
595
            'id' => $tableId,
596
            'class' => 'table grid-field__table',
597
            'cellpadding' => '0',
598
            'cellspacing' => '0'
599
        );
600
601
        if ($this->getDescription()) {
602
            $content['after'] .= HTML::createTag(
603
                'span',
604
                array('class' => 'description'),
605
                $this->getDescription()
606
            );
607
        }
608
609
        $table = HTML::createTag(
610
            'table',
611
            $tableAttributes,
612
            $header . "\n" . $footer . "\n" . $body
613
        );
614
615
        return HTML::createTag(
616
            'fieldset',
617
            $fieldsetAttributes,
618
            $content['before'] . $table . $content['after']
619
        );
620
    }
621
622
    /**
623
     * @param int $total
624
     * @param int $index
625
     * @param DataObject $record
626
     * @param array $attributes
627
     * @param string $content
628
     *
629
     * @return string
630
     */
631
    protected function newCell($total, $index, $record, $attributes, $content)
632
    {
633
        return HTML::createTag(
634
            'td',
635
            $attributes,
636
            $content
637
        );
638
    }
639
640
    /**
641
     * @param int $total
642
     * @param int $index
643
     * @param DataObject $record
644
     * @param array $attributes
645
     * @param string $content
646
     *
647
     * @return string
648
     */
649
    protected function newRow($total, $index, $record, $attributes, $content)
650
    {
651
        return HTML::createTag(
652
            'tr',
653
            $attributes,
654
            $content
655
        );
656
    }
657
658
    /**
659
     * @param int $total
660
     * @param int $index
661
     * @param DataObject $record
662
     *
663
     * @return array
664
     */
665
    protected function getRowAttributes($total, $index, $record)
666
    {
667
        $rowClasses = $this->newRowClasses($total, $index, $record);
668
669
        return array(
670
            'class' => implode(' ', $rowClasses),
671
            'data-id' => $record->ID,
672
            'data-class' => $record->ClassName,
673
        );
674
    }
675
676
    /**
677
     * @param int $total
678
     * @param int $index
679
     * @param DataObject $record
680
     *
681
     * @return array
682
     */
683
    protected function newRowClasses($total, $index, $record)
684
    {
685
        $classes = array('ss-gridfield-item');
686
687
        if ($index == 0) {
688
            $classes[] = 'first';
689
        }
690
691
        if ($index == $total - 1) {
692
            $classes[] = 'last';
693
        }
694
695
        if ($index % 2) {
696
            $classes[] = 'even';
697
        } else {
698
            $classes[] = 'odd';
699
        }
700
701
        $this->extend('updateNewRowClasses', $classes, $total, $index, $record);
702
703
        return $classes;
704
    }
705
706
    /**
707
     * @param array $properties
708
     * @return string
709
     */
710
    public function Field($properties = array())
711
    {
712
        $this->extend('onBeforeRender', $this);
713
        return $this->FieldHolder($properties);
714
    }
715
716
    /**
717
     * {@inheritdoc}
718
     */
719
    public function getAttributes()
720
    {
721
        return array_merge(
722
            parent::getAttributes(),
723
            array(
724
                'data-url' => $this->Link(),
725
            )
726
        );
727
    }
728
729
    /**
730
     * Get the columns of this GridField, they are provided by attached GridField_ColumnProvider.
731
     *
732
     * @return array
733
     */
734
    public function getColumns()
735
    {
736
        $columns = array();
737
738
        foreach ($this->getComponents() as $item) {
739
            if ($item instanceof GridField_ColumnProvider) {
740
                $item->augmentColumns($this, $columns);
741
            }
742
        }
743
744
        return $columns;
745
    }
746
747
    /**
748
     * Get the value from a column.
749
     *
750
     * @param DataObject $record
751
     * @param string $column
752
     *
753
     * @return string
754
     *
755
     * @throws InvalidArgumentException
756
     */
757
    public function getColumnContent($record, $column)
758
    {
759
        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...
760
            $this->buildColumnDispatch();
761
        }
762
763
        if (!empty($this->columnDispatch[$column])) {
764
            $content = '';
765
766
            foreach ($this->columnDispatch[$column] as $handler) {
767
                /**
768
                 * @var GridField_ColumnProvider $handler
769
                 */
770
                $content .= $handler->getColumnContent($this, $record, $column);
771
            }
772
773
            return $content;
774
        } else {
775
            throw new InvalidArgumentException(sprintf(
776
                'Bad column "%s"',
777
                $column
778
            ));
779
        }
780
    }
781
782
    /**
783
     * Add additional calculated data fields to be used on this GridField
784
     *
785
     * @param array $fields a map of fieldname to callback. The callback will
786
     *                      be passed the record as an argument.
787
     */
788
    public function addDataFields($fields)
789
    {
790
        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...
791
            $this->customDataFields = array_merge($this->customDataFields, $fields);
792
        } else {
793
            $this->customDataFields = $fields;
794
        }
795
    }
796
797
    /**
798
     * Get the value of a named field  on the given record.
799
     *
800
     * Use of this method ensures that any special rules around the data for this gridfield are
801
     * followed.
802
     *
803
     * @param DataObject $record
804
     * @param string $fieldName
805
     *
806
     * @return mixed
807
     */
808
    public function getDataFieldValue($record, $fieldName)
809
    {
810
        if (isset($this->customDataFields[$fieldName])) {
811
            $callback = $this->customDataFields[$fieldName];
812
813
            return $callback($record);
814
        }
815
816
        if ($record->hasMethod('relField')) {
817
            return $record->relField($fieldName);
818
        }
819
820
        if ($record->hasMethod($fieldName)) {
821
            return $record->$fieldName();
822
        }
823
824
        return $record->$fieldName;
825
    }
826
827
    /**
828
     * Get extra columns attributes used as HTML attributes.
829
     *
830
     * @param DataObject $record
831
     * @param string $column
832
     *
833
     * @return array
834
     *
835
     * @throws LogicException
836
     * @throws InvalidArgumentException
837
     */
838
    public function getColumnAttributes($record, $column)
839
    {
840
        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...
841
            $this->buildColumnDispatch();
842
        }
843
844
        if (!empty($this->columnDispatch[$column])) {
845
            $attributes = array();
846
847
            foreach ($this->columnDispatch[$column] as $handler) {
848
                /**
849
                 * @var GridField_ColumnProvider $handler
850
                 */
851
                $columnAttributes = $handler->getColumnAttributes($this, $record, $column);
852
853
                if (is_array($columnAttributes)) {
854
                    $attributes = array_merge($attributes, $columnAttributes);
855
                    continue;
856
                }
857
858
                throw new LogicException(sprintf(
859
                    'Non-array response from %s::getColumnAttributes().',
860
                    get_class($handler)
861
                ));
862
            }
863
864
            return $attributes;
865
        }
866
867
        throw new InvalidArgumentException(sprintf(
868
            'Bad column "%s"',
869
            $column
870
        ));
871
    }
872
873
    /**
874
     * Get metadata for a column.
875
     *
876
     * @example "array('Title'=>'Email address')"
877
     *
878
     * @param string $column
879
     *
880
     * @return array
881
     *
882
     * @throws LogicException
883
     * @throws InvalidArgumentException
884
     */
885
    public function getColumnMetadata($column)
886
    {
887
        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...
888
            $this->buildColumnDispatch();
889
        }
890
891
        if (!empty($this->columnDispatch[$column])) {
892
            $metaData = array();
893
894
            foreach ($this->columnDispatch[$column] as $handler) {
895
                /**
896
                 * @var GridField_ColumnProvider $handler
897
                 */
898
                $columnMetaData = $handler->getColumnMetadata($this, $column);
899
900
                if (is_array($columnMetaData)) {
901
                    $metaData = array_merge($metaData, $columnMetaData);
902
                    continue;
903
                }
904
905
                throw new LogicException(sprintf(
906
                    'Non-array response from %s::getColumnMetadata().',
907
                    get_class($handler)
908
                ));
909
            }
910
911
            return $metaData;
912
        }
913
914
        throw new InvalidArgumentException(sprintf(
915
            'Bad column "%s"',
916
            $column
917
        ));
918
    }
919
920
    /**
921
     * Return how many columns the grid will have.
922
     *
923
     * @return int
924
     */
925
    public function getColumnCount()
926
    {
927
        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...
928
            $this->buildColumnDispatch();
929
        }
930
931
        return count($this->columnDispatch);
932
    }
933
934
    /**
935
     * Build an columnDispatch that maps a GridField_ColumnProvider to a column for reference later.
936
     */
937
    protected function buildColumnDispatch()
938
    {
939
        $this->columnDispatch = array();
940
941
        foreach ($this->getComponents() as $item) {
942
            if ($item instanceof GridField_ColumnProvider) {
943
                $columns = $item->getColumnsHandled($this);
944
945
                foreach ($columns as $column) {
946
                    $this->columnDispatch[$column][] = $item;
947
                }
948
            }
949
        }
950
    }
951
952
    /**
953
     * This is the action that gets executed when a GridField_AlterAction gets clicked.
954
     *
955
     * @param array $data
956
     * @param Form $form
957
     * @param HTTPRequest $request
958
     *
959
     * @return string
960
     */
961
    public function gridFieldAlterAction($data, $form, HTTPRequest $request)
962
    {
963
        $data = $request->requestVars();
964
965
        // Protection against CSRF attacks
966
        $token = $this
967
            ->getForm()
968
            ->getSecurityToken();
969
        if (!$token->checkRequest($request)) {
970
            $this->httpError(400, _t(
971
                "SilverStripe\\Forms\\Form.CSRF_FAILED_MESSAGE",
972
                "There seems to have been a technical problem. Please click the back button, " . "refresh your browser, and try again."
973
            ));
974
        }
975
976
        $name = $this->getName();
977
978
        $fieldData = null;
979
980
        if (isset($data[$name])) {
981
            $fieldData = $data[$name];
982
        }
983
984
        $state = $this->getState(false);
985
986
        /** @skipUpgrade */
987
        if (isset($fieldData['GridState'])) {
988
            $state->setValue($fieldData['GridState']);
989
        }
990
991
        foreach ($data as $dataKey => $dataValue) {
992
            if (preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) {
993
                $stateChange = $request->getSession()->get($matches[1]);
994
                $actionName = $stateChange['actionName'];
995
996
                $arguments = array();
997
998
                if (isset($stateChange['args'])) {
999
                    $arguments = $stateChange['args'];
1000
                };
1001
1002
                $html = $this->handleAlterAction($actionName, $arguments, $data);
1003
1004
                if ($html) {
1005
                    return $html;
1006
                }
1007
            }
1008
        }
1009
1010
        if ($request->getHeader('X-Pjax') === 'CurrentField') {
1011
            return $this->FieldHolder();
1012
        }
1013
1014
        return $form->forTemplate();
1015
    }
1016
1017
    /**
1018
     * Pass an action on the first GridField_ActionProvider that matches the $actionName.
1019
     *
1020
     * @param string $actionName
1021
     * @param mixed $arguments
1022
     * @param array $data
1023
     *
1024
     * @return mixed
1025
     *
1026
     * @throws InvalidArgumentException
1027
     */
1028
    public function handleAlterAction($actionName, $arguments, $data)
1029
    {
1030
        $actionName = strtolower($actionName);
1031
1032
        foreach ($this->getComponents() as $component) {
1033
            if ($component instanceof GridField_ActionProvider) {
1034
                $actions = array_map('strtolower', (array) $component->getActions($this));
1035
1036
                if (in_array($actionName, $actions)) {
1037
                    return $component->handleAction($this, $actionName, $arguments, $data);
1038
                }
1039
            }
1040
        }
1041
1042
        throw new InvalidArgumentException(sprintf(
1043
            'Can\'t handle action "%s"',
1044
            $actionName
1045
        ));
1046
    }
1047
1048
    /**
1049
     * Custom request handler that will check component handlers before proceeding to the default
1050
     * implementation.
1051
     *
1052
     * @todo copy less code from RequestHandler.
1053
     *
1054
     * @param HTTPRequest $request
1055
     * @return array|RequestHandler|HTTPResponse|string
1056
     * @throws HTTPResponse_Exception
1057
     */
1058
    public function handleRequest(HTTPRequest $request)
1059
    {
1060
        if ($this->brokenOnConstruct) {
1061
            user_error(
1062
                sprintf(
1063
                    "parent::__construct() needs to be called on %s::__construct()",
1064
                    __CLASS__
1065
                ),
1066
                E_USER_WARNING
1067
            );
1068
        }
1069
1070
        $this->setRequest($request);
1071
1072
        $fieldData = $this->getRequest()->requestVar($this->getName());
1073
1074
        /** @skipUpgrade */
1075
        if ($fieldData && isset($fieldData['GridState'])) {
1076
            $this->getState(false)->setValue($fieldData['GridState']);
1077
        }
1078
1079
        foreach ($this->getComponents() as $component) {
1080
            if ($component instanceof GridField_URLHandler && $urlHandlers = $component->getURLHandlers($this)) {
1081
                foreach ($urlHandlers as $rule => $action) {
1082
                    if ($params = $request->match($rule, true)) {
1083
                        // Actions can reference URL parameters.
1084
                        // e.g. '$Action/$ID/$OtherID' → '$Action'
1085
1086
                        if ($action[0] == '$') {
1087
                            $action = $params[substr($action, 1)];
1088
                        }
1089
1090
                        if (!method_exists($component, 'checkAccessAction') || $component->checkAccessAction($action)) {
1091
                            if (!$action) {
1092
                                $action = "index";
1093
                            }
1094
1095
                            if (!is_string($action)) {
1096
                                throw new LogicException(sprintf(
1097
                                    'Non-string method name: %s',
1098
                                    var_export($action, true)
1099
                                ));
1100
                            }
1101
1102
                            try {
1103
                                $result = $component->$action($this, $request);
1104
                            } catch (HTTPResponse_Exception $responseException) {
1105
                                $result = $responseException->getResponse();
1106
                            }
1107
1108
                            if ($result instanceof HTTPResponse && $result->isError()) {
1109
                                return $result;
1110
                            }
1111
1112
                            if ($this !== $result &&
1113
                                !$request->isEmptyPattern($rule) &&
1114
                                ($result instanceof RequestHandler || $result instanceof HasRequestHandler)
1115
                            ) {
1116
                                if ($result instanceof HasRequestHandler) {
1117
                                    $result = $result->getRequestHandler();
1118
                                }
1119
                                $returnValue = $result->handleRequest($request);
1120
1121
                                if (is_array($returnValue)) {
1122
                                    throw new LogicException(
1123
                                        'GridField_URLHandler handlers can\'t return arrays'
1124
                                    );
1125
                                }
1126
1127
                                return $returnValue;
1128
                            }
1129
1130
                            if ($request->allParsed()) {
1131
                                return $result;
1132
                            }
1133
1134
                            return $this->httpError(
1135
                                404,
1136
                                sprintf(
1137
                                    'I can\'t handle sub-URLs of a %s object.',
1138
                                    get_class($result)
1139
                                )
1140
                            );
1141
                        }
1142
                    }
1143
                }
1144
            }
1145
        }
1146
1147
        return parent::handleRequest($request);
1148
    }
1149
1150
    /**
1151
     * {@inheritdoc}
1152
     */
1153
    public function saveInto(DataObjectInterface $record)
1154
    {
1155
        foreach ($this->getComponents() as $component) {
1156
            if ($component instanceof GridField_SaveHandler) {
1157
                $component->handleSave($this, $record);
1158
            }
1159
        }
1160
    }
1161
1162
    /**
1163
     * @param array $content
1164
     *
1165
     * @return string
1166
     */
1167
    protected function getOptionalTableHeader(array $content)
1168
    {
1169
        if ($content['header']) {
1170
            return HTML::createTag(
1171
                'thead',
1172
                array(),
1173
                $content['header']
1174
            );
1175
        }
1176
1177
        return '';
1178
    }
1179
1180
    /**
1181
     * @param array $content
1182
     *
1183
     * @return string
1184
     */
1185
    protected function getOptionalTableBody(array $content)
1186
    {
1187
        if ($content['body']) {
1188
            return HTML::createTag(
1189
                'tbody',
1190
                array('class' => 'ss-gridfield-items'),
1191
                $content['body']
1192
            );
1193
        }
1194
1195
        return '';
1196
    }
1197
1198
    /**
1199
     * @param $content
1200
     *
1201
     * @return string
1202
     */
1203
    protected function getOptionalTableFooter($content)
1204
    {
1205
        if ($content['footer']) {
1206
            return HTML::createTag(
1207
                'tfoot',
1208
                array(),
1209
                $content['footer']
1210
            );
1211
        }
1212
1213
        return '';
1214
    }
1215
}
1216