FormPropertyWidget::defaultFormFieldCssClasses()   A
last analyzed

Complexity

Conditions 6
Paths 20

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 12
nc 20
nop 0
dl 0
loc 25
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
namespace Charcoal\Admin\Widget;
4
5
use LogicException;
6
use RuntimeException;
7
use InvalidArgumentException;
8
use UnexpectedValueException;
9
10
// From Pimple
11
use Pimple\Container;
12
13
// From 'charcoal-factory'
14
use Charcoal\Factory\FactoryInterface;
15
16
// From 'charcoal-property'
17
use Charcoal\Property\PropertyInterface;
18
19
// From 'charcoal-view'
20
use Charcoal\View\ViewableInterface;
21
22
// From 'charcoal-ui'
23
use Charcoal\Ui\FormGroup\FormGroupInterface;
24
use Charcoal\Ui\FormInput\FormInputInterface;
25
26
// From 'charcoal-admin'
27
use Charcoal\Admin\AdminWidget;
28
29
/**
30
 * Form Control Widget
31
 *
32
 * For model properties.
33
 */
34
class FormPropertyWidget extends AdminWidget implements
35
    FormInputInterface
36
{
37
    const HIDDEN_FORM_CONTROL     = 'charcoal/admin/property/input/hidden';
38
    const DEFAULT_FORM_CONTROL    = 'charcoal/admin/property/input/text';
39
40
    const PROPERTY_CONTROL = 'input';
41
    const PROPERTY_DISPLAY = 'display';
42
    const DEFAULT_OUTPUT   = self::PROPERTY_CONTROL;
43
44
    /**
45
     * The widget's type.
46
     *
47
     * @var string|null
48
     */
49
    protected $type;
50
51
    /**
52
     * The widget's property output type.
53
     *
54
     * @var string
55
     */
56
    protected $outputType;
57
58
    /**
59
     * Store the model property.
60
     *
61
     * @var PropertyInterface|null
62
     */
63
    private $property;
64
65
    /**
66
     * The model's property type.
67
     *
68
     * @var string|null
69
     */
70
    protected $propertyType;
71
72
    /**
73
     * The model property's name.
74
     *
75
     * @var string|null
76
     */
77
    private $propertyIdent;
78
79
    /**
80
     * The model property's value.
81
     *
82
     * @var mixed
83
     */
84
    private $propertyVal;
85
86
    /**
87
     * The model property's metadata.
88
     *
89
     * @var array
90
     */
91
    private $propertyData = [];
92
93
    /**
94
     * Store the property control instance.
95
     *
96
     * @var PropertyInputInterface|null
0 ignored issues
show
Bug introduced by
The type Charcoal\Admin\Widget\PropertyInputInterface 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...
97
     */
98
    private $inputProperty;
99
100
    /**
101
     * The property control type.
102
     *
103
     * @var string|null
104
     */
105
    protected $inputType;
106
107
    /**
108
     * Store the property display instance.
109
     *
110
     * @var PropertyDisplayInterface|null
0 ignored issues
show
Bug introduced by
The type Charcoal\Admin\Widget\PropertyDisplayInterface 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...
111
     */
112
    private $displayProperty;
113
114
    /**
115
     * The property display type.
116
     *
117
     * @var string|null
118
     */
119
    protected $displayType;
120
121
    /**
122
     * The label is displayed by default.
123
     *
124
     * @var boolean
125
     */
126
    protected $showLabel;
127
128
    /**
129
     * The description is displayed by default.
130
     *
131
     * @var boolean
132
     */
133
    protected $showDescription;
134
135
    /**
136
     * The notes are displayed by default.
137
     *
138
     * @var boolean
139
     */
140
    protected $showNotes;
141
142
    /**
143
     * The CSS class names for the `.form-field`.
144
     *
145
     * @var string[]
146
     */
147
    protected $formFieldCssClass = [];
148
149
    /**
150
     * The CSS class names for the `.form-group`.
151
     *
152
     * @var string[]
153
     */
154
    protected $formGroupCssClass = [];
155
156
    /**
157
     * The L10N display mode.
158
     *
159
     * @var string
160
     */
161
    private $l10nMode;
162
163
    /**
164
     * The form group the input belongs to.
165
     *
166
     * @var FormGroupInterface
167
     */
168
    protected $formGroup;
169
170
    /**
171
     * Store the model property factory.
172
     *
173
     * @var FactoryInterface
174
     */
175
    private $propertyFactory;
176
177
    /**
178
     * Store the property form control factory.
179
     *
180
     * @var FactoryInterface
181
     */
182
    private $propertyInputFactory;
183
184
    /**
185
     * Store the property display factory.
186
     *
187
     * @var FactoryInterface
188
     */
189
    private $propertyDisplayFactory;
190
191
    /**
192
     * Track the state of data merging.
193
     *
194
     * @var boolean
195
     */
196
    private $isMergingWidgetData = false;
197
198
    /**
199
     * The UI item's icon.
200
     *
201
     * Note: Only icons from the {@link http://fontawesome.io/ Font Awesome}
202
     * library are supported.
203
     *
204
     * @var string|null
205
     */
206
    private $icon;
207
208
    /**
209
     * Retrieve the property factory.
210
     *
211
     * @throws RuntimeException If the property factory is missing.
212
     * @return FactoryInterface
213
     */
214
    public function propertyFactory()
215
    {
216
        if ($this->propertyFactory === null) {
217
            throw new RuntimeException(
218
                'Missing Property Factory'
219
            );
220
        }
221
222
        return $this->propertyFactory;
223
    }
224
225
226
227
    /**
228
     * Retrieve the property control factory.
229
     *
230
     * @throws RuntimeException If the property control factory is missing.
231
     * @return FactoryInterface
232
     */
233
    public function propertyInputFactory()
234
    {
235
        if ($this->propertyInputFactory === null) {
236
            throw new RuntimeException(
237
                'Missing Property Input Factory'
238
            );
239
        }
240
241
        return $this->propertyInputFactory;
242
    }
243
244
245
    /**
246
     * Retrieve the property display factory.
247
     *
248
     * @throws RuntimeException If the property display factory is missing.
249
     * @return FactoryInterface
250
     */
251
    public function propertyDisplayFactory()
252
    {
253
        if ($this->propertyDisplayFactory === null) {
254
            throw new RuntimeException(
255
                'Missing Property Display Factory'
256
            );
257
        }
258
259
        return $this->propertyDisplayFactory;
260
    }
261
262
    /**
263
     * Retrieve the widget ID.
264
     *
265
     * @return string
266
     */
267
    public function widgetId()
268
    {
269
        if (!$this->widgetId) {
270
            $type = $this->type();
271
            switch ($type) {
272
                case static::PROPERTY_DISPLAY:
273
                    $id = $this->display()->displayId();
274
                    break;
275
276
                case static::PROPERTY_CONTROL:
277
                    $id = $this->input()->inputId();
278
                    break;
279
280
                default:
281
                    $id = 'widget_'.uniqid();
282
                    break;
283
            }
284
285
            $this->widgetId = $id;
286
        }
287
288
        return $this->widgetId;
289
    }
290
291
    /**
292
     * Set the widget or property type.
293
     *
294
     * @param  string $type The widget or property type.
295
     * @throws InvalidArgumentException If the argument is not a string.
296
     * @return FormPropertyWidget Chainable
297
     */
298
    public function setType($type)
299
    {
300
        if (empty($type)) {
301
            $this->type = null;
302
            return $this;
303
        }
304
305
        if (!is_string($type)) {
0 ignored issues
show
introduced by
The condition is_string($type) is always true.
Loading history...
306
            throw new InvalidArgumentException(
307
                'Form property widget type must be a string'
308
            );
309
        }
310
311
        if ($this->propertyFactory()->isResolvable($type)) {
312
            $this->setPropertyType($type);
313
        }
314
315
        $this->type = $type;
316
317
        return $this;
318
    }
319
320
    /**
321
     * Set the model's property type.
322
     *
323
     * Can be either "input" or "display". A property input or property display identifier
324
     * is also accepted.
325
     *
326
     * @param  string $type The input or display property type.
327
     * @throws InvalidArgumentException If the argument is not a string.
328
     * @return FormPropertyWidget Chainable
329
     */
330
    public function setOutputType($type)
331
    {
332
        if (empty($type)) {
333
            $this->outputType = static::DEFAULT_OUTPUT;
334
            return $this;
335
        }
336
337
        if (!is_string($type)) {
0 ignored issues
show
introduced by
The condition is_string($type) is always true.
Loading history...
338
            throw new InvalidArgumentException(
339
                'Form property widget type must be a string'
340
            );
341
        }
342
343
        if (!in_array($type, $this->supportedOutputTypes())) {
344
            $type = $this->resolveOutputType($type);
345
        }
346
347
        $this->outputType = $type;
348
349
        return $this;
350
    }
351
352
    /**
353
     * Retrieve the widget's property output type.
354
     *
355
     * Defaults to "input_type".
356
     *
357
     * @throws LogicException If the "input_type" and "display_type" are disabled.
358
     * @return string|null
359
     */
360
    public function outputType()
361
    {
362
        if ($this->outputType === null) {
363
            if ($this->inputType === false && $this->displayType === false) {
0 ignored issues
show
introduced by
The condition $this->inputType === false is always false.
Loading history...
364
                throw new LogicException('Form property widget requires an "input_type" or a "display_type"');
365
            }
366
367
            $type = null;
368
369
            if ($this->inputType === false || is_string($this->displayType)) {
370
                $type = static::PROPERTY_DISPLAY;
371
            }
372
373
            if ($this->displayType === false || is_string($this->inputType)) {
374
                $type = static::PROPERTY_CONTROL;
375
            }
376
377
            $this->outputType = $type;
378
        }
379
380
        return $this->outputType;
381
    }
382
383
    /**
384
     * Retrieve the supported property output types.
385
     *
386
     * @return array
387
     */
388
    public function supportedOutputTypes()
389
    {
390
        return [ static::PROPERTY_CONTROL, static::PROPERTY_DISPLAY ];
391
    }
392
393
    /**
394
     * Set the form input's parent group.
395
     *
396
     * @param  FormGroupInterface $formGroup The parent form group object.
397
     * @return FormPropertyWidget Chainable
398
     */
399
    public function setFormGroup(FormGroupInterface $formGroup)
400
    {
401
        $this->formGroup = $formGroup;
402
403
        return $this;
404
    }
405
406
    /**
407
     * Retrieve the input's parent group.
408
     *
409
     * @return FormGroupInterface
410
     */
411
    public function formGroup()
412
    {
413
        return $this->formGroup;
414
    }
415
416
    /**
417
     * Clear the input's parent group.
418
     *
419
     * @return FormPropertyWidget Chainable
420
     */
421
    public function clearFormGroup()
422
    {
423
        $this->formGroup = null;
424
425
        return $this;
426
    }
427
428
429
430
    /**
431
     * Set the widget and property data.
432
     *
433
     * @param  array|ArrayAccess $data Widget and property data.
0 ignored issues
show
Bug introduced by
The type Charcoal\Admin\Widget\ArrayAccess was not found. Did you mean ArrayAccess? If so, make sure to prefix the type with \.
Loading history...
434
     * @return FormPropertyWidget Chainable
435
     */
436
    public function setData(array $data)
437
    {
438
        $this->isMergingWidgetData = true;
439
440
        $data = $this->setCoreData($data);
441
442
        parent::setData($data);
443
444
        // Keep the data in copy, this will be passed to the property and/or input later
445
        $this->setPropertyData($data);
446
447
        $this->isMergingWidgetData = false;
448
449
        return $this;
450
    }
451
452
    /**
453
     * Merge widget and property data.
454
     *
455
     * @param  array $data Widget and property data.
456
     * @return FormPropertyWidget Chainable
457
     */
458
    public function merge(array $data)
459
    {
460
        $this->isMergingWidgetData = true;
461
462
        $data = $this->setCoreData($data);
463
464
        $this->mergePropertyData($data);
465
466
        $this->isMergingWidgetData = false;
467
468
        return $this;
469
    }
470
471
    /**
472
     * Set the model's property type.
473
     *
474
     * @param  string $type The property type.
475
     * @throws InvalidArgumentException If the argument is not a string.
476
     * @return FormPropertyWidget Chainable
477
     */
478
    public function setPropertyType($type)
479
    {
480
        if (empty($type)) {
481
            $this->propertyType = null;
482
            return $this;
483
        }
484
485
        if (!is_string($type)) {
0 ignored issues
show
introduced by
The condition is_string($type) is always true.
Loading history...
486
            throw new InvalidArgumentException(
487
                'Property type must be a string'
488
            );
489
        }
490
491
        $this->propertyType = $type;
492
493
        return $this;
494
    }
495
496
    /**
497
     * Retrieve the model's property type.
498
     *
499
     * @return string|null
500
     */
501
    public function propertyType()
502
    {
503
        return $this->propertyType;
504
    }
505
506
    /**
507
     * Set the form's property identifier.
508
     *
509
     * @param  string $propertyIdent The property identifier.
510
     * @throws InvalidArgumentException If the property ident is not a string.
511
     * @return FormPropertyWidget Chainable
512
     */
513
    public function setPropertyIdent($propertyIdent)
514
    {
515
        if (!is_string($propertyIdent)) {
0 ignored issues
show
introduced by
The condition is_string($propertyIdent) is always true.
Loading history...
516
            throw new InvalidArgumentException(
517
                'Property identifier must be a string'
518
            );
519
        }
520
521
        $this->propertyIdent = $propertyIdent;
522
523
        return $this;
524
    }
525
526
    /**
527
     * Retrieve the form's property identifier.
528
     *
529
     * @return string|null
530
     */
531
    public function propertyIdent()
532
    {
533
        return $this->propertyIdent;
534
    }
535
536
    /**
537
     * Set the property metadata.
538
     *
539
     * @param  array $data The property configset.
540
     * @return FormPropertyWidget Chainable
541
     */
542
    public function setPropertyData(array $data)
543
    {
544
        $this->propertyData = $data;
545
546
        if (!$this->isMergingWidgetData) {
547
            $this->setCoreData($this->propertyData);
548
        }
549
550
        if ($this->property) {
551
            $this->property->setData($data);
552
        }
553
554
        return $this;
555
    }
556
557
    /**
558
     * Merge the property metadata.
559
     *
560
     * @param  array $data The property configset.
561
     * @return FormPropertyWidget Chainable
562
     */
563
    public function mergePropertyData(array $data)
564
    {
565
        $this->propertyData = array_replace($this->propertyData, $data);
566
567
        if (!$this->isMergingWidgetData) {
568
            $this->setCoreData($this->propertyData);
569
        }
570
571
        if ($this->property) {
572
            $this->property->setData($data);
573
        }
574
575
        return $this;
576
    }
577
578
    /**
579
     * Retrieve the property metadata.
580
     *
581
     * @return array
582
     */
583
    public function propertyData()
584
    {
585
        return $this->propertyData;
586
    }
587
588
    /**
589
     * Set the property's value.
590
     *
591
     * @param  mixed $propertyVal The property value.
592
     * @return FormPropertyWidget Chainable
593
     */
594
    public function setPropertyVal($propertyVal)
595
    {
596
        $this->propertyVal = $propertyVal;
597
598
        return $this;
599
    }
600
601
    /**
602
     * Retrieve the property's value.
603
     *
604
     * @return mixed
605
     */
606
    public function propertyVal()
607
    {
608
        return $this->propertyVal;
609
    }
610
611
    /**
612
     * Show/hide the property's label.
613
     *
614
     * @param  boolean $show Show (TRUE) or hide (FALSE) the label.
615
     * @return FormPropertyWidget Chainable
616
     */
617
    public function setShowLabel($show)
618
    {
619
        $this->showLabel = !!$show;
620
621
        return $this;
622
    }
623
624
    /**
625
     * Determine if the label is to be displayed.
626
     *
627
     * @return boolean If TRUE or unset, check if there is a label.
628
     */
629
    public function showLabel()
630
    {
631
        if ($this->showLabel === null) {
632
            $prop = $this->property();
633
            $show = $prop['show_label'];
634
            if ($show !== null) {
635
                $this->showLabel = $show;
636
            } else {
637
                $this->showLabel = true;
638
            }
639
        }
640
641
        if ($this->showLabel !== false) {
642
            return !!strval($this->property()->label());
643
        } else {
644
            return false;
645
        }
646
    }
647
648
    /**
649
     * Show/hide the property's description.
650
     *
651
     * @param  boolean $show Show (TRUE) or hide (FALSE) the description.
652
     * @return FormPropertyWidget Chainable
653
     */
654
    public function setShowDescription($show)
655
    {
656
        $this->showDescription = !!$show;
657
658
        return $this;
659
    }
660
661
    /**
662
     * Determine if the description is to be displayed.
663
     *
664
     * @return boolean If TRUE or unset, check if there is a description.
665
     */
666
    public function showDescription()
667
    {
668
        if ($this->showDescription === null) {
669
            $prop = $this->property();
670
            $show = $prop['show_description'];
671
            if ($show !== null) {
672
                $this->showDescription = $show;
673
            } else {
674
                $this->showDescription = true;
675
            }
676
        }
677
678
        if ($this->showDescription !== false) {
679
            return !!strval($this->property()->description());
680
        } else {
681
            return false;
682
        }
683
    }
684
685
    /**
686
     * Show/hide the property's notes.
687
     *
688
     * @param  boolean|string $show Show (TRUE) or hide (FALSE) the notes.
689
     * @return FormPropertyWidget Chainable
690
     */
691
    public function setShowNotes($show)
692
    {
693
        $this->showNotes = ($show === 'above' ? $show : !!$show);
0 ignored issues
show
Documentation Bug introduced by
It seems like $show === 'above' ? $show : ! ! $show can also be of type string. However, the property $showNotes is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
694
695
        return $this;
696
    }
697
698
    /**
699
     * Determine if the notes is to be displayed.
700
     *
701
     * @return boolean If TRUE or unset, check if there are notes.
702
     */
703
    public function showNotes()
704
    {
705
        if ($this->showNotes === null) {
706
            $prop = $this->property();
707
            $show = $prop['show_notes'];
708
            if ($show !== null) {
709
                $this->showNotes = $show;
710
            } else {
711
                $this->showNotes = true;
712
            }
713
        }
714
715
        if ($this->showNotes !== false) {
716
            return !!strval($this->property()->notes());
717
        } else {
718
            return false;
719
        }
720
    }
721
722
    /**
723
     * @return boolean
724
     */
725
    public function showNotesAbove()
726
    {
727
        if ($this->showNotes === null) {
728
            $this->showNotes();
729
        }
730
731
        $show = $this->showNotes;
732
733
        if ($show !== 'above') {
734
            return false;
735
        }
736
737
        $notes = $this->property()->notes();
738
739
        return !!$notes;
740
    }
741
742
    /**
743
     * @return Translation|string|null
0 ignored issues
show
Bug introduced by
The type Charcoal\Admin\Widget\Translation 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...
744
     */
745
    public function description()
746
    {
747
        return $this->renderTemplate((string)$this->property()->description());
748
    }
749
750
    /**
751
     * @return Translation|string|null
752
     */
753
    public function notes()
754
    {
755
        return $this->renderTemplate((string)$this->property()->notes());
756
    }
757
758
    /**
759
     * @return boolean
760
     */
761
    public function hidden()
762
    {
763
        return ($this->inputType() === static::HIDDEN_FORM_CONTROL || $this->property()->hidden());
764
    }
765
766
    /**
767
     * @return string
768
     */
769
    public function inputId()
770
    {
771
        return 'input_id';
772
    }
773
774
    /**
775
     * @return string
776
     */
777
    public function inputName()
778
    {
779
        return 'input_name';
780
    }
781
782
    /**
783
     * @return string
784
     */
785
    public function displayId()
786
    {
787
        return 'display_id';
788
    }
789
790
    /**
791
     * @return string
792
     */
793
    public function displayName()
794
    {
795
        return 'display_name';
796
    }
797
798
    /**
799
     * Set the property control type.
800
     *
801
     * @param  string $type The form control type.
802
     * @throws InvalidArgumentException If the argument is not a string.
803
     * @return FormPropertyWidget Chainable
804
     */
805
    public function setInputType($type)
806
    {
807
        if (empty($type)) {
808
            $this->inputType = null;
809
            return $this;
810
        }
811
812
        if (!is_string($type)) {
0 ignored issues
show
introduced by
The condition is_string($type) is always true.
Loading history...
813
            throw new InvalidArgumentException(
814
                'Property input type must be a string'
815
            );
816
        }
817
818
        $this->inputType = $type;
819
820
        return $this;
821
    }
822
823
    /**
824
     * Retrieve the property control type.
825
     *
826
     * @return string
827
     */
828
    public function inputType()
829
    {
830
        if ($this->inputType === null) {
831
            $this->inputType = $this->resolveInputType();
832
        }
833
834
        return $this->inputType;
835
    }
836
837
    /**
838
     * Set the property display type.
839
     *
840
     * @param  string $type The property display type.
841
     * @throws InvalidArgumentException If the argument is not a string.
842
     * @return FormPropertyWidget Chainable
843
     */
844
    public function setDisplayType($type)
845
    {
846
        if (empty($type)) {
847
            $this->displayType = null;
848
            return $this;
849
        }
850
851
        if (!is_string($type)) {
0 ignored issues
show
introduced by
The condition is_string($type) is always true.
Loading history...
852
            throw new InvalidArgumentException(
853
                'Property display type must be a string'
854
            );
855
        }
856
857
        $this->displayType = $type;
858
859
        return $this;
860
    }
861
862
    /**
863
     * Retrieve the property display type.
864
     *
865
     * @return string
866
     */
867
    public function displayType()
868
    {
869
        if ($this->displayType === null) {
870
            $this->displayType = $this->resolveDisplayType();
871
        }
872
873
        return $this->displayType;
874
    }
875
876
    /**
877
     * Set the widget's model property.
878
     *
879
     * @param  PropertyInterface $property The property.
880
     * @return FormPropertyWidget Chainable
881
     */
882
    public function setProperty(PropertyInterface $property)
883
    {
884
        $this->property      = $property;
885
        $this->propertyType  = $property->type();
886
        $this->propertyIdent = $property->ident();
887
888
        $inputType = $property['input_type'];
889
        if ($inputType) {
890
            $this->inputType = $inputType;
891
        }
892
893
        $displayType = $property['display_type'];
894
        if ($displayType) {
895
            $this->displayType = $displayType;
896
        }
897
898
        return $this;
899
    }
900
901
    /**
902
     * Retrieve the widget's model property.
903
     *
904
     * @return PropertyInterface
905
     */
906
    public function property()
907
    {
908
        if ($this->property === null) {
909
            $this->property = $this->createProperty();
910
        }
911
912
        return $this->property;
913
    }
914
915
    /**
916
     * Alias of {@see self::property()}
917
     *
918
     * @return PropertyInterface
919
     */
920
    public function prop()
921
    {
922
        return $this->property();
923
    }
924
925
    /**
926
     * Determine if the form control's active language should be displayed.
927
     *
928
     * @see    FormSidebarWidget::showLanguageSwitch()
929
     * @return boolean
930
     */
931
    public function showActiveLanguage()
932
    {
933
        $property = $this->property();
934
        $locales  = count($this->translator()->availableLocales());
935
936
        return ($locales > 1 && $property->l10n());
937
    }
938
939
    /**
940
     * Generate a CSS class name for the property's input name.
941
     *
942
     * @return string
943
     */
944
    public function inputNameAsCssClass()
945
    {
946
        $name = str_replace([ ']', '[' ], [ '', '-' ], $this->propertyIdent());
947
        $name = $this->camelize($name);
948
949
        return $name;
950
    }
951
952
    /**
953
     * Set the CSS class name(s) for the `.form-field`.
954
     *
955
     * @param  mixed $cssClass One or more CSS class names.
956
     * @return self
957
     */
958
    public function setFormFieldCssClass($cssClass)
959
    {
960
        $cssClass = array_merge($this->defaultFormFieldCssClasses(), $this->parseCssClasses($cssClass));
961
        $this->formFieldCssClass = array_unique($cssClass);
962
        return $this;
963
    }
964
965
    /**
966
     * Add CSS class name(s) for the `.form-field`.
967
     *
968
     * @param  mixed $cssClass One or more CSS class names.
969
     * @return self
970
     */
971
    public function addFormFieldCssClass($cssClass)
972
    {
973
        $cssClass = array_merge($this->formFieldCssClass, $this->parseCssClasses($cssClass));
974
        $this->formFieldCssClass = array_unique($cssClass);
975
        return $this;
976
    }
977
978
    /**
979
     * Retrieve the CSS class name(s) for the `.form-field`.
980
     *
981
     * @return string
982
     */
983
    public function formFieldCssClass()
984
    {
985
        if (empty($this->formFieldCssClass)) {
986
            $this->formFieldCssClass = $this->defaultFormFieldCssClasses();
987
        }
988
989
        return implode(' ', $this->formFieldCssClass);
990
    }
991
992
    /**
993
     * Set the CSS class name(s) for the `.form-group`.
994
     *
995
     * @param  mixed $cssClass One or more CSS class names.
996
     * @return self
997
     */
998
    public function setFormGroupCssClass($cssClass)
999
    {
1000
        $cssClass = array_merge($this->defaultFormGroupCssClasses(), $this->parseCssClasses($cssClass));
1001
        $this->formGroupCssClass = array_unique($cssClass);
1002
        return $this;
1003
    }
1004
1005
    /**
1006
     * Add CSS class name(s) for the `.form-group`.
1007
     *
1008
     * @param  mixed $cssClass One or more CSS class names.
1009
     * @return self
1010
     */
1011
    public function addFormGroupCssClass($cssClass)
1012
    {
1013
        $cssClass = array_merge($this->formGroupCssClass, $this->parseCssClasses($cssClass));
1014
        $this->formGroupCssClass = array_unique($cssClass);
1015
        return $this;
1016
    }
1017
1018
    /**
1019
     * Retrieve the CSS class name(s) for the `.form-group`.
1020
     *
1021
     * @return string
1022
     */
1023
    public function formGroupCssClass()
1024
    {
1025
        if (empty($this->formGroupCssClass)) {
1026
            $this->formGroupCssClass = $this->defaultFormGroupCssClasses();
1027
        }
1028
1029
        return implode(' ', $this->formGroupCssClass);
1030
    }
1031
1032
    /**
1033
     * Set the L10N display mode.
1034
     *
1035
     * @param  string $mode The L10N display mode.
1036
     * @return FormPropertyWidget Chainable
1037
     */
1038
    public function setL10nMode($mode)
1039
    {
1040
        $this->l10nMode = $mode;
1041
        return $this;
1042
    }
1043
1044
    /**
1045
     * Retrieve the L10N display mode.
1046
     *
1047
     * @return string
1048
     */
1049
    public function l10nMode()
1050
    {
1051
        return $this->l10nMode;
1052
    }
1053
1054
    /**
1055
     * Determine if the property should output for each language.
1056
     *
1057
     * @return boolean
1058
     */
1059
    public function loopL10n()
1060
    {
1061
        return ($this->l10nMode() === 'loop_inputs');
1062
    }
1063
1064
    /**
1065
     * Alias of {@see PropertyInterface::l10n()}.
1066
     *
1067
     * @return boolean
1068
     */
1069
    public function l10n()
1070
    {
1071
        return $this->property()->l10n();
1072
    }
1073
1074
    /**
1075
     * Retrieve the form control property.
1076
     *
1077
     * @return PropertyInputInterface
1078
     */
1079
    public function input()
1080
    {
1081
        if ($this->inputProperty === null) {
1082
            $this->inputProperty = $this->createInputProperty();
1083
        }
1084
1085
        return $this->inputProperty;
1086
    }
1087
1088
    /**
1089
     * Retrieve the display property.
1090
     *
1091
     * @return PropertyDisplayInterface
1092
     */
1093
    public function display()
1094
    {
1095
        if ($this->displayProperty === null) {
1096
            $this->displayProperty = $this->createDisplayProperty();
1097
        }
1098
1099
        return $this->displayProperty;
1100
    }
1101
1102
    /**
1103
     * Yield the property output.
1104
     *
1105
     * Either a display property or a form control property.
1106
     *
1107
     * @return PropertyInputInterface|PropertyDisplayInterface
1108
     */
1109
    public function output()
1110
    {
1111
        $output = $this->outputType();
1112
        switch ($output) {
1113
            case static::PROPERTY_DISPLAY:
1114
                $type   = $this->displayType();
1115
                $prop   = $this->display();
1116
                $getter = 'displayId';
1117
                $setter = 'setDisplayId';
1118
                break;
1119
1120
            case static::PROPERTY_CONTROL:
1121
                $type   = $this->inputType();
1122
                $prop   = $this->input();
1123
                $getter = 'inputId';
1124
                $setter = 'setInputId';
1125
                break;
1126
        }
1127
1128
        $GLOBALS['widget_template'] = $type;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $type does not seem to be defined for all execution paths leading up to this point.
Loading history...
1129
1130
        if ($this->l10n() && $this->loopL10n()) {
1131
            $locales  = $this->translator()->availableLocales();
1132
            $outputId = $prop->{$getter}();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $prop does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $getter does not seem to be defined for all execution paths leading up to this point.
Loading history...
1133
            foreach ($locales as $langCode) {
1134
                // Set a unique property output ID for each locale.
1135
                $prop->{$setter}($outputId.'_'.$langCode);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $setter does not seem to be defined for all execution paths leading up to this point.
Loading history...
1136
                $prop->setLang($langCode);
1137
1138
                yield $prop;
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $prop returns the type Generator which is incompatible with the documented return type Charcoal\Admin\Widget\Pr...\PropertyInputInterface.
Loading history...
1139
            }
1140
1141
            $GLOBALS['widget_template'] = '';
1142
            $prop->{$setter}($outputId);
1143
        } else {
1144
            yield $prop;
1145
1146
            $GLOBALS['widget_template'] = '';
1147
        }
1148
    }
1149
1150
    /**
1151
     * Set the widget's dependencies.
1152
     *
1153
     * @param  Container $container Service container.
1154
     * @return void
1155
     */
1156
    protected function setDependencies(Container $container)
1157
    {
1158
        parent::setDependencies($container);
1159
1160
        $this->setView($container['view']);
1161
        $this->setPropertyFactory($container['property/factory']);
1162
        $this->setPropertyInputFactory($container['property/input/factory']);
1163
        $this->setPropertyDisplayFactory($container['property/display/factory']);
1164
    }
1165
1166
    /**
1167
     * Set a property factory.
1168
     *
1169
     * @param  FactoryInterface $factory The factory to create property values.
1170
     * @return FormPropertyWidget Chainable
1171
     */
1172
    protected function setPropertyFactory(FactoryInterface $factory)
1173
    {
1174
        $this->propertyFactory = $factory;
1175
1176
        return $this;
1177
    }
1178
1179
    /**
1180
     * Set a property control factory.
1181
     *
1182
     * @param  FactoryInterface $factory The factory to create form controls for property values.
1183
     * @return FormPropertyWidget Chainable
1184
     */
1185
    protected function setPropertyInputFactory(FactoryInterface $factory)
1186
    {
1187
        $this->propertyInputFactory = $factory;
1188
1189
        return $this;
1190
    }
1191
1192
    /**
1193
     * Set a property display factory.
1194
     *
1195
     * @param  FactoryInterface $factory The factory to create displayable property values.
1196
     * @return FormPropertyWidget Chainable
1197
     */
1198
    protected function setPropertyDisplayFactory(FactoryInterface $factory)
1199
    {
1200
        $this->propertyDisplayFactory = $factory;
1201
1202
        return $this;
1203
    }
1204
1205
    /**
1206
     * Resolve the property output type.
1207
     *
1208
     * Note: The "input_type" or "display_type" will be set
1209
     * if the output type is a valid output property.
1210
     *
1211
     * @param  string $type The input or display property type.
1212
     * @throws InvalidArgumentException If the property output type is invalid.
1213
     * @return string Returns either "input" or "display".
1214
     */
1215
    protected function resolveOutputType($type)
1216
    {
1217
        if ($this->propertyInputFactory()->isResolvable($type)) {
1218
            $this->setInputType($type);
1219
            return static::PROPERTY_CONTROL;
1220
        } elseif ($this->propertyDisplayFactory()->isResolvable($type)) {
1221
            $this->setDisplayType($type);
1222
            return static::PROPERTY_DISPLAY;
1223
        } else {
1224
            throw new InvalidArgumentException(sprintf(
1225
                'Invalid form property output type, received %s',
1226
                is_object($type) ? get_class($type) : gettype($type)
0 ignored issues
show
introduced by
The condition is_object($type) is always false.
Loading history...
1227
            ));
1228
        }
1229
    }
1230
1231
    /**
1232
     * Retrieved the resolved the property output type.
1233
     *
1234
     * @return string|null Returns the property's "input_type" or "display_type".
1235
     */
1236
    protected function resolvedOutputType()
1237
    {
1238
        switch ($this->outputType()) {
1239
            case static::PROPERTY_DISPLAY:
1240
                return $this->displayType();
1241
1242
            case static::PROPERTY_CONTROL:
1243
                return $this->inputType();
1244
        }
1245
    }
1246
1247
    /**
1248
     * Retrieve the default CSS class name(s) for the `.form-field`.
1249
     *
1250
     * @return string[]
1251
     */
1252
    protected function defaultFormFieldCssClasses()
1253
    {
1254
        $classes = [ 'form-field', 'form-field-'.$this->widgetId() ];
1255
1256
        if ($this->prop()) {
1257
            $classes[] = 'form-property-'.$this->inputNameAsCssClass();
1258
1259
            if ($this->prop()->type()) {
1260
                $classes[] = 'form-property-'.$this->prop()->type();
1261
            }
1262
1263
            if ($this->prop()->multiple()) {
1264
                $classes[] = '-multiple';
1265
            }
1266
        }
1267
1268
        if ($this->showActiveLanguage()) {
1269
            $classes[] = '-l10n';
1270
        }
1271
1272
        if ($this->hidden()) {
1273
            $classes[] = 'd-none';
1274
        }
1275
1276
        return $classes;
1277
    }
1278
1279
    /**
1280
     * Retrieve the default CSS class name(s) for the `.form-group`.
1281
     *
1282
     * @return string[]
1283
     */
1284
    protected function defaultFormGroupCssClasses()
1285
    {
1286
        return [ 'form-group' ];
1287
    }
1288
1289
    /**
1290
     * Parse the CSS class name(s).
1291
     *
1292
     * @param  mixed $classes One or more CSS class names.
1293
     * @throws InvalidArgumentException If a class name is not a string.
1294
     * @return string[]
1295
     */
1296
    protected function parseCssClasses($classes)
1297
    {
1298
        if (is_string($classes)) {
1299
            $classes = explode(' ', $classes);
1300
        }
1301
1302
        if (!is_array($classes)) {
1303
            throw new InvalidArgumentException('CSS Class(es) must be a space-delimited string or an array');
1304
        }
1305
1306
        return array_filter($classes, 'strlen');
1307
    }
1308
1309
    /**
1310
     * Set the core data for the widget and property's first.
1311
     *
1312
     * @param  array $data The widget and property data.
1313
     * @return array The widget and property data.
1314
     */
1315
    private function setCoreData(array $data)
1316
    {
1317
        if (isset($data['input_type'])) {
1318
            $this->setInputType($data['input_type']);
1319
        }
1320
1321
        if (isset($data['display_type'])) {
1322
            $this->setDisplayType($data['display_type']);
1323
        }
1324
1325
        if (isset($data['property_type'])) {
1326
            $this->setPropertyType($data['property_type']);
1327
        }
1328
1329
        if (isset($data['output_type'])) {
1330
            $this->setOutputType($data['output_type']);
1331
        }
1332
1333
        if (isset($data['type'])) {
1334
            $this->setType($data['type']);
1335
        }
1336
1337
        return $data;
1338
    }
1339
1340
    /**
1341
     * Resolve the property control type.
1342
     *
1343
     * @return string
1344
     */
1345
    private function resolveInputType()
1346
    {
1347
        $type = null;
1348
1349
        /** Attempt input type resolution without instantiating the property, at first. */
1350
        $metadata = $this->propertyData();
1351
        if ($metadata) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $metadata 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...
1352
            if (isset($metadata['hidden']) && $metadata['hidden']) {
1353
                $type = static::HIDDEN_FORM_CONTROL;
1354
            }
1355
1356
            if (!$type && isset($metadata['input_type'])) {
1357
                $type = $metadata['input_type'];
1358
            }
1359
        }
1360
1361
        if ($this->propertyType || $this->property) {
1362
            $property = $this->property();
1363
            $metadata = $property->metadata();
1364
1365
            if ($property->hidden()) {
1366
                $type = static::HIDDEN_FORM_CONTROL;
1367
            }
1368
1369
            if (!$type && isset($metadata['input_type'])) {
1370
                $type = $metadata['input_type'];
1371
            }
1372
1373
            if (!$type && isset($metadata['admin']['input_type'])) {
1374
                $type = $metadata['admin']['input_type'];
1375
            }
1376
        }
1377
1378
        if (!$type) {
1379
            $type = static::DEFAULT_FORM_CONTROL;
1380
        }
1381
1382
        return $type;
1383
    }
1384
1385
    /**
1386
     * Resolve the property display type.
1387
     *
1388
     * @return string
1389
     */
1390
    private function resolveDisplayType()
1391
    {
1392
        $type = null;
1393
1394
        /** Attempt display type resolution without instantiating the property, at first. */
1395
        $metadata = $this->propertyData();
1396
        if ($metadata) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $metadata 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...
1397
            if (isset($metadata['display_type'])) {
1398
                $type = $metadata['display_type'];
1399
            }
1400
        }
1401
1402
        if ($this->propertyType || $this->property) {
1403
            $type = $this->property()->displayType();
0 ignored issues
show
Bug introduced by
The method displayType() does not exist on Charcoal\Property\PropertyInterface. Did you maybe mean displayVal()? ( Ignorable by Annotation )

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

1403
            $type = $this->property()->/** @scrutinizer ignore-call */ displayType();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1404
        }
1405
1406
        return $type;
1407
    }
1408
1409
1410
    /**
1411
     * Create the widget's model property from the property's dataset.
1412
     *
1413
     * @throws UnexpectedValueException If the property type is missing.
1414
     * @return PropertyInterface
1415
     */
1416
    private function createProperty()
1417
    {
1418
        $propType = $this->propertyType();
1419
        if (empty($propType)) {
1420
            $ident = $this->propertyIdent();
1421
            if ($ident && is_string($ident)) {
1422
                $message = sprintf('Missing property type for property "%s"', $ident);
1423
            } else {
1424
                $message = sprintf('Missing property type');
1425
            }
1426
            throw new UnexpectedValueException($message);
1427
        }
1428
1429
        $prop = $this->propertyFactory()->create($propType);
1430
1431
        $prop->setIdent($this->propertyIdent());
1432
        $prop->setData($this->propertyData());
1433
1434
        return $prop;
1435
    }
1436
1437
    /**
1438
     * Create the widget's form control property.
1439
     *
1440
     * @return PropertyInputInterface
1441
     */
1442
    private function createInputProperty()
1443
    {
1444
        $prop  = $this->property();
1445
        $type  = $this->inputType();
1446
        $input = $this->propertyInputFactory()->create($type);
1447
1448
        if ($this->formGroup() && ($input instanceof FormInputInterface)) {
1449
            $input->setFormGroup($this->formGroup());
1450
        }
1451
1452
        if ($input instanceof ViewableInterface) {
1453
            $input->setViewController($this->viewController());
0 ignored issues
show
Bug introduced by
The method setViewController() does not exist on Charcoal\View\ViewableInterface. Did you maybe mean setView()? ( Ignorable by Annotation )

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

1453
            $input->/** @scrutinizer ignore-call */ 
1454
                    setViewController($this->viewController());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1454
        }
1455
1456
        $metadata = $prop->metadata();
1457
        $data = $prop->data();
1458
1459
        if (isset($metadata['admin'])) {
1460
            $data = array_replace_recursive($metadata['admin'], $data);
1461
        }
1462
1463
        $input->setInputType($type);
0 ignored issues
show
Bug introduced by
The method setInputType() does not exist on Charcoal\Ui\FormInput\FormInputInterface. It seems like you code against a sub-type of Charcoal\Ui\FormInput\FormInputInterface such as Charcoal\Admin\Property\Input\NestedWidgetInput or Charcoal\Admin\Widget\FormPropertyWidget. ( Ignorable by Annotation )

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

1463
        $input->/** @scrutinizer ignore-call */ 
1464
                setInputType($type);
Loading history...
1464
        $input->setProperty($prop);
0 ignored issues
show
Bug introduced by
The method setProperty() does not exist on Charcoal\Ui\FormInput\FormInputInterface. It seems like you code against a sub-type of Charcoal\Ui\FormInput\FormInputInterface such as Charcoal\Admin\Property\Input\NestedWidgetInput or Charcoal\Admin\Widget\FormPropertyWidget. ( Ignorable by Annotation )

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

1464
        $input->/** @scrutinizer ignore-call */ 
1465
                setProperty($prop);
Loading history...
1465
        $input->setPropertyVal($this->propertyVal());
0 ignored issues
show
Bug introduced by
The method setPropertyVal() does not exist on Charcoal\Ui\FormInput\FormInputInterface. It seems like you code against a sub-type of Charcoal\Ui\FormInput\FormInputInterface such as Charcoal\Admin\Property\Input\NestedWidgetInput or Charcoal\Admin\Widget\FormPropertyWidget. ( Ignorable by Annotation )

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

1465
        $input->/** @scrutinizer ignore-call */ 
1466
                setPropertyVal($this->propertyVal());
Loading history...
1466
        $input->setData($data);
1467
1468
        return $input;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $input also could return the type Charcoal\Ui\FormInput\FormInputInterface which is incompatible with the documented return type Charcoal\Admin\Widget\PropertyInputInterface.
Loading history...
1469
    }
1470
1471
    /**
1472
     * Create the widget's display property.
1473
     *
1474
     * @return PropertyDisplayInterface
1475
     */
1476
    private function createDisplayProperty()
1477
    {
1478
        $prop    = $this->property();
1479
        $type    = $this->displayType();
1480
        $display = $this->propertyDisplayFactory()->create($type);
1481
1482
        if ($this->formGroup() && ($display instanceof FormInputInterface)) {
1483
            $display->setFormGroup($this->formGroup());
1484
        }
1485
1486
        if ($display instanceof ViewableInterface) {
1487
            $display->setViewController($this->viewController());
1488
        }
1489
1490
        $display->setDisplayType($type);
0 ignored issues
show
Bug introduced by
The method setDisplayType() does not exist on Charcoal\Ui\FormInput\FormInputInterface. It seems like you code against a sub-type of Charcoal\Ui\FormInput\FormInputInterface such as Charcoal\Admin\Widget\FormPropertyWidget. ( Ignorable by Annotation )

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

1490
        $display->/** @scrutinizer ignore-call */ 
1491
                  setDisplayType($type);
Loading history...
1491
        $display->setProperty($prop);
1492
        $display->setPropertyVal($this->propertyVal());
1493
        $display->setData($prop->data());
1494
1495
        $metadata = $prop->metadata();
1496
        if (isset($metadata['admin'])) {
1497
            $display->setData($metadata['admin']);
1498
        }
1499
1500
        return $display;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $display also could return the type Charcoal\Ui\FormInput\FormInputInterface which is incompatible with the documented return type Charcoal\Admin\Widget\PropertyDisplayInterface.
Loading history...
1501
    }
1502
1503
    /**
1504
     * Retrieve the path to the item's icon.
1505
     *
1506
     * @return string
1507
     */
1508
    public function icon()
1509
    {
1510
        return $this->icon;
1511
    }
1512
1513
    /**
1514
     * Set the path to the item's icon associated with the object.
1515
     *
1516
     * @param  string $icon A path to an image.
1517
     * @return self
1518
     */
1519
    public function setIcon($icon)
1520
    {
1521
        $this->icon = $icon;
1522
1523
        return $this;
1524
    }
1525
1526
    /**
1527
     * Check for icon(s)
1528
     *
1529
     * @return boolean
1530
     */
1531
    public function hasIcon()
1532
    {
1533
        if (is_array($this->icon())) {
0 ignored issues
show
introduced by
The condition is_array($this->icon()) is always false.
Loading history...
1534
            return !!count($this->icon());
1535
        }
1536
1537
        return !!$this->icon();
1538
    }
1539
}
1540