dadajuice/zephyrus

View on GitHub
src/Zephyrus/Network/Router/RouteDefinition.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php namespace Zephyrus\Network\Router;

use InvalidArgumentException;
use Zephyrus\Application\Callback;
use Zephyrus\Application\Controller;
use Zephyrus\Network\ContentType;
use Zephyrus\Network\Request;

class RouteDefinition
{
    private string $rawRouteRoot = "";
    private string $routeRoot = "";
    private string $route;
    private ?string $controllerClass = null;
    private ?string $controllerMethod = null;
    private mixed $callback = null;
    private array $acceptedContentTypes = [ContentType::ANY];
    private string $regexPattern;
    private array $argumentDefinitions; // List of URL arguments (e.g. id)
    private array $arguments = []; // List of key - value for the loaded arguments (e.g. id => 4)
    private array $authorizationRules = [];
    private bool $strictRuleInterpretation = false;

    /**
     * Initiates a route definition from the given URL (which can include arguments, e.g. /products/{id}).
     *
     * @param string $route
     */
    public function __construct(string $route, string $routeRoot = "")
    {
        $this->rawRouteRoot = ($routeRoot == "/") ? $routeRoot : rtrim($routeRoot, "/");
        $this->route = ($route == "/") ? $route : rtrim($route, "/");
        $this->initializeUrlArguments();
        $this->initializeRegexPattern();
    }

    /**
     * Verifies if the given url matches the route definition with arguments matching is any.
     *
     * @param string $url
     * @return bool
     */
    public function matchUrl(string $url): bool
    {
        if ($this->route == $url) {
            return true;
        }
        $pattern = '/^' . $this->regexPattern . '$/';
        return preg_match($pattern, $url);
    }

    /**
     * Determines if the given request is acceptable for the route.
     *
     * @param Request $request
     * @return bool
     */
    public function isAccepted(Request $request): bool
    {
        $acceptedContentTypes = $request->getAccept()->getAcceptedContentTypes();
        foreach ($this->acceptedContentTypes as $format) {
            if ($format == ContentType::ANY || in_array($format, $acceptedContentTypes)) {
                return true;
            }
        }
        return false;
    }

    public function getRouteRoot(): string
    {
        return $this->routeRoot;
    }

    public function getRawRouteRoot(): string
    {
        return $this->rawRouteRoot;
    }

    /**
     * Applies the callback to launch when the route is called.
     *
     * @param array|callable $callback
     * @return void
     */
    public function setCallback(array|callable $callback): void
    {
        if (is_array($callback)) {
            // TODO: Make sure is controller ... ?
            $this->controllerClass = $callback[0];
            $this->controllerMethod = $callback[1];
        } else {
            $this->callback = $callback;
        }
    }

    /**
     * Applies the set of rules to test if the route is authorized to be executed. Rules are defined in the
     * AuthorizationRepository class.
     *
     * @param array $rules
     * @param bool $strictInterpretation
     */
    public function setAuthorizationRules(array $rules, bool $strictInterpretation = false): void
    {
        $this->authorizationRules = $rules;
        $this->strictRuleInterpretation = $strictInterpretation;
    }

    public function getAuthorizationRules(): array
    {
        return $this->authorizationRules;
    }

    public function isStrictRuleInterpretation(): bool
    {
        return $this->strictRuleInterpretation;
    }

    public function hasCallback(): bool
    {
        return !is_null($this->callback);
    }

    public function getControllerInstance(): Controller
    {
        return new $this->controllerClass();
    }

    public function getCallback(?Controller $controllerInstance = null): Callback
    {
        return (is_null($controllerInstance)) ?
            new Callback($this->callback) :
            new Callback([$controllerInstance, $this->controllerMethod]);
    }

    public function extractArgumentsFromUrl(string $requestedUrl): void
    {
        $values = [];
        $pattern = '/^' . $this->regexPattern . '$/';
        preg_match_all($pattern, $requestedUrl, $matches);

        $matchCount = count($matches);
        for ($i = 1; $i < $matchCount; ++$i) {
            $values[] = $matches[$i][0];
        }

        $argumentValues = [];
        $i = 0;
        foreach ($this->argumentDefinitions as $argument) {
            $argumentValues[$argument] = $values[$i];
            ++$i;
        }
        $this->arguments = $argumentValues;

        if (!empty($this->rawRouteRoot)) {
            $this->routeRoot = $this->rawRouteRoot;
            foreach ($this->argumentDefinitions as $argumentName) {
                $this->routeRoot = localize($this->routeRoot, [$argumentName => $this->arguments[$argumentName]]);
            }
        }
    }

    public function getArgumentValues(): array
    {
        return array_values($this->arguments);
    }

    public function getArgumentNames(): array
    {
        return array_keys($this->arguments);
    }

    public function getArguments(): array
    {
        return $this->arguments;
    }

    public function getArgument(string $name, mixed $defaultValue = null): mixed
    {
        return $this->arguments[$name] ?? $defaultValue;
    }

    public function setArgument(string $name, mixed $value): void
    {
        $this->arguments[$name] = $value;
    }

    /**
     * Applies the supported accepted content type for the route response.
     *
     * @param array $contentTypes
     * @return void
     */
    public function setAcceptedContentTypes(array $contentTypes): void
    {
        $this->acceptedContentTypes = $contentTypes;
    }

    public function getRoute(): string
    {
        return $this->route;
    }

    /**
     * Retrieves all arguments from the specified $url. A valid parameter is defined inside braces (e.g. {id}). Keeps
     * the parameters ordinal order. Accept every character for parameter except "/". Each argument must be unique, an
     * InvalidArgumentException is thrown otherwise.
     */
    private function initializeUrlArguments(): void
    {
        $arguments = [];
        $pattern = '/\{([^\/]+)\}/';
        if (preg_match_all($pattern, $this->route, $results) > 0) {
            foreach ($results[1] as $result) {
                $arguments[] = $result;
            }
        }
        if (!empty($arguments) && count($arguments) != count(array_unique($arguments))) {
            throw new InvalidArgumentException("Route [$this->route] cannot be added since you have at
                                                 least one duplicate parameter. Each parameter must 
                                                 have a unique identifier.");
        }
        $this->argumentDefinitions = $arguments;
    }

    /**
     * Retrieve a regex pattern matching each parameter specified within arguments inside the provided $url. A valid
     * parameter is defined inside braces (e.g. {id}).
     */
    private function initializeRegexPattern(): void
    {
        $regex = str_replace('/', '\/', $this->route);
        foreach ($this->argumentDefinitions as $argument) {
            $regex = str_replace('{' . $argument . '}', '([^\/]+)', $regex);
        }
        $this->regexPattern = $regex;
    }
}