Passed
Pull Request — release/4.x (#44)
by Erik
07:43 queued 03:55
created

Aliasing::getRepository()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
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 = [];
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 = [];
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
    /**
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
    public function hasPublicAlias($internalUrl, $asObject = false, $mode = UrlAlias::REWRITE)
145
    {
146
        $ret = null;
147
148
        if ($alias = $this->repository->findOneByInternalUrl($internalUrl, $mode)) {
149
            $ret = ($asObject ? $alias : $alias->getPublicUrl());
150
        }
151
152
        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 = ['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
    public function getRepository()
180
    {
181
        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
    public function addAlias(
208
        $publicUrl,
209
        $internalUrl,
210
        $type,
211
        $conflictingPublicUrlStrategy = self::STRATEGY_OVERWRITE,
212
        $conflictingInternalUrlStrategy = self::STRATEGY_IGNORE
213
    ) {
214
        self::validateInternalConflictingStrategy($conflictingInternalUrlStrategy);
215
        self::validatePublicConflictingStrategy($conflictingPublicUrlStrategy);
216
217
        $ret = false;
218
        /** @var $alias UrlAlias */
219
220
        // check for INTERNAL alias conflict
221
        $alias = $this->hasPublicAlias($internalUrl, true);
222
        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
                case self::STRATEGY_MOVE_PREVIOUS_TO_NEW:
225
                    if ($alias && ($publicUrl !== $alias->getPublicUrl())) {
226
                        // $alias will now become the old alias, and will act as a redirect
227
                        $alias->setMode(UrlAlias::MOVE);
228
                        $this->save($alias);
229
                    }
230
                    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
        $alias = $this->hasInternalAlias($publicUrl, true);
245
        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
            if ($internalUrl === $alias->getInternalUrl()) {
249
                if (UrlAlias::REWRITE === $alias->getMode()) {
250
                    // no need to do anything
251
                    $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
                    $rewriteAlias = $this->hasPublicAlias($internalUrl, true, UrlAlias::REWRITE);
256
                    if (null === $rewriteAlias) {
257
                        // we can reuse an existing alias.  The page will get exactly the url it wants
258
                        $alias->setMode(UrlAlias::REWRITE);
259
                        $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
                    $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
            if (!$ret) {
267
                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
            if (!$ret) {
282
                switch ($conflictingPublicUrlStrategy) {
283
                    case self::STRATEGY_OVERWRITE:
284
                        $alias->setInternalUrl($internalUrl);
285
                        $this->save($alias);
286
                        $ret = true;
287
                        break;
288
                    case self::STRATEGY_KEEP:
289
                        // do nothing intentionally
290
                        break;
291
                    case self::STRATEGY_SUFFIX:
292
                        $original = $publicUrl;
293
                        $i = 1;
294
                        do {
295
                            $publicUrl = $original . '-' . ($i++);
296
                        } while ($this->hasInternalAlias($publicUrl));
297
298
                        $alias = new UrlAlias($publicUrl, $internalUrl, $type);
299
                        $this->save($alias);
300
                        $ret = true;
301
                        break;
302
                    default:
303
                        // case is handled in the `validatePublicConflictingStrategy` guard at top of the function
304
                }
305
            }
306
        } else {
307
            $alias = new UrlAlias($publicUrl, $internalUrl, $type);
308
            $this->save($alias);
309
            $ret = true;
310
        }
311
        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
            // change the old alias
343
            $existingAlias->setPublicUrl($newPublicUrl);
344
            // create a new one
345
            $newAlias = new UrlAlias();
346
            $newAlias->setPublicUrl($publicUrl);
347
            $newAlias->setInternalUrl($newPublicUrl);
348
            $newAlias->setMode($type);
349
350
            $this->save($existingAlias);
351
            $this->save($newAlias);
352
            $moved = true;
353
        }
354
        return $moved;
355
    }
356
357
358
    /**
359
     * Set the batch to 'true' if aliases are being batch processed (optimization).
360
     *
361
     * This method returns a callback that needs to be executed after the batch is done; this is up to the caller.
362
     *
363
     * @param bool $isBatch
364
     * @return callable
365
     */
366
    public function setIsBatch($isBatch)
367
    {
368
        $this->batch = [];
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...
369
        $this->isBatch = $isBatch;
370
        $mgr = $this->manager;
371
        $self = $this;
372
        return function () use ($mgr, $self) {
373
            $mgr->flush();
374
            $self->setIsBatch(true);
375
        };
376
    }
377
378
379
    /**
380
     * Persist the URL alias.
381
     *
382
     * @param \Zicht\Bundle\UrlBundle\Entity\UrlAlias $alias
383
     * @return void
384
     */
385
    protected function save(UrlAlias $alias)
386
    {
387
        $this->manager->persist($alias);
388
389
        if ($this->isBatch) {
390
            $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...
391
        } else {
392
            $this->manager->flush($alias);
393
        }
394
    }
395
396
397
    /**
398
     * Compact redirects; i.e. optimize redirects:
399
     *
400
     * If /a points to /b, and /b points to /c, let /a point to /c
401
     *
402
     * @return void
403
     */
404
    public function compact()
405
    {
406
        foreach ($this->getRepository()->findAll() as $urlAlias) {
407
            if ($cascadingAlias = $this->hasPublicAlias($urlAlias->internal_url)) {
408
                $urlAlias->setInternalUrl($cascadingAlias->getInternalUrl());
409
            }
410
        }
411
    }
412
413
414
    /**
415
     * Remove alias
416
     *
417
     * @param string $internalUrl
418
     * @return void
419
     */
420
    public function removeAlias($internalUrl)
421
    {
422
        if ($alias = $this->hasPublicAlias($internalUrl, true)) {
423
            $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

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

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