Test Failed
Push — master ( dc68d1...1f5199 )
by Mathieu
02:31
created

AbstractSection::parseAsMultiple()   C

Complexity

Conditions 8
Paths 4

Size

Total Lines 29
Code Lines 10

Duplication

Lines 29
Ratio 100 %

Importance

Changes 0
Metric Value
dl 29
loc 29
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 10
nc 4
nop 2
1
<?php
2
3
namespace Charcoal\Cms;
4
5
use InvalidArgumentException;
6
7
// From 'charcoal-core'
8
use Charcoal\Model\Collection;
9
use Charcoal\Loader\CollectionLoader;
10
11
// From 'charcoal-object'
12
use Charcoal\Object\Content;
13
use Charcoal\Object\HierarchicalInterface;
14
use Charcoal\Object\HierarchicalTrait;
15
use Charcoal\Object\RoutableInterface;
16
use Charcoal\Object\RoutableTrait;
17
18
// From 'charcoal-translator'
19
use Charcoal\Translator\Translation;
20
21
// From 'charcoal-cms'
22
use Charcoal\Cms\MetatagInterface;
23
use Charcoal\Cms\SearchableInterface;
24
use Charcoal\Cms\SectionInterface;
25
use Charcoal\Cms\TemplateableInterface;
26
27
/**
28
 * A Section is a unique, reachable page.
29
 *
30
 * ## Types of sections
31
 * There can be different types of section. 4 exists in the CMS module:
32
 * - `blocks`
33
 * - `content`
34
 * - `empty`
35
 * - `external`
36
 *
37
 * ## External implementations
38
 * Sections implement the following _Interface_ / _Trait_:
39
 * - From the `Charcoal\Object` namespace (in `charcoal-base`)
40
 *   - `Hierarchical`
41
 *   - `Routable`
42
 * - From the local `Charcoal\Cms` namespace
43
 *   - `Metatag`
44
 *   - `Searchable`
45
 *
46
 */
47
abstract class AbstractSection extends Content implements
48
    HierarchicalInterface,
49
    MetatagInterface,
50
    RoutableInterface,
51
    SearchableInterface,
52
    SectionInterface,
53
    TemplateableInterface
54
{
55
    use HierarchicalTrait;
56
    use MetatagTrait;
57
    use RoutableTrait;
58
    use SearchableTrait;
59
    use TemplateableTrait;
60
61
    const TYPE_BLOCKS = 'charcoal/cms/section/blocks-section';
62
    const TYPE_CONTENT = 'charcoal/cms/section/content-section';
63
    const TYPE_EMPTY = 'charcoal/cms/section/empty-section';
64
    const TYPE_EXTERNAL = 'charcoal/cms/section/external-section';
65
    const DEFAULT_TYPE = self::TYPE_CONTENT;
66
67
    /**
68
     * @var string
69
     */
70
    private $sectionType = self::DEFAULT_TYPE;
71
72
    /**
73
     * @var Translation|string|null
74
     */
75
    private $title;
76
77
    /**
78
     * @var Translation|string|null
79
     */
80
    private $subtitle;
81
82
    /**
83
     * @var Translation|string|null
84
     */
85
    private $content;
86
87
    /**
88
     * @var Translation|string|null
89
     */
90
    private $image;
91
92
    /**
93
     * The menus this object is shown in.
94
     *
95
     * @var string[]
96
     */
97
    protected $inMenu;
98
99
    /**
100
     * @var array
101
     */
102
    protected $keywords;
103
104
    /**
105
     * @var Translation|string $summary
106
     */
107
    protected $summary;
108
109
    /**
110
     * @var string $externalUrl
111
     */
112
    protected $externalUrl;
113
114
    /**
115
     * @var boolean $locked
116
     */
117
    protected $locked;
118
119
    // ==========================================================================
120
    // INIT
121
    // ==========================================================================
122
123
    /**
124
     * Section constructor.
125
     * @param array $data Init data.
126
     */
127 View Code Duplication
    public function __construct(array $data = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
128
    {
129
        parent::__construct($data);
130
131
        if (is_callable([ $this, 'defaultData' ])) {
132
            $this->setData($this->defaultData());
133
        }
134
    }
135
136
    // ==========================================================================
137
    // FUNCTIONS
138
    // ==========================================================================
139
140
    /**
141
     * Determine if the object can be deleted.
142
     *
143
     * @return boolean
144
     */
145
    public function isDeletable()
146
    {
147
        return !!$this->id() && !$this->locked();
148
    }
149
150
    /**
151
     * Retrieve the object's title.
152
     *
153
     * @return string
154
     */
155
    public function hierarchicalLabel()
156
    {
157
        return str_repeat('— ', ($this->hierarchyLevel() - 1)).$this->title();
158
    }
159
160
    /**
161
     * HierarchicalTrait > loadChildren
162
     *
163
     * @return \ArrayAccess|\Traversable
164
     */
165
    public function loadChildren()
166
    {
167
        $loader = new CollectionLoader([
168
            'logger'  => $this->logger,
169
            'factory' => $this->modelFactory()
170
        ]);
171
        $loader->setModel($this);
172
        $loader->addFilter([
173
            'property' => 'master',
174
            'val'      => $this->id()
175
        ]);
176
        $loader->addFilter([
177
            'property' => 'active',
178
            'val'      => true
179
        ]);
180
181
        $loader->addOrder([
182
            'property' => 'position',
183
            'mode'     => 'asc'
184
        ]);
185
186
        return $loader->load();
187
    }
188
189
    // ==========================================================================
190
    // SETTERS
191
    // ==========================================================================
192
193
    /**
194
     * Set the section's type.
195
     *
196
     * @param  string $type The section type.
197
     * @throws InvalidArgumentException If the section type is not a string or not a valid section type.
198
     * @return self
199
     */
200
    public function setSectionType($type)
201
    {
202
        if (!is_string($type)) {
203
            throw new InvalidArgumentException(
204
                'Section type must be a string'
205
            );
206
        }
207
208
        $this->sectionType = $type;
209
210
        return $this;
211
    }
212
213
    /**
214
     * Set the menus this object belongs to.
215
     *
216
     * @param  string|string[] $menu One or more menu identifiers.
217
     * @return self
218
     */
219
    public function setInMenu($menu)
220
    {
221
        $this->inMenu = $menu;
0 ignored issues
show
Documentation Bug introduced by
It seems like $menu can also be of type string. However, the property $inMenu is declared as type array<integer,string>. 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...
222
223
        return $this;
224
    }
225
226
    /**
227
     * Set the object's keywords.
228
     *
229
     * @param  string|string[] $keywords One or more entries.
230
     * @return self
231
     */
232
    public function setKeywords($keywords)
233
    {
234
        $this->keywords = $this->parseAsMultiple($keywords);
235
236
        return $this;
237
    }
238
239
    /**
240
     * @param Translation|string|null $summary The summary.
241
     * @return self
242
     */
243
    public function setSummary($summary)
244
    {
245
        $this->summary = $this->translator()->translation($summary);
246
247
        return $this;
248
    }
249
250
    /**
251
     * @param Translation|string|null $externalUrl The external url.
252
     * @return self
253
     */
254
    public function setExternalUrl($externalUrl)
255
    {
256
        $this->externalUrl = $this->translator()->translation($externalUrl);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->translator()->translation($externalUrl) can also be of type object<Charcoal\Translator\Translation>. However, the property $externalUrl is declared as type string. 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...
257
258
        return $this;
259
    }
260
261
    /**
262
     * Section is locked when you can't change the URL
263
     * @param boolean $locked Prevent new route creation about that object.
264
     * @return self
265
     */
266
    public function setLocked($locked)
267
    {
268
        $this->locked = $locked;
269
270
        return $this;
271
    }
272
273
    /**
274
     * @param Translation|string|null $title The section title (localized).
275
     * @return self
276
     */
277
    public function setTitle($title)
278
    {
279
        $this->title = $this->translator()->translation($title);
280
281
        return $this;
282
    }
283
284
    /**
285
     * @param Translation|string|null $subtitle The section subtitle (localized).
286
     * @return self
287
     */
288
    public function setSubtitle($subtitle)
289
    {
290
        $this->subtitle = $this->translator()->translation($subtitle);
291
292
        return $this;
293
    }
294
295
    /**
296
     * @param Translation|string|null $content The section content (localized).
297
     * @return self
298
     */
299
    public function setContent($content)
300
    {
301
        $this->content = $this->translator()->translation($content);
302
303
        return $this;
304
    }
305
306
    /**
307
     * @param mixed $image The section main image (localized).
308
     * @return self
309
     */
310
    public function setImage($image)
311
    {
312
        $this->image = $this->translator()->translation($image);
313
314
        return $this;
315
    }
316
317
    // ==========================================================================
318
    // GETTERS
319
    // ==========================================================================
320
321
    /**
322
     * Retrieve the section's type.
323
     *
324
     * @return string
325
     */
326
    public function sectionType()
327
    {
328
        return $this->sectionType;
329
    }
330
331
    /**
332
     * @return Translation|string|null
333
     */
334
    public function title()
335
    {
336
        return $this->title;
337
    }
338
339
    /**
340
     * @return Translation|string|null
341
     */
342
    public function subtitle()
343
    {
344
        return $this->subtitle;
345
    }
346
347
    /**
348
     * @return Translation|string|null
349
     */
350
    public function content()
351
    {
352
        return $this->content;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->content; of type Charcoal\Translator\Translation|string|null adds the type string to the return on line 352 which is incompatible with the return type declared by the interface Charcoal\Cms\SectionInterface::content of type Charcoal\Translator\Translation.
Loading history...
353
    }
354
355
    /**
356
     * @return Translation|string|null
357
     */
358
    public function image()
359
    {
360
        return $this->image;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->image; of type Charcoal\Translator\Translation|string|null adds the type string to the return on line 360 which is incompatible with the return type declared by the interface Charcoal\Cms\SectionInterface::image of type Charcoal\Translator\Translation.
Loading history...
361
    }
362
363
    /**
364
     * Retrieve the menus this object belongs to.
365
     *
366
     * @return Translation|string|null
367
     */
368
    public function inMenu()
369
    {
370
        return $this->inMenu;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->inMenu; (string[]) is incompatible with the return type declared by the interface Charcoal\Cms\SectionInterface::inMenu of type string|Charcoal\Translator\Translation.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
371
    }
372
373
    /**
374
     * Retrieve the object's keywords.
375
     *
376
     * @return string[]
377
     */
378
    public function keywords()
379
    {
380
        return $this->keywords;
381
    }
382
383
    /**
384
     * HierarchicalTrait > loadChildren
385
     *
386
     * @return Translation|string|null
387
     */
388
    public function summary()
389
    {
390
        return $this->summary;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->summary; of type Charcoal\Translator\Translation|string adds the type string to the return on line 390 which is incompatible with the return type declared by the interface Charcoal\Cms\SectionInterface::summary of type Charcoal\Translator\Translation.
Loading history...
391
    }
392
393
    /**
394
     * @return Translation|string|null
395
     */
396
    public function externalUrl()
397
    {
398
        return $this->externalUrl;
399
    }
400
401
    /**
402
     * @return boolean Or Null.
403
     */
404
    public function locked()
405
    {
406
        return $this->locked;
407
    }
408
409
    // ==========================================================================
410
    // DEFAULT META
411
    // ==========================================================================
412
413
    /**
414
     * MetatagTrait > canonicalUrl
415
     *
416
     * @todo
417
     * @return string
418
     */
419
    public function canonicalUrl()
420
    {
421
        return $this->url();
422
    }
423
424
    /**
425
     * @return Translation|string|null
426
     */
427
    public function defaultMetaTitle()
428
    {
429
        return $this->title();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->title(); of type Charcoal\Translator\Translation|string|null adds the type string to the return on line 429 which is incompatible with the return type declared by the interface Charcoal\Cms\SectionInterface::defaultMetaTitle of type Charcoal\Translator\Translation.
Loading history...
430
    }
431
432
    /**
433
     * @return Translation|string|null
434
     */
435 View Code Duplication
    public function defaultMetaDescription()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
436
    {
437
        $content = $this->translator()->translation($this->content());
438
        if ($content instanceof Translation) {
439
            $desc = [];
440
            foreach ($content->data() as $lang => $text) {
441
                $desc[$lang] = strip_tags($text);
442
            }
443
444
            return $this->translator()->translation($desc);
445
        }
446
447
        return null;
448
    }
449
450
    /**
451
     * @return Translation|string|null
452
     */
453
    public function defaultMetaImage()
454
    {
455
        return $this->image();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->image(); of type Charcoal\Translator\Translation|string|null adds the type string to the return on line 455 which is incompatible with the return type declared by the interface Charcoal\Cms\SectionInterface::defaultMetaImage of type Charcoal\Translator\Translation.
Loading history...
456
    }
457
458
    // ==========================================================================
459
    // Utils
460
    // ==========================================================================
461
462
    /**
463
     * Parse the property value as a "multiple" value type.
464
     *
465
     * @param  mixed                    $value     The value being converted to an array.
466
     * @param  string|PropertyInterface $separator The boundary string.
467
     * @return array
468
     */
469 View Code Duplication
    public function parseAsMultiple($value, $separator = ',')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
470
    {
471
        if (!isset($value) ||
472
            (is_string($value) && !strlen(trim($value))) ||
473
            (is_array($value) && !count(array_filter($value, 'strlen')))
474
        ) {
475
            return [];
476
        }
477
478
        /**
479
         * This property is marked as "multiple".
480
         * Manually handling the resolution to array
481
         * until the property itself manages this.
482
         */
483
        if (is_string($value)) {
484
            return explode($separator, $value);
485
        }
486
487
        /**
488
         * If the parameter isn't an array yet,
489
         * means we might be dealing with an integer,
490
         * an empty string, or an object.
491
         */
492
        if (!is_array($value)) {
493
            return [ $value ];
494
        }
495
496
        return $value;
497
    }
498
499
    // ==========================================================================
500
    // EVENTS
501
    // ==========================================================================
502
503
    /**
504
     * Route generated on postSave in case
505
     * it contains the ID of the section, which
506
     * you only get once you have save
507
     *
508
     * @return boolean
509
     */
510
    public function postSave()
511
    {
512
        // RoutableTrait
513
        if (!$this->locked()) {
514
            $this->generateObjectRoute($this->slug());
515
        }
516
517
        return parent::postSave();
518
    }
519
520
    /**
521
     * Check whatever before the update.
522
     *
523
     * @param  array|null $properties Properties.
524
     * @return boolean
525
     */
526
    public function postUpdate(array $properties = null)
527
    {
528
        if (!$this->locked()) {
529
            $this->generateObjectRoute($this->slug());
530
        }
531
532
        return parent::postUpdate($properties);
0 ignored issues
show
Bug introduced by
It seems like $properties defined by parameter $properties on line 526 can also be of type array; however, Charcoal\Source\StorableTrait::postUpdate() does only seem to accept null|array<integer,string>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
533
    }
534
535
    /**
536
     * {@inheritdoc}
537
     *
538
     * @return boolean
539
     */
540
    public function preSave()
541
    {
542
        if (!$this->locked()) {
543
            $this->setSlug($this->generateSlug());
544
        }
545
546
        return parent::preSave();
547
    }
548
549
    /**
550
     * {@inheritdoc}
551
     *
552
     * @param array $properties Optional properties to update.
553
     * @return boolean
554
     */
555
    public function preUpdate(array $properties = null)
556
    {
557
        if (!$this->locked()) {
558
            $this->setSlug($this->generateSlug());
559
        }
560
561
        return parent::preUpdate($properties);
562
    }
563
564
    /**
565
     * Event called before _deleting_ the object.
566
     *
567
     * @see    \Charcoal\Model\AbstractModel::preDelete() For the "delete" Event.
568
     * @return boolean
569
     */
570
    public function preDelete()
571
    {
572
        if ($this->locked()) {
573
            return false;
574
        }
575
        // Routable trait
576
        // Remove all unnecessary routes.
577
        $this->deleteObjectRoutes();
578
579
        return parent::preDelete();
580
    }
581
}
582