Aliasing   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 495
Duplicated Lines 0 %

Test Coverage

Coverage 51.79%

Importance

Changes 16
Bugs 2 Features 5
Metric Value
eloc 166
c 16
b 2
f 5
dl 0
loc 495
ccs 87
cts 168
cp 0.5179
rs 5.5199
wmc 56

16 Methods

Rating   Name   Duplication   Size   Complexity  
A validatePublicConflictingStrategy() 0 4 2
A addMapper() 0 3 1
A validateInternalConflictingStrategy() 0 4 2
A removeAlias() 0 5 2
A mapContent() 0 12 3
A findAlias() 0 10 2
A moveAlias() 0 25 4
A getRepository() 0 3 1
A save() 0 8 2
A setIsBatch() 0 9 1
A __construct() 0 5 1
A compact() 0 5 3
F addAlias() 0 105 20
A getAliasingMap() 0 41 5
A hasPublicAlias() 0 9 3
A hasInternalAlias() 0 13 4

How to fix   Complexity   

Complex Class

Complex classes like Aliasing often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Aliasing, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Gerard van Helden <[email protected]>
4
 * @copyright Zicht Online <http://zicht.nl>
5
 */
6
7
namespace Zicht\Bundle\UrlBundle\Aliasing;
8
9
use Doctrine\ORM\EntityManager;
10
use Zicht\Bundle\UrlBundle\Aliasing\Mapper\UrlMapperInterface;
11
use Zicht\Bundle\UrlBundle\Entity\Repository\UrlAliasRepository;
12
use Zicht\Bundle\UrlBundle\Entity\UrlAlias;
13
use Zicht\Bundle\UrlBundle\Url\Rewriter;
14
15
/**
16
 * Service that contains aliasing information
17
 */
18
class Aliasing
19
{
20
    /**
21
     * Overwrite an alias, if exists.
22
     *
23
     * @see addAlias
24
     */
25
    const STRATEGY_OVERWRITE    = 'overwrite';
26
27
    /**
28
     * Keep existing aliases and do nothing
29
     *
30
     * @see addAlias
31
     */
32
    const STRATEGY_KEEP         = 'keep';
33
34
    /**
35
     * Suffix existing aliases.
36
     *
37
     * @see addAlias
38
     */
39
    const STRATEGY_SUFFIX       = 'suffix';
40
41
    /**
42
     * @see AddAlias
43
     */
44
    const STRATEGY_IGNORE = 'ignore';
45
46
    /**
47
     * @see addAlias
48
     */
49
    const STRATEGY_MOVE_PREVIOUS_TO_NEW = 'redirect-previous-to-new';
50
51
    /**
52
     * @see addAlias
53
     */
54
    const STRATEGY_MOVE_NEW_TO_PREVIOUS = 'redirect-new-to-previous';
55
56
    /** @var EntityManager  */
57
    protected $manager;
58
59
    /** @var UrlAliasRepository */
60
    protected $repository;
61
62
    /** @var boolean */
63
    protected $isBatch = false;
64
65
66
    /**
67
     * Mappers that, based on the content type, can transform internal urls to public urls
68
     *
69
     * @var UrlMapperInterface[]
70
     */
71
    private $contentMappers = array();
72
73
    /**
74
     * Initialize with doctrine
75
     *
76
     * @param EntityManager $manager
77
     */
78 18
    public function __construct(EntityManager $manager)
79
    {
80 18
        $this->manager = $manager;
81 18
        $this->repository = $manager->getRepository('ZichtUrlBundle:UrlAlias');
82 18
        $this->batch = array();
0 ignored issues
show
Bug Best Practice introduced by
The property batch does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
83 18
    }
84
85
86
    /**
87
     * Assert if the strategy is ok when the public url already exists.
88
     *
89
     * @param string $conflictingPublicUrlStrategy
90
     * @return void
91
     */
92 10
    public static function validatePublicConflictingStrategy($conflictingPublicUrlStrategy)
93
    {
94 10
        if (!in_array($conflictingPublicUrlStrategy, [self::STRATEGY_KEEP, self::STRATEGY_OVERWRITE, self::STRATEGY_SUFFIX])) {
95 1
            throw new \InvalidArgumentException("Invalid \$conflictingPublicUrlStrategy '$conflictingPublicUrlStrategy'");
96
        }
97 9
    }
98
99
    /**
100
     * Assert if the strategy is ok when the internal url already has a public url.
101
     *
102
     * @param string $conflictingInternalUrlStrategy
103
     * @return void
104
     */
105 15
    public static function validateInternalConflictingStrategy($conflictingInternalUrlStrategy)
106
    {
107 15
        if (!in_array($conflictingInternalUrlStrategy, [self::STRATEGY_IGNORE, self::STRATEGY_MOVE_PREVIOUS_TO_NEW, self::STRATEGY_MOVE_NEW_TO_PREVIOUS])) {
108 1
            throw new \InvalidArgumentException("Invalid \$conflictingInternalUrlStrategy '$conflictingInternalUrlStrategy'");
109
        }
110 14
    }
111
112
    /**
113
     * Checks if the passed public url is currently mapped to an internal url
114
     *
115
     * @param string $publicUrl
116
     * @param bool $asObject
117
     * @param null|integer $mode
118
     * @return null|string|UrlAlias
119
     */
120 12
    public function hasInternalAlias($publicUrl, $asObject = false, $mode = null)
121
    {
122 12
        $ret = null;
123 12
        if (isset($this->batch[$publicUrl])) {
124 2
            $alias = $this->batch[$publicUrl];
125
        } else {
126 12
            $alias = $this->repository->findOneByPublicUrl($publicUrl, $mode);
127
        }
128 12
        if ($alias) {
129 10
            $ret = ($asObject ? $alias : $alias->getInternalUrl());
130
        }
131
132 12
        return $ret;
133
    }
134
135
136
    /**
137
     * Check if the passed internal URL has a public url alias.
138
     *
139
     * @param string $internalUrl
140
     * @param bool $asObject
141
     * @param integer $mode
142
     * @return null|string|UrlAlias
143
     */
144 11
    public function hasPublicAlias($internalUrl, $asObject = false, $mode = UrlAlias::REWRITE)
145
    {
146 11
        $ret = null;
147
148 11
        if ($alias = $this->repository->findOneByInternalUrl($internalUrl, $mode)) {
149 4
            $ret = ($asObject ? $alias : $alias->getPublicUrl());
150
        }
151
152 11
        return $ret;
153
    }
154
155
    /**
156
     * Find an alias matching both public and internal url
157
     *
158
     * @param string $publicUrl
159
     * @param string $internalUrl
160
     * @return null|UrlAlias
161
     */
162
    public function findAlias($publicUrl, $internalUrl)
163
    {
164
        $ret = null;
165
166
        $params = array('public_url' => $publicUrl, 'internal_url' => $internalUrl);
167
        if ($alias = $this->getRepository()->findOneBy($params)) {
168
            $ret = $alias;
169
        }
170
171
        return $ret;
172
    }
173
174
    /**
175
     * Returns the repository used for storing the aliases
176
     *
177
     * @return UrlAliasRepository
178
     */
179 4
    public function getRepository()
180
    {
181 4
        return $this->repository;
182
    }
183
184
185
    /**
186
     * Add an alias
187
     *
188
     * When the $publicUrl already exists we will use the $conflictingPublicUrlStrategy to resolve this conflict.
189
     * - STRATEGY_KEEP will not do anything, i.e. the $publicUrl will keep pointing to the previous internalUrl
190
     * - STRATEGY_OVERWRITE will remove the previous internalUrl and replace it with $internalUrl
191
     * - STRATEGY_SUFFIX will modify $publicUrl by adding a '-NUMBER' suffix to make it unique
192
     *
193
     * When the $internalUrl already exists we will use the $conflictingInternalUrlStrategy to resolve this conflict.
194
     * - STRATEGY_IGNORE will not do anything
195
     * - STRATEGY_MOVE_PREVIOUS_TO_NEW will make make the previous publicUrl 301 to the new $publicUrl
196
     * - STRATEGY_MOVE_NEW_TO_PREVIOUS will make the new $%publicUrl 301 to the previous publicUrl, leaving the previous publicUrl alone
197
     *
198
     * @param string $publicUrl
199
     * @param string $internalUrl
200
     * @param int $type
201
     * @param string $conflictingPublicUrlStrategy
202
     * @param string $conflictingInternalUrlStrategy
203
     * @return bool
204
     *
205
     * @throws \InvalidArgumentException
206
     */
207 11
    public function addAlias(
208
        $publicUrl,
209
        $internalUrl,
210
        $type,
211
        $conflictingPublicUrlStrategy = self::STRATEGY_OVERWRITE,
212
        $conflictingInternalUrlStrategy = self::STRATEGY_IGNORE
213
    ) {
214 11
        self::validateInternalConflictingStrategy($conflictingInternalUrlStrategy);
215 10
        self::validatePublicConflictingStrategy($conflictingPublicUrlStrategy);
216
217 9
        $ret = false;
218
        /** @var $alias UrlAlias */
219
220
        // check for INTERNAL alias conflict
221 9
        $alias = $this->hasPublicAlias($internalUrl, true);
222 9
        if ($alias) {
0 ignored issues
show
introduced by
$alias is of type Zicht\Bundle\UrlBundle\Entity\UrlAlias, thus it always evaluated to true.
Loading history...
223
            switch ($conflictingInternalUrlStrategy) {
224 2
                case self::STRATEGY_MOVE_PREVIOUS_TO_NEW:
225 2
                    if ($alias && ($publicUrl !== $alias->getPublicUrl())) {
226
                        // $alias will now become the old alias, and will act as a redirect
227 1
                        $alias->setMode(UrlAlias::MOVE);
228 1
                        $this->save($alias);
229
                    }
230 2
                    break;
231
                case self::STRATEGY_MOVE_NEW_TO_PREVIOUS:
232
                    $type = UrlAlias::MOVE;
233
                    break;
234
                case self::STRATEGY_IGNORE:
235
                    // Alias already exist, but the strategy is to ignore changes
236
                    return $ret;
237
                default:
238
                    // case is handled in the `validateInternalConflictingStrategy` guard at top of the function
239
                    break;
240
            }
241
        }
242
243
        // check for PUBLIC alias conflict
244 9
        $alias = $this->hasInternalAlias($publicUrl, true);
245 9
        if ($alias) {
246
            // when this alias is already mapped to the same internalUrl, then there is no conflict,
247
            // but we do need to make this alias active again
248 6
            if ($internalUrl === $alias->getInternalUrl()) {
249 2
                if (UrlAlias::REWRITE === $alias->getMode()) {
250
                    // no need to do anything
251 1
                    $ret = true;
252
                } else {
253
                    // it is possible that multiple aliases exist for this internalUrl, if none of them is
254
                    // UrlAlias::REWRITE, then we must make this $alias rewrite.
255 1
                    $rewriteAlias = $this->hasPublicAlias($internalUrl, true, UrlAlias::REWRITE);
256 1
                    if (null === $rewriteAlias) {
257
                        // we can reuse an existing alias.  The page will get exactly the url it wants
258 1
                        $alias->setMode(UrlAlias::REWRITE);
259 1
                        $this->save($alias);
0 ignored issues
show
Bug introduced by
It seems like $alias can also be of type string; however, parameter $alias of Zicht\Bundle\UrlBundle\Aliasing\Aliasing::save() does only seem to accept Zicht\Bundle\UrlBundle\Entity\UrlAlias, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

259
                        $this->save(/** @scrutinizer ignore-type */ $alias);
Loading history...
260
                    }
261 1
                    $ret = true;
262
                }
263
            }
264
265
            // it is also possible to use one of the pre-existing aliases (that were created using the STRATEGY_SUFFIX)
266 6
            if (!$ret) {
267 4
                foreach ($this->getRepository()->findAllByInternalUrl($internalUrl) as $alternate) {
268
                    if (UrlAlias::REWRITE !== $alternate->getMode()) {
269
                        if (preg_match(sprintf('#^%s-[0-9]+$#', preg_quote($publicUrl)), $alternate->getPublicUrl(), $match)) {
270
                            // we can reuse an existing alias.  The page will get a suffixed version of the url it wants
271
                            $alternate->setMode(UrlAlias::REWRITE);
272
                            $this->save($alternate);
273
                            $ret = true;
274
                            break;
275
                        }
276
                    }
277
                }
278
            }
279
280
            // otherwise we will need to solve the conflict using the supplied strategy
281 6
            if (!$ret) {
282
                switch ($conflictingPublicUrlStrategy) {
283 4
                    case self::STRATEGY_OVERWRITE:
284 1
                        $alias->setInternalUrl($internalUrl);
285 1
                        $this->save($alias);
286 1
                        $ret = true;
287 1
                        break;
288 3
                    case self::STRATEGY_KEEP:
289
                        // do nothing intentionally
290 1
                        break;
291 2
                    case self::STRATEGY_SUFFIX:
292 2
                        $original = $publicUrl;
293 2
                        $i = 1;
294
                        do {
295 2
                            $publicUrl = $original . '-' . ($i++);
296 2
                        } while ($this->hasInternalAlias($publicUrl));
297
298 2
                        $alias = new UrlAlias($publicUrl, $internalUrl, $type);
299 2
                        $this->save($alias);
300 2
                        $ret = true;
301 2
                        break;
302 6
                    default:
303
                        // case is handled in the `validatePublicConflictingStrategy` guard at top of the function
304
                }
305
            }
306
        } else {
307 3
            $alias = new UrlAlias($publicUrl, $internalUrl, $type);
308 3
            $this->save($alias);
309 3
            $ret = true;
310
        }
311 9
        return $ret;
312
    }
313
314
    /**
315
     * Changes the $publicUrl of an UrlAlias to $newPublicUrl
316
     * Adds a new UrlAlias with $type and point it to $newPublicUrl
317
     *
318
     * Given an existing UrlAlias pointing:
319
     *      A -> B
320
     * After this method has been run
321
     *      A -> C and C -> B
322
     * Where A -> C is a new UrlAlias of the type $type
323
     * Where C -> B is an existing UrlAlias with the publicUrl changed
324
     *
325
     * @param string $newPublicUrl The new public url to move to the alias to.
326
     * @param string $publicUrl    The current public url of the UrlAlias we're moving.
327
     * @param string $internalUrl  The current internal url of the UrlAlias we're moving
328
     * @param integer $type        The type of move we want to make. a.k.a. "mode"
329
     * @return boolean Wheter the move action was successful.
330
     */
331
    public function moveAlias($newPublicUrl, $publicUrl, $internalUrl, $type = UrlAlias::ALIAS)
332
    {
333
        $moved = false;
334
        if ($newPublicUrl === $publicUrl) {
335
            return $moved;
336
        }
337
        /** @var UrlAlias $existingAlias */
338
        $existingAlias = $this->findAlias($publicUrl, $internalUrl);
339
        $newAliasExists = $this->hasInternalAlias($newPublicUrl, true);
340
        // if the old alias exists, and the new one doesn't
341
        if (!is_null($existingAlias) && is_null($newAliasExists)) {
342
343
            // change the old alias
344
            $existingAlias->setPublicUrl($newPublicUrl);
345
            // create a new one
346
            $newAlias = new UrlAlias();
347
            $newAlias->setPublicUrl($publicUrl);
348
            $newAlias->setInternalUrl($newPublicUrl);
349
            $newAlias->setMode($type);
350
351
            $this->save($existingAlias);
352
            $this->save($newAlias);
353
            $moved = true;
354
        }
355
        return $moved;
356
    }
357
358
359
    /**
360
     * Set the batch to 'true' if aliases are being batch processed (optimization).
361
     *
362
     * This method returns a callback that needs to be executed after the batch is done; this is up to the caller.
363
     *
364
     * @param bool $isBatch
365
     * @return callable
366
     */
367 5
    public function setIsBatch($isBatch)
368
    {
369 5
        $this->batch = array();
0 ignored issues
show
Bug Best Practice introduced by
The property batch does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
370 5
        $this->isBatch = $isBatch;
371 5
        $mgr = $this->manager;
372 5
        $self = $this;
373
        return function () use ($mgr, $self) {
374 1
            $mgr->flush();
375 1
            $self->setIsBatch(true);
376 5
        };
377
    }
378
379
380
    /**
381
     * Persist the URL alias.
382
     *
383
     * @param \Zicht\Bundle\UrlBundle\Entity\UrlAlias $alias
384
     * @return void
385
     */
386 7
    protected function save(UrlAlias $alias)
387
    {
388 7
        $this->manager->persist($alias);
389
390 7
        if ($this->isBatch) {
391 3
            $this->batch[$alias->getPublicUrl()]= $alias;
0 ignored issues
show
Bug Best Practice introduced by
The property batch does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
392
        } else {
393 4
            $this->manager->flush($alias);
394
        }
395 7
    }
396
397
398
    /**
399
     * Compact redirects; i.e. optimize redirects:
400
     *
401
     * If /a points to /b, and /b points to /c, let /a point to /c
402
     *
403
     * @return void
404
     */
405
    public function compact()
406
    {
407
        foreach ($this->getRepository()->findAll() as $urlAlias) {
408
            if ($cascadingAlias = $this->hasPublicAlias($urlAlias->internal_url)) {
409
                $urlAlias->setInternalUrl($cascadingAlias->getInternalUrl());
410
            }
411
        }
412
    }
413
414
415
    /**
416
     * Remove alias
417
     *
418
     * @param string $internalUrl
419
     * @return void
420
     */
421
    public function removeAlias($internalUrl)
422
    {
423
        if ($alias = $this->hasPublicAlias($internalUrl, true)) {
424
            $this->manager->remove($alias);
0 ignored issues
show
Bug introduced by
It seems like $alias can also be of type string; however, parameter $entity of Doctrine\ORM\EntityManager::remove() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

424
            $this->manager->remove(/** @scrutinizer ignore-type */ $alias);
Loading history...
425
            $this->manager->flush($alias);
426
        }
427
    }
428
429
430
    /**
431
     * Returns key/value pairs of a list of url's.
432
     *
433
     * @param string[] $urls
434
     * @param string $mode
435
     * @return array
436
     */
437
    public function getAliasingMap($urls, $mode)
438
    {
439
        if (0 === count($urls)) {
440
            return [];
441
        }
442
443
        switch ($mode) {
444
            case 'internal-to-public':
445
                $from = 'internal_url';
446
                $to = 'public_url';
447
                break;
448
            case 'public-to-internal':
449
                $from = 'public_url';
450
                $to = 'internal_url';
451
                break;
452
            default:
453
                throw new \InvalidArgumentException("Invalid mode supplied: {$mode}");
454
        }
455
456
        $connection = $this->manager->getConnection()->getWrappedConnection();
457
458
        $sql = sprintf(
459
            'SELECT %1$s, %2$s FROM url_alias WHERE mode=%3$d AND %1$s IN(%4$s)',
460
            $from,
461
            $to,
462
            UrlAlias::REWRITE,
463
            join(
464
                ', ',
465
                array_map(
466
                    function ($v) use ($connection) {
467
                        return $connection->quote($v, \PDO::PARAM_STR);
468
                    },
469
                    $urls
470
                )
471
            )
472
        );
473
474
        if ($stmt = $connection->query($sql)) {
0 ignored issues
show
Unused Code introduced by
The call to Doctrine\DBAL\Driver\Connection::query() has too many arguments starting with $sql. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

474
        if ($stmt = $connection->/** @scrutinizer ignore-call */ query($sql)) {

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
475
            return $stmt->fetchAll(\PDO::FETCH_KEY_PAIR);
476
        }
477
        return array();
478
    }
479
480
    /**
481
     * Transform internal URLS to public URLS using our defined mappers
482
     *
483
     * @param string $contentType
484
     * @param string $mode
485
     * @param string $content
486
     * @param string[] $hosts
487
     * @return string
488
     */
489
    public function mapContent($contentType, $mode, $content, $hosts)
490
    {
491
        $rewriter = new Rewriter($this);
492
        $rewriter->setLocalDomains($hosts);
493
494
        foreach ($this->contentMappers as $mapper) {
495
            if ($mapper->supports($contentType)) {
496
                return $mapper->processAliasing($content, $mode, $rewriter);
497
            }
498
        }
499
500
        return $content;
501
    }
502
503
    /**
504
     * Add a new content mapper to our aliasing class
505
     *
506
     * @param UrlMapperInterface $mapper
507
     *
508
     * @return void
509
     */
510
    public function addMapper(UrlMapperInterface $mapper)
511
    {
512
        $this->contentMappers[] = $mapper;
513
    }
514
}
515