TableWidget::dataFromObject()   F
last analyzed

Complexity

Conditions 21
Paths 522

Size

Total Lines 88
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 52
nc 522
nop 0
dl 0
loc 88
rs 0.6637
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $column returns the type Generator which is incompatible with the documented return type Charcoal\Admin\Widget\Generator|array.
Loading history...
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
        $model = $this->proto();
801
        $url   = 'object/edit?main_menu={{ main_menu }}&obj_type='.$this->objType();
802
803
        if ($model->view()) {
804
            $url = $model->render((string)$url);
805
        } else {
806
            $url = preg_replace('~{{\s*id\s*}}~', $this->currentObjId, $url);
807
        }
808
809
        return $url;
810
    }
811
812
    /**
813
     * Generate URL for creating an object
814
     * @return string
815
     */
816
    public function objectCreateUrl()
817
    {
818
        $actions = $this->listActions();
819
        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...
820
            foreach ($actions as $action) {
821
                if (isset($action['ident']) && $action['ident'] === 'create') {
822
                    if (isset($action['url'])) {
823
                        $model = $this->proto();
824
                        if ($model->view()) {
825
                            $action['url'] = $model->render((string)$action['url']);
826
                        } else {
827
                            $action['url'] = preg_replace('~{{\s*id\s*}}~', $this->currentObjId, $action['url']);
828
                        }
829
830
                        return $action['url'];
831
                    }
832
                }
833
            }
834
        }
835
836
        return $this->objectEditUrl();
837
    }
838
839
    /**
840
     * Determine if the object can be created.
841
     *
842
     * If TRUE, the "Create" button is shown. Objects can still be
843
     * inserted programmatically or via direct action on the database.
844
     *
845
     * @return boolean
846
     */
847
    public function isObjCreatable()
848
    {
849
        $model = $this->proto();
850
        $method = [ $model, 'isCreatable' ];
851
852
        if (is_callable($method)) {
853
            return call_user_func($method);
854
        }
855
856
        return true;
857
    }
858
859
    /**
860
     * Determine if the object can be modified.
861
     *
862
     * If TRUE, the "Modify" button is shown. Objects can still be
863
     * updated programmatically or via direct action on the database.
864
     *
865
     * @return boolean
866
     */
867
    public function isObjEditable()
868
    {
869
        $model = ($this->currentObj) ? $this->currentObj : $this->proto();
870
        $method = [ $model, 'isEditable' ];
871
872
        if (is_callable($method)) {
873
            return call_user_func($method);
874
        }
875
876
        return true;
877
    }
878
879
    /**
880
     * Determine if the object can be deleted.
881
     *
882
     * If TRUE, the "Delete" button is shown. Objects can still be
883
     * deleted programmatically or via direct action on the database.
884
     *
885
     * @return boolean
886
     */
887
    public function isObjDeletable()
888
    {
889
        $model  = ($this->currentObj) ? $this->currentObj : $this->proto();
890
        $method = [ $model, 'isDeletable' ];
891
892
        if (is_callable($method)) {
893
            return call_user_func($method);
894
        }
895
896
        return true;
897
    }
898
899
    /**
900
     * Determine if the object can be viewed (on the front-end).
901
     *
902
     * If TRUE, any "View" button is shown. The object can still be
903
     * saved programmatically.
904
     *
905
     * @return boolean
906
     */
907
    public function isObjViewable()
908
    {
909
        $model = ($this->currentObj) ? $this->currentObj : $this->proto();
910
        if (!$model->id()) {
911
            return false;
912
        }
913
914
        $method = [ $model, 'isViewable' ];
915
        if (is_callable($method)) {
916
            return call_user_func($method);
917
        }
918
919
        return true;
920
    }
921
922
    /**
923
     * @param Container $container Pimple DI container.
924
     * @return void
925
     */
926
    protected function setDependencies(Container $container)
927
    {
928
        parent::setDependencies($container);
929
930
        // Satisfies HttpAwareTrait dependencies
931
        $this->setHttpRequest($container['request']);
932
933
        $this->setView($container['view']);
934
        $this->setCollectionLoader($container['model/collection/loader']);
935
        $this->setWidgetFactory($container['widget/factory']);
936
        $this->setPropertyFactory($container['property/factory']);
937
        $this->setPropertyDisplayFactory($container['property/display/factory']);
938
    }
939
940
    /**
941
     * Create a collection loader.
942
     *
943
     * @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...
944
     */
945
    protected function createCollectionLoader()
946
    {
947
        $loader = $this->createCollectionLoaderFromTrait();
948
949
        $mainMenu = filter_input(INPUT_GET, 'main_menu', FILTER_SANITIZE_STRING);
950
        if ($mainMenu) {
951
            $loader->setCallback(function (&$obj) use ($mainMenu) {
952
                if (!$obj['main_menu']) {
953
                    $obj['main_menu'] = $mainMenu;
954
                }
955
            });
956
        }
957
958
        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...
959
    }
960
961
    /**
962
     * Retrieve the widget factory.
963
     *
964
     * @throws RuntimeException If the widget factory was not previously set.
965
     * @return FactoryInterface
966
     */
967
    protected function widgetFactory()
968
    {
969
        if ($this->widgetFactory === null) {
970
            throw new RuntimeException(
971
                sprintf('Widget Factory is not defined for "%s"', get_class($this))
972
            );
973
        }
974
975
        return $this->widgetFactory;
976
    }
977
978
    /**
979
     * @throws RuntimeException If the property factory was not previously set / injected.
980
     * @return FactoryInterface
981
     */
982
    protected function propertyFactory()
983
    {
984
        if ($this->propertyFactory === null) {
985
            throw new RuntimeException(
986
                'Property factory is not set for table widget'
987
            );
988
        }
989
990
        return $this->propertyFactory;
991
    }
992
993
    /**
994
     * Retrieve the default data source filters (when setting data on an entity).
995
     *
996
     * Note: Adapted from {@see \Slim\CallableResolver}.
997
     *
998
     * @link   https://github.com/slimphp/Slim/blob/3.x/Slim/CallableResolver.php
999
     * @param  mixed $toResolve A callable used when merging data.
1000
     * @return callable|null
1001
     */
1002
    protected function resolveDataSourceFilter($toResolve)
1003
    {
1004
        if (is_string($toResolve)) {
1005
            $model = $this->proto();
1006
1007
            $resolved = [ $model, $toResolve ];
1008
1009
            // Check for Slim callable
1010
            $callablePattern = '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!';
1011
            if (preg_match($callablePattern, $toResolve, $matches)) {
1012
                $class = $matches[1];
1013
                $method = $matches[2];
1014
1015
                if ($class === 'parent') {
1016
                    $resolved = [ $model, $class.'::'.$method ];
1017
                }
1018
            }
1019
1020
            $toResolve = $resolved;
1021
        }
1022
1023
        return parent::resolveDataSourceFilter($toResolve);
1024
    }
1025
1026
    /**
1027
     * Set the table's collection actions.
1028
     *
1029
     * @param  array $actions One or more actions.
1030
     * @return TableWidget Chainable.
1031
     */
1032
    protected function setListActions(array $actions)
1033
    {
1034
        $this->parsedListActions = false;
1035
1036
        $this->listActions = $this->mergeActions($this->defaultListActions(), $actions);
1037
1038
        return $this;
1039
    }
1040
1041
    /**
1042
     * Build the table collection actions.
1043
     *
1044
     * List actions should come from the collection settings defined by the "collection_ident".
1045
     * It is still possible to completly override those externally by setting the "list_actions"
1046
     * with the {@see self::setListActions()} method.
1047
     *
1048
     * @param  array $actions Actions to resolve.
1049
     * @return array List actions.
1050
     */
1051
    protected function createListActions(array $actions)
1052
    {
1053
        $this->actionsPriority = $this->defaultActionPriority();
1054
1055
        $listActions = $this->parseAsListActions($actions);
1056
1057
        return $listActions;
1058
    }
1059
1060
    /**
1061
     * Parse the given actions as collection actions.
1062
     *
1063
     * @param  array $actions Actions to resolve.
1064
     * @return array
1065
     */
1066
    protected function parseAsListActions(array $actions)
1067
    {
1068
        $listActions = [];
1069
        foreach ($actions as $ident => $action) {
1070
            $ident  = $this->parseActionIdent($ident, $action);
1071
            $action = $this->parseActionItem($action, $ident, true);
1072
1073
            if (!isset($action['priority'])) {
1074
                $action['priority'] = $this->actionsPriority++;
1075
            }
1076
1077
            if ($action['ident'] === 'create') {
1078
                $action['empty'] = true;
1079
1080
                if (!$this->isObjCreatable()) {
1081
                    $action['active'] = false;
1082
                }
1083
            } else {
1084
                $action['empty'] = (isset($action['empty']) ? boolval($action['empty']) : false);
1085
            }
1086
1087
            if (is_array($action['actions'])) {
1088
                $action['actions']    = $this->parseAsListActions($action['actions']);
1089
                $action['hasActions'] = !!array_filter($action['actions'], function ($action) {
1090
                    return $action['active'];
1091
                });
1092
            }
1093
1094
            if (isset($listActions[$ident])) {
1095
                $hasPriority = ($action['priority'] > $listActions[$ident]['priority']);
1096
                if ($hasPriority || $action['isSubmittable']) {
1097
                    $listActions[$ident] = array_replace($listActions[$ident], $action);
1098
                } else {
1099
                    $listActions[$ident] = array_replace($action, $listActions[$ident]);
1100
                }
1101
            } else {
1102
                $listActions[$ident] = $action;
1103
            }
1104
        }
1105
1106
        usort($listActions, [ $this, 'sortActionsByPriority' ]);
1107
1108
        while (($first = reset($listActions)) && $first['isSeparator']) {
1109
            array_shift($listActions);
1110
        }
1111
1112
        while (($last = end($listActions)) && $last['isSeparator']) {
1113
            array_pop($listActions);
1114
        }
1115
1116
        return $listActions;
1117
    }
1118
1119
    /**
1120
     * Retrieve the table's default collection actions.
1121
     *
1122
     * @return array
1123
     */
1124
    protected function defaultListActions()
1125
    {
1126
        if ($this->defaultListActions === null) {
1127
            $this->defaultListActions = [];
1128
        }
1129
1130
        return $this->defaultListActions;
1131
    }
1132
1133
    /**
1134
     * Retrieve the table's default object actions.
1135
     *
1136
     * @return array
1137
     */
1138
    protected function defaultObjectActions()
1139
    {
1140
        if ($this->defaultObjectActions === null) {
1141
            $edit = [
1142
                'label'    => $this->translator()->translation('Modify'),
1143
                'url'      => $this->objectEditUrl().'&obj_id={{id}}',
1144
                'ident'    => 'edit',
1145
                'priority' => 1
1146
            ];
1147
            $this->defaultObjectActions = [ $edit ];
1148
        }
1149
1150
        return $this->defaultObjectActions;
1151
    }
1152
1153
    /**
1154
     * Retrieve the default property customizations.
1155
     *
1156
     * The default configset is determined by the collection ident and object type, if assigned.
1157
     *
1158
     * @return array|null
1159
     */
1160
    protected function defaultPropertiesOptions()
1161
    {
1162
        $collectionConfig = $this->collectionConfig();
1163
1164
        if (empty($collectionConfig['properties_options'])) {
1165
            return [];
1166
        }
1167
1168
        return $collectionConfig['properties_options'];
1169
    }
1170
1171
    /**
1172
     * Filter the property before its assigned to the object row.
1173
     *
1174
     * This method is useful for classes using this trait.
1175
     *
1176
     * @param  ModelInterface    $object        The current row's object.
1177
     * @param  PropertyInterface $property      The current property.
1178
     * @param  string            $propertyValue The property $key's display value.
1179
     * @return array
1180
     */
1181
    protected function parsePropertyCell(
1182
        ModelInterface $object,
1183
        PropertyInterface $property,
1184
        $propertyValue
1185
    ) {
1186
        $cell    = $this->parseCollectionPropertyCell($object, $property, $propertyValue);
1187
        $ident   = $property->ident();
1188
        $options = $this->viewOptions($ident);
1189
        $classes = $this->parsePropertyCellClasses($property, $object);
1190
1191
        $cell['truncate'] = (isset($options['truncate']) ? boolval($options['truncate']) : false);
1192
1193
        if (!isset($cell['attr'])) {
1194
            $cell['attr'] = [];
1195
        }
1196
1197
        if (isset($options['attr'])) {
1198
            unset($options['attr']['width']);
1199
            $cell['attr'] = array_merge($cell['attr'], $options['attr']);
0 ignored issues
show
Bug introduced by
$cell['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

1199
            $cell['attr'] = array_merge(/** @scrutinizer ignore-type */ $cell['attr'], $options['attr']);
Loading history...
1200
        }
1201
1202
        if (isset($classes)) {
1203
            if (isset($cell['attr']['class'])) {
1204
                if (is_string($classes)) {
0 ignored issues
show
introduced by
The condition is_string($classes) is always false.
Loading history...
1205
                    $classes = explode(' ', $cell['attr']['class']);
1206
                }
1207
1208
                if (is_string($cell['attr']['class'])) {
1209
                    $cell['attr']['class'] = explode(' ', $cell['attr']['class']);
1210
                }
1211
1212
                $cell['attr']['class'] = array_unique(array_merge($cell['attr']['class'], $classes));
1213
            } else {
1214
                $cell['attr']['class'] = $classes;
1215
            }
1216
1217
            unset($classes);
1218
        }
1219
1220
        $cell['attr'] = html_build_attributes($cell['attr']);
1221
1222
        return $cell;
1223
    }
1224
1225
    /**
1226
     * Filter the table cell's CSS classes before the property is assigned
1227
     * to the object row.
1228
     *
1229
     * This method is useful for classes using this trait.
1230
     *
1231
     * @param  PropertyInterface   $property The current property.
1232
     * @param  ModelInterface|null $object   Optional. The current row's object.
1233
     * @return array
1234
     */
1235
    protected function parsePropertyCellClasses(
1236
        PropertyInterface $property,
1237
        ModelInterface $object = null
1238
    ) {
1239
        unset($object);
1240
1241
        $ident = $property->ident();
1242
        $classes = [ sprintf('property-%s', $ident) ];
1243
        $options = $this->viewOptions($ident);
1244
1245
        if (isset($options['classes'])) {
1246
            if (is_array($options['classes'])) {
1247
                $classes = array_merge($classes, $options['classes']);
1248
            } else {
1249
                $classes[] = $options['classes'];
1250
            }
1251
        }
1252
1253
        return $classes;
1254
    }
1255
1256
    /**
1257
     * Filter the object before its assigned to the row.
1258
     *
1259
     * This method is useful for classes using this trait.
1260
     *
1261
     * @param  ModelInterface $object           The current row's object.
1262
     * @param  array          $objectProperties The $object's display properties.
1263
     * @return array
1264
     */
1265
    protected function parseObjectRow(ModelInterface $object, array $objectProperties)
1266
    {
1267
        $row = $this->parseCollectionObjectRow($object, $objectProperties);
1268
        $row['objectActions'] = $this->objectActions();
1269
        $row['showObjectActions'] = ($this->showObjectActions() === false) ? false : !!$row['objectActions'];
1270
1271
        $row['attr'] = [
1272
            'class' => []
1273
        ];
1274
1275
        $method = [ $object, 'isActiveTableRow' ];
1276
        if (is_callable($method)) {
1277
            if (call_user_func($method)) {
1278
                $row['attr']['class'][] = 'active';
1279
            }
1280
        }
1281
1282
        $row['attr']['class'][] = 'js-table-row';
1283
1284
        $row['attr'] = html_build_attributes($row['attr']);
1285
1286
        return $row;
1287
    }
1288
1289
    /**
1290
     * Set an widget factory.
1291
     *
1292
     * @param FactoryInterface $factory The factory to create widgets.
1293
     * @return void
1294
     */
1295
    private function setWidgetFactory(FactoryInterface $factory)
1296
    {
1297
        $this->widgetFactory = $factory;
1298
    }
1299
1300
    /**
1301
     * @param FactoryInterface $factory The property factory, to create properties.
1302
     * @return TableWidget Chainable
1303
     */
1304
    private function setPropertyFactory(FactoryInterface $factory)
1305
    {
1306
        $this->propertyFactory = $factory;
1307
1308
        return $this;
1309
    }
1310
}
1311