ActionContainerTrait   F
last analyzed

Complexity

Total Complexity 119

Size/Duplication

Total Lines 546
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 247
dl 0
loc 546
rs 2
c 0
b 0
f 0
wmc 119

14 Methods

Rating   Name   Duplication   Size   Complexity  
B parseActions() 0 36 10
A parseActionRenderables() 0 23 6
B resolveActionType() 0 18 8
C parseActionUrl() 0 45 15
A parseActionIdent() 0 7 2
A compareActions() 0 7 6
B getActionRenderer() 0 18 7
A sortActionsByPriority() 0 9 5
C mergeActions() 0 47 12
F parseActionItem() 0 105 29
C parseActionCondition() 0 33 12
A defaultActionStruct() 0 28 1
A parseActionCssClasses() 0 16 4
A defaultActionPriority() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like ActionContainerTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ActionContainerTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Charcoal\Admin\Ui;
4
5
use RuntimeException;
6
7
// From 'charcoal-translator'
8
use Charcoal\Translator\Translation;
9
10
// From 'charcoal-view'
11
use Charcoal\View\ViewableInterface;
12
13
// From 'charcoal-user'
14
use Charcoal\User\AuthAwareInterface;
15
16
// From 'charcoal-admin'
17
use Charcoal\Admin\Ui\CollectionContainerInterface;
18
use Charcoal\Admin\Ui\FormSidebarInterface;
19
use Charcoal\Admin\Ui\ObjectContainerInterface;
20
21
/**
22
 * Provides methods for building groups of linked/action-capable items.
23
 *
24
 * Can be used to build navigation bars, lists, and dropdowns.
25
 * This trait {@todo should be replaced} with {@see \Charcoal\Ui\Menu}.
26
 */
27
trait ActionContainerTrait
28
{
29
    /**
30
     * Keep track of priority pool.
31
     *
32
     * @var integer|null
33
     */
34
    protected $actionsPriority;
35
36
    /**
37
     * Parse the given UI actions.
38
     *
39
     * @param  array $actions  Actions to resolve.
40
     * @param  mixed $renderer Either a {@see ViewableInterface} or TRUE
41
     *     to determine if any renderables should be processed.
42
     * @return array Returns a collection of parsed actions.
43
     */
44
    protected function parseActions(array $actions, $renderer = false)
45
    {
46
        $this->actionsPriority = $this->defaultActionPriority();
47
48
        $parsedActions = [];
49
        foreach ($actions as $ident => $action) {
50
            $ident  = $this->parseActionIdent($ident, $action);
51
            $action = $this->parseActionItem($action, $ident, $renderer);
52
53
            if (!isset($action['priority'])) {
54
                $action['priority'] = $this->actionsPriority++;
55
            }
56
57
            if (isset($parsedActions[$ident])) {
58
                $hasPriority = ($action['priority'] > $parsedActions[$ident]['priority']);
59
                if ($hasPriority || $action['isSubmittable']) {
60
                    $parsedActions[$ident] = array_replace($parsedActions[$ident], $action);
61
                } else {
62
                    $parsedActions[$ident] = array_replace($action, $parsedActions[$ident]);
63
                }
64
            } else {
65
                $parsedActions[$ident] = $action;
66
            }
67
        }
68
69
        usort($parsedActions, [ $this, 'sortActionsByPriority' ]);
70
71
        while (($first = reset($parsedActions)) && $first['isSeparator']) {
72
            array_shift($parsedActions);
73
        }
74
75
        while (($last = end($parsedActions)) && $last['isSeparator']) {
76
            array_pop($parsedActions);
77
        }
78
79
        return $parsedActions;
80
    }
81
82
    /**
83
     * Merge the given (raw) UI actions into a unique set.
84
     *
85
     * @param  array ...$params Variable list of actions to merge.
86
     * @return array Returns a collection of merged actions.
87
     */
88
    protected function mergeActions(array ...$params)
89
    {
90
        $unique = [];
91
        foreach ($params as $actions) {
92
            foreach ($actions as $ident => $action) {
93
                if ($action === '|') {
94
                    $unique[$ident] = $action;
95
                    continue;
96
                }
97
98
                $ident = $this->parseActionIdent($ident, $action);
99
                $action['ident'] = $ident;
100
101
                $hasActions = (isset($action['actions']) && is_array($action['actions']));
102
                if ($hasActions) {
103
                    $action['actions'] = $this->mergeActions($action['actions']);
104
                } else {
105
                    $action['actions'] = [];
106
                }
107
108
                if (isset($unique[$ident])) {
109
                    if (static::compareActions($action, $unique[$ident])) {
0 ignored issues
show
Bug Best Practice introduced by
The method Charcoal\Admin\Ui\Action...Trait::compareActions() is not static, but was called statically. ( Ignorable by Annotation )

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

109
                    if (static::/** @scrutinizer ignore-call */ compareActions($action, $unique[$ident])) {
Loading history...
110
                        if ($hasActions && !!$unique[$ident]['actions']) {
111
                            $action['actions'] = $this->mergeActions(
112
                                $unique[$ident]['actions'],
113
                                $action['actions']
114
                            );
115
                            unset($unique[$ident]['actions']);
116
                        }
117
                        $unique[$ident] = array_replace($unique[$ident], $action);
118
                    } else {
119
                        if ($hasActions && !!$unique[$ident]['actions']) {
120
                            $unique[$ident]['actions'] = $this->mergeActions(
121
                                $unique[$ident]['actions'],
122
                                $action['actions']
123
                            );
124
                            unset($action['actions']);
125
                        }
126
                        $unique[$ident] = array_replace($action, $unique[$ident]);
127
                    }
128
                } else {
129
                    $unique[$ident] = $action;
130
                }
131
            }
132
        }
133
134
        return $unique;
135
    }
136
137
    /**
138
     * Parse the given UI action identifier.
139
     *
140
     * @param  string $ident  The action identifier.
141
     * @param  mixed  $action The action structure.
142
     * @return string Resolved action identifier.
143
     */
144
    protected function parseActionIdent($ident, $action)
145
    {
146
        if (isset($action['ident'])) {
147
            return $action['ident'];
148
        }
149
150
        return $ident;
151
    }
152
153
    /**
154
     * Parse the given UI action structure.
155
     *
156
     * @param  mixed  $action   The action structure.
157
     * @param  string $ident    The action identifier.
158
     * @param  mixed  $renderer Either a {@see ViewableInterface} or TRUE
159
     *     to determine if any renderables should be processed.
160
     * @return array Resolved action structure.
161
     */
162
    protected function parseActionItem($action, $ident, $renderer = false)
163
    {
164
        if ($action === '|') {
165
            $action = $this->defaultActionStruct();
166
            $action['isSeparator'] = true;
167
        } elseif (is_array($action)) {
168
            $buttonTypes = [ 'button', 'menu', 'reset', 'submit', 'action'];
169
            // Normalize structure keys
170
            foreach ($action as $key => $val) {
171
                $attr = $this->camelize($key);
172
                if ($key !== $attr) {
173
                    $action[$attr] = $val;
174
                    unset($action[$key]);
175
                }
176
            }
177
178
            if (!isset($action['ident'])) {
179
                $action['ident'] = $ident;
180
            }
181
182
            if (isset($action['buttonType'])) {
183
                if (!in_array($action['buttonType'], $buttonTypes)) {
184
                    $action['actionType'] = $action['buttonType'];
185
                    $action['buttonType'] = 'button';
186
                }
187
            }
188
189
            if (!isset($action['actionType'])) {
190
                $action['actionType'] = $this->resolveActionType($action);
191
            }
192
193
            if (isset($action['label'])) {
194
                $action['label'] = $this->translator()->translation($action['label']);
0 ignored issues
show
Bug introduced by
It seems like translator() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

194
                $action['label'] = $this->/** @scrutinizer ignore-call */ translator()->translation($action['label']);
Loading history...
195
            } else {
196
                $action['label'] = ucwords(str_replace([ '.', '_' ], ' ', $action['ident']));
197
198
                $model = $this->getActionRenderer();
199
                if ($model) {
200
                    $meta  = $model->metadata();
0 ignored issues
show
Bug introduced by
The method metadata() does not exist on Charcoal\View\ViewableInterface. It seems like you code against a sub-type of Charcoal\View\ViewableInterface such as Charcoal\Model\AbstractModel or Charcoal\Admin\Property\AbstractPropertyInput. ( Ignorable by Annotation )

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

200
                    /** @scrutinizer ignore-call */ 
201
                    $meta  = $model->metadata();
Loading history...
201
                    $label = sprintf('%s_item', $action['ident']);
202
                    if (isset($meta['labels'][$label])) {
203
                        $action['label'] = $this->translator()->translation($meta['labels'][$label]);
204
                    }
205
                }
206
            }
207
208
            if (isset($action['url'])) {
209
                $action['url']      = $this->translator()->translation($action['url']);
210
                $action['isText']   = false;
211
                $action['isLink']   = true;
212
                $action['isButton'] = false;
213
            } else {
214
                $action['url'] = null;
215
            }
216
217
            if (isset($action['buttonType'])) {
218
                if ($action['url'] === '#' && in_array($action['buttonType'], $buttonTypes)) {
219
                    $action['isLink']   = false;
220
                    $action['isButton'] = true;
221
                }
222
223
                if ($action['buttonType'] === 'submit') {
224
                    $action['isSubmittable'] = true;
225
                }
226
227
                if ($action['buttonType'] === 'action') {
228
                    $action['actionType'] = 'primary';
229
                    $action['cssClasses'] = 'js-action-button btn btn-primary';
230
                }
231
            }
232
233
            if (isset($action['extraTemplate'])) {
234
                    $action['extraTemplate'] = $this->render($action['extraTemplate']);
0 ignored issues
show
Bug introduced by
It seems like render() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

234
                    /** @scrutinizer ignore-call */ 
235
                    $action['extraTemplate'] = $this->render($action['extraTemplate']);
Loading history...
235
            }
236
237
            if (isset($action['dataAttributes']) && is_array($action['dataAttributes'])) {
238
                $action['dataAttributes'] = array_filter($action['dataAttributes'], function ($attribute) {
239
                    return  !empty($attribute['key']) &&
240
                            is_string($attribute['key']) &&
241
                            !empty($attribute['value']) &&
242
                            is_string($attribute['value']);
243
                });
244
            } else {
245
                $action['dataAttributes'] = [];
246
            }
247
248
            if (isset($action['actions']) && is_array($action['actions'])) {
249
                $action['actions']    = $this->parseActions($action['actions']);
250
                $action['hasActions'] = !!array_filter($action['actions'], function ($action) {
251
                    return $action['active'];
252
                });
253
            } else {
254
                $action['actions']    = [];
255
                $action['hasActions'] = false;
256
            }
257
258
            $action = array_replace($this->defaultActionStruct(), $action);
259
            $action = $this->parseActionRenderables($action, $renderer);
260
261
            if ($action['active'] === true && !empty($action['permissions']) && $this instanceof AuthAwareInterface) {
262
                $action['active'] = $this->hasPermissions($action['permissions']);
263
            }
264
        }
265
266
        return $action;
267
    }
268
269
    /**
270
     * Resolve the action's type.
271
     *
272
     * @param  mixed $action The action structure.
273
     * @return string
274
     */
275
    protected function resolveActionType($action)
276
    {
277
        switch ($action['ident']) {
278
            case 'create':
279
            case 'save':
280
            case 'submit':
281
            case 'update':
282
            case 'edit':
283
                return 'primary';
284
285
            case 'reset':
286
                return 'warning';
287
288
            case 'delete':
289
                return 'danger';
290
291
            default:
292
                return 'dark';
293
        }
294
    }
295
296
    /**
297
     * Fetch a viewable instance to process an action's renderables.
298
     *
299
     * @return ViewableInterface|null
300
     */
301
    protected function getActionRenderer()
302
    {
303
        $obj = null;
304
        if ($this instanceof FormSidebarInterface) {
305
            if ($this->form()) {
306
                $obj = $this->form()->obj();
0 ignored issues
show
Bug introduced by
The method obj() does not exist on Charcoal\Ui\Form\FormInterface. It seems like you code against a sub-type of Charcoal\Ui\Form\FormInterface such as Charcoal\Admin\Widget\ObjectFormWidget or Charcoal\Admin\Widget\DocWidget. ( Ignorable by Annotation )

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

306
                $obj = $this->form()->/** @scrutinizer ignore-call */ obj();
Loading history...
307
            }
308
        }
309
310
        if ($this instanceof ObjectContainerInterface) {
311
            $obj = $this->obj();
312
        }
313
314
        if ($this instanceof CollectionContainerInterface) {
315
            $obj = isset($this->currentObj) ? $this->currentObj : $this->proto();
316
        }
317
318
        return $obj instanceof ViewableInterface ? $obj : null;
319
    }
320
321
    /**
322
     * Parse the given UI action's renderables (e.g., URLs).
323
     *
324
     * @param  mixed $action   The action structure.
325
     * @param  mixed $renderer Either a {@see ViewableInterface} or TRUE
326
     *     to determine if any renderables should be processed.
327
     * @throws RuntimeException If a renderer is unavailable.
328
     * @return array Resolved action structure.
329
     */
330
    protected function parseActionRenderables($action, $renderer)
331
    {
332
        if ($renderer === false) {
333
            return $action;
334
        }
335
336
        if ($renderer === true) {
337
            $renderer = $this->getActionRenderer();
338
        }
339
340
        if ($action['active'] === true && isset($action['condition'])) {
341
            $action['active'] = $this->parseActionCondition($action['condition'], $action, $renderer);
342
            unset($action['condition']);
343
        }
344
345
        if (isset($action['url'])) {
346
            $action['url'] = $this->parseActionUrl($action['url'], $action, $renderer);
347
        }
348
349
        $action['cssClasses'] = $this->parseActionCssClasses($action['cssClasses'], $action, $renderer);
350
        $action['cssClasses'] = implode(' ', array_unique($action['cssClasses']));
351
352
        return $action;
353
    }
354
355
    /**
356
     * Parse the given UI action conditional check.
357
     *
358
     * @param  mixed $condition The action's conditional check.
359
     * @param  mixed $action    The action structure.
360
     * @param  mixed $renderer  The renderer.
361
     * @return array Resolved action structure.
362
     */
363
    protected function parseActionCondition($condition, $action = null, $renderer = null)
364
    {
365
        unset($action);
366
367
        if ($renderer === null) {
368
            $renderer = $this->getActionRenderer();
369
        }
370
371
        if (is_bool($condition)) {
372
            return $condition;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $condition returns the type boolean which is incompatible with the documented return type array.
Loading history...
373
        } elseif (is_string($condition)) {
374
            $not = ($condition[0] === '!');
375
            if ($not) {
376
                $condition = ltrim($condition, '!');
377
            }
378
379
            $result = null;
380
            if ($renderer && is_callable([ $renderer, $condition ])) {
381
                $result = !!$renderer->{$condition}();
382
            } elseif (is_callable([ $this, $condition ])) {
383
                $result = !!$this->{$condition}();
384
            } elseif (is_callable($condition)) {
385
                $result = !!$condition();
386
            } elseif ($renderer) {
387
                $result = !!$renderer->renderTemplate($condition);
388
            }
389
390
            if ($result !== null) {
391
                return $not ? !$result : $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $not ? ! $result : $result returns the type boolean which is incompatible with the documented return type array.
Loading history...
392
            }
393
        }
394
395
        return $condition;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $condition also could return the type string which is incompatible with the documented return type array.
Loading history...
396
    }
397
398
    /**
399
     * Parse the given UI action URL.
400
     *
401
     * @param  string $url      The action's URL.
402
     * @param  mixed  $action   The action structure.
403
     * @param  mixed  $renderer The renderer.
404
     * @return array Resolved action structure.
405
     */
406
    protected function parseActionUrl($url, $action = null, $renderer = null)
407
    {
408
        unset($action);
409
410
        if ($renderer === null) {
411
            $renderer = $this->getActionRenderer();
412
        }
413
414
        if ($url instanceof Translation) {
0 ignored issues
show
introduced by
$url is never a sub-type of Charcoal\Translator\Translation.
Loading history...
415
            $url = (string)$url;
416
        }
417
418
        $url = trim($url);
419
420
        if (empty($url) && !is_numeric($url)) {
421
            return '#';
0 ignored issues
show
Bug Best Practice introduced by
The expression return '#' returns the type string which is incompatible with the documented return type array.
Loading history...
422
        }
423
424
        if ($renderer === null) {
425
            /** @todo Shame! Force `{{ id }}` to use "obj_id" GET parameter… */
426
            $objId = filter_input(INPUT_GET, 'obj_id', FILTER_SANITIZE_STRING);
427
            if ($objId) {
428
                $url = preg_replace('~\{\{\s*(obj_)?id\s*\}\}~', $objId, $url);
429
            }
430
431
            /** @todo Shame! Force `{{ type }}` to use "obj_type" GET parameter… */
432
            $objType = filter_input(INPUT_GET, 'obj_type', FILTER_SANITIZE_STRING);
433
            if ($objType) {
434
                $url = preg_replace('~\{\{\s*(obj_)?type\s*\}\}~', $objType, $url);
435
            }
436
437
            if ($url && strpos($url, ':') === false && !in_array($url[0], [ '/', '#', '?' ])) {
438
                $url = $this->adminUrl().$url;
0 ignored issues
show
Bug introduced by
It seems like adminUrl() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

438
                $url = $this->/** @scrutinizer ignore-call */ adminUrl().$url;
Loading history...
439
            }
440
441
            return $url;
442
        } elseif ($renderer instanceof ViewableInterface) {
443
            $url = $renderer->renderTemplate($url);
444
445
            if ($url && strpos($url, ':') === false && !in_array($url[0], [ '/', '#', '?' ])) {
446
                $url = $this->adminUrl().$url;
447
            }
448
        }
449
450
        return $url;
451
    }
452
453
    /**
454
     * Parse the given UI action CSS classes.
455
     *
456
     * @param  mixed $classes  The action's CSS classes.
457
     * @param  mixed $action   The action structure.
458
     * @param  mixed $renderer The renderer.
459
     * @return array
460
     */
461
    protected function parseActionCssClasses($classes, $action = null, $renderer = null)
462
    {
463
        if ($renderer === null) {
464
            $renderer = $this->getActionRenderer();
0 ignored issues
show
Unused Code introduced by
The assignment to $renderer is dead and can be removed.
Loading history...
465
        }
466
467
        if (is_string($classes)) {
468
            $classes = explode(' ', $classes);
469
        } elseif (!is_array($classes)) {
470
            $classes = [];
471
        }
472
        $classes[] = 'btn';
473
        $classes[] = 'btn-'.$action['actionType'];
474
        $classes[] = $this->jsActionPrefix().'-'.$action['ident'];
475
476
        return $classes;
477
    }
478
479
    /**
480
     * Retrieve the default action structure.
481
     *
482
     * @return array
483
     */
484
    protected function defaultActionStruct()
485
    {
486
        return [
487
            'ident'         => null,
488
            'priority'      => null,
489
            'permissions'   => [],
490
            'condition'     => null,
491
            'active'        => true,
492
            'empty'         => false,
493
            'label'         => null,
494
            'showLabel'     => true,
495
            'icon'          => null,
496
            'url'           => null,
497
            'name'          => null,
498
            'value'         => null,
499
            'target'        => null,
500
            'isText'        => false,
501
            'isLink'        => false,
502
            'isButton'      => true,
503
            'isHeader'      => false,
504
            'isSubmittable' => false,
505
            'isSeparator'   => false,
506
            'cssClasses'    => null,
507
            'actionType'    => 'dark',
508
            'buttonType'    => 'button',
509
            'splitButton'   => false,
510
            'hasActions'    => false,
511
            'actions'       => [],
512
        ];
513
    }
514
515
    /**
516
     * Retrieve the default sorting priority for actions.
517
     *
518
     * @return integer
519
     */
520
    protected function defaultActionPriority()
521
    {
522
        return defined('static::DEFAULT_ACTION_PRIORITY') ? static::DEFAULT_ACTION_PRIORITY : 10;
0 ignored issues
show
Bug introduced by
The constant Charcoal\Admin\Ui\Action...DEFAULT_ACTION_PRIORITY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
523
    }
524
525
    /**
526
     * To be called with {@see uasort()}.
527
     *
528
     * @param  array $a Sortable action A.
529
     * @param  array $b Sortable action B.
530
     * @return integer
531
     */
532
    protected function sortActionsByPriority(array $a, array $b)
533
    {
534
        $a = isset($a['priority']) ? $a['priority'] : 0;
535
        $b = isset($b['priority']) ? $b['priority'] : 0;
536
537
        if ($a === $b) {
538
            return 0;
539
        }
540
        return ($a < $b) ? (-1) : 1;
541
    }
542
543
    /**
544
     * To be called when merging actions.
545
     *
546
     * Note: Practical for extended classes.
547
     *
548
     * @param  array $a First action object to sort.
549
     * @param  array $b Second action object to sort.
550
     * @return boolean Returns TRUE if $a has priority. Otherwise, FALSE for $b.
551
     */
552
    protected function compareActions(array $a, array $b)
553
    {
554
        $a = isset($a['priority']) ? $a['priority'] : 0;
555
        $b = isset($b['priority']) ? $b['priority'] : 0;
556
        $c = isset($action['isSubmittable']) && $action['isSubmittable'];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $action seems to never exist and therefore isset should always be false.
Loading history...
557
558
        return ($c || ($a === 0) || ($a >= $b));
559
    }
560
561
    /**
562
     * @return string
563
     */
564
    abstract public function jsActionPrefix();
565
566
    /**
567
     * Transform a snake_case string to camelCase.
568
     *
569
     * @param  string $str The snake_case string to camelize.
570
     * @return string The camelcase'd string.
571
     */
572
    abstract protected function camelize($str);
573
}
574