Passed
Push — master ( 426c6c...e81718 )
by Chauncey
41s queued 10s
created

ObjectRoute::getRouteOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
use Charcoal\Model\ModelFactoryTrait;
18
use Charcoal\Loader\CollectionLoaderAwareTrait;
19
20
// From 'charcoal-factory'
21
use Charcoal\Factory\FactoryInterface;
22
23
// From 'charcoal-object'
24
use Charcoal\Object\ObjectRouteInterface;
25
26
/**
27
 * Represents a route to an object (i.e., a permalink).
28
 *
29
 * Intended to be used to collect all routes related to models
30
 * under a single source (e.g., database table).
31
 *
32
 * {@see Charcoal\Object\ObjectRevision} for a similar model that aggregates data
33
 * under a common source.
34
 *
35
 * Requirements:
36
 *
37
 * - 'model/factory'
38
 * - 'model/collection/loader'
39
 */
40
class ObjectRoute extends AbstractModel implements
0 ignored issues
show
Bug introduced by
There is at least one abstract method in this class. Maybe declare it as abstract, or implement the remaining methods: hasProperty, p, properties, property
Loading history...
41
    ObjectRouteInterface
42
{
43
    use ModelFactoryTrait;
44
    use CollectionLoaderAwareTrait;
45
46
    /**
47
     * A route is active by default.
48
     *
49
     * @var boolean
50
     */
51
    protected $active = true;
52
53
    /**
54
     * The route's URI.
55
     *
56
     * @var string
57
     */
58
    protected $slug;
59
60
    /**
61
     * The route's locale.
62
     *
63
     * @var string
64
     */
65
    protected $lang;
66
67
    /**
68
     * The creation timestamp.
69
     *
70
     * @var DateTime
71
     */
72
    protected $creationDate;
73
74
    /**
75
     * The last modification timestamp.
76
     *
77
     * @var DateTime
78
     */
79
    protected $lastModificationDate;
80
81
    /**
82
     * The foreign object type related to this route.
83
     *
84
     * @var string
85
     */
86
    protected $routeObjType;
87
88
    /**
89
     * The foreign object ID related to this route.
90
     *
91
     * @var mixed
92
     */
93
    protected $routeObjId;
94
95
    /**
96
     * The foreign object's template identifier.
97
     *
98
     * @var string
99
     */
100
    protected $routeTemplate;
101
102
    /**
103
     * Retrieve the foreign object's routes options.
104
     *
105
     * @var array
106
     */
107
    protected $routeOptions;
108
109
    /**
110
     * Retrieve the foreign object's routes options ident.
111
     *
112
     * @var string
113
     */
114
    protected $routeOptionsIdent;
115
116
    /**
117
     * Store a copy of the original—_preferred_—slug before alterations are made.
118
     *
119
     * @var string
120
     */
121
    private $originalSlug;
122
123
    /**
124
     * Store the increment used to create a unique slug.
125
     *
126
     * @var integer
127
     */
128
    private $slugInc = 0;
129
130
    /**
131
     * Inject dependencies from a DI Container.
132
     *
133
     * @param  Container $container A dependencies container instance.
134
     * @return void
135
     */
136
    protected function setDependencies(Container $container)
137
    {
138
        parent::setDependencies($container);
139
140
        $this->setModelFactory($container['model/factory']);
141
        $this->setCollectionLoader($container['model/collection/loader']);
142
    }
143
144
    /**
145
     * Event called before _creating_ the object.
146
     *
147
     * @see    Charcoal\Source\StorableTrait::preSave() For the "create" Event.
148
     * @return boolean
149
     */
150
    protected function preSave()
151
    {
152
        $this->generateUniqueSlug();
153
        $this->setCreationDate('now');
154
        $this->setLastModificationDate('now');
155
156
        return parent::preSave();
157
    }
158
159
    /**
160
     * Event called before _updating_ the object.
161
     *
162
     * @see    Charcoal\Source\StorableTrait::preUpdate() For the "update" Event.
163
     * @param  array $properties Optional. The list of properties to update.
164
     * @return boolean
165
     */
166
    protected function preUpdate(array $properties = null)
167
    {
168
        $this->setCreationDate('now');
169
        $this->setLastModificationDate('now');
170
171
        return parent::preUpdate($properties);
172
    }
173
174
    /**
175
     * Determine if the current slug is unique.
176
     *
177
     * @return boolean
178
     */
179
    public function isSlugUnique()
180
    {
181
        $proto = $this->modelFactory()->get(self::class);
182
        $loader = $this->collectionLoader();
183
        $loader
184
            ->reset()
185
            ->setModel($proto)
186
            ->addFilter('active', true)
187
            ->addFilter('slug', $this->getSlug())
188
            ->addFilter('lang', $this->getLang())
189
            ->addOrder('creation_date', 'desc')
190
            ->setPage(1)
191
            ->setNumPerPage(1);
192
193
        $routes = $loader->load()->objects();
194
        if (!$routes) {
195
            return true;
196
        }
197
        $obj = reset($routes);
198
        if (!$obj->id()) {
199
            return true;
200
        }
201
        if ($obj->id() === $this->id()) {
202
            return true;
203
        }
204
        if ($obj->getRouteObjId() === $this->getRouteObjId() &&
205
            $obj->getRouteObjType() === $this->getRouteObjType() &&
206
            $obj->getLang() === $this->getLang()
207
        ) {
208
            $this->setId($obj->id());
209
210
            return true;
211
        }
212
213
        return false;
214
    }
215
216
    /**
217
     * Generate a unique URL slug for routable object.
218
     *
219
     * @return self
220
     */
221
    public function generateUniqueSlug()
222
    {
223
        if (!$this->isSlugUnique()) {
224
            if (!$this->originalSlug) {
225
                $this->originalSlug = $this->getSlug();
226
            }
227
            $this->slugInc++;
228
            $this->setSlug($this->originalSlug.'-'.$this->slugInc);
229
230
            return $this->generateUniqueSlug();
231
        }
232
233
        return $this;
234
    }
235
236
    /**
237
     * Set the object route URI.
238
     *
239
     * @param  string|null $slug The route.
240
     * @throws InvalidArgumentException If the slug argument is not a string.
241
     * @return self
242
     */
243 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...
244
    {
245
        if ($slug === null) {
246
            $this->slug = null;
247
248
            return $this;
249
        }
250
        if (!is_string($slug)) {
251
            throw new InvalidArgumentException(
252
                'Slug is not a string'
253
            );
254
        }
255
        $this->slug = $slug;
256
257
        return $this;
258
    }
259
260
    /**
261
     * Set the locale of the object route.
262
     *
263
     * @param  string $lang The route's locale.
264
     * @return self
265
     */
266
    public function setLang($lang)
267
    {
268
        $this->lang = $lang;
269
270
        return $this;
271
    }
272
273
    /**
274
     * Set the route's last creation date.
275
     *
276
     * @param  string|DateTimeInterface|null $time The date/time value.
277
     * @throws InvalidArgumentException If the date/time value is invalid.
278
     * @return self
279
     */
280
    public function setCreationDate($time)
281
    {
282
        if (empty($time) && !is_numeric($time)) {
283
            $this->creationDate = null;
284
285
            return $this;
286
        }
287
288
        if (is_string($time)) {
289
            try {
290
                $time = new DateTime($time);
291
            } catch (Exception $e) {
292
                throw new InvalidArgumentException(sprintf(
293
                    'Invalid Creation Date: %s',
294
                    $e->getMessage()
295
                ), $e->getCode(), $e);
296
            }
297
        }
298
299
        if (!$time instanceof DateTimeInterface) {
300
            throw new InvalidArgumentException(
301
                'Creation Date must be a date/time string or an instance of DateTimeInterface'
302
            );
303
        }
304
305
        $this->creationDate = $time;
306
307
        return $this;
308
    }
309
310
    /**
311
     * Set the route's last modification date.
312
     *
313
     * @param  string|DateTimeInterface|null $time The date/time value.
314
     * @throws InvalidArgumentException If the date/time value is invalid.
315
     * @return self
316
     */
317
    public function setLastModificationDate($time)
318
    {
319
        if (empty($time) && !is_numeric($time)) {
320
            $this->lastModificationDate = null;
321
322
            return $this;
323
        }
324
325
        if (is_string($time)) {
326
            try {
327
                $time = new DateTime($time);
328
            } catch (Exception $e) {
329
                throw new InvalidArgumentException(sprintf(
330
                    'Invalid Updated Date: %s',
331
                    $e->getMessage()
332
                ), $e->getCode(), $e);
333
            }
334
        }
335
336
        if (!$time instanceof DateTimeInterface) {
337
            throw new InvalidArgumentException(
338
                'Updated Date must be a date/time string or an instance of DateTimeInterface'
339
            );
340
        }
341
342
        $this->lastModificationDate = $time;
343
344
        return $this;
345
    }
346
347
    /**
348
     * Set the foreign object type related to this route.
349
     *
350
     * @param  string $type The object type.
351
     * @return self
352
     */
353
    public function setRouteObjType($type)
354
    {
355
        $this->routeObjType = $type;
356
357
        return $this;
358
    }
359
360
    /**
361
     * Set the foreign object ID related to this route.
362
     *
363
     * @param  string $id The object ID.
364
     * @return self
365
     */
366
    public function setRouteObjId($id)
367
    {
368
        $this->routeObjId = $id;
369
370
        return $this;
371
    }
372
373
    /**
374
     * Set the foreign object's template identifier.
375
     *
376
     * @param  string $template The template identifier.
377
     * @return self
378
     */
379
    public function setRouteTemplate($template)
380
    {
381
        $this->routeTemplate = $template;
382
383
        return $this;
384
    }
385
386
    /**
387
     * Customize the template's options.
388
     *
389
     * @param  mixed $options Template options.
390
     * @return self
391
     */
392 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...
393
    {
394
        if (is_string($options)) {
395
            $options = json_decode($options, true);
396
        }
397
398
        $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...
399
400
        return $this;
401
    }
402
403
    /**
404
     * @param string $routeOptionsIdent Template options ident.
405
     * @return self
406
     */
407
    public function setRouteOptionsIdent($routeOptionsIdent)
408
    {
409
        $this->routeOptionsIdent = $routeOptionsIdent;
410
411
        return $this;
412
    }
413
414
    /**
415
     * Retrieve the object model factory.
416
     *
417
     * @throws RuntimeException If the model factory was not previously set.
418
     * @return FactoryInterface
419
     */
420 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...
421
    {
422
        if (!isset($this->modelFactory)) {
423
            throw new RuntimeException(sprintf(
424
                'Model Factory is not defined for "%s"',
425
                get_class($this)
426
            ));
427
        }
428
429
        return $this->modelFactory;
430
    }
431
432
    /**
433
     * Retrieve the model collection loader.
434
     *
435
     * @throws RuntimeException If the collection loader was not previously set.
436
     * @return CollectionLoader
437
     */
438
    public function collectionLoader()
439
    {
440
        if (!isset($this->collectionLoader)) {
441
            throw new RuntimeException(sprintf(
442
                'Collection Loader is not defined for "%s"',
443
                get_class($this)
444
            ));
445
        }
446
447
        return $this->collectionLoader;
448
    }
449
450
    /**
451
     * Retrieve the object route.
452
     *
453
     * @return string
454
     */
455
    public function getSlug()
456
    {
457
        return $this->slug;
458
    }
459
460
    /**
461
     * Retrieve the locale of the object route.
462
     *
463
     * @return string
464
     */
465
    public function getLang()
466
    {
467
        return $this->lang;
468
    }
469
470
    /**
471
     * Retrieve the route's creation date.
472
     *
473
     * @return DateTimeInterface|null
474
     */
475
    public function getCreationDate()
476
    {
477
        return $this->creationDate;
478
    }
479
480
    /**
481
     * Retrieve the route's last modification date.
482
     *
483
     * @return DateTimeInterface|null
484
     */
485
    public function getLastModificationDate()
486
    {
487
        return $this->lastModificationDate;
488
    }
489
490
    /**
491
     * Retrieve the foreign object type related to this route.
492
     *
493
     * @return string
494
     */
495
    public function getRouteObjType()
496
    {
497
        return $this->routeObjType;
498
    }
499
500
    /**
501
     * Retrieve the foreign object ID related to this route.
502
     *
503
     * @return string
504
     */
505
    public function getRouteObjId()
506
    {
507
        return $this->routeObjId;
508
    }
509
510
    /**
511
     * Retrieve the foreign object's template identifier.
512
     *
513
     * @return string
514
     */
515
    public function getRouteTemplate()
516
    {
517
        return $this->routeTemplate;
518
    }
519
520
    /**
521
     * Retrieve the template's customized options.
522
     *
523
     * @return array
524
     */
525
    public function getRouteOptions()
526
    {
527
        return $this->routeOptions;
528
    }
529
530
    /**
531
     * @return string
532
     */
533
    public function getRouteOptionsIdent()
534
    {
535
        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...e::getRouteOptionsIdent 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...
536
    }
537
538
    /**
539
     * Alias of {@see self::slug()}.
540
     *
541
     * @return string
542
     */
543
    public function __toString()
544
    {
545
        return (string)$this->slug();
546
    }
547
}
548