Passed
Pull Request — master (#2)
by
unknown
02:47
created

ObjectRoute   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 541
Duplicated Lines 17.01 %

Coupling/Cohesion

Components 2
Dependencies 2

Importance

Changes 0
Metric Value
wmc 51
lcom 2
cbo 2
dl 92
loc 541
rs 8.3206
c 0
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A setDependencies() 0 5 1
A preSave() 0 8 1
A preUpdate() 0 7 1
C isSlugUnique() 0 34 7
A generateUniqueSlug() 0 14 3
A setModelFactory() 0 6 1
A setCollectionLoader() 0 6 1
A setSlug() 15 16 3
A setLang() 0 6 1
B setCreationDate() 28 29 6
B setLastModificationDate() 28 29 6
A setRouteObjType() 0 6 1
A setRouteObjId() 0 6 1
A setRouteTemplate() 0 6 1
A setRouteOptions() 10 10 2
A setRouteOptionsIdent() 0 6 1
A collectionLoader() 0 11 2
A slug() 0 4 1
A lang() 0 4 1
A creationDate() 0 4 1
A lastModificationDate() 0 4 1
A routeObjType() 0 4 1
A routeObjId() 0 4 1
A routeTemplate() 0 4 1
A routeOptions() 0 4 1
A routeOptionsIdent() 0 4 1
A __toString() 0 4 1
A modelFactory() 11 11 2

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

1
<?php
2
namespace Charcoal\Object;
3
4
use DateTime;
5
use DateTimeInterface;
6
use InvalidArgumentException;
7
use RuntimeException;
8
use Exception;
9
10
// From Pimple
11
use Pimple\Container;
12
13
// From 'charcoal-core'
14
use Charcoal\Model\AbstractModel;
15
use Charcoal\Loader\CollectionLoader;
16
17
// From 'charcoal-factory'
18
use Charcoal\Factory\FactoryInterface;
19
20
// From 'charcoal-object'
21
use Charcoal\Object\ObjectRouteInterface;
22
23
/**
24
 * Represents a route to an object (i.e., a permalink).
25
 *
26
 * Intended to be used to collect all routes related to models
27
 * under a single source (e.g., database table).
28
 *
29
 * {@see Charcoal\Object\ObjectRevision} for a similar model that aggregates data
30
 * under a common source.
31
 *
32
 * Requirements:
33
 *
34
 * - 'model/factory'
35
 * - 'model/collection/loader'
36
 */
37
class ObjectRoute extends AbstractModel implements
38
    ObjectRouteInterface
39
{
40
    /**
41
     * A route is active by default.
42
     *
43
     * @var boolean
44
     */
45
    protected $active = true;
46
47
    /**
48
     * The route's URI.
49
     *
50
     * @var string
51
     */
52
    protected $slug;
53
54
    /**
55
     * The route's locale.
56
     *
57
     * @var string
58
     */
59
    protected $lang;
60
61
    /**
62
     * The creation timestamp.
63
     *
64
     * @var DateTime
65
     */
66
    protected $creationDate;
67
68
    /**
69
     * The last modification timestamp.
70
     *
71
     * @var DateTime
72
     */
73
    protected $lastModificationDate;
74
75
    /**
76
     * The foreign object type related to this route.
77
     *
78
     * @var string
79
     */
80
    protected $routeObjType;
81
82
    /**
83
     * The foreign object ID related to this route.
84
     *
85
     * @var mixed
86
     */
87
    protected $routeObjId;
88
89
    /**
90
     * The foreign object's template identifier.
91
     *
92
     * @var string
93
     */
94
    protected $routeTemplate;
95
96
    /**
97
     * Retrieve the foreign object's routes options.
98
     *
99
     * @var array
100
     */
101
    protected $routeOptions;
102
103
    /**
104
     * Retrieve the foreign object's routes options ident.
105
     *
106
     * @var string
107
     */
108
    protected $routeOptionsIdent;
109
110
    /**
111
     * Store a copy of the original—_preferred_—slug before alterations are made.
112
     *
113
     * @var string
114
     */
115
    private $originalSlug;
116
117
    /**
118
     * Store the increment used to create a unique slug.
119
     *
120
     * @var integer
121
     */
122
    private $slugInc = 0;
123
124
    /**
125
     * Store the factory instance for the current class.
126
     *
127
     * @var FactoryInterface
128
     */
129
    private $modelFactory;
130
131
    /**
132
     * Store the collection loader for the current class.
133
     *
134
     * @var CollectionLoader
135
     */
136
    private $collectionLoader;
137
138
    /**
139
     * Inject dependencies from a DI Container.
140
     *
141
     * @param  Container $container A dependencies container instance.
142
     * @return void
143
     */
144
    public function setDependencies(Container $container)
145
    {
146
        $this->setModelFactory($container['model/factory']);
147
        $this->setCollectionLoader($container['model/collection/loader']);
148
    }
149
150
    /**
151
     * Event called before _creating_ the object.
152
     *
153
     * @see    Charcoal\Source\StorableTrait::preSave() For the "create" Event.
154
     * @return boolean
155
     */
156
    public function preSave()
157
    {
158
        $this->generateUniqueSlug();
159
        $this->setCreationDate('now');
160
        $this->setLastModificationDate('now');
161
162
        return parent::preSave();
163
    }
164
165
    /**
166
     * Event called before _updating_ the object.
167
     *
168
     * @see    Charcoal\Source\StorableTrait::preUpdate() For the "update" Event.
169
     * @param  array $properties Optional. The list of properties to update.
170
     * @return boolean
171
     */
172
    public function preUpdate(array $properties = null)
173
    {
174
        $this->setCreationDate('now');
175
        $this->setLastModificationDate('now');
176
177
        return parent::preUpdate($properties);
0 ignored issues
show
Bug introduced by
It seems like $properties defined by parameter $properties on line 172 can also be of type array; however, Charcoal\Model\AbstractModel::preUpdate() 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...
178
    }
179
180
    /**
181
     * Determine if the current slug is unique.
182
     *
183
     * @return boolean
184
     */
185
    public function isSlugUnique()
186
    {
187
        $proto = $this->modelFactory()->get(self::class);
188
        $loader = $this->collectionLoader();
189
        $loader
190
            ->reset()
191
            ->setModel($proto)
192
            ->addFilter('active', true)
193
            ->addFilter('slug', $this->slug())
194
            ->addOrder('creation_date', 'desc')
195
            ->setPage(1)
196
            ->setNumPerPage(1);
197
        $routes = $loader->load()->objects();
198
        if (!$routes) {
199
            return true;
200
        }
201
        $obj = reset($routes);
202
        if (!$obj->id()) {
203
            return true;
204
        }
205
        if ($obj->id() === $this->id()) {
206
            return true;
207
        }
208
        if ($obj->routeObjId() === $this->routeObjId() &&
209
            $obj->routeObjType() === $this->routeObjType() &&
210
            $obj->lang() === $this->lang()
211
        ) {
212
            $this->setId($obj->id());
213
214
            return true;
215
        }
216
217
        return false;
218
    }
219
220
    /**
221
     * Generate a unique URL slug for routable object.
222
     *
223
     * @return self
224
     */
225
    public function generateUniqueSlug()
226
    {
227
        if (!$this->isSlugUnique()) {
228
            if (!$this->originalSlug) {
229
                $this->originalSlug = $this->slug();
230
            }
231
            $this->slugInc++;
232
            $this->setSlug($this->originalSlug.'-'.$this->slugInc);
233
234
            return $this->generateUniqueSlug();
235
        }
236
237
        return $this;
238
    }
239
240
    /**
241
     * Set an object model factory.
242
     *
243
     * @param  FactoryInterface $factory The model factory, to create objects.
244
     * @return self
245
     */
246
    protected function setModelFactory(FactoryInterface $factory)
247
    {
248
        $this->modelFactory = $factory;
249
250
        return $this;
251
    }
252
253
    /**
254
     * Set a model collection loader.
255
     *
256
     * @param  CollectionLoader $loader The collection loader.
257
     * @return self
258
     */
259
    protected function setCollectionLoader(CollectionLoader $loader)
260
    {
261
        $this->collectionLoader = $loader;
262
263
        return $this;
264
    }
265
266
    /**
267
     * Set the object route URI.
268
     *
269
     * @param  string|null $slug The route.
270
     * @throws InvalidArgumentException If the slug argument is not a string.
271
     * @return self
272
     */
273 View Code Duplication
    public function setSlug($slug)
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...
274
    {
275
        if ($slug === null) {
276
            $this->slug = null;
277
278
            return $this;
279
        }
280
        if (!is_string($slug)) {
281
            throw new InvalidArgumentException(
282
                'Slug is not a string'
283
            );
284
        }
285
        $this->slug = $slug;
286
287
        return $this;
288
    }
289
290
    /**
291
     * Set the locale of the object route.
292
     *
293
     * @param  string $lang The route's locale.
294
     * @return self
295
     */
296
    public function setLang($lang)
297
    {
298
        $this->lang = $lang;
299
300
        return $this;
301
    }
302
303
    /**
304
     * Set the route's last creation date.
305
     *
306
     * @param  string|DateTimeInterface|null $time The date/time value.
307
     * @throws InvalidArgumentException If the date/time value is invalid.
308
     * @return self
309
     */
310 View Code Duplication
    public function setCreationDate($time)
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...
311
    {
312
        if (empty($time) && !is_numeric($time)) {
313
            $this->creationDate = null;
314
315
            return $this;
316
        }
317
318
        if (is_string($time)) {
319
            try {
320
                $time = new DateTime($time);
321
            } catch (Exception $e) {
322
                throw new InvalidArgumentException(sprintf(
323
                    'Invalid Creation Date: %s',
324
                    $e->getMessage()
325
                ), $e->getCode(), $e);
326
            }
327
        }
328
329
        if (!$time instanceof DateTimeInterface) {
330
            throw new InvalidArgumentException(
331
                'Creation Date must be a date/time string or an instance of DateTimeInterface'
332
            );
333
        }
334
335
        $this->creationDate = $time;
336
337
        return $this;
338
    }
339
340
    /**
341
     * Set the route's last modification date.
342
     *
343
     * @param  string|DateTimeInterface|null $time The date/time value.
344
     * @throws InvalidArgumentException If the date/time value is invalid.
345
     * @return self
346
     */
347 View Code Duplication
    public function setLastModificationDate($time)
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...
348
    {
349
        if (empty($time) && !is_numeric($time)) {
350
            $this->lastModificationDate = null;
351
352
            return $this;
353
        }
354
355
        if (is_string($time)) {
356
            try {
357
                $time = new DateTime($time);
358
            } catch (Exception $e) {
359
                throw new InvalidArgumentException(sprintf(
360
                    'Invalid Updated Date: %s',
361
                    $e->getMessage()
362
                ), $e->getCode(), $e);
363
            }
364
        }
365
366
        if (!$time instanceof DateTimeInterface) {
367
            throw new InvalidArgumentException(
368
                'Updated Date must be a date/time string or an instance of DateTimeInterface'
369
            );
370
        }
371
372
        $this->lastModificationDate = $time;
373
374
        return $this;
375
    }
376
377
    /**
378
     * Set the foreign object type related to this route.
379
     *
380
     * @param  string $type The object type.
381
     * @return self
382
     */
383
    public function setRouteObjType($type)
384
    {
385
        $this->routeObjType = $type;
386
387
        return $this;
388
    }
389
390
    /**
391
     * Set the foreign object ID related to this route.
392
     *
393
     * @param  string $id The object ID.
394
     * @return self
395
     */
396
    public function setRouteObjId($id)
397
    {
398
        $this->routeObjId = $id;
399
400
        return $this;
401
    }
402
403
    /**
404
     * Set the foreign object's template identifier.
405
     *
406
     * @param  string $template The template identifier.
407
     * @return self
408
     */
409
    public function setRouteTemplate($template)
410
    {
411
        $this->routeTemplate = $template;
412
413
        return $this;
414
    }
415
416
    /**
417
     * Customize the template's options.
418
     *
419
     * @param  mixed $options Template options.
420
     * @return self
421
     */
422 View Code Duplication
    public function setRouteOptions($options)
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...
423
    {
424
        if (is_string($options)) {
425
            $options = json_decode($options, true);
426
        }
427
428
        $this->routeOptions = $options;
0 ignored issues
show
Documentation Bug introduced by
It seems like $options of type * is incompatible with the declared type array of property $routeOptions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
429
430
        return $this;
431
    }
432
433
    /**
434
     * @param string $routeOptionsIdent Template options ident.
435
     * @return self
436
     */
437
    public function setRouteOptionsIdent($routeOptionsIdent)
438
    {
439
        $this->routeOptionsIdent = $routeOptionsIdent;
440
441
        return $this;
442
    }
443
444
    /**
445
     * Retrieve the object model factory.
446
     *
447
     * @throws RuntimeException If the model factory was not previously set.
448
     * @return FactoryInterface
449
     */
450 View Code Duplication
    public function modelFactory()
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...
451
    {
452
        if (!isset($this->modelFactory)) {
453
            throw new RuntimeException(sprintf(
454
                'Model Factory is not defined for "%s"',
455
                get_class($this)
456
            ));
457
        }
458
459
        return $this->modelFactory;
460
    }
461
462
    /**
463
     * Retrieve the model collection loader.
464
     *
465
     * @throws RuntimeException If the collection loader was not previously set.
466
     * @return CollectionLoader
467
     */
468
    public function collectionLoader()
469
    {
470
        if (!isset($this->collectionLoader)) {
471
            throw new RuntimeException(sprintf(
472
                'Collection Loader is not defined for "%s"',
473
                get_class($this)
474
            ));
475
        }
476
477
        return $this->collectionLoader;
478
    }
479
480
    /**
481
     * Retrieve the object route.
482
     *
483
     * @return string
484
     */
485
    public function slug()
486
    {
487
        return $this->slug;
488
    }
489
490
    /**
491
     * Retrieve the locale of the object route.
492
     *
493
     * @return string
494
     */
495
    public function lang()
496
    {
497
        return $this->lang;
498
    }
499
500
    /**
501
     * Retrieve the route's creation date.
502
     *
503
     * @return DateTimeInterface|null
504
     */
505
    public function creationDate()
506
    {
507
        return $this->creationDate;
508
    }
509
510
    /**
511
     * Retrieve the route's last modification date.
512
     *
513
     * @return DateTimeInterface|null
514
     */
515
    public function lastModificationDate()
516
    {
517
        return $this->lastModificationDate;
518
    }
519
520
    /**
521
     * Retrieve the foreign object type related to this route.
522
     *
523
     * @return string
524
     */
525
    public function routeObjType()
526
    {
527
        return $this->routeObjType;
528
    }
529
530
    /**
531
     * Retrieve the foreign object ID related to this route.
532
     *
533
     * @return string
534
     */
535
    public function routeObjId()
536
    {
537
        return $this->routeObjId;
538
    }
539
540
    /**
541
     * Retrieve the foreign object's template identifier.
542
     *
543
     * @return string
544
     */
545
    public function routeTemplate()
546
    {
547
        return $this->routeTemplate;
548
    }
549
550
    /**
551
     * Retrieve the template's customized options.
552
     *
553
     * @return array
554
     */
555
    public function routeOptions()
556
    {
557
        return $this->routeOptions;
558
    }
559
560
    /**
561
     * @return string
562
     */
563
    public function routeOptionsIdent()
564
    {
565
        return $this->routeOptionsIdent;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->routeOptionsIdent; (string) is incompatible with the return type declared by the interface Charcoal\Object\ObjectRo...face::routeOptionsIdent of type array.

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...
566
    }
567
568
    /**
569
     * Alias of {@see self::slug()}.
570
     *
571
     * @return string
572
     */
573
    public function __toString()
574
    {
575
        return (string)$this->slug();
576
    }
577
}
578