Completed
Push — master ( e81718...2850eb )
by Chauncey
02:26
created

RoutableTrait::getObjectRouteClass()   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
3
namespace Charcoal\Object;
4
5
use Exception;
6
use InvalidArgumentException;
7
use UnexpectedValueException;
8
9
// From 'charcoal-core'
10
use Charcoal\Loader\CollectionLoader;
11
12
// From 'charcoal-translator'
13
use Charcoal\Translator\Translation;
14
15
// From 'charcoal-view'
16
use Charcoal\View\ViewableInterface;
17
18
// From 'charcoal-object'
19
use Charcoal\Object\ObjectRoute;
20
use Charcoal\Object\ObjectRouteInterface;
21
22
/**
23
 * Full implementation, as Trait, of the {@see \Charcoal\Object\RoutableInterface}.
24
 *
25
 * This implementation uses a secondary model, {@see \Charcoal\Object\ObjectRoute},
26
 * to collect all routes of routable models under a single source.
27
 */
28
trait RoutableTrait
29
{
30
    /**
31
     * The object's route.
32
     *
33
     * @var \Charcoal\Translator\Translation|null
34
     */
35
    protected $slug;
36
37
    /**
38
     * Whether the slug is editable.
39
     *
40
     * If FALSE, the slug is always auto-generated from its pattern.
41
     * If TRUE, the slug is auto-generated only if the slug is empty.
42
     *
43
     * @var boolean|null
44
     */
45
    private $isSlugEditable;
46
47
    /**
48
     * The object's route pattern.
49
     *
50
     * @var \Charcoal\Translator\Translation|null
51
     */
52
    private $slugPattern = '';
53
54
    /**
55
     * A prefix for the object's route.
56
     *
57
     * @var \Charcoal\Translator\Translation|null
58
     */
59
    private $slugPrefix = '';
60
61
    /**
62
     * A suffix for the object's route.
63
     *
64
     * @var \Charcoal\Translator\Translation|null
65
     */
66
    private $slugSuffix = '';
67
68
    /**
69
     * The class name of the object route model.
70
     *
71
     * Must be a fully-qualified PHP namespace and an implementation of
72
     * {@see \Charcoal\Object\ObjectRouteInterface}. Used by the model factory.
73
     *
74
     * @var string
75
     */
76
    private $objectRouteClass = ObjectRoute::class;
77
78
    /**
79
     * The object's route options.
80
     *
81
     * @var array|null
82
     */
83
    protected $routeOptions;
84
85
    /**
86
     * Retrieve the foreign object's routes options ident.
87
     *
88
     * @var string
89
     */
90
    protected $routeOptionsIdent;
91
92
    /**
93
     * Set the object's URL slug pattern.
94
     *
95
     * @param  mixed $pattern The slug pattern.
96
     * @return RoutableInterface Chainable
97
     */
98
    public function setSlugPattern($pattern)
99
    {
100
        $this->slugPattern = $this->translator()->translation($pattern);
101
102
        return $this;
103
    }
104
105
    /**
106
     * Retrieve the object's URL slug pattern.
107
     *
108
     * @throws Exception If a slug pattern is not defined.
109
     * @return \Charcoal\Translator\Translation|null
110
     */
111
    public function slugPattern()
112
    {
113
        if (!$this->slugPattern) {
114
            $metadata = $this->metadata();
0 ignored issues
show
Bug introduced by
It seems like metadata() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
115
116
            if (isset($metadata['routable']['pattern'])) {
117
                $this->setSlugPattern($metadata['routable']['pattern']);
118
            } elseif (isset($metadata['slug_pattern'])) {
119
                $this->setSlugPattern($metadata['slug_pattern']);
120
            } else {
121
                throw new Exception(sprintf(
122
                    'Undefined route pattern (slug) for %s',
123
                    get_called_class()
124
                ));
125
            }
126
        }
127
128
        return $this->slugPattern;
129
    }
130
131
    /**
132
     * Retrieve route prefix for the object's URL slug pattern.
133
     *
134
     * @return \Charcoal\Translator\Translation|null
135
     */
136 View Code Duplication
    public function slugPrefix()
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...
137
    {
138
        if (!$this->slugPrefix) {
139
            $metadata = $this->metadata();
0 ignored issues
show
Bug introduced by
It seems like metadata() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
140
141
            if (isset($metadata['routable']['prefix'])) {
142
                $this->slugPrefix = $this->translator()->translation($metadata['routable']['prefix']);
143
            }
144
        }
145
146
        return $this->slugPrefix;
147
    }
148
149
    /**
150
     * Retrieve route suffix for the object's URL slug pattern.
151
     *
152
     * @return \Charcoal\Translator\Translation|null
153
     */
154 View Code Duplication
    public function slugSuffix()
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...
155
    {
156
        if (!$this->slugSuffix) {
157
            $metadata = $this->metadata();
0 ignored issues
show
Bug introduced by
It seems like metadata() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
158
159
            if (isset($metadata['routable']['suffix'])) {
160
                $this->slugSuffix = $this->translator()->translation($metadata['routable']['suffix']);
161
            }
162
        }
163
164
        return $this->slugSuffix;
165
    }
166
167
    /**
168
     * Determine if the slug is editable.
169
     *
170
     * @return boolean
171
     */
172
    public function isSlugEditable()
173
    {
174
        if ($this->isSlugEditable === null) {
175
            $metadata = $this->metadata();
0 ignored issues
show
Bug introduced by
It seems like metadata() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
176
177
            if (isset($metadata['routable']['editable'])) {
178
                $this->isSlugEditable = !!$metadata['routable']['editable'];
179
            } else {
180
                $this->isSlugEditable = false;
181
            }
182
        }
183
184
        return $this->isSlugEditable;
185
    }
186
187
    /**
188
     * Set the object's URL slug.
189
     *
190
     * @param  mixed $slug The slug.
191
     * @return RoutableInterface Chainable
192
     */
193
    public function setSlug($slug)
194
    {
195
        $slug = $this->translator()->translation($slug);
196
        if ($slug !== null) {
197
            $this->slug = $slug;
198
199
            $values = $this->slug->data();
200
            foreach ($values as $lang => $val) {
201
                $this->slug[$lang] = $this->slugify($val);
202
            }
203
        } else {
204
            /** @todo Hack used for regenerating route */
205
            if (isset($_POST['slug'])) {
206
                $this->slug = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type object<Charcoal\Translator\Translation>|null of property $slug.

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...
207
            } else {
208
                $this->slug = null;
209
            }
210
        }
211
212
        return $this;
213
    }
214
215
    /**
216
     * Retrieve the object's URL slug.
217
     *
218
     * @return \Charcoal\Translator\Translation|null
219
     */
220
    public function getSlug()
221
    {
222
        return $this->slug;
223
    }
224
225
    /**
226
     * Generate a URL slug from the object's URL slug pattern.
227
     *
228
     * @throws UnexpectedValueException If the slug is empty.
229
     * @return \Charcoal\Translator\Translation
230
     */
231
    public function generateSlug()
232
    {
233
        $languages = $this->translator()->availableLocales();
234
        $patterns  = $this->slugPattern();
235
        $curSlug   = $this->getSlug();
236
        $newSlug   = [];
237
238
        $origLang = $this->translator()->getLocale();
239
        foreach ($languages as $lang) {
240
            $pattern = $patterns[$lang];
241
242
            $this->translator()->setLocale($lang);
243
            if ($this->isSlugEditable() && isset($curSlug[$lang]) && strlen($curSlug[$lang])) {
244
                $newSlug[$lang] = $curSlug[$lang];
245
            } else {
246
                $newSlug[$lang] = $this->generateRoutePattern($pattern);
247
                if (!strlen($newSlug[$lang])) {
248
                    throw new UnexpectedValueException(sprintf(
249
                        'The slug is empty. The pattern is "%s"',
250
                        $pattern
251
                    ));
252
                }
253
            }
254
            $newSlug[$lang] = $this->finalizeSlug($newSlug[$lang]);
255
256
            $newRoute = $this->createRouteObject();
257
            $newRoute->setData([
258
                'lang'           => $lang,
259
                'slug'           => $newSlug[$lang],
260
                'route_obj_type' => $this->objType(),
0 ignored issues
show
Bug introduced by
It seems like objType() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
261
                'route_obj_id'   => $this->id(),
0 ignored issues
show
Bug introduced by
It seems like id() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
262
            ]);
263
264
            if (!$newRoute->isSlugUnique()) {
265
                $newRoute->generateUniqueSlug();
266
                $newSlug[$lang] = $newRoute['slug'];
267
            }
268
        }
269
        $this->translator()->setLocale($origLang);
270
271
        return $this->translator()->translation($newSlug);
272
    }
273
274
    /**
275
     * Generate a route from the given pattern.
276
     *
277
     * @uses   self::parseRouteToken() If a view renderer is unavailable.
278
     * @param  string $pattern The slug pattern.
279
     * @return string Returns the generated route.
280
     */
281
    protected function generateRoutePattern($pattern)
282
    {
283
        if ($this instanceof ViewableInterface && $this->view() !== null) {
284
            $route = $this->view()->render($pattern, $this->viewController());
285
        } else {
286
            $route = preg_replace_callback('~\{\{\s*(.*?)\s*\}\}~i', [ $this, 'parseRouteToken' ], $pattern);
287
        }
288
289
        return $this->slugify($route);
290
    }
291
292
    /**
293
     * Parse the given slug (URI token) for the current object.
294
     *
295
     * @used-by self::generateRoutePattern() If a view renderer is unavailable.
296
     * @uses    self::filterRouteToken() For customize the route value filtering,
297
     * @param   string|array $token The token to parse relative to the model entry.
298
     * @throws  InvalidArgumentException If a route token is not a string.
299
     * @return  string
300
     */
301
    protected function parseRouteToken($token)
302
    {
303
        // Processes matches from a regular expression operation
304
        if (is_array($token) && isset($token[1])) {
305
            $token = $token[1];
306
        }
307
308
        $token = trim($token);
309
        $method = [ $this, $token ];
310
311
        if (is_callable($method)) {
312
            $value = call_user_func($method);
313
            /** @see \Charcoal\Config\AbstractEntity::offsetGet() */
314
        } elseif (isset($this[$token])) {
315
            $value = $this[$token];
316
        } else {
317
            return '';
318
        }
319
320
        $value = $this->filterRouteToken($value, $token);
321
        if (!is_string($value) && !is_numeric($value)) {
322
            throw new InvalidArgumentException(sprintf(
323
                'Route token "%1$s" must be a string with %2$s; received %3$s',
324
                $token,
325
                get_called_class(),
326
                (is_object($value) ? get_class($value) : gettype($value))
327
            ));
328
        }
329
330
        return $value;
331
    }
332
333
    /**
334
     * Filter the given value for a URI.
335
     *
336
     * @used-by self::parseRouteToken() To resolve the token's value.
337
     * @param   mixed  $value A value to filter.
338
     * @param   string $token The parsed token.
339
     * @return  string The filtered $value.
340
     */
341
    protected function filterRouteToken($value, $token = null)
342
    {
343
        unset($token);
344
345
        if ($value instanceof \Closure) {
346
            $value = $value();
347
        }
348
349
        if ($value instanceof \DateTime) {
350
            $value = $value->format('Y-m-d-H:i');
351
        }
352
353
        if (method_exists($value, '__toString')) {
354
            $value = strval($value);
355
        }
356
357
        return $value;
358
    }
359
360
    /**
361
     * Route generation.
362
     *
363
     * Saves all routes to {@see \Charcoal\Object\ObjectRoute}.
364
     *
365
     * @param  mixed $slug Slug by langs.
366
     * @param  array $data Object route custom data.
367
     * @throws InvalidArgumentException If the slug is invalid.
368
     * @return void
369
     */
370
    protected function generateObjectRoute($slug = null, array $data = [])
371
    {
372
        if (!$slug) {
373
            $slug = $this->generateSlug();
374
        }
375
376
        if ($slug instanceof Translation) {
377
            $slugs = $slug->data();
378
        } else {
379
            throw new InvalidArgumentException(sprintf(
380
                '[%s] slug parameter must be an instance of %s, received %s',
381
                get_called_class().'::'.__FUNCTION__,
382
                Translation::class,
383
                is_object($slug) ? get_class($slug) : gettype($slug)
384
            ));
385
        }
386
387
        if (!is_array($data)) {
388
            $data = [];
389
        }
390
391
        $origLang = $this->translator()->getLocale();
392
        foreach ($slugs as $lang => $slug) {
393
            if (!in_array($lang, $this->translator()->availableLocales())) {
394
                continue;
395
            }
396
            $this->translator()->setLocale($lang);
397
398
            $newRoute = $this->createRouteObject();
399
            $oldRoute = $this->getLatestObjectRoute();
400
401
            $defaultData = [
402
                // Not used, might be too much.
403
                'route_template'      => $this->templateIdent(),
404
                'route_options'       => $this->routeOptions(),
405
                'route_options_ident' => $this->routeOptionsIdent(),
406
            ];
407
408
            $immutableData = [
409
                'lang'                => $lang,
410
                'slug'                => $slug,
411
                'route_obj_type'      => $this->objType(),
0 ignored issues
show
Bug introduced by
It seems like objType() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
412
                'route_obj_id'        => $this->id(),
0 ignored issues
show
Bug introduced by
It seems like id() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
413
                'active'              => true,
414
            ];
415
416
            $newData = array_merge($defaultData, $data, $immutableData);
417
418
            // Unchanged but sync extra properties
419
            if ($slug === $oldRoute['slug']) {
420
                $oldRoute->setData([
421
                    'route_template'      => $newData['route_template'],
422
                    'route_options'       => $newData['route_options'],
423
                    'route_options_ident' => $newData['route_options_ident'],
424
                ]);
425
                $oldRoute->update([ 'route_template', 'route_options' ]);
426
427
                continue;
428
            }
429
430
            $newRoute->setData($newData);
431
432
            if (!$newRoute->isSlugUnique()) {
433
                $newRoute->generateUniqueSlug();
434
            }
435
436
            if ($newRoute->id()) {
437
                $newRoute->update();
438
            } else {
439
                $newRoute->save();
440
            }
441
        }
442
443
        $this->translator()->setLocale($origLang);
444
    }
445
446
    /**
447
     * Retrieve the latest object route.
448
     *
449
     * @param  string|null $lang If object is multilingual, return the object route for the specified locale.
450
     * @throws InvalidArgumentException If the given language is invalid.
451
     * @return ObjectRouteInterface Latest object route.
452
     */
453
    protected function getLatestObjectRoute($lang = null)
454
    {
455
        if ($lang === null) {
456
            $lang = $this->translator()->getLocale();
457 View Code Duplication
        } elseif (!in_array($lang, $this->translator()->availableLocales())) {
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...
458
            throw new InvalidArgumentException(sprintf(
459
                'Invalid language, received %s',
460
                (is_object($lang) ? get_class($lang) : gettype($lang))
461
            ));
462
        }
463
464
        if (!$this->objType() || !$this->id()) {
0 ignored issues
show
Bug introduced by
It seems like objType() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
Bug introduced by
It seems like id() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
465
            return $this->createRouteObject();
466
        }
467
468
        $loader = $this->createRouteObjectCollectionLoader();
469
        $loader
470
            ->setNumPerPage(1)
471
            ->setPage(1)
472
            ->addOrder('creation_date', 'desc')
473
            ->addFilters([
474
                [
475
                    'property' => 'route_obj_type',
476
                    'value'    => $this->objType(),
0 ignored issues
show
Bug introduced by
It seems like objType() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
477
                ],
478
                [
479
                    'property' => 'route_obj_id',
480
                    'value'    => $this->id(),
0 ignored issues
show
Bug introduced by
It seems like id() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
481
                ],
482
                [
483
                    'property' => 'route_options_ident',
484
                    'operator' => 'IS NULL'
485
                ],
486
                [
487
                    'property' => 'lang',
488
                    'value'    => $lang,
489
                ],
490
                [
491
                    'property' => 'active',
492
                    'value'    => true,
493
                ],
494
            ]);
495
496
        $collection = $loader->load()->objects();
497
498
        if (!count($collection)) {
499
            return $this->createRouteObject();
500
        }
501
502
        return $collection[0];
503
    }
504
505
    /**
506
     * Retrieve the object's URI.
507
     *
508
     * @param  string|null $lang If object is multilingual, return the object route for the specified locale.
509
     * @return string
510
     */
511
    public function url($lang = null)
512
    {
513
        $slug = $this->getSlug();
514
515
        if ($slug instanceof Translation && $lang) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lang of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
516
            return $slug[$lang];
517
        }
518
519
        if ($slug) {
520
            return $slug;
521
        }
522
523
        $url = (string)$this->getLatestObjectRoute($lang)->getSlug();
524
        return $url;
525
    }
526
527
    /**
528
     * Convert a string into a slug.
529
     *
530
     * @param  string $str The string to slugify.
531
     * @return string The slugified string.
532
     */
533
    public function slugify($str)
534
    {
535
        static $sluggedArray;
536
537
        if (isset($sluggedArray[$str])) {
538
            return $sluggedArray[$str];
539
        }
540
541
        $metadata    = $this->metadata();
0 ignored issues
show
Bug introduced by
It seems like metadata() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
542
        $separator   = isset($metadata['routable']['separator']) ? $metadata['routable']['separator'] : '-';
543
        $delimiters  = '-_|';
544
        $pregDelim   = preg_quote($delimiters);
545
        $directories = '\\/';
546
        $pregDir     = preg_quote($directories);
547
548
        // Do NOT remove forward slashes.
549
        $slug = preg_replace('![^(\p{L}|\p{N})(\s|\/)]!u', $separator, $str);
550
551
        if (!isset($metadata['routable']['lowercase']) || $metadata['routable']['lowercase'] === false) {
552
            $slug = mb_strtolower($slug, 'UTF-8');
553
        }
554
555
        // Strip HTML
556
        $slug = strip_tags($slug);
557
558
        // Remove diacritics
559
        $slug = htmlentities($slug, ENT_COMPAT, 'UTF-8');
560
        $slug = preg_replace('!&([a-zA-Z])(uml|acute|grave|circ|tilde|cedil|ring);!', '$1', $slug);
561
562
        // Simplify ligatures
563
        $slug = preg_replace('!&([a-zA-Z]{2})(lig);!', '$1', $slug);
564
565
        // Remove unescaped HTML characters
566
        $unescaped = '!&(raquo|laquo|rsaquo|lsaquo|rdquo|ldquo|rsquo|lsquo|hellip|amp|nbsp|quot|ordf|ordm);!';
567
        $slug = preg_replace($unescaped, '', $slug);
568
569
        // Unify all dashes/underscores as one separator character
570
        $flip = ($separator === '-') ? '_' : '-';
571
        $slug = preg_replace('!['.preg_quote($flip).']+!u', $separator, $slug);
572
573
        // Remove all whitespace and normalize delimiters
574
        $slug = preg_replace('![_\|\s|\(\)]+!', $separator, $slug);
575
576
        // Squeeze multiple delimiters and whitespace with a single separator
577
        $slug = preg_replace('!['.$pregDelim.'\s]{2,}!', $separator, $slug);
578
579
        // Squeeze multiple URI path delimiters
580
        $slug = preg_replace('!['.$pregDir.']{2,}!', $separator, $slug);
581
582
        // Remove delimiters surrouding URI path delimiters
583
        $slug = preg_replace('!(?<=['.$pregDir.'])['.$pregDelim.']|['.$pregDelim.'](?=['.$pregDir.'])!', '', $slug);
584
585
        // Strip leading and trailing dashes or underscores
586
        $slug = trim($slug, $delimiters);
587
588
        // Cache the slugified string
589
        $sluggedArray[$str] = $slug;
590
591
        return $slug;
592
    }
593
594
    /**
595
     * Finalize slug.
596
     *
597
     * Adds any prefix and suffix defined in the routable configuration set.
598
     *
599
     * @param  string $slug A slug.
600
     * @throws UnexpectedValueException If the slug affixes are invalid.
601
     * @return string
602
     */
603
    protected function finalizeSlug($slug)
604
    {
605
        $prefix = $this->slugPrefix();
606 View Code Duplication
        if ($prefix) {
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...
607
            $prefix = $this->generateRoutePattern((string)$prefix);
608
            if ($slug === $prefix) {
609
                throw new UnexpectedValueException('The slug is the same as the prefix.');
610
            }
611
            $slug = $prefix.preg_replace('!^'.preg_quote($prefix).'\b!', '', $slug);
612
        }
613
614
        $suffix = $this->slugSuffix();
615 View Code Duplication
        if ($suffix) {
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...
616
            $suffix = $this->generateRoutePattern((string)$suffix);
617
            if ($slug === $suffix) {
618
                throw new UnexpectedValueException('The slug is the same as the suffix.');
619
            }
620
            $slug = preg_replace('!\b'.preg_quote($suffix).'$!', '', $slug).$suffix;
621
        }
622
623
        $slug = rtrim($slug, '/');
624
625
        return $slug;
626
    }
627
628
    /**
629
     * Delete all object routes.
630
     *
631
     * Should be called on object deletion {@see \Charcoal\Model\AbstractModel::preDelete()}.
632
     *
633
     * @return boolean Success or failure.
634
     */
635
    protected function deleteObjectRoutes()
636
    {
637
        if (!$this->objType()) {
0 ignored issues
show
Bug introduced by
It seems like objType() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
638
            return false;
639
        }
640
641
        if (!$this->id()) {
0 ignored issues
show
Bug introduced by
It seems like id() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
642
            return false;
643
        }
644
645
        $loader = $this->createRouteObjectCollectionLoader();
646
        $loader
647
            ->addFilters([
648
                [
649
                    'property' => 'route_obj_type',
650
                    'value'    => $this->objType(),
0 ignored issues
show
Bug introduced by
It seems like objType() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
651
                ],
652
                [
653
                    'property' => 'route_obj_id',
654
                    'value'    => $this->id(),
0 ignored issues
show
Bug introduced by
It seems like id() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
655
                ],
656
            ]);
657
658
        $collection = $loader->load();
659
        foreach ($collection as $route) {
660
            $route->delete();
661
        }
662
663
        return true;
664
    }
665
666
    /**
667
     * Create a route collection loader.
668
     *
669
     * @return CollectionLoader
670
     */
671
    public function createRouteObjectCollectionLoader()
672
    {
673
        $loader = new CollectionLoader([
674
            'logger'  => $this->logger,
0 ignored issues
show
Bug introduced by
The property logger does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
675
            'factory' => $this->modelFactory(),
676
            'model'   => $this->getRouteObjectPrototype(),
677
        ]);
678
679
        return $loader;
680
    }
681
682
    /**
683
     * Create a route object.
684
     *
685
     * @return ObjectRouteInterface
686
     */
687
    public function createRouteObject()
688
    {
689
        $route = $this->modelFactory()->create($this->getObjectRouteClass());
690
691
        return $route;
692
    }
693
694
    /**
695
     * Retrieve the route object prototype.
696
     *
697
     * @return ObjectRouteInterface
698
     */
699
    public function getRouteObjectPrototype()
700
    {
701
        $proto = $this->modelFactory()->get($this->getObjectRouteClass());
702
703
        return $proto;
704
    }
705
706
    /**
707
     * Set the class name of the object route model.
708
     *
709
     * @param  string $className The class name of the object route model.
710
     * @throws InvalidArgumentException If the class name is not a string.
711
     * @return AbstractPropertyDisplay Chainable
712
     */
713
    protected function setObjectRouteClass($className)
714
    {
715
        if (!is_string($className)) {
716
            throw new InvalidArgumentException(
717
                'Route class name must be a string.'
718
            );
719
        }
720
721
        $this->objectRouteClass = $className;
722
723
        return $this;
724
    }
725
726
    /**
727
     * Retrieve the class name of the object route model.
728
     *
729
     * @return string
730
     */
731
    public function getObjectRouteClass()
732
    {
733
        return $this->objectRouteClass;
734
    }
735
736
    /**
737
     * Alias of {@see self::getObjectRouteClass()}.
738
     *
739
     * @return string
740
     */
741
    public function objectRouteClass()
742
    {
743
        return $this->getObjectRouteClass();
744
    }
745
746
    /**
747
     * Set the object's route options
748
     *
749
     * @param  mixed $options The object routes's options.
750
     * @return self
751
     */
752 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...
753
    {
754
        if (is_string($options)) {
755
            $options = json_decode($options, true);
756
        }
757
758
        $this->routeOptions = $options;
759
760
        return $this;
761
    }
762
763
    /**
764
     * Retrieve the object's route options
765
     *
766
     * @return array|null
767
     */
768
    public function routeOptions()
769
    {
770
        return $this->routeOptions;
771
    }
772
773
    /**
774
     * @param string $routeOptionsIdent Template options ident.
775
     * @return self
776
     */
777
    public function setRouteOptionsIdent($routeOptionsIdent)
778
    {
779
        $this->routeOptionsIdent = $routeOptionsIdent;
780
781
        return $this;
782
    }
783
784
    /**
785
     * @return string
786
     */
787
    public function routeOptionsIdent()
788
    {
789
        return $this->routeOptionsIdent;
790
    }
791
792
    /**
793
     * Determine if the routable object is active.
794
     *
795
     * The route controller will validate the object via this method. If the routable object
796
     * is NOT active, the route controller will usually default to _404 Not Found_.
797
     *
798
     * By default — if the object has an "active" property, that value is checked, else —
799
     * the route is always _active_.
800
     *
801
     * @return boolean
802
     */
803
    public function isActiveRoute()
804
    {
805
        if (isset($this['active'])) {
806
            return !!$this['active'];
807
        } else {
808
            return true;
809
        }
810
    }
811
812
    /**
813
     * Retrieve the object model factory.
814
     *
815
     * @return \Charcoal\Factory\FactoryInterface
816
     */
817
    abstract public function modelFactory();
818
819
    /**
820
     * Retrieve the routable object's template identifier.
821
     *
822
     * @return mixed
823
     */
824
    abstract public function templateIdent();
825
826
    /**
827
     * @return \Charcoal\Translator\Translator
828
     */
829
    abstract protected function translator();
830
}
831