Completed
Push — master ( 1be2e7...d38097 )
by Sam
23s
created

GridField::getDataFieldValue()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 4
nop 2
dl 0
loc 18
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms\GridField;
4
5
use SilverStripe\ORM\ArrayList;
6
use SilverStripe\ORM\DataList;
7
use SilverStripe\ORM\DataObject;
8
use SilverStripe\ORM\SS_List;
9
use SilverStripe\ORM\FieldType\DBField;
10
use SilverStripe\ORM\DataModel;
11
use SilverStripe\ORM\DataObjectInterface;
12
use SilverStripe\Control\HTTPRequest;
13
use SilverStripe\Control\Session;
14
use SilverStripe\Control\HTTPResponse_Exception;
15
use SilverStripe\Control\HTTPResponse;
16
use SilverStripe\Control\RequestHandler;
17
use SilverStripe\Forms\FormField;
18
use SilverStripe\Forms\Form;
19
use LogicException;
20
use InvalidArgumentException;
21
use SilverStripe\View\Requirements;
22
23
/**
24
 * Displays a {@link SS_List} in a grid format.
25
 *
26
 * GridField is a field that takes an SS_List and displays it in an table with rows and columns.
27
 * It reminds of the old TableFields but works with SS_List types and only loads the necessary
28
 * rows from the list.
29
 *
30
 * The minimum configuration is to pass in name and title of the field and a SS_List.
31
 *
32
 * <code>
33
 * $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page'));
34
 * </code>
35
 *
36
 * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
37
 * since the required frontend dependencies are included through CMS bundling.
38
 *
39
 * @see SS_List
40
 *
41
 * @property GridState_Data $State The gridstate of this object
42
 */
43
class GridField extends FormField
44
{
45
    /**
46
     * @var array
47
     */
48
    private static $allowed_actions = array(
49
        'index',
50
        'gridFieldAlterAction',
51
    );
52
53
    /**
54
     * Data source.
55
     *
56
     * @var SS_List
57
     */
58
    protected $list = null;
59
60
    /**
61
     * Class name of the DataObject that the GridField will display.
62
     *
63
     * Defaults to the value of $this->list->dataClass.
64
     *
65
     * @var string
66
     */
67
    protected $modelClassName = '';
68
69
    /**
70
     * Current state of the GridField.
71
     *
72
     * @var GridState
73
     */
74
    protected $state = null;
75
76
    /**
77
     * @var GridFieldConfig
78
     */
79
    protected $config = null;
80
81
    /**
82
     * Components list.
83
     *
84
     * @var array
85
     */
86
    protected $components = array();
87
88
    /**
89
     * Internal dispatcher for column handlers.
90
     *
91
     * Keys are column names and values are GridField_ColumnProvider objects.
92
     *
93
     * @var array
94
     */
95
    protected $columnDispatch = null;
96
97
    /**
98
     * Map of callbacks for custom data fields.
99
     *
100
     * @var array
101
     */
102
    protected $customDataFields = array();
103
104
    /**
105
     * @var string
106
     */
107
    protected $name = '';
108
109
    /**
110
     * @param string $name
111
     * @param string $title
112
     * @param SS_List $dataList
113
     * @param GridFieldConfig $config
114
     */
115
    public function __construct($name, $title = null, SS_List $dataList = null, GridFieldConfig $config = null)
116
    {
117
        parent::__construct($name, $title, null);
118
119
        $this->name = $name;
120
121
        if ($dataList) {
122
            $this->setList($dataList);
123
        }
124
125
        if (!$config) {
126
            $config = GridFieldConfig_Base::create();
127
        }
128
129
        $this->setConfig($config);
130
131
        $state = $this->config->getComponentByType('SilverStripe\\Forms\\GridField\\GridState_Component');
132
133
        if (!$state) {
134
            $this->config->addComponent(new GridState_Component());
135
        }
136
137
        $this->state = new GridState($this);
138
139
        $this->addExtraClass('grid-field');
140
    }
141
142
    /**
143
     * @param HTTPRequest $request
144
     *
145
     * @return string
146
     */
147
    public function index($request)
148
    {
149
        return $this->gridFieldAlterAction(array(), $this->getForm(), $request);
150
    }
151
152
    /**
153
     * Set the modelClass (data object) that this field will get it column headers from.
154
     *
155
     * If no $displayFields has been set, the display fields will be $summary_fields.
156
     *
157
     * @see GridFieldDataColumns::getDisplayFields()
158
     *
159
     * @param string $modelClassName
160
     *
161
     * @return $this
162
     */
163
    public function setModelClass($modelClassName)
164
    {
165
        $this->modelClassName = $modelClassName;
166
167
        return $this;
168
    }
169
170
    /**
171
     * Returns a data class that is a DataObject type that this GridField should look like.
172
     *
173
     * @return string
174
     *
175
     * @throws LogicException
176
     */
177
    public function getModelClass()
178
    {
179
        if ($this->modelClassName) {
180
            return $this->modelClassName;
181
        }
182
183
        /** @var DataList|ArrayList $list */
184
        $list = $this->list;
185
        if ($list && $list->hasMethod('dataClass')) {
186
            $class = $list->dataClass();
187
188
            if ($class) {
189
                return $class;
190
            }
191
        }
192
193
        throw new LogicException(
194
            'GridField doesn\'t have a modelClassName, so it doesn\'t know the columns of this grid.'
195
        );
196
    }
197
198
    /**
199
     * @return GridFieldConfig
200
     */
201
    public function getConfig()
202
    {
203
        return $this->config;
204
    }
205
206
    /**
207
     * @param GridFieldConfig $config
208
     *
209
     * @return $this
210
     */
211
    public function setConfig(GridFieldConfig $config)
212
    {
213
        $this->config = $config;
214
215
        return $this;
216
    }
217
218
    /**
219
     * @return ArrayList
220
     */
221
    public function getComponents()
222
    {
223
        return $this->config->getComponents();
224
    }
225
226
    /**
227
     * Cast an arbitrary value with the help of a $castingDefinition.
228
     *
229
     * @todo refactor this into GridFieldComponent
230
     *
231
     * @param mixed $value
232
     * @param string|array $castingDefinition
233
     *
234
     * @return mixed
235
     */
236
    public function getCastedValue($value, $castingDefinition)
237
    {
238
        $castingParams = array();
239
240
        if (is_array($castingDefinition)) {
241
            $castingParams = $castingDefinition;
242
            array_shift($castingParams);
243
            $castingDefinition = array_shift($castingDefinition);
244
        }
245
246
        if (strpos($castingDefinition, '->') === false) {
247
            $castingFieldType = $castingDefinition;
248
            $castingField = DBField::create_field($castingFieldType, $value);
249
250
            return call_user_func_array(array($castingField, 'XML'), $castingParams);
251
        }
252
253
        list($castingFieldType, $castingMethod) = explode('->', $castingDefinition);
254
255
        $castingField = DBField::create_field($castingFieldType, $value);
256
257
        return call_user_func_array(array($castingField, $castingMethod), $castingParams);
258
    }
259
260
    /**
261
     * Set the data source.
262
     *
263
     * @param SS_List $list
264
     *
265
     * @return $this
266
     */
267
    public function setList(SS_List $list)
268
    {
269
        $this->list = $list;
270
271
        return $this;
272
    }
273
274
    /**
275
     * Get the data source.
276
     *
277
     * @return SS_List
278
     */
279
    public function getList()
280
    {
281
        return $this->list;
282
    }
283
284
    /**
285
     * Get the data source after applying every {@link GridField_DataManipulator} to it.
286
     *
287
     * @return SS_List
288
     */
289
    public function getManipulatedList()
290
    {
291
        $list = $this->getList();
292
293
        foreach ($this->getComponents() as $item) {
294
            if ($item instanceof GridField_DataManipulator) {
295
                $list = $item->getManipulatedData($this, $list);
296
            }
297
        }
298
299
        return $list;
300
    }
301
302
    /**
303
     * Get the current GridState_Data or the GridState.
304
     *
305
     * @param bool $getData
306
     *
307
     * @return GridState_Data|GridState
308
     */
309
    public function getState($getData = true)
310
    {
311
        if ($getData) {
312
            return $this->state->getData();
313
        }
314
315
        return $this->state;
316
    }
317
318
    /**
319
     * Returns the whole gridfield rendered with all the attached components.
320
     *
321
     * @param array $properties
322
     * @return string
323
     */
324
    public function FieldHolder($properties = array())
325
    {
326
        $columns = $this->getColumns();
327
328
        $list = $this->getManipulatedList();
329
330
        $content = array(
331
            'before' => '',
332
            'after' => '',
333
            'header' => '',
334
            'footer' => '',
335
        );
336
337
        foreach ($this->getComponents() as $item) {
338
            if ($item instanceof GridField_HTMLProvider) {
339
                $fragments = $item->getHTMLFragments($this);
340
341
                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...
342
                    foreach ($fragments as $fragmentKey => $fragmentValue) {
343
                        $fragmentKey = strtolower($fragmentKey);
344
345
                        if (!isset($content[$fragmentKey])) {
346
                            $content[$fragmentKey] = '';
347
                        }
348
349
                        $content[$fragmentKey] .= $fragmentValue . "\n";
350
                    }
351
                }
352
            }
353
        }
354
355
        foreach ($content as $contentKey => $contentValue) {
356
            $content[$contentKey] = trim($contentValue);
357
        }
358
359
        // Replace custom fragments and check which fragments are defined. Circular dependencies
360
        // are detected by disallowing any item to be deferred more than 5 times.
361
362
        $fragmentDefined = array(
363
            'header' => true,
364
            'footer' => true,
365
            'before' => true,
366
            'after' => true,
367
        );
368
369
        reset($content);
370
371
        while (list($contentKey, $contentValue) = each($content)) {
372
            if (preg_match_all('/\$DefineFragment\(([a-z0-9\-_]+)\)/i', $contentValue, $matches)) {
373
                foreach ($matches[1] as $match) {
0 ignored issues
show
Bug introduced by
The expression $matches[1] of type string|array<integer,string> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
374
                    $fragmentName = strtolower($match);
375
                    $fragmentDefined[$fragmentName] = true;
376
377
                    $fragment = '';
378
379
                    if (isset($content[$fragmentName])) {
380
                        $fragment = $content[$fragmentName];
381
                    }
382
383
                    // If the fragment still has a fragment definition in it, when we should defer
384
                    // this item until later.
385
386
                    if (preg_match('/\$DefineFragment\(([a-z0-9\-_]+)\)/i', $fragment, $matches)) {
387
                        if (isset($fragmentDeferred[$contentKey]) && $fragmentDeferred[$contentKey] > 5) {
388
                            throw new LogicException(sprintf(
389
                                'GridField HTML fragment "%s" and "%s" appear to have a circular dependency.',
390
                                $fragmentName,
391
                                $matches[1]
392
                            ));
393
                        }
394
395
                        unset($content[$contentKey]);
396
397
                        $content[$contentKey] = $contentValue;
398
399
                        if (!isset($fragmentDeferred[$contentKey])) {
400
                            $fragmentDeferred[$contentKey] = 0;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$fragmentDeferred was never initialized. Although not strictly required by PHP, it is generally a good practice to add $fragmentDeferred = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
401
                        }
402
403
                        $fragmentDeferred[$contentKey]++;
0 ignored issues
show
Bug introduced by
The variable $fragmentDeferred does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
404
405
                        break;
406
                    } else {
407
                        $content[$contentKey] = preg_replace(
408
                            sprintf('/\$DefineFragment\(%s\)/i', $fragmentName),
409
                            $fragment,
410
                            $content[$contentKey]
411
                        );
412
                    }
413
                }
414
            }
415
        }
416
417
        // Check for any undefined fragments, and if so throw an exception.
418
        // While we're at it, trim whitespace off the elements.
419
420
        foreach ($content as $contentKey => $contentValue) {
421
            if (empty($fragmentDefined[$contentKey])) {
422
                throw new LogicException(sprintf(
423
                    'GridField HTML fragment "%s" was given content, but not defined. Perhaps there is a supporting GridField component you need to add?',
424
                    $contentKey
425
                ));
426
            }
427
        }
428
429
        $total = count($list);
430
431
        if ($total > 0) {
432
            $rows = array();
433
434
            foreach ($list as $index => $record) {
435
                if ($record->hasMethod('canView') && !$record->canView()) {
436
                    continue;
437
                }
438
439
                $rowContent = '';
440
441
                foreach ($this->getColumns() as $column) {
442
                    $colContent = $this->getColumnContent($record, $column);
443
444
                    // Null means this columns should be skipped altogether.
445
446
                    if ($colContent === null) {
447
                        continue;
448
                    }
449
450
                    $colAttributes = $this->getColumnAttributes($record, $column);
451
452
                    $rowContent .= $this->newCell(
453
                        $total,
454
                        $index,
455
                        $record,
456
                        $colAttributes,
457
                        $colContent
458
                    );
459
                }
460
461
                $rowAttributes = $this->getRowAttributes($total, $index, $record);
462
463
                $rows[] = $this->newRow($total, $index, $record, $rowAttributes, $rowContent);
464
            }
465
            $content['body'] = implode("\n", $rows);
466
        }
467
468
        // Display a message when the grid field is empty.
469
470
        if (empty($content['body'])) {
471
            $cell = FormField::create_tag(
472
                'td',
473
                array(
474
                    'colspan' => count($columns),
475
                ),
476
                _t('GridField.NoItemsFound', 'No items found')
477
            );
478
479
            $row = FormField::create_tag(
480
                'tr',
481
                array(
482
                    'class' => 'ss-gridfield-item ss-gridfield-no-items',
483
                ),
484
                $cell
485
            );
486
487
            $content['body'] = $row;
488
        }
489
490
        $header = $this->getOptionalTableHeader($content);
491
        $body = $this->getOptionalTableBody($content);
492
        $footer = $this->getOptionalTableFooter($content);
493
494
        $this->addExtraClass('ss-gridfield grid-field field');
495
496
        $fieldsetAttributes = array_diff_key(
497
            $this->getAttributes(),
498
            array(
499
                'value' => false,
500
                'type' => false,
501
                'name' => false,
502
            )
503
        );
504
505
        $fieldsetAttributes['data-name'] = $this->getName();
506
507
        $tableId = null;
508
509
        if ($this->id) {
510
            $tableId = $this->id;
0 ignored issues
show
Documentation introduced by
The property id does not exist on object<SilverStripe\Forms\GridField\GridField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
511
        }
512
513
        $tableAttributes = array(
514
            'id' => $tableId,
515
            'class' => 'table grid-field__table',
516
            'cellpadding' => '0',
517
            'cellspacing' => '0'
518
        );
519
520
        if ($this->getDescription()) {
521
            $content['after'] .= FormField::create_tag(
522
                'span',
523
                array('class' => 'description'),
524
                $this->getDescription()
525
            );
526
        }
527
528
        $table = FormField::create_tag(
529
            'table',
530
            $tableAttributes,
531
            $header . "\n" . $footer . "\n" . $body
532
        );
533
534
        return FormField::create_tag(
535
            'fieldset',
536
            $fieldsetAttributes,
537
            $content['before'] . $table . $content['after']
538
        );
539
    }
540
541
    /**
542
     * @param int $total
543
     * @param int $index
544
     * @param DataObject $record
545
     * @param array $attributes
546
     * @param string $content
547
     *
548
     * @return string
549
     */
550
    protected function newCell($total, $index, $record, $attributes, $content)
0 ignored issues
show
Unused Code introduced by
The parameter $total is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $index is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $record is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
551
    {
552
        return FormField::create_tag(
553
            'td',
554
            $attributes,
555
            $content
556
        );
557
    }
558
559
    /**
560
     * @param int $total
561
     * @param int $index
562
     * @param DataObject $record
563
     * @param array $attributes
564
     * @param string $content
565
     *
566
     * @return string
567
     */
568
    protected function newRow($total, $index, $record, $attributes, $content)
0 ignored issues
show
Unused Code introduced by
The parameter $total is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $index is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $record is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
569
    {
570
        return FormField::create_tag(
571
            'tr',
572
            $attributes,
573
            $content
574
        );
575
    }
576
577
    /**
578
     * @param int $total
579
     * @param int $index
580
     * @param DataObject $record
581
     *
582
     * @return array
583
     */
584
    protected function getRowAttributes($total, $index, $record)
585
    {
586
        $rowClasses = $this->newRowClasses($total, $index, $record);
587
588
        return array(
589
            'class' => implode(' ', $rowClasses),
590
            'data-id' => $record->ID,
591
            'data-class' => $record->ClassName,
592
        );
593
    }
594
595
    /**
596
     * @param int $total
597
     * @param int $index
598
     * @param DataObject $record
599
     *
600
     * @return array
601
     */
602
    protected function newRowClasses($total, $index, $record)
603
    {
604
        $classes = array('ss-gridfield-item');
605
606
        if ($index == 0) {
607
            $classes[] = 'first';
608
        }
609
610
        if ($index == $total - 1) {
611
            $classes[] = 'last';
612
        }
613
614
        if ($index % 2) {
615
            $classes[] = 'even';
616
        } else {
617
            $classes[] = 'odd';
618
        }
619
620
        $this->extend('updateNewRowClasses', $classes, $total, $index, $record);
621
622
        return $classes;
623
    }
624
625
    /**
626
     * @param array $properties
627
     * @return string
628
     */
629
    public function Field($properties = array())
630
    {
631
        $this->extend('onBeforeRender', $this);
632
        return $this->FieldHolder($properties);
633
    }
634
635
    /**
636
     * {@inheritdoc}
637
     */
638
    public function getAttributes()
639
    {
640
        return array_merge(
641
            parent::getAttributes(),
642
            array(
643
                'data-url' => $this->Link(),
644
            )
645
        );
646
    }
647
648
    /**
649
     * Get the columns of this GridField, they are provided by attached GridField_ColumnProvider.
650
     *
651
     * @return array
652
     */
653
    public function getColumns()
654
    {
655
        $columns = array();
656
657
        foreach ($this->getComponents() as $item) {
658
            if ($item instanceof GridField_ColumnProvider) {
659
                $item->augmentColumns($this, $columns);
660
            }
661
        }
662
663
        return $columns;
664
    }
665
666
    /**
667
     * Get the value from a column.
668
     *
669
     * @param DataObject $record
670
     * @param string $column
671
     *
672
     * @return string
673
     *
674
     * @throws InvalidArgumentException
675
     */
676
    public function getColumnContent($record, $column)
677
    {
678
        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...
679
            $this->buildColumnDispatch();
680
        }
681
682
        if (!empty($this->columnDispatch[$column])) {
683
            $content = '';
684
685
            foreach ($this->columnDispatch[$column] as $handler) {
686
                /**
687
                 * @var GridField_ColumnProvider $handler
688
                 */
689
                $content .= $handler->getColumnContent($this, $record, $column);
690
            }
691
692
            return $content;
693
        } else {
694
            throw new InvalidArgumentException(sprintf(
695
                'Bad column "%s"',
696
                $column
697
            ));
698
        }
699
    }
700
701
    /**
702
     * Add additional calculated data fields to be used on this GridField
703
     *
704
     * @param array $fields a map of fieldname to callback. The callback will
705
     *                      be passed the record as an argument.
706
     */
707
    public function addDataFields($fields)
708
    {
709
        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...
710
            $this->customDataFields = array_merge($this->customDataFields, $fields);
711
        } else {
712
            $this->customDataFields = $fields;
713
        }
714
    }
715
716
    /**
717
     * Get the value of a named field  on the given record.
718
     *
719
     * Use of this method ensures that any special rules around the data for this gridfield are
720
     * followed.
721
     *
722
     * @param DataObject $record
723
     * @param string $fieldName
724
     *
725
     * @return mixed
726
     */
727
    public function getDataFieldValue($record, $fieldName)
728
    {
729
        if (isset($this->customDataFields[$fieldName])) {
730
            $callback = $this->customDataFields[$fieldName];
731
732
            return $callback($record);
733
        }
734
735
        if ($record->hasMethod('relField')) {
736
            return $record->relField($fieldName);
737
        }
738
739
        if ($record->hasMethod($fieldName)) {
740
            return $record->$fieldName();
741
        }
742
743
        return $record->$fieldName;
744
    }
745
746
    /**
747
     * Get extra columns attributes used as HTML attributes.
748
     *
749
     * @param DataObject $record
750
     * @param string $column
751
     *
752
     * @return array
753
     *
754
     * @throws LogicException
755
     * @throws InvalidArgumentException
756
     */
757
    public function getColumnAttributes($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
            $attributes = array();
765
766
            foreach ($this->columnDispatch[$column] as $handler) {
767
                /**
768
                 * @var GridField_ColumnProvider $handler
769
                 */
770
                $columnAttributes = $handler->getColumnAttributes($this, $record, $column);
771
772
                if (is_array($columnAttributes)) {
773
                    $attributes = array_merge($attributes, $columnAttributes);
774
                    continue;
775
                }
776
777
                throw new LogicException(sprintf(
778
                    'Non-array response from %s::getColumnAttributes().',
779
                    get_class($handler)
780
                ));
781
            }
782
783
            return $attributes;
784
        }
785
786
        throw new InvalidArgumentException(sprintf(
787
            'Bad column "%s"',
788
            $column
789
        ));
790
    }
791
792
    /**
793
     * Get metadata for a column.
794
     *
795
     * @example "array('Title'=>'Email address')"
796
     *
797
     * @param string $column
798
     *
799
     * @return array
800
     *
801
     * @throws LogicException
802
     * @throws InvalidArgumentException
803
     */
804
    public function getColumnMetadata($column)
805
    {
806
        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...
807
            $this->buildColumnDispatch();
808
        }
809
810
        if (!empty($this->columnDispatch[$column])) {
811
            $metaData = array();
812
813
            foreach ($this->columnDispatch[$column] as $handler) {
814
                /**
815
                 * @var GridField_ColumnProvider $handler
816
                 */
817
                $columnMetaData = $handler->getColumnMetadata($this, $column);
818
819
                if (is_array($columnMetaData)) {
820
                    $metaData = array_merge($metaData, $columnMetaData);
821
                    continue;
822
                }
823
824
                throw new LogicException(sprintf(
825
                    'Non-array response from %s::getColumnMetadata().',
826
                    get_class($handler)
827
                ));
828
            }
829
830
            return $metaData;
831
        }
832
833
        throw new InvalidArgumentException(sprintf(
834
            'Bad column "%s"',
835
            $column
836
        ));
837
    }
838
839
    /**
840
     * Return how many columns the grid will have.
841
     *
842
     * @return int
843
     */
844
    public function getColumnCount()
845
    {
846
        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...
847
            $this->buildColumnDispatch();
848
        }
849
850
        return count($this->columnDispatch);
851
    }
852
853
    /**
854
     * Build an columnDispatch that maps a GridField_ColumnProvider to a column for reference later.
855
     */
856
    protected function buildColumnDispatch()
857
    {
858
        $this->columnDispatch = array();
859
860
        foreach ($this->getComponents() as $item) {
861
            if ($item instanceof GridField_ColumnProvider) {
862
                $columns = $item->getColumnsHandled($this);
863
864
                foreach ($columns as $column) {
865
                    $this->columnDispatch[$column][] = $item;
866
                }
867
            }
868
        }
869
    }
870
871
    /**
872
     * This is the action that gets executed when a GridField_AlterAction gets clicked.
873
     *
874
     * @param array $data
875
     * @param Form $form
876
     * @param HTTPRequest $request
877
     *
878
     * @return string
879
     */
880
    public function gridFieldAlterAction($data, $form, HTTPRequest $request)
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
881
    {
882
        $data = $request->requestVars();
883
884
        // Protection against CSRF attacks
885
        $token = $this
886
            ->getForm()
887
            ->getSecurityToken();
888
        if (!$token->checkRequest($request)) {
889
            $this->httpError(400, _t(
890
                "Form.CSRF_FAILED_MESSAGE",
891
                "There seems to have been a technical problem. Please click the back button, ".
892
                "refresh your browser, and try again."
893
            ));
894
        }
895
896
        $name = $this->getName();
897
898
        $fieldData = null;
899
900
        if (isset($data[$name])) {
901
            $fieldData = $data[$name];
902
        }
903
904
        $state = $this->getState(false);
905
906
        /** @skipUpgrade */
907
        if (isset($fieldData['GridState'])) {
908
            $state->setValue($fieldData['GridState']);
0 ignored issues
show
Bug introduced by
The method setValue does only exist in SilverStripe\Forms\GridField\GridState, but not in SilverStripe\Forms\GridField\GridState_Data.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
909
        }
910
911
        foreach ($data as $dataKey => $dataValue) {
912
            if (preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) {
913
                $stateChange = Session::get($matches[1]);
914
                $actionName = $stateChange['actionName'];
915
916
                $arguments = array();
917
918
                if (isset($stateChange['args'])) {
919
                    $arguments = $stateChange['args'];
920
                };
921
922
                $html = $this->handleAlterAction($actionName, $arguments, $data);
923
924
                if ($html) {
925
                    return $html;
926
                }
927
            }
928
        }
929
930
        if ($request->getHeader('X-Pjax') === 'CurrentField') {
931
            return $this->FieldHolder();
932
        }
933
934
        return $form->forTemplate();
935
    }
936
937
    /**
938
     * Pass an action on the first GridField_ActionProvider that matches the $actionName.
939
     *
940
     * @param string $actionName
941
     * @param mixed $arguments
942
     * @param array $data
943
     *
944
     * @return mixed
945
     *
946
     * @throws InvalidArgumentException
947
     */
948
    public function handleAlterAction($actionName, $arguments, $data)
949
    {
950
        $actionName = strtolower($actionName);
951
952
        foreach ($this->getComponents() as $component) {
953
            if ($component instanceof GridField_ActionProvider) {
954
                $actions = array_map('strtolower', (array) $component->getActions($this));
955
956
                if (in_array($actionName, $actions)) {
957
                    return $component->handleAction($this, $actionName, $arguments, $data);
958
                }
959
            }
960
        }
961
962
        throw new InvalidArgumentException(sprintf(
963
            'Can\'t handle action "%s"',
964
            $actionName
965
        ));
966
    }
967
968
    /**
969
     * Custom request handler that will check component handlers before proceeding to the default
970
     * implementation.
971
     *
972
     * @todo copy less code from RequestHandler.
973
     *
974
     * @param HTTPRequest $request
975
     * @param DataModel $model
976
     *
977
     * @return array|RequestHandler|HTTPResponse|string|void
978
     *
979
     * @throws HTTPResponse_Exception
980
     */
981
    public function handleRequest(HTTPRequest $request, DataModel $model)
982
    {
983
        if ($this->brokenOnConstruct) {
984
            user_error(
985
                sprintf(
986
                    "parent::__construct() needs to be called on %s::__construct()",
987
                    __CLASS__
988
                ),
989
                E_USER_WARNING
990
            );
991
        }
992
993
        $this->setRequest($request);
994
        $this->setDataModel($model);
995
996
        $fieldData = $this->getRequest()->requestVar($this->getName());
997
998
        /** @skipUpgrade */
999
        if ($fieldData && isset($fieldData['GridState'])) {
1000
            $this->getState(false)->setValue($fieldData['GridState']);
0 ignored issues
show
Bug introduced by
The method setValue does only exist in SilverStripe\Forms\GridField\GridState, but not in SilverStripe\Forms\GridField\GridState_Data.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1001
        }
1002
1003
        foreach ($this->getComponents() as $component) {
1004
            if ($component instanceof GridField_URLHandler && $urlHandlers = $component->getURLHandlers($this)) {
1005
                foreach ($urlHandlers as $rule => $action) {
1006
                    if ($params = $request->match($rule, true)) {
1007
                        // Actions can reference URL parameters.
1008
                        // e.g. '$Action/$ID/$OtherID' → '$Action'
1009
1010
                        if ($action[0] == '$') {
1011
                            $action = $params[substr($action, 1)];
1012
                        }
1013
1014
                        if (!method_exists($component, 'checkAccessAction') || $component->checkAccessAction($action)) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Forms\GridField\GridField_URLHandler as the method checkAccessAction() does only exist in the following implementations of said interface: SilverStripe\Forms\Tests...ndlerTest\TestComponent.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1015
                            if (!$action) {
1016
                                $action = "index";
1017
                            }
1018
1019
                            if (!is_string($action)) {
1020
                                throw new LogicException(sprintf(
1021
                                    'Non-string method name: %s',
1022
                                    var_export($action, true)
1023
                                ));
1024
                            }
1025
1026
                            try {
1027
                                $result = $component->$action($this, $request);
1028
                            } catch (HTTPResponse_Exception $responseException) {
1029
                                $result = $responseException->getResponse();
1030
                            }
1031
1032
                            if ($result instanceof HTTPResponse && $result->isError()) {
1033
                                return $result;
1034
                            }
1035
1036
                            if ($this !== $result && !$request->isEmptyPattern($rule) && is_object($result) && $result instanceof RequestHandler) {
1037
                                $returnValue = $result->handleRequest($request, $model);
1038
1039
                                if (is_array($returnValue)) {
1040
                                    throw new LogicException(
1041
                                        'GridField_URLHandler handlers can\'t return arrays'
1042
                                    );
1043
                                }
1044
1045
                                return $returnValue;
1046
                            }
1047
1048
                            if ($request->allParsed()) {
1049
                                return $result;
1050
                            }
1051
1052
                            return $this->httpError(
1053
                                404,
1054
                                sprintf(
1055
                                    'I can\'t handle sub-URLs of a %s object.',
1056
                                    get_class($result)
1057
                                )
1058
                            );
1059
                        }
1060
                    }
1061
                }
1062
            }
1063
        }
1064
1065
        return parent::handleRequest($request, $model);
1066
    }
1067
1068
    /**
1069
     * {@inheritdoc}
1070
     */
1071
    public function saveInto(DataObjectInterface $record)
1072
    {
1073
        foreach ($this->getComponents() as $component) {
1074
            if ($component instanceof GridField_SaveHandler) {
1075
                $component->handleSave($this, $record);
1076
            }
1077
        }
1078
    }
1079
1080
    /**
1081
     * @param array $content
1082
     *
1083
     * @return string
1084
     */
1085
    protected function getOptionalTableHeader(array $content)
1086
    {
1087
        if ($content['header']) {
1088
            return FormField::create_tag(
1089
                'thead',
1090
                array(),
1091
                $content['header']
1092
            );
1093
        }
1094
1095
        return '';
1096
    }
1097
1098
    /**
1099
     * @param array $content
1100
     *
1101
     * @return string
1102
     */
1103
    protected function getOptionalTableBody(array $content)
1104
    {
1105
        if ($content['body']) {
1106
            return FormField::create_tag(
1107
                'tbody',
1108
                array('class' => 'ss-gridfield-items'),
1109
                $content['body']
1110
            );
1111
        }
1112
1113
        return '';
1114
    }
1115
1116
    /**
1117
     * @param $content
1118
     *
1119
     * @return string
1120
     */
1121
    protected function getOptionalTableFooter($content)
1122
    {
1123
        if ($content['footer']) {
1124
            return FormField::create_tag(
1125
                'tfoot',
1126
                array(),
1127
                $content['footer']
1128
            );
1129
        }
1130
1131
        return '';
1132
    }
1133
}
1134