Aliasing::addAlias()   F
last analyzed

Complexity

Conditions 20
Paths 506

Size

Total Lines 105
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 23.7308

Importance

Changes 9
Bugs 2 Features 2
Metric Value
cc 20
eloc 62
c 9
b 2
f 2
nc 506
nop 5
dl 0
loc 105
ccs 45
cts 57
cp 0.7895
crap 23.7308
rs 0.6861

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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