Completed
Push — master ( 843223...7f5174 )
by
unknown
08:52
created

TableWidget::parsePropertyCell()   C

Complexity

Conditions 8
Paths 48

Size

Total Lines 42
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 22
nc 48
nop 3
dl 0
loc 42
rs 5.3846
c 0
b 0
f 0
1
<?php
2
3
namespace Charcoal\Admin\Widget;
4
5
use RuntimeException;
6
7
// From Pimple
8
use Pimple\Container;
9
10
// From 'charcoal-core'
11
use Charcoal\Model\ModelInterface;
12
13
// From 'charcoal-factory'
14
use Charcoal\Factory\FactoryInterface;
15
16
// From 'charcoal-property'
17
use Charcoal\Property\PropertyInterface;
18
19
// From 'charcoal-admin'
20
use Charcoal\Admin\AdminWidget;
21
use Charcoal\Admin\Support\HttpAwareTrait;
22
use Charcoal\Admin\Ui\ActionContainerTrait;
23
use Charcoal\Admin\Ui\CollectionContainerInterface;
24
use Charcoal\Admin\Ui\CollectionContainerTrait;
25
26
/**
27
 * Displays a collection of models in a tabular (table) format.
28
 */
29
class TableWidget extends AdminWidget implements CollectionContainerInterface
30
{
31
    use ActionContainerTrait;
32
    use CollectionContainerTrait {
33
        CollectionContainerTrait::createCollectionLoader as createCollectionLoaderFromTrait;
34
        CollectionContainerTrait::parsePropertyCell as parseCollectionPropertyCell;
35
        CollectionContainerTrait::parseObjectRow as parseCollectionObjectRow;
36
    }
37
    use HttpAwareTrait;
38
39
    /**
40
     * Default sorting priority for an action.
41
     *
42
     * @const integer
43
     */
44
    const DEFAULT_ACTION_PRIORITY = 10;
45
46
    /**
47
     * @var array $properties
48
     */
49
    protected $properties;
50
51
    /**
52
     * @var boolean $parsedProperties
53
     */
54
    protected $parsedProperties = false;
55
56
    /**
57
     * @var array $propertiesOptions
58
     */
59
    protected $propertiesOptions;
60
61
    /**
62
     * @var boolean $sortable
63
     */
64
    protected $sortable;
65
66
    /**
67
     * @var boolean $showTableHeader
68
     */
69
    protected $showTableHeader = true;
70
71
    /**
72
     * @var boolean $showTableHead
73
     */
74
    protected $showTableHead = true;
75
76
    /**
77
     * @var boolean $showTableFoot
78
     */
79
    protected $showTableFoot = false;
80
81
    /**
82
     * Store the factory instance for the current class.
83
     *
84
     * @var FactoryInterface
85
     */
86
    private $widgetFactory;
87
88
    /**
89
     * @var FactoryInterface $propertyFactory
90
     */
91
    private $propertyFactory;
92
93
    /**
94
     * @var mixed $adminMetadata
95
     */
96
    private $adminMetadata;
0 ignored issues
show
introduced by
The private property $adminMetadata is not used, and could be removed.
Loading history...
97
98
    /**
99
     * List actions ars displayed by default.
100
     *
101
     * @var boolean
102
     */
103
    private $showListActions = true;
104
105
    /**
106
     * Store the list actions.
107
     *
108
     * @var array|null
109
     */
110
    protected $listActions;
111
112
    /**
113
     * Store the default list actions.
114
     *
115
     * @var array|null
116
     */
117
    protected $defaultListActions;
118
119
    /**
120
     * Keep track if list actions are finalized.
121
     *
122
     * @var boolean
123
     */
124
    protected $parsedListActions = false;
125
126
    /**
127
     * Object actions ars displayed by default.
128
     *
129
     * @var boolean
130
     */
131
    private $showObjectActions = true;
132
133
    /**
134
     * Store the object actions.
135
     *
136
     * @var array|null
137
     */
138
    protected $objectActions;
139
140
    /**
141
     * Store the default object actions.
142
     *
143
     * @var array|null
144
     */
145
    protected $defaultObjectActions;
146
147
    /**
148
     * Keep track if object actions are finalized.
149
     *
150
     * @var boolean
151
     */
152
    protected $parsedObjectActions = false;
153
154
    /**
155
     * @param array $data The widget data.
156
     * @return TableWidget Chainable
157
     */
158
    public function setData(array $data)
159
    {
160
        parent::setData($data);
161
162
        $this->mergeDataSources($data);
163
164
        return $this;
165
    }
166
167
    /**
168
     * Fetch metadata from the current request.
169
     *
170
     * @return array
171
     */
172
    public function dataFromRequest()
173
    {
174
        return $this->httpRequest()->getParams($this->acceptedRequestData());
0 ignored issues
show
Bug introduced by
The method getParams() does not exist on Psr\Http\Message\RequestInterface. It seems like you code against a sub-type of Psr\Http\Message\RequestInterface such as Slim\Http\Request. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

174
        return $this->httpRequest()->/** @scrutinizer ignore-call */ getParams($this->acceptedRequestData());
Loading history...
175
    }
176
177
    /**
178
     * Retrieve the accepted metadata from the current request.
179
     *
180
     * @return array
181
     */
182
    public function acceptedRequestData()
183
    {
184
        return [
185
            'obj_type',
186
            'obj_id',
187
            'collection_ident',
188
            'sortable',
189
            'template',
190
        ];
191
    }
192
193
    /**
194
     * Fetch metadata from the current object type.
195
     *
196
     * @return array
197
     */
198
    public function dataFromObject()
199
    {
200
        $proto = $this->proto();
201
        $objMetadata = $proto->metadata();
202
        $adminMetadata = (isset($objMetadata['admin']) ? $objMetadata['admin'] : null);
203
204
        if (empty($adminMetadata['lists'])) {
205
            return [];
206
        }
207
208
        $collectionIdent = $this->collectionIdent();
209
        if (!$collectionIdent) {
210
            $collectionIdent = $this->collectionIdentFallback();
211
        }
212
213
        if ($collectionIdent && $proto->view()) {
214
            $collectionIdent = $proto->render($collectionIdent);
215
        }
216
217
        if (!$collectionIdent) {
218
            return [];
219
        }
220
221
        if (isset($adminMetadata['lists'][$collectionIdent])) {
222
            $objListData = $adminMetadata['lists'][$collectionIdent];
223
        } else {
224
            $objListData = [];
225
        }
226
227
        $collectionConfig = [];
228
229
        if (isset($objListData['list_actions']) && isset($adminMetadata['list_actions'])) {
230
            $extraListActions = array_intersect(
231
                array_keys($adminMetadata['list_actions']),
232
                array_keys($objListData['list_actions'])
233
            );
234
            foreach ($extraListActions as $listIdent) {
235
                $objListData['list_actions'][$listIdent] = array_replace_recursive(
236
                    $adminMetadata['list_actions'][$listIdent],
237
                    $objListData['list_actions'][$listIdent]
238
                );
239
            }
240
        }
241
242
        if (isset($objListData['object_actions']) && isset($adminMetadata['list_object_actions'])) {
243
            $extraObjectActions = array_intersect(
244
                array_keys($adminMetadata['list_object_actions']),
245
                array_keys($objListData['object_actions'])
246
            );
247
            foreach ($extraObjectActions as $listIdent) {
248
                $objListData['object_actions'][$listIdent] = array_replace_recursive(
249
                    $adminMetadata['list_object_actions'][$listIdent],
250
                    $objListData['object_actions'][$listIdent]
251
                );
252
            }
253
        }
254
255
        if (isset($objListData['orders']) && isset($adminMetadata['list_orders'])) {
256
            $extraOrders = array_intersect(
257
                array_keys($adminMetadata['list_orders']),
258
                array_keys($objListData['orders'])
259
            );
260
            foreach ($extraOrders as $listIdent) {
261
                $collectionConfig['orders'][$listIdent] = array_replace_recursive(
262
                    $adminMetadata['list_orders'][$listIdent],
263
                    $objListData['orders'][$listIdent]
264
                );
265
            }
266
        }
267
268
        if (isset($objListData['filters']) && isset($adminMetadata['list_filters'])) {
269
            $extraFilters = array_intersect(
270
                array_keys($adminMetadata['list_filters']),
271
                array_keys($objListData['filters'])
272
            );
273
            foreach ($extraFilters as $listIdent) {
274
                $collectionConfig['filters'][$listIdent] = array_replace_recursive(
275
                    $adminMetadata['list_filters'][$listIdent],
276
                    $objListData['filters'][$listIdent]
277
                );
278
            }
279
        }
280
281
        if ($collectionConfig) {
282
            $this->mergeCollectionConfig($collectionConfig);
283
        }
284
285
        return $objListData;
286
    }
287
288
    /**
289
     * Retrieve the widget's data options for JavaScript components.
290
     *
291
     * @return array
292
     */
293
    public function widgetDataForJs()
294
    {
295
        return [
296
            'obj_type'         => $this->objType(),
297
            'template'         => $this->template(),
298
            'collection_ident' => $this->collectionIdent(),
299
            'properties'       => $this->propertiesIdents(),
300
            'filters'          => $this->filters(),
301
            'orders'           => $this->orders(),
302
            'list_actions'     => $this->listActions(),
303
            'object_actions'   => $this->rawObjectActions(),
304
            'pagination'       => $this->pagination(),
305
        ];
306
    }
307
308
    /**
309
     * Sets and returns properties
310
     *
311
     * Manages which to display, and their order, as set in object metadata
312
     *
313
     * @return FormPropertyWidget[]
314
     */
315
    public function properties()
316
    {
317
        if ($this->properties === null || $this->parsedProperties === false) {
318
            $this->parsedProperties = true;
319
320
            $model = $this->proto();
321
            $properties = $model->metadata()->properties();
322
323
            $listProperties = null;
324
            if ($this->properties === null) {
325
                $collectionConfig = $this->collectionConfig();
326
                if (isset($collectionConfig['properties'])) {
327
                    $listProperties = array_flip($collectionConfig['properties']);
328
                }
329
            } else {
330
                $listProperties = array_flip($this->properties);
331
            }
332
333
            if ($listProperties) {
334
                // Replacing values of listProperties from index to actual property values
335
                $properties = array_replace($listProperties, $properties);
336
                // Get only the keys that are in listProperties from props
337
                $properties = array_intersect_key($properties, $listProperties);
338
            }
339
340
            $this->properties = $properties;
341
        }
342
343
        return $this->properties;
344
    }
345
346
    /**
347
     * Retrieve the property keys shown in the collection.
348
     *
349
     * @return array
350
     */
351
    public function propertiesIdents()
352
    {
353
        $collectionConfig = $this->collectionConfig();
354
        if (isset($collectionConfig['properties'])) {
355
            return $collectionConfig['properties'];
356
        }
357
358
        return [];
359
    }
360
361
    /**
362
     * Retrieve the property customizations for the collection.
363
     *
364
     * @return array|null
365
     */
366
    public function propertiesOptions()
367
    {
368
        if ($this->propertiesOptions === null) {
369
            $this->propertiesOptions = $this->defaultPropertiesOptions();
370
        }
371
372
        return $this->propertiesOptions;
373
    }
374
375
    /**
376
     * Retrieve the view options for the given property.
377
     *
378
     * @param  string $propertyIdent The property identifier to lookup.
379
     * @return array
380
     */
381
    public function viewOptions($propertyIdent)
382
    {
383
        if (!$propertyIdent) {
384
            return [];
385
        }
386
387
        if ($propertyIdent instanceof PropertyInterface) {
0 ignored issues
show
introduced by
$propertyIdent is never a sub-type of Charcoal\Property\PropertyInterface.
Loading history...
388
            $propertyIdent = $propertyIdent->ident();
389
        }
390
391
        $options = $this->propertiesOptions();
392
393
        if (isset($options[$propertyIdent]['view_options'])) {
394
            return $options[$propertyIdent]['view_options'];
395
        } else {
396
            return [];
397
        }
398
    }
399
400
    /**
401
     * Properties to display in collection template, and their order, as set in object metadata
402
     *
403
     * @return array|Generator
0 ignored issues
show
Bug introduced by
The type Charcoal\Admin\Widget\Generator was not found. Did you mean Generator? If so, make sure to prefix the type with \.
Loading history...
404
     */
405
    public function collectionProperties()
406
    {
407
        $props = $this->properties();
408
409
        foreach ($props as $propertyIdent => $property) {
410
            $propertyMetadata = $props[$propertyIdent];
411
412
            $p = $this->propertyFactory()->create($propertyMetadata['type']);
413
            $p->setIdent($propertyIdent);
414
            $p->setData($propertyMetadata);
415
416
            $options = $this->viewOptions($propertyIdent);
417
            $classes = $this->parsePropertyCellClasses($p);
418
419
            if (isset($options['label'])) {
420
                $label = $this->translator()->translate($options['label']);
421
            } else {
422
                $label = strval($p->label());
423
            }
424
425
            $column = [
426
                'label' => trim($label)
427
            ];
428
429
            if (!isset($column['attr'])) {
430
                $column['attr'] = [];
431
            }
432
433
            if (isset($options['attr'])) {
434
                $column['attr'] = array_merge($column['attr'], $options['attr']);
0 ignored issues
show
Bug introduced by
$column['attr'] of type string is incompatible with the type array expected by parameter $array1 of array_merge(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

434
                $column['attr'] = array_merge(/** @scrutinizer ignore-type */ $column['attr'], $options['attr']);
Loading history...
435
            }
436
437
            if (isset($classes)) {
438
                if (isset($column['attr']['class'])) {
439
                    if (is_string($classes)) {
440
                        $classes = explode(' ', $column['attr']['class']);
441
                    }
442
443
                    if (is_string($column['attr']['class'])) {
444
                        $column['attr']['class'] = explode(' ', $column['attr']['class']);
445
                    }
446
447
                    $column['attr']['class'] = array_unique(array_merge($column['attr']['class'], $classes));
448
                } else {
449
                    $column['attr']['class'] = $classes;
450
                }
451
452
                unset($classes);
453
            }
454
455
            $column['attr'] = html_build_attributes($column['attr']);
456
457
            yield $column;
458
        }
459
    }
460
461
    /**
462
     * Show/hide the table's object actions.
463
     *
464
     * @param  boolean $show Show (TRUE) or hide (FALSE) the actions.
465
     * @return TableWidget Chainable
466
     */
467
    public function setShowObjectActions($show)
468
    {
469
        $this->showObjectActions = !!$show;
470
471
        return $this;
472
    }
473
474
    /**
475
     * Determine if the table's object actions should be shown.
476
     *
477
     * @return boolean
478
     */
479
    public function showObjectActions()
480
    {
481
        if ($this->showObjectActions === false) {
482
            return false;
483
        } else {
484
            return count($this->objectActions());
0 ignored issues
show
Bug Best Practice introduced by
The expression return count($this->objectActions()) returns the type integer which is incompatible with the documented return type boolean.
Loading history...
485
        }
486
    }
487
488
    /**
489
     * Retrieve the table's object actions.
490
     *
491
     * @return array
492
     */
493
    public function objectActions()
494
    {
495
        $this->rawObjectActions();
496
497
        $objectActions = [];
498
        if (is_array($this->objectActions)) {
499
            $objectActions = $this->parseAsObjectActions($this->objectActions);
500
        }
501
502
        return $objectActions;
503
    }
504
505
    /**
506
     * Retrieve the table's object actions without rendering it.
507
     *
508
     * @return array
509
     */
510
    public function rawObjectActions()
511
    {
512
        if ($this->objectActions === null) {
513
            $parsed = $this->parsedObjectActions;
514
515
            $collectionConfig = $this->collectionConfig();
516
            if (isset($collectionConfig['object_actions'])) {
517
                $actions = $collectionConfig['object_actions'];
518
            } else {
519
                $actions = [];
520
            }
521
522
            $this->setObjectActions($actions);
523
524
            $this->parsedObjectActions = $parsed;
525
        }
526
527
        if ($this->parsedObjectActions === false) {
528
            $this->parsedObjectActions = true;
529
            $this->objectActions = $this->createObjectActions($this->objectActions);
530
        }
531
532
        return $this->objectActions;
533
    }
534
535
    /**
536
     * Set the table's object actions.
537
     *
538
     * @param  array $actions One or more actions.
539
     * @return TableWidget Chainable.
540
     */
541
    public function setObjectActions(array $actions)
542
    {
543
        $this->parsedObjectActions = false;
544
545
        $actions = $this->mergeActions($this->defaultObjectActions(), $actions);
546
547
        /** Enable seamless button group */
548
        if (isset($actions['edit'])) {
549
            $actions['edit']['actionType'] = 'seamless';
550
        }
551
552
        $this->objectActions = $actions;
553
554
        return $this;
555
    }
556
557
    /**
558
     * Build the table's object actions (row).
559
     *
560
     * Object actions should come from the collection settings defined by the "collection_ident".
561
     * It is still possible to completly override those externally by setting the "object_actions"
562
     * with the {@see self::setObjectActions()} method.
563
     *
564
     * @param  array $actions Actions to resolve.
565
     * @return array Object actions.
566
     */
567
    public function createObjectActions(array $actions)
568
    {
569
        $objectActions = $this->parseActions($actions);
570
571
        return $objectActions;
572
    }
573
574
    /**
575
     * Parse the given actions as (row) object actions.
576
     *
577
     * @param  array $actions Actions to resolve.
578
     * @return array
579
     */
580
    protected function parseAsObjectActions(array $actions)
581
    {
582
        $objectActions = [];
583
        foreach ($actions as $action) {
584
            $action = $this->parseActionRenderables($action, true);
585
586
            if (isset($action['ident'])) {
587
                if ($action['ident'] === 'view' && !$this->isObjViewable()) {
588
                    $action['active'] = false;
589
                } elseif ($action['ident'] === 'create' && !$this->isObjCreatable()) {
590
                    $action['active'] = false;
591
                } elseif ($action['ident'] === 'edit' && !$this->isObjEditable()) {
592
                    $action['active'] = false;
593
                } elseif ($action['ident'] === 'delete' && !$this->isObjDeletable()) {
594
                    $action['active'] = false;
595
                }
596
            }
597
598
            if ($action['actions']) {
599
                $action['actions']    = $this->parseAsObjectActions($action['actions']);
600
                $action['hasActions'] = !!array_filter($action['actions'], function ($action) {
601
                    return $action['active'];
602
                });
603
            }
604
605
            $objectActions[] = $action;
606
        }
607
608
        return $objectActions;
609
    }
610
611
612
613
    /**
614
     * Determine if the table's empty collection actions should be shown.
615
     *
616
     * @return boolean
617
     */
618
    public function showEmptyListActions()
619
    {
620
        $actions = $this->emptyListActions();
621
622
        return count($actions);
0 ignored issues
show
Bug Best Practice introduced by
The expression return count($actions) returns the type integer which is incompatible with the documented return type boolean.
Loading history...
623
    }
624
625
    /**
626
     * Retrieve the table's empty collection actions.
627
     *
628
     * @return array
629
     */
630
    public function emptyListActions()
631
    {
632
        $actions = $this->listActions();
633
634
        $filteredArray = array_filter($actions, function ($action) {
635
            return $action['empty'];
636
        });
637
638
        return array_values($filteredArray);
639
    }
640
641
    /**
642
     * Show/hide the table's collection actions.
643
     *
644
     * @param  boolean $show Show (TRUE) or hide (FALSE) the actions.
645
     * @return TableWidget Chainable
646
     */
647
    public function setShowListActions($show)
648
    {
649
        $this->showListActions = !!$show;
650
651
        return $this;
652
    }
653
654
    /**
655
     * Determine if the table's collection actions should be shown.
656
     *
657
     * @return boolean
658
     */
659
    public function showListActions()
660
    {
661
        if ($this->showListActions === false) {
662
            return false;
663
        } else {
664
            return count($this->listActions());
0 ignored issues
show
Bug Best Practice introduced by
The expression return count($this->listActions()) returns the type integer which is incompatible with the documented return type boolean.
Loading history...
665
        }
666
    }
667
668
    /**
669
     * Retrieve the table's collection actions.
670
     *
671
     * @return array
672
     */
673
    public function listActions()
674
    {
675
        if ($this->listActions === null) {
676
            $collectionConfig = $this->collectionConfig();
677
            if (isset($collectionConfig['list_actions'])) {
678
                $actions = $collectionConfig['list_actions'];
679
            } else {
680
                $actions = [];
681
            }
682
            $this->setListActions($actions);
683
        }
684
685
        if ($this->parsedListActions === false) {
686
            $this->parsedListActions = true;
687
            $this->listActions = $this->createListActions($this->listActions);
688
        }
689
690
        return $this->listActions;
691
    }
692
693
694
    /**
695
     * @return PaginationWidget
696
     */
697
    public function paginationWidget()
698
    {
699
        $pagination = $this->widgetFactory()->create(PaginationWidget::class);
700
        $pagination->setData([
701
            'page'         => $this->page(),
702
            'num_per_page' => $this->numPerPage(),
703
            'num_total'    => $this->numTotal(),
704
            'label'        => $this->translator()->translation('Objects list navigation')
705
        ]);
706
707
        return $pagination;
708
    }
709
710
    /**
711
     * @param boolean $show The show flag.
712
     * @return TableWidget Chainable
713
     */
714
    public function setShowTableHeader($show)
715
    {
716
        $this->showTableHeader = !!$show;
717
718
        return $this;
719
    }
720
721
    /**
722
     * @return boolean
723
     */
724
    public function showTableHeader()
725
    {
726
        return $this->showTableHeader;
727
    }
728
729
    /**
730
     * @param boolean $show The show flag.
731
     * @return TableWidget Chainable
732
     */
733
    public function setShowTableHead($show)
734
    {
735
        $this->showTableHead = !!$show;
736
737
        return $this;
738
    }
739
740
    /**
741
     * @return boolean
742
     */
743
    public function showTableHead()
744
    {
745
        return $this->showTableHead;
746
    }
747
748
    /**
749
     * @param boolean $show The show flag.
750
     * @return TableWidget Chainable
751
     */
752
    public function setShowTableFoot($show)
753
    {
754
        $this->showTableFoot = !!$show;
755
756
        return $this;
757
    }
758
759
    /**
760
     * @return boolean
761
     */
762
    public function showTableFoot()
763
    {
764
        return $this->showTableFoot;
765
    }
766
767
    /**
768
     * @param boolean $sortable The sortable flag.
769
     * @return TableWidget Chainable
770
     */
771
    public function setSortable($sortable)
772
    {
773
        $this->sortable = !!$sortable;
774
775
        return $this;
776
    }
777
778
    /**
779
     * @return boolean
780
     */
781
    public function sortable()
782
    {
783
        return $this->sortable;
784
    }
785
786
    /**
787
     * @return string
788
     */
789
    public function jsActionPrefix()
790
    {
791
        return ($this->currentObj) ? 'js-obj' : 'js-list';
792
    }
793
794
    /**
795
     * Generate URL for editing an object
796
     * @return string
797
     */
798
    public function objectEditUrl()
799
    {
800
        return 'object/edit?main_menu={{ main_menu }}&obj_type='.$this->objType();
801
    }
802
803
    /**
804
     * Generate URL for creating an object
805
     * @return string
806
     */
807
    public function objectCreateUrl()
808
    {
809
        $actions = $this->listActions();
810
        if ($actions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $actions 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...
811
            foreach ($actions as $action) {
812
                if (isset($action['ident']) && $action['ident'] === 'create') {
813
                    if (isset($action['url'])) {
814
                        $model = $this->proto();
815
                        if ($model->view()) {
816
                            $action['url'] = $model->render((string)$action['url']);
817
                        } else {
818
                            $action['url'] = preg_replace('~{{\s*id\s*}}~', $this->currentObjId, $action['url']);
819
                        }
820
821
                        return $action['url'];
822
                    }
823
                }
824
            }
825
        }
826
827
        return $this->objectEditUrl();
828
    }
829
830
    /**
831
     * Determine if the object can be created.
832
     *
833
     * If TRUE, the "Create" button is shown. Objects can still be
834
     * inserted programmatically or via direct action on the database.
835
     *
836
     * @return boolean
837
     */
838
    public function isObjCreatable()
839
    {
840
        $model = $this->proto();
841
        $method = [ $model, 'isCreatable' ];
842
843
        if (is_callable($method)) {
844
            return call_user_func($method);
845
        }
846
847
        return true;
848
    }
849
850
    /**
851
     * Determine if the object can be modified.
852
     *
853
     * If TRUE, the "Modify" button is shown. Objects can still be
854
     * updated programmatically or via direct action on the database.
855
     *
856
     * @return boolean
857
     */
858
    public function isObjEditable()
859
    {
860
        $model = ($this->currentObj) ? $this->currentObj : $this->proto();
861
        $method = [ $model, 'isEditable' ];
862
863
        if (is_callable($method)) {
864
            return call_user_func($method);
865
        }
866
867
        return true;
868
    }
869
870
    /**
871
     * Determine if the object can be deleted.
872
     *
873
     * If TRUE, the "Delete" button is shown. Objects can still be
874
     * deleted programmatically or via direct action on the database.
875
     *
876
     * @return boolean
877
     */
878
    public function isObjDeletable()
879
    {
880
        $model  = ($this->currentObj) ? $this->currentObj : $this->proto();
881
        $method = [ $model, 'isDeletable' ];
882
883
        if (is_callable($method)) {
884
            return call_user_func($method);
885
        }
886
887
        return true;
888
    }
889
890
    /**
891
     * Determine if the object can be viewed (on the front-end).
892
     *
893
     * If TRUE, any "View" button is shown. The object can still be
894
     * saved programmatically.
895
     *
896
     * @return boolean
897
     */
898
    public function isObjViewable()
899
    {
900
        $model = ($this->currentObj) ? $this->currentObj : $this->proto();
901
        if (!$model->id()) {
902
            return false;
903
        }
904
905
        $method = [ $model, 'isViewable' ];
906
        if (is_callable($method)) {
907
            return call_user_func($method);
908
        }
909
910
        return true;
911
    }
912
913
    /**
914
     * @param Container $container Pimple DI container.
915
     * @return void
916
     */
917
    protected function setDependencies(Container $container)
918
    {
919
        parent::setDependencies($container);
920
921
        // Satisfies HttpAwareTrait dependencies
922
        $this->setHttpRequest($container['request']);
923
924
        $this->setView($container['view']);
925
        $this->setCollectionLoader($container['model/collection/loader']);
926
        $this->setWidgetFactory($container['widget/factory']);
927
        $this->setPropertyFactory($container['property/factory']);
928
        $this->setPropertyDisplayFactory($container['property/display/factory']);
929
    }
930
931
    /**
932
     * Create a collection loader.
933
     *
934
     * @return CollectionLoader
0 ignored issues
show
Bug introduced by
The type Charcoal\Admin\Widget\CollectionLoader was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
935
     */
936
    protected function createCollectionLoader()
937
    {
938
        $loader = $this->createCollectionLoaderFromTrait();
939
940
        $mainMenu = filter_input(INPUT_GET, 'main_menu', FILTER_SANITIZE_STRING);
941
        if ($mainMenu) {
942
            $loader->setCallback(function (&$obj) use ($mainMenu) {
943
                if (!$obj['main_menu']) {
944
                    $obj['main_menu'] = $mainMenu;
945
                }
946
            });
947
        }
948
949
        return $loader;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $loader returns the type Charcoal\Loader\CollectionLoader which is incompatible with the documented return type Charcoal\Admin\Widget\CollectionLoader.
Loading history...
950
    }
951
952
    /**
953
     * Retrieve the widget factory.
954
     *
955
     * @throws RuntimeException If the widget factory was not previously set.
956
     * @return FactoryInterface
957
     */
958
    protected function widgetFactory()
959
    {
960
        if ($this->widgetFactory === null) {
961
            throw new RuntimeException(
962
                sprintf('Widget Factory is not defined for "%s"', get_class($this))
963
            );
964
        }
965
966
        return $this->widgetFactory;
967
    }
968
969
    /**
970
     * @throws RuntimeException If the property factory was not previously set / injected.
971
     * @return FactoryInterface
972
     */
973
    protected function propertyFactory()
974
    {
975
        if ($this->propertyFactory === null) {
976
            throw new RuntimeException(
977
                'Property factory is not set for table widget'
978
            );
979
        }
980
981
        return $this->propertyFactory;
982
    }
983
984
    /**
985
     * Retrieve the default data source filters (when setting data on an entity).
986
     *
987
     * Note: Adapted from {@see \Slim\CallableResolver}.
988
     *
989
     * @link   https://github.com/slimphp/Slim/blob/3.x/Slim/CallableResolver.php
990
     * @param  mixed $toResolve A callable used when merging data.
991
     * @return callable|null
992
     */
993
    protected function resolveDataSourceFilter($toResolve)
994
    {
995
        if (is_string($toResolve)) {
996
            $model = $this->proto();
997
998
            $resolved = [ $model, $toResolve ];
999
1000
            // check for slim callable as "class:method"
1001
            $callablePattern = '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!';
1002
            if (preg_match($callablePattern, $toResolve, $matches)) {
1003
                $class = $matches[1];
1004
                $method = $matches[2];
1005
1006
                if ($class === 'parent') {
1007
                    $resolved = [ $model, $class.'::'.$method ];
1008
                }
1009
            }
1010
1011
            $toResolve = $resolved;
1012
        }
1013
1014
        return parent::resolveDataSourceFilter($toResolve);
1015
    }
1016
1017
    /**
1018
     * Set the table's collection actions.
1019
     *
1020
     * @param  array $actions One or more actions.
1021
     * @return TableWidget Chainable.
1022
     */
1023
    protected function setListActions(array $actions)
1024
    {
1025
        $this->parsedListActions = false;
1026
1027
        $this->listActions = $this->mergeActions($this->defaultListActions(), $actions);
1028
1029
        return $this;
1030
    }
1031
1032
    /**
1033
     * Build the table collection actions.
1034
     *
1035
     * List actions should come from the collection settings defined by the "collection_ident".
1036
     * It is still possible to completly override those externally by setting the "list_actions"
1037
     * with the {@see self::setListActions()} method.
1038
     *
1039
     * @param  array $actions Actions to resolve.
1040
     * @return array List actions.
1041
     */
1042
    protected function createListActions(array $actions)
1043
    {
1044
        $this->actionsPriority = $this->defaultActionPriority();
1045
1046
        $listActions = $this->parseAsListActions($actions);
1047
1048
        return $listActions;
1049
    }
1050
1051
    /**
1052
     * Parse the given actions as collection actions.
1053
     *
1054
     * @param  array $actions Actions to resolve.
1055
     * @return array
1056
     */
1057
    protected function parseAsListActions(array $actions)
1058
    {
1059
        $listActions = [];
1060
        foreach ($actions as $ident => $action) {
1061
            $ident  = $this->parseActionIdent($ident, $action);
1062
            $action = $this->parseActionItem($action, $ident, true);
1063
1064
            if (!isset($action['priority'])) {
1065
                $action['priority'] = $this->actionsPriority++;
1066
            }
1067
1068
            if ($action['ident'] === 'create') {
1069
                $action['empty'] = true;
1070
1071
                if (!$this->isObjCreatable()) {
1072
                    $action['active'] = false;
1073
                }
1074
            } else {
1075
                $action['empty'] = (isset($action['empty']) ? boolval($action['empty']) : false);
1076
            }
1077
1078
            if (is_array($action['actions'])) {
1079
                $action['actions']    = $this->parseAsListActions($action['actions']);
1080
                $action['hasActions'] = !!array_filter($action['actions'], function ($action) {
1081
                    return $action['active'];
1082
                });
1083
            }
1084
1085
            if (isset($listActions[$ident])) {
1086
                $hasPriority = ($action['priority'] > $listActions[$ident]['priority']);
1087
                if ($hasPriority || $action['isSubmittable']) {
1088
                    $listActions[$ident] = array_replace($listActions[$ident], $action);
1089
                } else {
1090
                    $listActions[$ident] = array_replace($action, $listActions[$ident]);
1091
                }
1092
            } else {
1093
                $listActions[$ident] = $action;
1094
            }
1095
        }
1096
1097
        usort($listActions, [ $this, 'sortActionsByPriority' ]);
1098
1099
        while (($first = reset($listActions)) && $first['isSeparator']) {
1100
            array_shift($listActions);
1101
        }
1102
1103
        while (($last = end($listActions)) && $last['isSeparator']) {
1104
            array_pop($listActions);
1105
        }
1106
1107
        return $listActions;
1108
    }
1109
1110
    /**
1111
     * Retrieve the table's default collection actions.
1112
     *
1113
     * @return array
1114
     */
1115
    protected function defaultListActions()
1116
    {
1117
        if ($this->defaultListActions === null) {
1118
            $this->defaultListActions = [];
1119
        }
1120
1121
        return $this->defaultListActions;
1122
    }
1123
1124
    /**
1125
     * Retrieve the table's default object actions.
1126
     *
1127
     * @return array
1128
     */
1129
    protected function defaultObjectActions()
1130
    {
1131
        if ($this->defaultObjectActions === null) {
1132
            $edit = [
1133
                'label'    => $this->translator()->translation('Modify'),
1134
                'url'      => $this->objectEditUrl().'&obj_id={{id}}',
1135
                'ident'    => 'edit',
1136
                'priority' => 1
1137
            ];
1138
            $this->defaultObjectActions = [ $edit ];
1139
        }
1140
1141
        return $this->defaultObjectActions;
1142
    }
1143
1144
    /**
1145
     * Retrieve the default property customizations.
1146
     *
1147
     * The default configset is determined by the collection ident and object type, if assigned.
1148
     *
1149
     * @return array|null
1150
     */
1151
    protected function defaultPropertiesOptions()
1152
    {
1153
        $collectionConfig = $this->collectionConfig();
1154
1155
        if (empty($collectionConfig['properties_options'])) {
1156
            return [];
1157
        }
1158
1159
        return $collectionConfig['properties_options'];
1160
    }
1161
1162
    /**
1163
     * Filter the property before its assigned to the object row.
1164
     *
1165
     * This method is useful for classes using this trait.
1166
     *
1167
     * @param  ModelInterface    $object        The current row's object.
1168
     * @param  PropertyInterface $property      The current property.
1169
     * @param  string            $propertyValue The property $key's display value.
1170
     * @return array
1171
     */
1172
    protected function parsePropertyCell(
1173
        ModelInterface $object,
1174
        PropertyInterface $property,
1175
        $propertyValue
1176
    ) {
1177
        $cell    = $this->parseCollectionPropertyCell($object, $property, $propertyValue);
1178
        $ident   = $property->ident();
1179
        $options = $this->viewOptions($ident);
1180
        $classes = $this->parsePropertyCellClasses($property, $object);
1181
1182
        $cell['truncate'] = (isset($options['truncate']) ? boolval($options['truncate']) : false);
1183
1184
        if (!isset($cell['attr'])) {
1185
            $cell['attr'] = [];
1186
        }
1187
1188
        if (isset($options['attr'])) {
1189
            unset($options['attr']['width']);
1190
            $cell['attr'] = array_merge($cell['attr'], $options['attr']);
1191
        }
1192
1193
        if (isset($classes)) {
1194
            if (isset($cell['attr']['class'])) {
1195
                if (is_string($classes)) {
0 ignored issues
show
introduced by
The condition is_string($classes) is always false.
Loading history...
1196
                    $classes = explode(' ', $cell['attr']['class']);
1197
                }
1198
1199
                if (is_string($cell['attr']['class'])) {
1200
                    $cell['attr']['class'] = explode(' ', $cell['attr']['class']);
1201
                }
1202
1203
                $cell['attr']['class'] = array_unique(array_merge($cell['attr']['class'], $classes));
1204
            } else {
1205
                $cell['attr']['class'] = $classes;
1206
            }
1207
1208
            unset($classes);
1209
        }
1210
1211
        $cell['attr'] = html_build_attributes($cell['attr']);
1212
1213
        return $cell;
1214
    }
1215
1216
    /**
1217
     * Filter the table cell's CSS classes before the property is assigned
1218
     * to the object row.
1219
     *
1220
     * This method is useful for classes using this trait.
1221
     *
1222
     * @param  PropertyInterface   $property The current property.
1223
     * @param  ModelInterface|null $object   Optional. The current row's object.
1224
     * @return array
1225
     */
1226
    protected function parsePropertyCellClasses(
1227
        PropertyInterface $property,
1228
        ModelInterface $object = null
1229
    ) {
1230
        unset($object);
1231
1232
        $ident = $property->ident();
1233
        $classes = [ sprintf('property-%s', $ident) ];
1234
        $options = $this->viewOptions($ident);
1235
1236
        if (isset($options['classes'])) {
1237
            if (is_array($options['classes'])) {
1238
                $classes = array_merge($classes, $options['classes']);
1239
            } else {
1240
                $classes[] = $options['classes'];
1241
            }
1242
        }
1243
1244
        return $classes;
1245
    }
1246
1247
    /**
1248
     * Filter the object before its assigned to the row.
1249
     *
1250
     * This method is useful for classes using this trait.
1251
     *
1252
     * @param  ModelInterface $object           The current row's object.
1253
     * @param  array          $objectProperties The $object's display properties.
1254
     * @return array
1255
     */
1256
    protected function parseObjectRow(ModelInterface $object, array $objectProperties)
1257
    {
1258
        $row = $this->parseCollectionObjectRow($object, $objectProperties);
1259
        $row['objectActions'] = $this->objectActions();
1260
        $row['showObjectActions'] = ($this->showObjectActions === false) ? false : !!$row['objectActions'];
1261
1262
        $row['attr'] = [
1263
            'class' => []
1264
        ];
1265
1266
        $method = [ $object, 'isActiveTableRow' ];
1267
        if (is_callable($method)) {
1268
            if (call_user_func($method)) {
1269
                $row['attr']['class'][] = 'active';
1270
            }
1271
        }
1272
1273
        $row['attr']['class'][] = 'js-table-row';
1274
1275
        $row['attr'] = html_build_attributes($row['attr']);
1276
1277
        return $row;
1278
    }
1279
1280
    /**
1281
     * Set an widget factory.
1282
     *
1283
     * @param FactoryInterface $factory The factory to create widgets.
1284
     * @return void
1285
     */
1286
    private function setWidgetFactory(FactoryInterface $factory)
1287
    {
1288
        $this->widgetFactory = $factory;
1289
    }
1290
1291
    /**
1292
     * @param FactoryInterface $factory The property factory, to create properties.
1293
     * @return TableWidget Chainable
1294
     */
1295
    private function setPropertyFactory(FactoryInterface $factory)
1296
    {
1297
        $this->propertyFactory = $factory;
1298
1299
        return $this;
1300
    }
1301
}
1302