Passed
Push — master ( 70965b...f67450 )
by Mathieu
02:27
created

RoutableTrait::getLatestObjectRoute()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 43
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 27
nc 7
nop 1
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-translation'
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)
0 ignored issues
show
Coding Style introduced by
setSlug uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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 slug()
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->slug();
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
            $objectRoute = $this->createRouteObject();
257
            $objectRoute->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 (!$objectRoute->isSlugUnique()) {
265
                $objectRoute->generateUniqueSlug();
266
                $newSlug[$lang] = $objectRoute->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
            $objectRoute = $this->createRouteObject();
399
400
            $oldRoute = $this->getLatestObjectRoute();
401
402
            $defaultData = [
403
                'lang'                => $lang,
404
                '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...
405
                '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...
406
                // Not used, might be too much.
407
                'route_template'      => $this->templateIdent(),
408
                'route_options'       => $this->routeOptions(),
409
                'route_options_ident' => $this->routeOptionsIdent(),
410
                'active'              => true
411
            ];
412
413
            $data = array_merge($defaultData, $data);
414
415
            // Unchanged but sync extra properties
416
            if ($slug === $oldRoute->slug()) {
417
                $oldRoute->setData([
418
                    'route_template'      => $data['route_template'],
419
                    'route_options'       => $data['route_options'],
420
                    'route_options_ident' => $data['route_options_ident']
421
                ]);
422
                $oldRoute->update([ 'route_template', 'route_options' ]);
423
                continue;
424
            }
425
426
            $objectRoute->setData($data);
427
            $objectRoute->setSlug($slug);
428
            $objectRoute->setLang($lang);
429
430
            if (!$objectRoute->isSlugUnique()) {
431
                $objectRoute->generateUniqueSlug();
432
            }
433
434
            if ($objectRoute->id()) {
435
                $objectRoute->update();
436
            } else {
437
                $objectRoute->save();
438
            }
439
        }
440
441
        $this->translator()->setLocale($origLang);
442
    }
443
444
    /**
445
     * Retrieve the latest object route.
446
     *
447
     * @param  string|null $lang If object is multilingual, return the object route for the specified locale.
448
     * @throws InvalidArgumentException If the given language is invalid.
449
     * @return ObjectRouteInterface Latest object route.
450
     */
451
    protected function getLatestObjectRoute($lang = null)
452
    {
453
454
        if ($lang === null) {
455
            $lang = $this->translator()->getLocale();
456
        } elseif (!in_array($lang, $this->translator()->availableLocales())) {
457
            throw new InvalidArgumentException(sprintf(
458
                'Invalid language, received %s',
459
                (is_object($lang) ? get_class($lang) : gettype($lang))
460
            ));
461
        }
462
463
        $model = $this->createRouteObject();
464
465
        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...
466
            return $model;
467
        }
468
469
        // For URL.
470
        $loader = new CollectionLoader([
471
            '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...
472
            'factory' => $this->modelFactory()
473
        ]);
474
475
        $loader
476
            ->setModel($model)
0 ignored issues
show
Documentation introduced by
$model is of type object<Charcoal\Object\ObjectRouteInterface>, but the function expects a string|object<Charcoal\Model\ModelInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
477
            ->addFilter('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...
478
            ->addFilter('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...
479
            ->addFilter('route_options_ident', '', ['operator' => 'IS NULL'])
480
            ->addFilter('lang', $lang)
481
            ->addFilter('active', true)
482
            ->addOrder('creation_date', 'desc')
483
            ->setPage(1)
484
            ->setNumPerPage(1);
485
486
        $collection = $loader->load()->objects();
487
488
        if (!count($collection)) {
489
            return $model;
490
        }
491
492
        return $collection[0];
493
    }
494
495
    /**
496
     * Retrieve the object's URI.
497
     *
498
     * @param  string|null $lang If object is multilingual, return the object route for the specified locale.
499
     * @return string
500
     */
501
    public function url($lang = null)
502
    {
503
        $slug = $this->slug();
504
505
        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...
506
            return $slug[$lang];
507
        }
508
509
        if ($slug) {
510
            return $slug;
511
        }
512
513
        $url = (string)$this->getLatestObjectRoute($lang)->slug();
514
        return $url;
515
    }
516
517
    /**
518
     * Convert a string into a slug.
519
     *
520
     * @param  string $str The string to slugify.
521
     * @return string The slugified string.
522
     */
523
    public function slugify($str)
524
    {
525
        static $sluggedArray;
526
527
        if (isset($sluggedArray[$str])) {
528
            return $sluggedArray[$str];
529
        }
530
531
        $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...
532
        $separator = isset($metadata['routable']['separator']) ? $metadata['routable']['separator'] : '-';
533
        $delimiters = '-_|';
534
        $pregDelim = preg_quote($delimiters);
535
        $directories = '\\/';
536
        $pregDir = preg_quote($directories);
537
538
        // Do NOT remove forward slashes.
539
        $slug = preg_replace('![^(\p{L}|\p{N})(\s|\/)]!u', $separator, $str);
540
541
        if (!isset($metadata['routable']['lowercase']) || $metadata['routable']['lowercase'] === false) {
542
            $slug = mb_strtolower($slug, 'UTF-8');
543
        }
544
545
        // Strip HTML
546
        $slug = strip_tags($slug);
547
548
        // Remove diacritics
549
        $slug = htmlentities($slug, ENT_COMPAT, 'UTF-8');
550
        $slug = preg_replace('!&([a-zA-Z])(uml|acute|grave|circ|tilde|cedil|ring);!', '$1', $slug);
551
552
        // Simplify ligatures
553
        $slug = preg_replace('!&([a-zA-Z]{2})(lig);!', '$1', $slug);
554
555
        // Remove unescaped HTML characters
556
        $unescaped = '!&(raquo|laquo|rsaquo|lsaquo|rdquo|ldquo|rsquo|lsquo|hellip|amp|nbsp|quot|ordf|ordm);!';
557
        $slug = preg_replace($unescaped, '', $slug);
558
559
        // Unify all dashes/underscores as one separator character
560
        $flip = ($separator === '-') ? '_' : '-';
561
        $slug = preg_replace('!['.preg_quote($flip).']+!u', $separator, $slug);
562
563
        // Remove all whitespace and normalize delimiters
564
        $slug = preg_replace('![_\|\s]+!', $separator, $slug);
565
566
        // Squeeze multiple delimiters and whitespace with a single separator
567
        $slug = preg_replace('!['.$pregDelim.'\s]{2,}!', $separator, $slug);
568
569
        // Squeeze multiple URI path delimiters
570
        $slug = preg_replace('!['.$pregDir.']{2,}!', $separator, $slug);
571
572
        // Remove delimiters surrouding URI path delimiters
573
        $slug = preg_replace('!(?<=['.$pregDir.'])['.$pregDelim.']|['.$pregDelim.'](?=['.$pregDir.'])!', '', $slug);
574
575
        // Strip leading and trailing dashes or underscores
576
        $slug = trim($slug, $delimiters);
577
578
        // Cache the slugified string
579
        $sluggedArray[$str] = $slug;
580
581
        return $slug;
582
    }
583
584
    /**
585
     * Finalize slug.
586
     *
587
     * Adds any prefix and suffix defined in the routable configuration set.
588
     *
589
     * @param  string $slug A slug.
590
     * @throws UnexpectedValueException If the slug affixes are invalid.
591
     * @return string
592
     */
593
    protected function finalizeSlug($slug)
594
    {
595
        $prefix = $this->slugPrefix();
596 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...
597
            $prefix = $this->generateRoutePattern((string)$prefix);
598
            if ($slug === $prefix) {
599
                throw new UnexpectedValueException('The slug is the same as the prefix.');
600
            }
601
            $slug = $prefix.preg_replace('!^'.preg_quote($prefix).'\b!', '', $slug);
602
        }
603
604
        $suffix = $this->slugSuffix();
605 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...
606
            $suffix = $this->generateRoutePattern((string)$suffix);
607
            if ($slug === $suffix) {
608
                throw new UnexpectedValueException('The slug is the same as the suffix.');
609
            }
610
            $slug = preg_replace('!\b'.preg_quote($suffix).'$!', '', $slug).$suffix;
611
        }
612
613
        return $slug;
614
    }
615
616
    /**
617
     * Delete all object routes.
618
     *
619
     * Should be called on object deletion {@see \Charcoal\Model\AbstractModel::preDelete()}.
620
     *
621
     * @return boolean Success or failure.
622
     */
623
    protected function deleteObjectRoutes()
624
    {
625
        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...
626
            return false;
627
        }
628
629
        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...
630
            return false;
631
        }
632
633
        $model = $this->modelFactory()->get($this->objectRouteClass());
634
        $loader = new CollectionLoader([
635
            'logger'  => $this->logger,
636
            'factory' => $this->modelFactory()
637
        ]);
638
639
        $loader
640
            ->setModel($model)
641
            ->addFilter('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...
642
            ->addFilter('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...
643
644
        $collection = $loader->load();
645
        foreach ($collection as $route) {
646
            $route->delete();
647
        }
648
649
        return true;
650
    }
651
652
    /**
653
     * Create a route object.
654
     *
655
     * @return ObjectRouteInterface
656
     */
657
    public function createRouteObject()
658
    {
659
        $route = $this->modelFactory()->create($this->objectRouteClass());
660
661
        return $route;
662
    }
663
664
    /**
665
     * Set the class name of the object route model.
666
     *
667
     * @param  string $className The class name of the object route model.
668
     * @throws InvalidArgumentException If the class name is not a string.
669
     * @return AbstractPropertyDisplay Chainable
670
     */
671
    protected function setObjectRouteClass($className)
672
    {
673
        if (!is_string($className)) {
674
            throw new InvalidArgumentException(
675
                'Route class name must be a string.'
676
            );
677
        }
678
679
        $this->objectRouteClass = $className;
680
681
        return $this;
682
    }
683
684
    /**
685
     * Retrieve the class name of the object route model.
686
     *
687
     * @return string
688
     */
689
    public function objectRouteClass()
690
    {
691
        return $this->objectRouteClass;
692
    }
693
694
    /**
695
     * Set the object's route options
696
     *
697
     * @param  mixed $options The object routes's options.
698
     * @return self
699
     */
700 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...
701
    {
702
        if (is_string($options)) {
703
            $options = json_decode($options, true);
704
        }
705
706
        $this->routeOptions = $options;
707
708
        return $this;
709
    }
710
711
    /**
712
     * Retrieve the object's route options
713
     *
714
     * @return array|null
715
     */
716
    public function routeOptions()
717
    {
718
        return $this->routeOptions;
719
    }
720
721
    /**
722
     * @param string $routeOptionsIdent Template options ident.
723
     * @return self
724
     */
725
    public function setRouteOptionsIdent($routeOptionsIdent)
726
    {
727
        $this->routeOptionsIdent = $routeOptionsIdent;
728
729
        return $this;
730
    }
731
732
    /**
733
     * @return string
734
     */
735
    public function routeOptionsIdent()
736
    {
737
        return $this->routeOptionsIdent;
738
    }
739
740
    /**
741
     * Determine if the routable object is active.
742
     *
743
     * The route controller will validate the object via this method. If the routable object
744
     * is NOT active, the route controller will usually default to _404 Not Found_.
745
     *
746
     * By default — if the object has an "active" property, that value is checked, else —
747
     * the route is always _active_.
748
     *
749
     * @return boolean
750
     */
751
    public function isActiveRoute()
752
    {
753
        if (isset($this['active'])) {
754
            return !!$this['active'];
755
        } else {
756
            return true;
757
        }
758
    }
759
760
    /**
761
     * Retrieve the object model factory.
762
     *
763
     * @return \Charcoal\Factory\FactoryInterface
764
     */
765
    abstract public function modelFactory();
766
767
    /**
768
     * Retrieve the routable object's template identifier.
769
     *
770
     * @return mixed
771
     */
772
    abstract public function templateIdent();
773
774
    /**
775
     * @return \Charcoal\Translator\Translator
776
     */
777
    abstract protected function translator();
778
}
779