Passed
Push — master ( 5b6f7f...57f3ff )
by Melech
03:58
created

Processor::verifyRoute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 4
rs 10
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\Parameter;
19
use Valkyrja\Http\Routing\Data\Contract\Route;
20
use Valkyrja\Http\Routing\Exception\InvalidRoutePathException;
21
use Valkyrja\Http\Routing\Processor\Contract\Processor as Contract;
22
use Valkyrja\Http\Routing\Support\Helpers;
23
use Valkyrja\Orm\Data\EntityCast;
24
use Valkyrja\Orm\Entity\Contract\Entity;
25
26
/**
27
 * Class Processor.
28
 *
29
 * @author Melech Mizrachi
30
 */
31
class Processor implements Contract
32
{
33
    /**
34
     * Process a route.
35
     *
36
     * @param Route $route The route
37
     *
38
     * @throws InvalidRoutePathException
39
     *
40
     * @return Route
41
     */
42
    #[Override]
43
    public function route(Route $route): Route
44
    {
45
        // Set the path to the validated cleaned path (/some/path)
46
        $route = $route->withPath(Helpers::trimPath($route->getPath()));
47
48
        // If this is a dynamic route
49
        if (str_contains($route->getPath(), '{')) {
50
            $route = $this->modifyRegex($route);
51
        }
52
53
        return $route;
54
    }
55
56
    /**
57
     * Create the regex for a route.
58
     *
59
     * @param Route $route The route
60
     *
61
     * @throws InvalidRoutePathException
62
     *
63
     * @return Route
64
     */
65
    protected function modifyRegex(Route $route): Route
66
    {
67
        // If the regex has already been set then don't do anything
68
        if ($route->getRegex() !== null) {
69
            return $route;
70
        }
71
72
        // Replace all slashes with \/
73
        $regex = str_replace('/', Regex::PATH, $route->getPath());
74
75
        // Iterate through the route's parameters
76
        foreach ($route->getParameters() as $parameter) {
77
            // Validate the parameter
78
            $route     = $this->processParameterEntity($route, $parameter);
79
            $parameter = $this->processParameterInRegex($parameter, $regex);
80
81
            $regex = $this->replaceParameterNameInRegex($route, $parameter, $regex);
82
        }
83
84
        $regex = Regex::START . $regex . Regex::END;
85
86
        return $route->withRegex($regex);
87
    }
88
89
    /**
90
     * Validate the parameter entity.
91
     *
92
     * @param Route     $route     The route
93
     * @param Parameter $parameter The parameter
94
     *
95
     * @return Route
96
     */
97
    protected function processParameterEntity(Route $route, Parameter $parameter): Route
98
    {
99
        $cast   = $parameter->getCast();
100
        $entity = $cast->type ?? null;
101
102
        if ($entity !== null && is_a($entity, Entity::class, true)) {
103
            $route = $this->removeEntityFromDependencies($route, $entity);
104
105
            if (! $cast instanceof EntityCast) {
106
                return $route;
107
            }
108
109
            $entityColumn = $cast->column;
110
111
            if ($entityColumn !== null && ! property_exists($entity, $entityColumn)) {
112
                throw new InvalidRoutePathException('Entity column ' . $entityColumn . ' does not exist');
113
            }
114
        }
115
116
        return $route;
117
    }
118
119
    /**
120
     * Remove the entity from the route's dependencies list.
121
     *
122
     * @param Route                $route      The route
123
     * @param class-string<Entity> $entityName The entity class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<Entity> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<Entity>.
Loading history...
124
     *
125
     * @return Route
126
     */
127
    protected function removeEntityFromDependencies(Route $route, string $entityName): Route
128
    {
129
        $dispatch     = $route->getDispatch();
130
        $dependencies = $dispatch->getDependencies();
131
132
        if ($dependencies === null || $dependencies === []) {
133
            return $route;
134
        }
135
136
        $updatedDependencies = [];
137
138
        foreach ($dependencies as $dependency) {
139
            if ($dependency !== $entityName) {
140
                $updatedDependencies[] = $dependency;
141
            }
142
        }
143
144
        return $route->withDispatch($dispatch->withDependencies($updatedDependencies));
145
    }
146
147
    /**
148
     * Validate the parameter name exists in the regex.
149
     *
150
     * @param Parameter $parameter The parameter
151
     * @param string    $regex     The regex
152
     *
153
     * @return Parameter
154
     */
155
    protected function processParameterInRegex(Parameter $parameter, string $regex): Parameter
156
    {
157
        // If the parameter is optional or the name has a ? affixed to it
158
        if ($parameter->isOptional() || str_contains($regex, $parameter->getName() . '?')) {
159
            // Ensure the parameter is set to optional
160
            return $parameter->withIsOptional(true);
161
        }
162
163
        return $parameter;
164
    }
165
166
    /**
167
     * Replace the parameter name in the route's regex.
168
     *
169
     * @param Route     $route     The route
170
     * @param Parameter $parameter The parameter
171
     * @param string    $regex     The regex
172
     *
173
     * @throws InvalidRoutePathException
174
     *
175
     * @return string
176
     */
177
    protected function replaceParameterNameInRegex(Route $route, Parameter $parameter, string $regex): string
178
    {
179
        // Get whether this parameter is optional
180
        $isOptional = $parameter->isOptional();
181
182
        // Get the replacement for this parameter's name (something like {name} or {name?}
183
        // Prepend \/ if it optional so we can replace the path slash and set it in the
184
        // regex below as a non-capture-optional group
185
        $nameReplacement = ($isOptional ? Regex::PATH : '')
186
            . '{' . $parameter->getName() . ($isOptional ? '?' : '') . '}';
187
188
        // Check if the path doesn't contain the parameter's name replacement
189
        if (! str_contains($regex, $nameReplacement)) {
190
            throw new InvalidRoutePathException("{$route->getPath()} is missing $nameReplacement");
191
        }
192
193
        // If optional we don't want to capture the / before the value
194
        $parameterRegex = ($isOptional ? Regex::START_OPTIONAL_CAPTURE_GROUP : '')
195
            // Start the actual value's capture group
196
            . (! $parameter->shouldCapture() ? Regex::START_NON_CAPTURE_GROUP : Regex::START_CAPTURE_GROUP)
197
            // Set the parameter's regex to match the value
198
            . $parameter->getRegex()
199
            // End the capture group
200
            . ($isOptional ? Regex::END_OPTIONAL_CAPTURE_GROUP : Regex::END_CAPTURE_GROUP);
201
202
        // Replace the {name} or \/{name?} with the finished regex
203
        return str_replace($nameReplacement, $parameterRegex, $regex);
204
    }
205
}
206