Passed
Push — master ( 10bb7d...3ae379 )
by Mathias
01:51
created

HashidsValueResolver::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 0
c 1
b 0
f 0
nc 1
nop 4
dl 0
loc 6
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Roukmoute\HashidsBundle\ValueResolver;
6
7
use Hashids\HashidsInterface;
8
use Roukmoute\HashidsBundle\Attribute\Hashid;
9
use Symfony\Component\HttpFoundation\Request;
10
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
11
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
12
13
class HashidsValueResolver implements ValueResolverInterface
14
{
15
    public function __construct(
16
        private readonly HashidsInterface $hashids,
17
        private readonly bool $passthrough,
18
        private readonly bool $autoConvert,
19
        private readonly string $alphabet,
20
    ) {
21
    }
22
23
    /**
24
     * @return iterable<int>
25
     */
26
    public function resolve(Request $request, ArgumentMetadata $argument): iterable
27
    {
28
        $name = $argument->getName();
29
        $hashidAttribute = $this->getHashidAttribute($argument);
30
        $routeParameter = $hashidAttribute?->parameter ?? $name;
31
        [$hash, $isExplicit] = $this->getHash($request, $routeParameter, $hashidAttribute !== null);
32
33
        if ($this->isSkippable($hash)) {
34
            return [];
35
        }
36
37
        $hashids = $this->hashids->decode($hash);
38
39
        if ($this->hasHashidDecoded($hashids)) {
40
            /** @var int $decodedValue */
41
            $decodedValue = reset($hashids);
42
43
            if ($this->passthrough) {
44
                $request->attributes->set($name, $decodedValue);
45
46
                return [];
47
            }
48
49
            return [$decodedValue];
50
        }
51
52
        if ($isExplicit) {
53
            throw new \LogicException(sprintf('Unable to decode parameter "%s".', $name));
54
        }
55
56
        return [];
57
    }
58
59
    private function getHashidAttribute(ArgumentMetadata $argument): ?Hashid
60
    {
61
        /** @var Hashid[] $attributes */
62
        $attributes = $argument->getAttributes(Hashid::class, ArgumentMetadata::IS_INSTANCEOF);
63
64
        return $attributes[0] ?? null;
65
    }
66
67
    /**
68
     * @return array{0: string, 1: bool}
69
     */
70
    private function getHash(Request $request, string $name, bool $hasHashidAttribute): array
71
    {
72
        if (empty($name)) {
73
            return ['', false];
74
        }
75
76
        $hash = $request->attributes->get('_hash_' . $name);
77
        if (isset($hash) && is_string($hash)) {
78
            return [$hash, true];
79
        }
80
81
        if ($this->autoConvert || $hasHashidAttribute) {
82
            $hash = $request->attributes->get($name);
83
            if (is_string($hash)) {
84
                return [$hash, $hasHashidAttribute];
85
            }
86
        }
87
88
        $hash = $this->getHashFromAliases($request);
89
        if ($hash !== '') {
90
            return [$hash, true];
91
        }
92
93
        return ['', false];
94
    }
95
96
    private function getHashFromAliases(Request $request): string
97
    {
98
        $hash = '';
99
100
        if (!$request->attributes->has('hashids_prevent_alias')) {
101
            foreach (['hashid', 'id'] as $alias) {
102
                if ($request->attributes->has($alias)) {
103
                    $aliasAttribute = $request->attributes->get($alias);
104
                    if (!is_string($aliasAttribute)) {
105
                        continue;
106
                    }
107
                    $hash = $aliasAttribute;
108
                    $request->attributes->set('hashids_prevent_alias', true);
109
                    break;
110
                }
111
            }
112
        }
113
114
        return $hash;
115
    }
116
117
    private function isSkippable(string $hash): bool
118
    {
119
        return empty($hash) || !$this->allCharsAreInAlphabet($hash);
120
    }
121
122
    private function allCharsAreInAlphabet(string $hash): bool
123
    {
124
        return (bool) preg_match(sprintf('{^[%s]+$}', preg_quote($this->alphabet, '{')), $hash);
125
    }
126
127
    /**
128
     * @param array<int, ?int> $hashids
129
     */
130
    private function hasHashidDecoded(array $hashids): bool
131
    {
132
        return is_int(reset($hashids));
133
    }
134
}
135