AdminWidget   F
last analyzed

Complexity

Total Complexity 77

Size/Duplication

Total Lines 636
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 184
dl 0
loc 636
rs 2.24
c 0
b 0
f 0
wmc 77

34 Methods

Rating   Name   Duplication   Size   Complexity  
A setTemplate() 0 16 3
A dataSourceFilter() 0 13 3
A mergeDataSources() 0 22 6
A setModelFactory() 0 3 1
A setType() 0 16 3
A addDataSources() 0 30 6
A active() 0 7 2
A template() 0 7 2
A escapedWidgetDataForJsAsJson() 0 3 1
A widgetId() 0 7 2
A defaultDataSources() 0 3 1
A setLabel() 0 5 1
A showActions() 0 6 2
A ident() 0 3 1
A setDependencies() 0 22 1
B resolveDataSourceFilter() 0 33 7
A type() 0 3 1
A parseConditionalLogic() 0 21 6
A setDataSources() 0 17 4
A resolveConditionalLogic() 0 11 4
A widgetDataForJsAsJson() 0 3 1
A setWidgetId() 0 5 1
A label() 0 3 1
A setIdent() 0 16 3
A actions() 0 3 1
A showLabel() 0 6 2
A setShowLabel() 0 4 1
A acceptedDataSources() 0 3 1
A setActive() 0 11 3
A defaultDataSourceFilters() 0 3 1
A modelFactory() 0 3 1
A dataSources() 0 7 2
A setShowActions() 0 4 1
A widgetDataForJs() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like AdminWidget 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 AdminWidget, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Charcoal\Admin;
4
5
use InvalidArgumentException;
6
7
// From Pimple
8
use Pimple\Container;
9
10
// From 'charcoal-factory'
11
use Charcoal\Factory\FactoryInterface;
12
13
// From 'charcoal-translator'
14
use Charcoal\Translator\Translation;
15
use Charcoal\Translator\TranslatorAwareTrait;
16
17
// From 'charcoal-user'
18
use Charcoal\User\AuthAwareInterface;
19
use Charcoal\User\AuthAwareTrait;
20
21
// From 'charcoal-ui'
22
use Charcoal\Ui\ConditionalizableInterface;
23
use Charcoal\Ui\ConditionalizableTrait;
24
use Charcoal\Ui\PrioritizableInterface;
25
use Charcoal\Ui\PrioritizableTrait;
26
27
// From 'charcoal-app'
28
use Charcoal\App\Template\AbstractWidget;
29
use Charcoal\Admin\Support\AdminTrait;
30
use Charcoal\Admin\Support\BaseUrlTrait;
31
32
/**
33
 * The base Widget for the `admin` module.
34
 */
35
class AdminWidget extends AbstractWidget implements
36
    AuthAwareInterface,
37
    PrioritizableInterface,
38
    ConditionalizableInterface
39
{
40
    use AdminTrait;
41
    use AuthAwareTrait;
42
    use BaseUrlTrait;
43
    use PrioritizableTrait;
44
    use ConditionalizableTrait;
45
    use TranslatorAwareTrait;
46
47
    const DATA_SOURCE_REQUEST = 'request';
48
    const DATA_SOURCE_OBJECT  = 'object';
49
    const DATA_SOURCE_METADATA = 'metadata';
50
51
    /**
52
     * @var string $widgetId
53
     */
54
    public $widgetId;
55
56
    /**
57
     * @var string $type
58
     */
59
    private $type;
60
61
    /**
62
     * @var string $template
63
     */
64
    private $template;
65
66
    /**
67
     * @var string $ident
68
     */
69
    private $ident = '';
70
71
    /**
72
     * @var Translation|string|null $label
73
     */
74
    private $label;
75
76
    /**
77
     * @var string $lang
78
     */
79
    private $lang;
0 ignored issues
show
introduced by
The private property $lang is not used, and could be removed.
Loading history...
80
81
    /**
82
     * @var boolean $showLabel
83
     */
84
    private $showLabel;
85
86
    /**
87
     * @var boolean $showActions
88
     */
89
    private $showActions;
90
91
    /**
92
     * The widget's conditional logic.
93
     *
94
     * @var callable|string|null
95
     */
96
    private $activeCondition;
97
98
    /**
99
     * Extra data sources to merge when setting data on an entity.
100
     *
101
     * @var array
102
     */
103
    private $dataSources;
104
105
    /**
106
     * Associative array of source identifiers and options to apply when merging.
107
     *
108
     * @var array
109
     */
110
    private $dataSourceFilters = [];
111
112
    /**
113
     * @var FactoryInterface $modelFactory
114
     */
115
    private $modelFactory;
116
117
    /**
118
     * Enable / Disable the widget.
119
     *
120
     * Accepts, as a string, a callable or renderable condition.
121
     *
122
     * @param  mixed $active The active flag or condition.
123
     * @return self
124
     */
125
    public function setActive($active)
126
    {
127
        if (is_callable($active) || is_string($active)) {
128
            $condition = $active;
129
        } else {
130
            $condition = null;
131
        }
132
133
        $this->activeCondition = $condition;
134
135
        return parent::setActive($active);
136
    }
137
138
    /**
139
     * @return boolean
140
     */
141
    public function active()
142
    {
143
        if ($this->activeCondition !== null) {
144
            return $this->parseConditionalLogic($this->activeCondition);
145
        }
146
147
        return parent::active();
148
    }
149
150
    /**
151
     * @param string $template The UI item's template (identifier).
152
     * @throws InvalidArgumentException If the template identifier is not a string.
153
     * @return self
154
     */
155
    public function setTemplate($template)
156
    {
157
        if ($template === null) {
0 ignored issues
show
introduced by
The condition $template === null is always false.
Loading history...
158
            $this->template = null;
159
            return $this;
160
        }
161
162
        if (!is_string($template)) {
0 ignored issues
show
introduced by
The condition is_string($template) is always true.
Loading history...
163
            throw new InvalidArgumentException(
164
                'The admin widget template must be a string'
165
            );
166
        }
167
168
        $this->template = $template;
169
170
        return $this;
171
    }
172
173
    /**
174
     * @return string
175
     */
176
    public function template()
177
    {
178
        if ($this->template === null) {
179
            return $this->type();
180
        }
181
182
        return $this->template;
183
    }
184
185
    /**
186
     * @param string $widgetId The widget identifier.
187
     * @return self
188
     */
189
    public function setWidgetId($widgetId)
190
    {
191
        $this->widgetId = $widgetId;
192
193
        return $this;
194
    }
195
196
    /**
197
     * @return string
198
     */
199
    public function widgetId()
200
    {
201
        if (!$this->widgetId) {
202
            $this->widgetId = 'widget_'.uniqid();
203
        }
204
205
        return $this->widgetId;
206
    }
207
208
    /**
209
     * @param string $type The widget type.
210
     * @throws InvalidArgumentException If the argument is not a string.
211
     * @return self
212
     */
213
    public function setType($type)
214
    {
215
        if ($type === null) {
0 ignored issues
show
introduced by
The condition $type === null is always false.
Loading history...
216
            $this->type = null;
217
            return $this;
218
        }
219
220
        if (!is_string($type)) {
0 ignored issues
show
introduced by
The condition is_string($type) is always true.
Loading history...
221
            throw new InvalidArgumentException(
222
                'The admin widget type must be a string'
223
            );
224
        }
225
226
        $this->type = $type;
227
228
        return $this;
229
    }
230
231
    /**
232
     * @return string
233
     */
234
    public function type()
235
    {
236
        return $this->type;
237
    }
238
239
    /**
240
     * @param string $ident The widget ident.
241
     * @throws InvalidArgumentException If the ident is not a string.
242
     * @return AdminWidget (Chainable)
243
     */
244
    public function setIdent($ident)
245
    {
246
        if ($ident === null) {
0 ignored issues
show
introduced by
The condition $ident === null is always false.
Loading history...
247
            $this->ident = null;
248
            return $this;
249
        }
250
251
        if (!is_string($ident)) {
0 ignored issues
show
introduced by
The condition is_string($ident) is always true.
Loading history...
252
            throw new InvalidArgumentException(
253
                'The admin widget identifier must be a string'
254
            );
255
        }
256
257
        $this->ident = $ident;
258
259
        return $this;
260
    }
261
262
    /**
263
     * @return string
264
     */
265
    public function ident()
266
    {
267
        return $this->ident;
268
    }
269
270
    /**
271
     * Set extra data sources to merge when setting data on an entity.
272
     *
273
     * @param mixed $sources One or more data source identifiers to merge data from.
274
     *     Pass NULL to reset the entity back to default sources.
275
     *     Pass FALSE, an empty string or array to disable extra sources.
276
     * @return self
277
     */
278
    public function setDataSources($sources)
279
    {
280
        if ($sources === null) {
281
            $this->dataSources = null;
282
283
            return $this;
284
        }
285
286
        if (!is_array($sources)) {
287
            $sources = [ $sources ];
288
        }
289
290
        foreach ($sources as $ident => $filter) {
291
            $this->addDataSources($ident, $filter);
292
        }
293
294
        return $this;
295
    }
296
297
    /**
298
     * Retrieve the extra data sources to merge when setting data on an entity.
299
     *
300
     * @return string[]
301
     */
302
    public function dataSources()
303
    {
304
        if ($this->dataSources === null) {
305
            return $this->defaultDataSources();
306
        }
307
308
        return $this->dataSources;
309
    }
310
311
    /**
312
     * Retrieve the callable filter for the given data source.
313
     *
314
     * @param string $sourceIdent A data source identifier.
315
     * @throws InvalidArgumentException If the data source is invalid.
316
     * @return callable|null Returns a callable variable.
317
     */
318
    public function dataSourceFilter($sourceIdent)
319
    {
320
        if (!is_string($sourceIdent)) {
0 ignored issues
show
introduced by
The condition is_string($sourceIdent) is always true.
Loading history...
321
            throw new InvalidArgumentException('Data source identifier must be a string');
322
        }
323
324
        $filters = array_merge($this->defaultDataSourceFilters(), $this->dataSourceFilters);
325
326
        if (isset($filters[$sourceIdent])) {
327
            return $filters[$sourceIdent];
328
        }
329
330
        return null;
331
    }
332
333
    /**
334
     * Retrieve the widget's data options for JavaScript components.
335
     *
336
     * @return array
337
     */
338
    public function widgetDataForJs()
339
    {
340
        return [];
341
    }
342
343
    /**
344
     * Converts the widget's {@see self::widgetDataForJs() options} as a JSON string.
345
     *
346
     * @return string Returns data serialized with {@see json_encode()}.
347
     */
348
    final public function widgetDataForJsAsJson()
349
    {
350
        return json_encode($this->widgetDataForJs(), JSON_UNESCAPED_UNICODE);
351
    }
352
353
    /**
354
     * Converts the widget's {@see self::widgetDataForJs() options} as a JSON string, protected from Mustache.
355
     *
356
     * @return string Returns a stringified JSON object, protected from Mustache rendering.
357
     */
358
    final public function escapedWidgetDataForJsAsJson()
359
    {
360
        return '{{=<% %>=}}'.$this->widgetDataForJsAsJson().'<%={{ }}=%>';
361
    }
362
363
    /**
364
     * @param mixed $label The label.
365
     * @return self
366
     */
367
    public function setLabel($label)
368
    {
369
        $this->label = $this->translator()->translation($label);
370
371
        return $this;
372
    }
373
374
    /**
375
     * @return Translation|string|null
376
     */
377
    public function label()
378
    {
379
        return $this->label;
380
    }
381
382
    /**
383
     * @return array
384
     */
385
    public function actions()
386
    {
387
        return [];
388
    }
389
390
    /**
391
     * @param boolean $show The show actions flag.
392
     * @return self
393
     */
394
    public function setShowActions($show)
395
    {
396
        $this->showActions = !!$show;
397
        return $this;
398
    }
399
400
    /**
401
     * @return boolean
402
     */
403
    public function showActions()
404
    {
405
        if ($this->showActions !== false) {
406
            return (count($this->actions()) > 0);
407
        } else {
408
            return false;
409
        }
410
    }
411
412
    /**
413
     * @param boolean $show The show label flag.
414
     * @return self
415
     */
416
    public function setShowLabel($show)
417
    {
418
        $this->showLabel = !!$show;
419
        return $this;
420
    }
421
422
    /**
423
     * @return boolean
424
     */
425
    public function showLabel()
426
    {
427
        if ($this->showLabel !== false) {
428
            return !!strval($this->label());
429
        } else {
430
            return false;
431
        }
432
    }
433
434
    /**
435
     * Set common dependencies used in all admin widgets.
436
     *
437
     * @param  Container $container DI Container.
438
     * @return void
439
     */
440
    protected function setDependencies(Container $container)
441
    {
442
        parent::setDependencies($container);
443
444
        // Satisfies TranslatorAwareTrait dependencies
445
        $this->setTranslator($container['translator']);
446
447
        // Satisfies AuthAwareInterface dependencies
448
        $this->setAuthenticator($container['admin/authenticator']);
449
        $this->setAuthorizer($container['admin/authorizer']);
450
451
        // Satisfies AdminTrait dependencies
452
        $this->setDebug($container['config']);
453
        $this->setAppConfig($container['config']);
454
        $this->setAdminConfig($container['admin/config']);
455
456
        // Satisfies BaseUrlTrait dependencies
457
        $this->setBaseUrl($container['base-url']);
458
        $this->setAdminUrl($container['admin/base-url']);
459
460
        // Satisfies AdminWidget dependencies
461
        $this->setModelFactory($container['model/factory']);
462
    }
463
464
    /**
465
     * @param FactoryInterface $factory The factory used to create models.
466
     * @return void
467
     */
468
    protected function setModelFactory(FactoryInterface $factory)
469
    {
470
        $this->modelFactory = $factory;
471
    }
472
473
    /**
474
     * @return FactoryInterface The model factory.
475
     */
476
    protected function modelFactory()
477
    {
478
        return $this->modelFactory;
479
    }
480
481
    /**
482
     * Resolve the conditional logic.
483
     *
484
     * @param  mixed $condition The condition.
485
     * @return boolean|null
486
     */
487
    final protected function parseConditionalLogic($condition)
488
    {
489
        if ($condition === null) {
490
            return null;
491
        }
492
493
        if (is_bool($condition)) {
494
            return $condition;
495
        }
496
497
        $not = false;
498
        if (is_string($condition)) {
499
            $not = ($condition[0] === '!');
500
            if ($not) {
501
                $condition = ltrim($condition, '!');
502
            }
503
        }
504
505
        $result = $this->resolveConditionalLogic($condition);
506
507
        return $not ? !$result : $result;
508
    }
509
510
    /**
511
     * Parse the widget's conditional logic.
512
     *
513
     * @param  callable|string $condition The callable or renderable condition.
514
     * @return boolean
515
     */
516
    protected function resolveConditionalLogic($condition)
517
    {
518
        if (is_callable([ $this, $condition ])) {
519
            return !!$this->{$condition}();
520
        } elseif (is_callable($condition)) {
521
            return !!$condition();
522
        } elseif ($this->view()) {
523
            return !!$this->renderTemplate($condition);
0 ignored issues
show
Bug introduced by
It seems like $condition can also be of type callable; however, parameter $templateString of Charcoal\App\Template\Ab...idget::renderTemplate() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

523
            return !!$this->renderTemplate(/** @scrutinizer ignore-type */ $condition);
Loading history...
524
        }
525
526
        return !!$condition;
527
    }
528
529
    /**
530
     * Set extra data sources to merge when setting data on an entity.
531
     *
532
     * @param mixed $sourceIdent  The data source identifier.
533
     * @param mixed $sourceFilter Optional filter to apply to the source's data.
534
     * @throws InvalidArgumentException If the data source is invalid.
535
     * @return self
536
     */
537
    protected function addDataSources($sourceIdent, $sourceFilter = null)
538
    {
539
        $validSources = $this->acceptedDataSources();
540
541
        if (is_numeric($sourceIdent) && is_string($sourceFilter)) {
542
            $sourceIdent   = $sourceFilter;
543
            $sourceFilter = null;
544
        }
545
546
        if (!is_string($sourceIdent)) {
547
            throw new InvalidArgumentException('Data source identifier must be a string');
548
        }
549
550
        if (!in_array($sourceIdent, $validSources)) {
551
            throw new InvalidArgumentException(
552
                sprintf(
553
                    'Invalid data source. Must be one of %s',
554
                    implode(', ', $validSources)
555
                )
556
            );
557
        }
558
559
        if ($this->dataSources === null) {
560
            $this->dataSources = [];
561
        }
562
563
        $this->dataSources[] = $sourceIdent;
564
        $this->dataSourceFilters[$sourceIdent] = $this->resolveDataSourceFilter($sourceFilter);
565
566
        return $this;
567
    }
568
569
    /**
570
     * Retrieve the available data sources (when setting data on an entity).
571
     *
572
     * @return string[]
573
     */
574
    protected function acceptedDataSources()
575
    {
576
        return [ static::DATA_SOURCE_REQUEST, static::DATA_SOURCE_OBJECT, static::DATA_SOURCE_METADATA ];
577
    }
578
579
    /**
580
     * Retrieve the default data sources (when setting data on an entity).
581
     *
582
     * @return string[]
583
     */
584
    protected function defaultDataSources()
585
    {
586
        return [];
587
    }
588
589
    /**
590
     * Retrieve the default data source filters (when setting data on an entity).
591
     *
592
     * @return array
593
     */
594
    protected function defaultDataSourceFilters()
595
    {
596
        return [];
597
    }
598
599
    /**
600
     * Retrieve the default data source filters (when setting data on an entity).
601
     *
602
     * Note: Adapted from {@see \Slim\CallableResolver}.
603
     *
604
     * @link   https://github.com/slimphp/Slim/blob/3.x/Slim/CallableResolver.php
605
     * @param  mixed $toResolve A callable used when merging data.
606
     * @return callable|null
607
     */
608
    protected function resolveDataSourceFilter($toResolve)
609
    {
610
        if (is_callable($toResolve)) {
611
            return $toResolve;
612
        }
613
614
        $resolved = $toResolve;
615
616
        if (is_string($toResolve)) {
617
            // Check for Slim callable
618
            $callablePattern = '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!';
619
            if (preg_match($callablePattern, $toResolve, $matches)) {
620
                $class  = $matches[1];
621
                $method = $matches[2];
622
623
                if ($class === 'parent') {
624
                    $resolved = [ $this, $class.'::'.$method ];
625
                } else {
626
                    if (!class_exists($class)) {
627
                        return null;
628
                    }
629
                    $resolved = [ $class, $method ];
630
                }
631
            } else {
632
                $resolved = [ $this, $toResolve ];
633
            }
634
        }
635
636
        if (!is_callable($resolved)) {
637
            return null;
638
        }
639
640
        return $resolved;
641
    }
642
643
    /**
644
     * Retrieve the available data sources (when setting data on an entity).
645
     *
646
     * @param array|mixed $dataset The entity data.
647
     * @return self
648
     */
649
    protected function mergeDataSources($dataset = null)
650
    {
651
        $sources = $this->dataSources();
652
        foreach ($sources as $sourceIdent) {
653
            $filter = $this->dataSourceFilter($sourceIdent);
654
            $getter = $this->camelize('data_from_'.$sourceIdent);
655
            $method = [ $this, $getter ];
656
657
            if (is_callable($method)) {
658
                $data = call_user_func($method);
659
660
                if ($data) {
661
                    if ($filter && $dataset) {
662
                        $data = call_user_func($filter, $data, $dataset);
663
                    }
664
665
                    parent::setData($data);
666
                }
667
            }
668
        }
669
670
        return $this;
671
    }
672
}
673