Completed
Push — master ( d999ce...9544ea )
by Chauncey
02:59
created

RoutableTrait::createRouteObjectCollectionLoader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
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-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
            $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
                'lang'                => $lang,
403
                'slug'                => $slug,
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
            $newRoute->setData($data);
427
428
            if (!$newRoute->isSlugUnique()) {
429
                $newRoute->generateUniqueSlug();
430
            }
431
432
            if ($newRoute->id()) {
433
                $newRoute->update();
434
            } else {
435
                $newRoute->save();
436
            }
437
        }
438
439
        $this->translator()->setLocale($origLang);
440
    }
441
442
    /**
443
     * Retrieve the latest object route.
444
     *
445
     * @param  string|null $lang If object is multilingual, return the object route for the specified locale.
446
     * @throws InvalidArgumentException If the given language is invalid.
447
     * @return ObjectRouteInterface Latest object route.
448
     */
449
    protected function getLatestObjectRoute($lang = null)
450
    {
451
        if ($lang === null) {
452
            $lang = $this->translator()->getLocale();
453 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...
454
            throw new InvalidArgumentException(sprintf(
455
                'Invalid language, received %s',
456
                (is_object($lang) ? get_class($lang) : gettype($lang))
457
            ));
458
        }
459
460
        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...
461
            return $this->createRouteObject();
462
        }
463
464
        $loader = $this->createRouteObjectCollectionLoader();
465
        $loader
466
            ->setNumPerPage(1)
467
            ->setPage(1)
468
            ->addOrder('creation_date', 'desc')
469
            ->addFilters([
470
                [
471
                    'property' => 'route_obj_type',
472
                    '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...
473
                ],
474
                [
475
                    'property' => 'route_obj_id',
476
                    '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...
477
                ],
478
                [
479
                    'property' => 'route_options_ident',
480
                    'operator' => 'IS NULL'
481
                ],
482
                [
483
                    'property' => 'lang',
484
                    'value'    => $lang,
485
                ],
486
                [
487
                    'property' => 'active',
488
                    'value'    => true,
489
                ],
490
            ]);
491
492
        $collection = $loader->load()->objects();
493
494
        if (!count($collection)) {
495
            return $model;
0 ignored issues
show
Bug introduced by
The variable $model does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

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