Test Setup Failed
Push — feature/slash-suffixed-url-han... ( 6a2715...c1bf9c )
by
unknown
15:45
created

Aliasing   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 522
Duplicated Lines 0 %

Importance

Changes 16
Bugs 2 Features 5
Metric Value
eloc 178
dl 0
loc 522
rs 3.44
c 16
b 2
f 5
wmc 62

17 Methods

Rating   Name   Duplication   Size   Complexity  
A validatePublicConflictingStrategy() 0 4 2
A validateInternalConflictingStrategy() 0 4 2
A __construct() 0 5 1
A hasInternalAlias() 0 13 4
A removeAlias() 0 5 2
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 addMapper() 0 3 1
A compact() 0 5 3
A mapContent() 0 12 3
F addAlias() 0 105 20
A getAliasingMap() 0 41 5
A getInternalAliases() 0 21 6
A hasPublicAlias() 0 9 3

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
    public function __construct(EntityManager $manager)
79
    {
80
        $this->manager = $manager;
81
        $this->repository = $manager->getRepository('ZichtUrlBundle:UrlAlias');
82
        $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
    }
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
    public static function validatePublicConflictingStrategy($conflictingPublicUrlStrategy)
93
    {
94
        if (!in_array($conflictingPublicUrlStrategy, [self::STRATEGY_KEEP, self::STRATEGY_OVERWRITE, self::STRATEGY_SUFFIX])) {
95
            throw new \InvalidArgumentException("Invalid \$conflictingPublicUrlStrategy '$conflictingPublicUrlStrategy'");
96
        }
97
    }
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
    public static function validateInternalConflictingStrategy($conflictingInternalUrlStrategy)
106
    {
107
        if (!in_array($conflictingInternalUrlStrategy, [self::STRATEGY_IGNORE, self::STRATEGY_MOVE_PREVIOUS_TO_NEW, self::STRATEGY_MOVE_NEW_TO_PREVIOUS])) {
108
            throw new \InvalidArgumentException("Invalid \$conflictingInternalUrlStrategy '$conflictingInternalUrlStrategy'");
109
        }
110
    }
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
    public function hasInternalAlias($publicUrl, $asObject = false, $mode = null)
121
    {
122
        $ret = null;
123
        if (isset($this->batch[$publicUrl])) {
124
            $alias = $this->batch[$publicUrl];
125
        } else {
126
            $alias = $this->repository->findOneByPublicUrl($publicUrl, $mode);
127
        }
128
        if ($alias) {
129
            $ret = ($asObject ? $alias : $alias->getInternalUrl());
130
        }
131
132
        return $ret;
133
    }
134
135
    /**
136
     * @param string[] $publicUrls
137
     * @param int|null $mode
138
     * @return UrlAlias[]
139
     */
140
    public function getInternalAliases($publicUrls, $mode = null)
141
    {
142
        if (null !== $mode && !is_int($mode)) {
0 ignored issues
show
introduced by
The condition is_int($mode) is always true.
Loading history...
143
            throw new \UnexpectedValueException(sprintf('Mode argument is expected to be an integer or null, %s given', gettype($mode)));
144
        }
145
        if (!is_iterable($publicUrls)) {
146
            throw new \UnexpectedValueException(sprintf('Public URLs argument is expected to be an array of strings, %s given', gettype($publicUrls)));
147
        }
148
149
        if (count($publicUrls) === 0) {
150
            return [];
151
        }
152
153
        $qb = $this->repository->createQueryBuilder('u');
154
        $qb->where($qb->expr()->in('u.public_url', $publicUrls));
155
        if (null !== $mode) {
156
            $qb->andWhere($qb->expr()->eq('u.mode', (int)$mode));
157
        }
158
        $qb->indexBy('u', 'u.public_url');
159
160
        return $qb->getQuery()->getResult();
161
    }
162
163
    /**
164
     * Check if the passed internal URL has a public url alias.
165
     *
166
     * @param string $internalUrl
167
     * @param bool $asObject
168
     * @param integer $mode
169
     * @return null|string|UrlAlias
170
     */
171
    public function hasPublicAlias($internalUrl, $asObject = false, $mode = UrlAlias::REWRITE)
172
    {
173
        $ret = null;
174
175
        if ($alias = $this->repository->findOneByInternalUrl($internalUrl, $mode)) {
176
            $ret = ($asObject ? $alias : $alias->getPublicUrl());
177
        }
178
179
        return $ret;
180
    }
181
182
    /**
183
     * Find an alias matching both public and internal url
184
     *
185
     * @param string $publicUrl
186
     * @param string $internalUrl
187
     * @return null|UrlAlias
188
     */
189
    public function findAlias($publicUrl, $internalUrl)
190
    {
191
        $ret = null;
192
193
        $params = array('public_url' => $publicUrl, 'internal_url' => $internalUrl);
194
        if ($alias = $this->getRepository()->findOneBy($params)) {
195
            $ret = $alias;
196
        }
197
198
        return $ret;
199
    }
200
201
    /**
202
     * Returns the repository used for storing the aliases
203
     *
204
     * @return UrlAliasRepository
205
     */
206
    public function getRepository()
207
    {
208
        return $this->repository;
209
    }
210
211
212
    /**
213
     * Add an alias
214
     *
215
     * When the $publicUrl already exists we will use the $conflictingPublicUrlStrategy to resolve this conflict.
216
     * - STRATEGY_KEEP will not do anything, i.e. the $publicUrl will keep pointing to the previous internalUrl
217
     * - STRATEGY_OVERWRITE will remove the previous internalUrl and replace it with $internalUrl
218
     * - STRATEGY_SUFFIX will modify $publicUrl by adding a '-NUMBER' suffix to make it unique
219
     *
220
     * When the $internalUrl already exists we will use the $conflictingInternalUrlStrategy to resolve this conflict.
221
     * - STRATEGY_IGNORE will not do anything
222
     * - STRATEGY_MOVE_PREVIOUS_TO_NEW will make make the previous publicUrl 301 to the new $publicUrl
223
     * - STRATEGY_MOVE_NEW_TO_PREVIOUS will make the new $%publicUrl 301 to the previous publicUrl, leaving the previous publicUrl alone
224
     *
225
     * @param string $publicUrl
226
     * @param string $internalUrl
227
     * @param int $type
228
     * @param string $conflictingPublicUrlStrategy
229
     * @param string $conflictingInternalUrlStrategy
230
     * @return bool
231
     *
232
     * @throws \InvalidArgumentException
233
     */
234
    public function addAlias(
235
        $publicUrl,
236
        $internalUrl,
237
        $type,
238
        $conflictingPublicUrlStrategy = self::STRATEGY_OVERWRITE,
239
        $conflictingInternalUrlStrategy = self::STRATEGY_IGNORE
240
    ) {
241
        self::validateInternalConflictingStrategy($conflictingInternalUrlStrategy);
242
        self::validatePublicConflictingStrategy($conflictingPublicUrlStrategy);
243
244
        $ret = false;
245
        /** @var $alias UrlAlias */
246
247
        // check for INTERNAL alias conflict
248
        $alias = $this->hasPublicAlias($internalUrl, true);
249
        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...
250
            switch ($conflictingInternalUrlStrategy) {
251
                case self::STRATEGY_MOVE_PREVIOUS_TO_NEW:
252
                    if ($alias && ($publicUrl !== $alias->getPublicUrl())) {
253
                        // $alias will now become the old alias, and will act as a redirect
254
                        $alias->setMode(UrlAlias::MOVE);
255
                        $this->save($alias);
256
                    }
257
                    break;
258
                case self::STRATEGY_MOVE_NEW_TO_PREVIOUS:
259
                    $type = UrlAlias::MOVE;
260
                    break;
261
                case self::STRATEGY_IGNORE:
262
                    // Alias already exist, but the strategy is to ignore changes
263
                    return $ret;
264
                default:
265
                    // case is handled in the `validateInternalConflictingStrategy` guard at top of the function
266
                    break;
267
            }
268
        }
269
270
        // check for PUBLIC alias conflict
271
        $alias = $this->hasInternalAlias($publicUrl, true);
272
        if ($alias) {
273
            // when this alias is already mapped to the same internalUrl, then there is no conflict,
274
            // but we do need to make this alias active again
275
            if ($internalUrl === $alias->getInternalUrl()) {
276
                if (UrlAlias::REWRITE === $alias->getMode()) {
277
                    // no need to do anything
278
                    $ret = true;
279
                } else {
280
                    // it is possible that multiple aliases exist for this internalUrl, if none of them is
281
                    // UrlAlias::REWRITE, then we must make this $alias rewrite.
282
                    $rewriteAlias = $this->hasPublicAlias($internalUrl, true, UrlAlias::REWRITE);
283
                    if (null === $rewriteAlias) {
284
                        // we can reuse an existing alias.  The page will get exactly the url it wants
285
                        $alias->setMode(UrlAlias::REWRITE);
286
                        $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

286
                        $this->save(/** @scrutinizer ignore-type */ $alias);
Loading history...
287
                    }
288
                    $ret = true;
289
                }
290
            }
291
292
            // it is also possible to use one of the pre-existing aliases (that were created using the STRATEGY_SUFFIX)
293
            if (!$ret) {
294
                foreach ($this->getRepository()->findAllByInternalUrl($internalUrl) as $alternate) {
295
                    if (UrlAlias::REWRITE !== $alternate->getMode()) {
296
                        if (preg_match(sprintf('#^%s-[0-9]+$#', preg_quote($publicUrl)), $alternate->getPublicUrl(), $match)) {
297
                            // we can reuse an existing alias.  The page will get a suffixed version of the url it wants
298
                            $alternate->setMode(UrlAlias::REWRITE);
299
                            $this->save($alternate);
300
                            $ret = true;
301
                            break;
302
                        }
303
                    }
304
                }
305
            }
306
307
            // otherwise we will need to solve the conflict using the supplied strategy
308
            if (!$ret) {
309
                switch ($conflictingPublicUrlStrategy) {
310
                    case self::STRATEGY_OVERWRITE:
311
                        $alias->setInternalUrl($internalUrl);
312
                        $this->save($alias);
313
                        $ret = true;
314
                        break;
315
                    case self::STRATEGY_KEEP:
316
                        // do nothing intentionally
317
                        break;
318
                    case self::STRATEGY_SUFFIX:
319
                        $original = $publicUrl;
320
                        $i = 1;
321
                        do {
322
                            $publicUrl = $original . '-' . ($i++);
323
                        } while ($this->hasInternalAlias($publicUrl));
324
325
                        $alias = new UrlAlias($publicUrl, $internalUrl, $type);
326
                        $this->save($alias);
327
                        $ret = true;
328
                        break;
329
                    default:
330
                        // case is handled in the `validatePublicConflictingStrategy` guard at top of the function
331
                }
332
            }
333
        } else {
334
            $alias = new UrlAlias($publicUrl, $internalUrl, $type);
335
            $this->save($alias);
336
            $ret = true;
337
        }
338
        return $ret;
339
    }
340
341
    /**
342
     * Changes the $publicUrl of an UrlAlias to $newPublicUrl
343
     * Adds a new UrlAlias with $type and point it to $newPublicUrl
344
     *
345
     * Given an existing UrlAlias pointing:
346
     *      A -> B
347
     * After this method has been run
348
     *      A -> C and C -> B
349
     * Where A -> C is a new UrlAlias of the type $type
350
     * Where C -> B is an existing UrlAlias with the publicUrl changed
351
     *
352
     * @param string $newPublicUrl The new public url to move to the alias to.
353
     * @param string $publicUrl    The current public url of the UrlAlias we're moving.
354
     * @param string $internalUrl  The current internal url of the UrlAlias we're moving
355
     * @param integer $type        The type of move we want to make. a.k.a. "mode"
356
     * @return boolean Wheter the move action was successful.
357
     */
358
    public function moveAlias($newPublicUrl, $publicUrl, $internalUrl, $type = UrlAlias::ALIAS)
359
    {
360
        $moved = false;
361
        if ($newPublicUrl === $publicUrl) {
362
            return $moved;
363
        }
364
        /** @var UrlAlias $existingAlias */
365
        $existingAlias = $this->findAlias($publicUrl, $internalUrl);
366
        $newAliasExists = $this->hasInternalAlias($newPublicUrl, true);
367
        // if the old alias exists, and the new one doesn't
368
        if (!is_null($existingAlias) && is_null($newAliasExists)) {
369
370
            // change the old alias
371
            $existingAlias->setPublicUrl($newPublicUrl);
372
            // create a new one
373
            $newAlias = new UrlAlias();
374
            $newAlias->setPublicUrl($publicUrl);
375
            $newAlias->setInternalUrl($newPublicUrl);
376
            $newAlias->setMode($type);
377
378
            $this->save($existingAlias);
379
            $this->save($newAlias);
380
            $moved = true;
381
        }
382
        return $moved;
383
    }
384
385
386
    /**
387
     * Set the batch to 'true' if aliases are being batch processed (optimization).
388
     *
389
     * This method returns a callback that needs to be executed after the batch is done; this is up to the caller.
390
     *
391
     * @param bool $isBatch
392
     * @return callable
393
     */
394
    public function setIsBatch($isBatch)
395
    {
396
        $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...
397
        $this->isBatch = $isBatch;
398
        $mgr = $this->manager;
399
        $self = $this;
400
        return function () use ($mgr, $self) {
401
            $mgr->flush();
402
            $self->setIsBatch(true);
403
        };
404
    }
405
406
407
    /**
408
     * Persist the URL alias.
409
     *
410
     * @param \Zicht\Bundle\UrlBundle\Entity\UrlAlias $alias
411
     * @return void
412
     */
413
    protected function save(UrlAlias $alias)
414
    {
415
        $this->manager->persist($alias);
416
417
        if ($this->isBatch) {
418
            $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...
419
        } else {
420
            $this->manager->flush($alias);
421
        }
422
    }
423
424
425
    /**
426
     * Compact redirects; i.e. optimize redirects:
427
     *
428
     * If /a points to /b, and /b points to /c, let /a point to /c
429
     *
430
     * @return void
431
     */
432
    public function compact()
433
    {
434
        foreach ($this->getRepository()->findAll() as $urlAlias) {
435
            if ($cascadingAlias = $this->hasPublicAlias($urlAlias->internal_url)) {
436
                $urlAlias->setInternalUrl($cascadingAlias->getInternalUrl());
437
            }
438
        }
439
    }
440
441
442
    /**
443
     * Remove alias
444
     *
445
     * @param string $internalUrl
446
     * @return void
447
     */
448
    public function removeAlias($internalUrl)
449
    {
450
        if ($alias = $this->hasPublicAlias($internalUrl, true)) {
451
            $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

451
            $this->manager->remove(/** @scrutinizer ignore-type */ $alias);
Loading history...
452
            $this->manager->flush($alias);
453
        }
454
    }
455
456
457
    /**
458
     * Returns key/value pairs of a list of url's.
459
     *
460
     * @param string[] $urls
461
     * @param string $mode
462
     * @return array
463
     */
464
    public function getAliasingMap($urls, $mode)
465
    {
466
        if (0 === count($urls)) {
467
            return [];
468
        }
469
470
        switch ($mode) {
471
            case 'internal-to-public':
472
                $from = 'internal_url';
473
                $to = 'public_url';
474
                break;
475
            case 'public-to-internal':
476
                $from = 'public_url';
477
                $to = 'internal_url';
478
                break;
479
            default:
480
                throw new \InvalidArgumentException("Invalid mode supplied: {$mode}");
481
        }
482
483
        $connection = $this->manager->getConnection()->getWrappedConnection();
484
485
        $sql = sprintf(
486
            'SELECT %1$s, %2$s FROM url_alias WHERE mode=%3$d AND %1$s IN(%4$s)',
487
            $from,
488
            $to,
489
            UrlAlias::REWRITE,
490
            join(
491
                ', ',
492
                array_map(
493
                    function ($v) use ($connection) {
494
                        return $connection->quote($v, \PDO::PARAM_STR);
495
                    },
496
                    $urls
497
                )
498
            )
499
        );
500
501
        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

501
        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...
502
            return $stmt->fetchAll(\PDO::FETCH_KEY_PAIR);
503
        }
504
        return array();
505
    }
506
507
    /**
508
     * Transform internal URLS to public URLS using our defined mappers
509
     *
510
     * @param string $contentType
511
     * @param string $mode
512
     * @param string $content
513
     * @param string[] $hosts
514
     * @return string
515
     */
516
    public function mapContent($contentType, $mode, $content, $hosts)
517
    {
518
        $rewriter = new Rewriter($this);
519
        $rewriter->setLocalDomains($hosts);
520
521
        foreach ($this->contentMappers as $mapper) {
522
            if ($mapper->supports($contentType)) {
523
                return $mapper->processAliasing($content, $mode, $rewriter);
524
            }
525
        }
526
527
        return $content;
528
    }
529
530
    /**
531
     * Add a new content mapper to our aliasing class
532
     *
533
     * @param UrlMapperInterface $mapper
534
     *
535
     * @return void
536
     */
537
    public function addMapper(UrlMapperInterface $mapper)
538
    {
539
        $this->contentMappers[] = $mapper;
540
    }
541
}
542