Passed
Push — master ( 11dd35...5da5e2 )
by Melech
03:58
created

Processor::removeEntityFromDependencies()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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