Processor::replaceParameterNameInRegex()   B
last analyzed

Complexity

Conditions 8
Paths 68

Size

Total Lines 29
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 29
rs 8.4444
c 0
b 0
f 0
cc 8
nc 68
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Http\Routing\Processor;
15
16
use Override;
0 ignored issues
show
Bug introduced by
The type Override was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Valkyrja\Http\Routing\Constant\Regex;
0 ignored issues
show
Bug introduced by
The type Valkyrja\Http\Routing\Constant\Regex was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use Valkyrja\Http\Routing\Data\Contract\ParameterContract;
19
use Valkyrja\Http\Routing\Data\Contract\RouteContract;
20
use Valkyrja\Http\Routing\Processor\Contract\ProcessorContract as Contract;
21
use Valkyrja\Http\Routing\Support\Helpers;
22
use Valkyrja\Http\Routing\Throwable\Exception\InvalidRoutePathException;
23
24
class Processor implements Contract
25
{
26
    /**
27
     * Process a route.
28
     *
29
     * @param RouteContract $route The route
30
     *
31
     * @throws InvalidRoutePathException
32
     *
33
     * @return RouteContract
34
     */
35
    #[Override]
36
    public function route(RouteContract $route): RouteContract
37
    {
38
        // Set the path to the validated cleaned path (/some/path)
39
        $route = $route->withPath(Helpers::trimPath($route->getPath()));
40
41
        // If this is a dynamic route
42
        if (str_contains($route->getPath(), '{')) {
43
            $route = $this->modifyRegex($route);
44
        }
45
46
        return $route;
47
    }
48
49
    /**
50
     * Create the regex for a route.
51
     *
52
     * @param RouteContract $route The route
53
     *
54
     * @throws InvalidRoutePathException
55
     *
56
     * @return RouteContract
57
     */
58
    protected function modifyRegex(RouteContract $route): RouteContract
59
    {
60
        // If the regex has already been set then don't do anything
61
        if ($route->getRegex() !== null) {
62
            return $route;
63
        }
64
65
        // Replace all slashes with \/
66
        $regex = str_replace('/', Regex::PATH, $route->getPath());
67
68
        // Iterate through the route's parameters
69
        foreach ($route->getParameters() as $parameter) {
70
            // Validate the parameter
71
            $parameter = $this->processParameterInRegex($parameter, $regex);
72
73
            $regex = $this->replaceParameterNameInRegex($route, $parameter, $regex);
74
        }
75
76
        $regex = Regex::START . $regex . Regex::END;
77
78
        return $route->withRegex($regex);
79
    }
80
81
    /**
82
     * Validate the parameter name exists in the regex.
83
     *
84
     * @param ParameterContract $parameter The parameter
85
     * @param string            $regex     The regex
86
     *
87
     * @return ParameterContract
88
     */
89
    protected function processParameterInRegex(ParameterContract $parameter, string $regex): ParameterContract
90
    {
91
        // If the parameter is optional or the name has a ? affixed to it
92
        if ($parameter->isOptional() || str_contains($regex, $parameter->getName() . '?')) {
93
            // Ensure the parameter is set to optional
94
            return $parameter->withIsOptional(true);
95
        }
96
97
        return $parameter;
98
    }
99
100
    /**
101
     * Replace the parameter name in the route's regex.
102
     *
103
     * @param RouteContract     $route     The route
104
     * @param ParameterContract $parameter The parameter
105
     * @param string            $regex     The regex
106
     *
107
     * @throws InvalidRoutePathException
108
     *
109
     * @return string
110
     */
111
    protected function replaceParameterNameInRegex(RouteContract $route, ParameterContract $parameter, string $regex): string
112
    {
113
        // Get whether this parameter is optional
114
        $isOptional = $parameter->isOptional();
115
116
        // Get the replacement for this parameter's name (something like {name} or {name?}
117
        // Prepend \/ if it optional so we can replace the path slash and set it in the
118
        // regex below as a non-capture-optional group
119
        $nameReplacement = ($isOptional ? Regex::PATH : '')
120
            . '{' . $parameter->getName() . ($isOptional ? '?' : '') . '}';
121
122
        // Check if the path doesn't contain the parameter's name replacement
123
        if (! str_contains($regex, $nameReplacement)) {
124
            throw new InvalidRoutePathException("{$route->getPath()} is missing $nameReplacement");
125
        }
126
127
        // If optional we don't want to capture the / before the value
128
        $parameterRegex = ($isOptional ? Regex::START_OPTIONAL_CAPTURE_GROUP : '')
129
            // Start the actual value's capture group
130
            . (! $parameter->shouldCapture() ? Regex::START_NON_CAPTURE_GROUP : Regex::START_CAPTURE_GROUP)
131
            // Add the parameter name only for a capture group (non capture groups won't be captured so we don't need them to be attributed to a param name)
132
            . ($parameter->shouldCapture() ? Regex::START_CAPTURE_GROUP_NAME . $parameter->getName() . Regex::END_CAPTURE_GROUP_NAME : '')
133
            // Set the parameter's regex to match the value
134
            . $parameter->getRegex()
135
            // End the capture group
136
            . ($isOptional ? Regex::END_OPTIONAL_CAPTURE_GROUP : Regex::END_CAPTURE_GROUP);
137
138
        // Replace the {name} or \/{name?} with the finished regex
139
        return str_replace($nameReplacement, $parameterRegex, $regex);
140
    }
141
}
142