Completed
Push — master ( 500873...ee76d0 )
by Mathieu
05:28
created

AbstractProperty   D

Complexity

Total Complexity 75

Size/Duplication

Total Lines 724
Duplicated Lines 1.38 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 5
Bugs 2 Features 0
Metric Value
wmc 75
c 5
b 2
f 0
lcom 1
cbo 11
dl 10
loc 724
rs 4.923

50 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 2
A __toString() 0 13 3
type() 0 1 ?
A setData() 0 14 3
A setIdent() 0 10 2
A ident() 0 9 2
B setVal() 10 25 6
A val() 0 4 1
B displayVal() 0 25 6
A setLabel() 0 5 1
A label() 0 7 2
A setL10n() 0 5 1
A l10n() 0 4 1
A setHidden() 0 5 1
A hidden() 0 4 1
A setMultiple() 0 5 1
A multiple() 0 4 1
A setMultipleOptions() 0 7 1
A multipleOptions() 0 7 2
A defaultMultipleOptions() 0 8 1
A multipleSeparator() 0 5 1
A setAllowNull() 0 5 1
A allowNull() 0 4 1
A setRequired() 0 5 1
A required() 0 4 1
A setUnique() 0 5 1
A unique() 0 4 1
A setActive() 0 5 1
A active() 0 4 1
A setStorable() 0 5 1
A storable() 0 4 1
A setDescription() 0 5 1
A description() 0 4 1
A setNotes() 0 5 1
A notes() 0 4 1
A validationMethods() 0 8 1
A validateRequired() 0 9 3
A validateUnique() 0 9 2
A validateAllowNull() 0 8 3
A propertyValue() 0 8 2
A createMetadata() 0 8 2
A createValidator() 0 5 1
A createView() 0 10 2
save() 0 1 ?
A serialize() 0 5 1
A unserialize() 0 5 1
A jsonSerialize() 0 4 1
A getter() 0 5 1
A setter() 0 5 1
A camelize() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AbstractProperty 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 AbstractProperty, and based on these observations, apply Extract Interface, too.

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 135 and the first side effect is on line 35.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
namespace Charcoal\Property;
4
5
// Dependencies from `PHP`
6
use \Exception;
7
use \InvalidArgumentException;
8
use \JsonSerializable;
9
use \Serializable;
10
11
// PSR-3 (logger) dependencies
12
use \Psr\Log\LoggerAwareInterface;
13
use \Psr\Log\LoggerAwareTrait;
14
15
// Intra-module (`charcoal-core`) dependencies
16
use \Charcoal\Model\DescribableInterface;
17
use \Charcoal\Model\DescribableTrait;
18
use \Charcoal\Translation\TranslationConfig;
19
use \Charcoal\Translation\TranslationString;
20
use \Charcoal\Validator\ValidatableInterface;
21
use \Charcoal\Validator\ValidatableTrait;
22
use \Charcoal\View\GenericView;
23
use \Charcoal\View\ViewableInterface;
24
use \Charcoal\View\ViewableTrait;
25
26
// Local namespace dependencies
27
use \Charcoal\Property\PropertyInterface;
28
use \Charcoal\Property\PropertyValidator;
29
use \Charcoal\Property\StorablePropertyInterface;
30
use \Charcoal\Property\StorablePropertyTrait;
31
32
/**
33
 * An abstract class that implements the full `PropertyInterface`.
34
 */
35
abstract class AbstractProperty implements
0 ignored issues
show
Bug introduced by
Possible parse error: class missing opening or closing brace
Loading history...
36
    JsonSerializable,
37
    Serializable,
38
    PropertyInterface,
39
    DescribableInterface,
40
    LoggerAwareInterface,
41
    StorablePropertyInterface,
42
    ValidatableInterface,
43
    ViewableInterface
44
{
45
    use LoggerAwareTrait;
46
    use DescribableTrait;
47
    use StorablePropertyTrait;
48
    use ValidatableTrait;
49
    use ViewableTrait;
50
51
    /**
52
     * @var string $ident
53
     */
54
    private $ident = '';
55
56
    /**
57
     * @var mixed $Val
58
     */
59
    protected $val;
60
61
    /**
62
     * @var TranslationString $label
63
     */
64
    private $label;
65
66
    /**
67
     * @var boolean $l10n
68
     */
69
    private $l10n = false;
70
71
    /**
72
     * @var boolean $hidden;
73
     */
74
    private $hidden = false;
75
76
    /**
77
     * @var boolean $multiple
78
     */
79
    private $multiple = false;
80
81
    /**
82
     * Array of options for multiple properties
83
     * - `separator` (default=",") How the values will be separated in the storage (sql).
84
     * - `min` (default=null) The min number of values. If null, <0 or NaN, then this is not taken into consideration.
85
     * - `max` (default=null) The max number of values. If null, <0 or NaN, then there is not limit.
86
     * @var mixed $multipleOptions
87
     */
88
    private $multipleOptions;
89
90
    /**
91
     * If true, this property *must* have a value
92
     * @var boolean $required
93
     */
94
    private $required = false;
95
96
    /**
97
     * Unique properties should not share he same value across 2 objects
98
     * @var boolean $unique
99
     */
100
    private $unique = false;
101
102
    /**
103
     * @var boolean $allowNull
104
     */
105
    private $allowNull = true;
106
107
    /**
108
     * Only the storable properties should be saved in storage.
109
     * @var boolean $storable
110
     */
111
    private $storable = true;
112
113
    /**
114
     * Inactive properties should be hidden everywhere / unused
115
     * @var boolean $active
116
     */
117
    private $active = true;
118
119
    /**
120
     * @var TranslationString $description
121
     */
122
    private $description = '';
123
124
    /**
125
     * @var TranslationString $_notes
126
     */
127
    private $notes = '';
128
129
    /**
130
     * Required dependencies:
131
     * - `logger` a PSR3-compliant logger.
132
     *
133
     * @param array $data Optional. Class Dependencies.
134
     */
135
    public function __construct(array $data = null)
136
    {
137
        if (isset($data['logger'])) {
138
            $this->setLogger($data['logger']);
139
        }
140
    }
141
142
    /**
143
     *
144
     *
145
     * @return string
146
     */
147
    public function __toString()
148
    {
149
        $val = $this->val();
150
        if (is_string($val)) {
151
            return $val;
152
        } else {
153
            if (is_object($val)) {
154
                return (string)$val;
155
            } else {
156
                return '';
157
            }
158
        }
159
    }
160
161
    /**
162
     * Get the "property type" string.
163
     *
164
     * ## Notes
165
     * - Type can not be set, so it must be explicitely provided by each implementing property classes.
166
     *
167
     * @return string
168
     */
169
    abstract public function type();
170
171
    /**
172
     * This function takes an array and fill the property with its value.
173
     *
174
     * This method either calls a setter for each key (`set_{$key}()`) or sets a public member.
175
     *
176
     * For example, calling with `set_data(['ident'=>$ident])` would call `setIdent($ident)`
177
     * becasue `setIdent()` exists.
178
     *
179
     * But calling with `set_data(['foobar'=>$foo])` would set the `$foobar` member
180
     * on the metadata object, because the method `set_foobar()` does not exist.
181
     *
182
     * @param array $data The property data.
183
     * @return AbstractProperty Chainable
184
     */
185
    public function setData(array $data)
186
    {
187
        foreach ($data as $prop => $val) {
188
            $setter = $this->setter($prop);
189
            if (is_callable([$this, $setter])) {
190
                $this->{$setter}($val);
191
            } else {
192
                // Set as public member if setter is not set on object.
193
                $this->{$prop} = $val;
194
            }
195
        }
196
197
        return $this;
198
    }
199
200
    /**
201
     * @param string $ident The property identifier.
202
     * @throws InvalidArgumentException  If the ident parameter is not a string.
203
     * @return AbstractProperty Chainable
204
     */
205
    public function setIdent($ident)
206
    {
207
        if (!is_string($ident)) {
208
            throw new InvalidArgumentException(
209
                'Ident needs to be string.'
210
            );
211
        }
212
        $this->ident = $ident;
213
        return $this;
214
    }
215
216
    /**
217
     * @throws Exception If trying to access getter before setter.
218
     * @return string
219
     */
220
    public function ident()
221
    {
222
        if ($this->ident === null) {
223
            throw new Exception(
224
                'Can not get ident(): Ident was never set.'
225
            );
226
        }
227
        return $this->ident;
228
    }
229
230
    /**
231
     * @param mixed $val The property (raw) value.
232
     * @throws InvalidArgumentException If the value is invalid (null or not multiple when supposed to).
233
     * @return PropertyInterface Chainable
234
     */
235
    public function setVal($val)
236
    {
237 View Code Duplication
        if ($val === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
238
            if ($this->allowNull()) {
239
                $this->val = null;
240
                return $this;
241
            } else {
242
                throw new InvalidArgumentException(
243
                    'Val can not be null (Not allowed)'
244
                );
245
            }
246
        }
247
        if ($this->multiple()) {
248
            if (is_string($val)) {
249
                $val = explode($this->multipleSeparator(), $val);
250
            }
251
            if (!is_array($val)) {
252
                throw new InvalidArgumentException(
253
                    'Val is multiple so it must be a string (convertable to array by separator) or an array'
254
                );
255
            }
256
        }
257
        $this->val = $val;
258
        return $this;
259
    }
260
261
    /**
262
     * @return mixed
263
     */
264
    public function val()
265
    {
266
        return $this->val;
267
    }
268
269
270
271
    /**
272
     * @param mixed $val Optional. The value to to convert to display.
273
     * @return string
274
     */
275
    public function displayVal($val = null)
276
    {
277
        if ($val === null) {
278
            $val = $this->val();
279
        }
280
281
        if ($val === null) {
282
            return '';
283
        }
284
285
        $propertyValue = $val;
286
287
        if ($this->l10n() === true) {
288
            $translator = TranslationConfig::instance();
289
290
            $propertyValue = $propertyValue[$translator->currentLanguage()];
291
        }
292
293
        if ($this->multiple() === true) {
294
            if (is_array($propertyValue)) {
295
                $propertyValue = implode($this->multipleSeparator(), $propertyValue);
296
            }
297
        }
298
        return (string)$propertyValue;
299
    }
300
301
    /**
302
     * @param mixed $label The property label.
303
     * @return PropertyInterface Chainable
304
     */
305
    public function setLabel($label)
306
    {
307
        $this->label = new TranslationString($label);
308
        return $this;
309
    }
310
311
    /**
312
     * @return string
313
     */
314
    public function label()
315
    {
316
        if ($this->label === null) {
317
            return ucwords(str_replace(['.', '_'], ' ', $this->ident()));
318
        }
319
        return $this->label;
320
    }
321
322
    /**
323
     * @param boolean $l10n The l10n, or "translatable" flag.
324
     * @return PropertyInterface Chainable
325
     */
326
    public function setL10n($l10n)
327
    {
328
        $this->l10n = !!$l10n;
329
        return $this;
330
    }
331
332
    /**
333
     * The l10n flag sets the property as being translatable, meaning the data is held for multple languages.
334
     *
335
     * @return boolean
336
     */
337
    public function l10n()
338
    {
339
        return $this->l10n;
340
    }
341
342
    /**
343
     * @param boolean $hidden The hidden flag.
344
     * @return PropertyInterface Chainable
345
     */
346
    public function setHidden($hidden)
347
    {
348
        $this->hidden = !!$hidden;
349
        return $this;
350
    }
351
352
    /**
353
     * @return boolean
354
     */
355
    public function hidden()
356
    {
357
        return $this->hidden;
358
    }
359
360
    /**
361
     * @param boolean $multiple The multiple flag.
362
     * @return PropertyInterface Chainable
363
     */
364
    public function setMultiple($multiple)
365
    {
366
        $this->multiple = !!$multiple;
367
        return $this;
368
    }
369
370
    /**
371
     * The multiple flags sets the property as being "repeatable", or allow to represent an array of multiple values.
372
     *
373
     * ## Notes
374
     * - The multiple flag can be forced to false (or true) in implementing property class.
375
     * - How a multiple behaves also depend on `multipleOptions`.
376
     *
377
     * @return boolean
378
     */
379
    public function multiple()
380
    {
381
        return $this->multiple;
382
    }
383
384
    /**
385
     * Set the multiple options / configuration, when property is `multiple`.
386
     *
387
     * ## Options structure
388
     * - `separator` (string) The separator charactor.
389
     * - `min` (integer) The minimum number of values. (0 = no limit).
390
     * - `max` (integer) The maximum number of values. (0 = no limit).
391
     *
392
     * @param array $multipleOptions The property multiple options.
393
     * @return PropertyInterface Chainable
394
     */
395
    public function setMultipleOptions(array $multipleOptions)
396
    {
397
        // The options are always merged with the defaults, to ensure minimum required array structure.
398
        $options = array_merge($this->defaultMultipleOptions(), $multipleOptions);
399
        $this->multipleOptions = $options;
400
        return $this;
401
    }
402
403
    /**
404
     * The options defining the property behavior when the multiple flag is set to true.
405
     *
406
     * @return array
407
     * @see self::defaultMultipleOptions
408
     */
409
    public function multipleOptions()
410
    {
411
        if ($this->multipleOptions === null) {
412
            return $this->defaultMultipleOptions();
413
        }
414
        return $this->multipleOptions;
415
    }
416
417
    /**
418
     * @return array
419
     */
420
    public function defaultMultipleOptions()
421
    {
422
        return [
423
            'separator' => ',',
424
            'min'       => 0,
425
            'max'       => 0
426
        ];
427
    }
428
429
    /**
430
     * @return string
431
     */
432
    public function multipleSeparator()
433
    {
434
        $multipleOptions = $this->multipleOptions();
435
        return $multipleOptions['separator'];
436
    }
437
438
    /**
439
     * @param boolean $allow The property allow null flag.
440
     * @return PropertyInterface Chainable
441
     */
442
    public function setAllowNull($allow)
443
    {
444
        $this->allowNull = !!$allow;
445
        return $this;
446
    }
447
448
    /**
449
     * The allow null flag sets the property as being able to be of a "null" value.
450
     *
451
     * ## Notes
452
     * - This flag typically modifies the storage database to also allow null values.
453
     *
454
     * @return boolean
455
     */
456
    public function allowNull()
457
    {
458
        return $this->allowNull;
459
    }
460
461
    /**
462
     * @param boolean $required The property required flag.
463
     * @return PropertyInterface Chainable
464
     */
465
    public function setRequired($required)
466
    {
467
        $this->required = !!$required;
468
        return $this;
469
    }
470
471
    /**
472
     * Required flag sets the property as being required, meaning not allowed to be null / empty.
473
     *
474
     * ## Notes
475
     * - The actual meaning of "required" might be different for implementing property class.
476
     *
477
     * @return boolean
478
     */
479
    public function required()
480
    {
481
        return $this->required;
482
    }
483
484
    /**
485
     * @param boolean $unique The property unique flag.
486
     * @return PropertyInterface Chainable
487
     */
488
    public function setUnique($unique)
489
    {
490
        $this->unique = !!$unique;
491
        return $this;
492
    }
493
494
    /**
495
     * @return boolean
496
     */
497
    public function unique()
498
    {
499
        return $this->unique;
500
    }
501
502
    /**
503
     * @param boolean $active The property active flag. Inactive properties should have no effects.
504
     * @return PropertyInterface Chainable
505
     */
506
    public function setActive($active)
507
    {
508
        $this->active = !!$active;
509
        return $this;
510
    }
511
512
    /**
513
     * @return boolean
514
     */
515
    public function active()
516
    {
517
        return $this->active;
518
    }
519
520
    /**
521
     * @param boolean $storable The storable flag.
522
     * @return PropertyInterface Chainable
523
     */
524
    public function setStorable($storable)
525
    {
526
        $this->storable = !!$storable;
527
        return $this;
528
    }
529
530
    /**
531
     * @return boolean
532
     */
533
    public function storable()
534
    {
535
        return $this->storable;
536
    }
537
538
    /**
539
     * @param mixed $description The property description.
540
     * @return PropertyInterface Chainable
541
     */
542
    public function setDescription($description)
543
    {
544
        $this->description = new TranslationString($description);
545
        return $this;
546
    }
547
548
    /**
549
     * @return string
550
     */
551
    public function description()
552
    {
553
        return $this->description;
554
    }
555
556
    /**
557
     * @param mixed $notes The property notes.
558
     * @return PropertyInterface Chainable
559
     */
560
    public function setNotes($notes)
561
    {
562
        $this->notes = new TranslationString($notes);
563
        return $this;
564
    }
565
566
    /**
567
     * @return string
568
     */
569
    public function notes()
570
    {
571
        return $this->notes;
572
    }
573
574
575
576
    /**
577
     * The property's default validation methods/
578
     *
579
     * - `required`
580
     * - `unique`
581
     * - `allowNull`
582
     *
583
     * ## Notes
584
     * - Those 3 base validation methods should always be merged, in implementing factory class.
585
     *
586
     * @return array
587
     */
588
    public function validationMethods()
589
    {
590
        return [
591
            'required',
592
            'unique',
593
            'allowNull'
594
        ];
595
    }
596
597
    /**
598
     * @return boolean
599
     */
600
    public function validateRequired()
601
    {
602
        if ($this->required() && !$this->val()) {
603
            $this->validator()->error('Value is required.', 'required');
0 ignored issues
show
Unused Code introduced by
The call to ValidatorInterface::error() has too many arguments starting with 'required'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
604
            return false;
605
        }
606
607
        return true;
608
    }
609
610
    /**
611
     * @return boolean
612
     */
613
    public function validateUnique()
614
    {
615
        if (!$this->unique()) {
616
            return true;
617
        }
618
619
        /** @todo Check in the model's storage if the value already exists. */
620
        return true;
621
    }
622
623
    /**
624
     * @return boolean
625
     */
626
    public function validateAllowNull()
627
    {
628
        if (!$this->allowNull() && $this->val() === null) {
629
            $this->validator()->error('Value can not be null.', 'allowNull');
0 ignored issues
show
Unused Code introduced by
The call to ValidatorInterface::error() has too many arguments starting with 'allowNull'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
630
            return false;
631
        }
632
        return true;
633
    }
634
635
    /**
636
     * @param string $propertyIdent The ident of the property to retrieve.
637
     * @return mixed
638
     */
639
    protected function propertyValue($propertyIdent)
640
    {
641
        if (isset($this->{$propertyIdent})) {
642
            return $this->{$propertyIdent};
643
        } else {
644
            return null;
645
        }
646
    }
647
648
    /**
649
     * @param array $data Optional. Metadata data.
650
     * @return PropertyMetadata
651
     */
652
    protected function createMetadata(array $data = null)
653
    {
654
        $metadata = new PropertyMetadata();
655
        if (is_array($data)) {
656
            $metadata->setData($data);
0 ignored issues
show
Bug introduced by
The method setData() does not seem to exist on object<Charcoal\Property\PropertyMetadata>.

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...
657
        }
658
        return $metadata;
659
    }
660
661
    /**
662
     * ValidatableTrait > createValidator(). Create a Validator object
663
     *
664
     * @return ValidatorInterface
665
     */
666
    protected function createValidator()
667
    {
668
        $validator = new PropertyValidator($this);
669
        return $validator;
670
    }
671
672
    /**
673
     * @param array $data Optional. View data.
674
     * @return ViewInterface
675
     */
676
    public function createView(array $data = null)
677
    {
678
        $view = new GenericView([
679
            'logger'=>$this->logger
680
        ]);
681
        if ($data !== null) {
682
            $view->setData($data);
683
        }
684
        return $view;
685
    }
686
687
    /**
688
     * @return mixed
689
     */
690
    abstract public function save();
691
692
    /**
693
     * Serializable > serialize()
694
     *
695
     * @return string
696
     */
697
    public function serialize()
698
    {
699
        $data = $this->val();
700
        return serialize($data);
701
    }
702
    /**
703
     * Serializable > unsierialize()
704
     *
705
     * @param string $data Serialized data.
706
     * @return void
707
     */
708
    public function unserialize($data)
709
    {
710
        $data = unserialize($data);
711
        $this->setVal($data);
712
    }
713
714
    /**
715
     * JsonSerializable > jsonSerialize()
716
     *
717
     * @return mixed
718
     */
719
    public function jsonSerialize()
720
    {
721
        return $this->val();
722
    }
723
724
   /**
725
    * Allow an object to define how the key getter are called.
726
    *
727
    * @param string $key The key to get the getter from.
728
    * @return string The getter method name, for a given key.
729
    */
730
    protected function getter($key)
731
    {
732
        $getter = $key;
733
        return $this->camelize($getter);
734
    }
735
736
    /**
737
     * Allow an object to define how the key setter are called.
738
     *
739
     * @param string $key The key to get the setter from.
740
     * @return string The setter method name, for a given key.
741
     */
742
    protected function setter($key)
743
    {
744
        $setter = 'set_'.$key;
745
        return $this->camelize($setter);
746
    }
747
748
    /**
749
     * Transform a snake_case string to camelCase.
750
     *
751
     * @param string $str The snake_case string to camelize.
752
     * @return string The camelCase string.
753
     */
754
    private function camelize($str)
755
    {
756
        return lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
757
    }
758
}
759