Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 85 additions & 22 deletions src/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ class Router implements RouterInterface

private array $reflectionCache = [];

/**
* Performance optimization caches
*/
private bool $isMethodSpoofed = false; // Cache spoofing - run only once per request
private ?array $cachedUriSegments = null; // Cache parsed URI segments
private array $patternCache = []; // Cache regex validation results
private $cachedRequest = null; // Cache request object
private $cachedServer = null; // Cache server object

/**
* Sets the route, parsing HTTP methods (if multiple are specified via |).
* Registers a route handler for each method.
Expand All @@ -47,8 +56,7 @@ public function set(array $route): void
*/
private function handleRequestUri(array $route): void
{
$request = $this->rudra->request();
$server = $request->server();
$server = $this->getServer();

$this->spoofRequestMethod();

Expand All @@ -58,34 +66,77 @@ private function handleRequestUri(array $route): void

$this->parseRequestBody();

$uriRaw = $server->get('REQUEST_URI');
$parsed = parse_url($uriRaw);
$requestPath = $parsed && isset($parsed['path']) ? ltrim($parsed['path'], '/') : '';
$uriSegments = explode('/', $requestPath);
$uriSegments = $this->getRequestUriSegments();
[$uri, $params] = $this->handlePattern($route, $uriSegments);

if ($uri === $uriSegments) {
$this->setCallable($route, $params);
}
}

/**
* Returns cached request object to avoid repeated container calls
*/
private function getRequest()
{
return $this->cachedRequest ??= $this->rudra->request();
}

/**
* Returns cached server object to avoid repeated method calls
*/
private function getServer()
{
return $this->cachedServer ??= $this->getRequest()->server();
}

/**
* Returns cached URI segments - parsed only once per request
*/
private function getRequestUriSegments(): array
{
if ($this->cachedUriSegments !== null) {
return $this->cachedUriSegments;
}

$uriRaw = $this->getServer()->get('REQUEST_URI');
$parsed = parse_url($uriRaw);
$requestPath = $parsed && isset($parsed['path']) ? ltrim($parsed['path'], '/') : '';

return $this->cachedUriSegments = explode('/', $requestPath);
}

/**
* Spoofs the request method via _method parameter in POST requests.
* Cached to run only once per request.
*/
private function spoofRequestMethod(): void
{
$request = $this->rudra->request();
$requestMethod = $request->server()->get('REQUEST_METHOD');
if ($this->isMethodSpoofed) {
return;
}

$request = $this->getRequest();
$server = $this->getServer();
$requestMethod = $server->get('REQUEST_METHOD');

if ($requestMethod === 'POST' && $request->post()->has('_method')) {
$spoofedMethod = strtoupper($request->post()->get('_method'));
if (in_array($spoofedMethod, ['PUT', 'PATCH', 'DELETE'])) {
$request->server()->set(['REQUEST_METHOD' => $spoofedMethod]);
$server->set(['REQUEST_METHOD' => $spoofedMethod]);
}
}

$this->isMethodSpoofed = true;
}

/**
* Parses request body for PUT, PATCH, DELETE methods
*/
private function parseRequestBody(): void
{
$request = $this->rudra->request();
$requestMethod = $request->server()->get('REQUEST_METHOD');
$request = $this->getRequest();
$requestMethod = $this->getServer()->get('REQUEST_METHOD');

if (in_array($requestMethod, ['PUT', 'PATCH', 'DELETE'])) {
$rawInput = file_get_contents('php://input');
Expand All @@ -94,6 +145,18 @@ private function parseRequestBody(): void
}
}

/**
* Checks if the pattern is a simple parameter (cached)
*/
private function isSimpleParam(string $pattern): bool
{
$cacheKey = 'simple_' . $pattern;
if (!isset($this->patternCache[$cacheKey])) {
$this->patternCache[$cacheKey] = (bool) preg_match('/^[a-zA-Z0-9_-]+$/', $pattern);
}
return $this->patternCache[$cacheKey];
}

/**
* Matches the URI from the route with the actual request, processing parameters of the form :param and :regexp.
* This method is used to extract dynamic segments from a URI pattern
Expand All @@ -102,18 +165,18 @@ private function handlePattern(array $route, array $request): array
{
$uri = [];
$params = null;

foreach (explode('/', ltrim($route['url'], '/')) as $i => $segment) {
if (!str_starts_with($segment, ':')) {
$uri[] = $segment;
continue;
}

$pattern = substr($segment, 1);
$hasSegment = array_key_exists($i, $request);
// Simple parameter like :id, :page
if (preg_match('/^[a-zA-Z0-9_-]+$/', $pattern)) {

// Simple parameter like :id, :page (cached check)
if ($this->isSimpleParam($pattern)) {
if ($hasSegment) {
$uri[] = $request[$i];
$params[] = $request[$i];
Expand All @@ -122,7 +185,7 @@ private function handlePattern(array $route, array $request): array
}
continue;
}

// Regex parameter like :[\d]+ or :[a-z]{1,3}
if ($hasSegment && @preg_match("/^$pattern$/", $request[$i]) === 1) {
$uri[] = $request[$i];
Expand Down Expand Up @@ -160,7 +223,7 @@ private function setCallable(array $route, ?array $params): void
/**
* Calls the controller and its method directly, performing the full lifecycle:
* This method is used to fully dispatch a route after matching it with the current request.
*
*
* @throws RouterException
*/
#[\Override]
Expand Down Expand Up @@ -203,7 +266,7 @@ public function directCall(array $route, ?array $params = null): void
*
* This method is typically used when the zend.exception_ignore_args setting is enabled,
* allowing for more flexible and type-safe dependency resolution.
*
*
* @throws RouterException
*/
private function callActionThroughReflection(?array $params, string $action, object $controller): void
Expand All @@ -230,13 +293,13 @@ private function callActionThroughReflection(?array $params, string $action, obj
*
* If the argument type or number does not match — tries to automatically inject required dependencies.
* This is a fallback mechanism for cases where Reflection-based injection is disabled or unavailable.
*
*
* Handles two types of errors during invocation:
* - \ArgumentCountError — thrown when the number of arguments doesn't match the method signature.
* - \TypeError — thrown when an argument is not compatible with the expected type.
*
* In both cases, Rudra's autowire system attempts to resolve and inject the correct dependencies.
*
*
* @throws RouterException
* @throws \TypeError
* @throws \ArgumentCountError
Expand Down Expand Up @@ -271,7 +334,7 @@ private function callActionThroughException(?array $params, string $action, obje
* - ['MiddlewareClass', $parameter] (array with class and parameter) — passes the parameter to the middleware.
*
* Each middleware must implement the __invoke() method to be callable.
*
*
* @throws \Rudra\Router\Exceptions\MiddlewareException
*/
public function handleMiddleware(array $chain): void
Expand Down
Loading