• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valkyrjaio / valkyrja / 16211621473

11 Jul 2025 04:12AM UTC coverage: 43.861% (+0.6%) from 43.306%
16211621473

push

github

MelechMizrachi
Http Routing: Require all routing dispatches to return Response objects.

16 of 22 new or added lines in 6 files covered. (72.73%)

102 existing lines in 3 files now uncovered.

3933 of 8967 relevant lines covered (43.86%)

11.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Valkyrja/Http/Routing/Processor/Processor.php
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 <melechmizrachi@gmail.com>
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;
18
use Valkyrja\Http\Routing\Constant\Regex;
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
NEW
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
158
     *
159
     * @return Route
160
     */
161
    protected function removeEntityFromDependencies(Route $route, string $entityName): Route
162
    {
NEW
163
        $dispatch     = $route->getDispatch();
×
UNCOV
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
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc