dadajuice/zephyrus

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

Summary

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

use Zephyrus\Network\ContentType;
use Zephyrus\Network\HttpMethod;
use Zephyrus\Utilities\Cache;

class RouteRepository
{
    private const CACHE_ROUTE_KEY = 'router_repository';
    private const CACHE_UPDATE_KEY = 'router_repository_update_time';

    /**
     * Associative array that contains all defined routes. Routes are organized by HTTP method as main key, value is an
     * array of stdClass representing the route.
     *
     * @var array
     */
    private array $routes = [];

    /**
     * APCu php cache to keep a reference to all the defined routes.
     *
     * @var Cache
     */
    private Cache $cache;

    /**
     * APCu php cache to keep a reference to the last time the route were updated.
     *
     * @var Cache
     */
    private Cache $cacheUpdate;

    public function __construct()
    {
        $this->cache = new Cache(self::CACHE_ROUTE_KEY);
        $this->cacheUpdate = new Cache(self::CACHE_UPDATE_KEY);
    }

    /**
     * Verifies if the currently cached route definitions (if it exists) are outdated and need regeneration. Returns
     * true if the given time is newer than the cache creation time or if no cache exist.
     *
     * @param int $time
     * @return bool
     */
    public function isCacheOutdated(int $time): bool
    {
        if (!$this->cache->exists()) {
            return true;
        }
        $lastUpdate = $this->cacheUpdate->read() ?? 0;
        return $lastUpdate < $time;
    }

    /**
     * Saves the instance currently defined routes into the APCu PHP cache. To optimize futur calls, the method
     * initializeFromCache() should be called.
     *
     * @return void
     */
    public function cache(): void
    {
        $this->cache->cache($this->routes);
        $this->cacheUpdate->cache(time());
    }

    public function clear(): void
    {
        $this->cache->remove();
        $this->cacheUpdate->remove();
        $this->routes = [];
    }

    /**
     * Initializes the application route definitions from the APCu PHP cache and thus avoiding unnecessary looping
     * through the various project controllers.
     *
     * @return void
     */
    public function initializeFromCache(): void
    {
        $this->routes = $this->cache->read() ?? [];
    }

    /**
     * @param HttpMethod|null $method
     * @return RouteDefinition[]
     */
    public function getRoutes(?HttpMethod $method = null): array
    {
        if ($method) {
            if (array_key_exists($method->value, $this->routes)) {
                return $this->routes[$method->value];
            }
            return [];
        }
        return $this->routes;
    }

    /**
     * @param HttpMethod $method
     * @param string $requestedUri
     * @return RouteDefinition[]
     */
    public function findRoutes(HttpMethod $method, string $requestedUri): array
    {
        $routes = $this->getRoutes($method);
        $matchingRoutes = [];
        foreach ($routes as $route) {
            if ($route->matchUrl($requestedUri)) {
                $matchingRoutes[] = $route;
            }
        }
        if (!empty($matchingRoutes)) {
            usort($matchingRoutes, function (RouteDefinition $a, RouteDefinition $b) {
                return strcmp($a->getRoute(), $b->getRoute());
            });
        }
        return $matchingRoutes;
    }

    /**
     * Adds a new GET route for the application. The GET method must be used to represent a specific resource (or
     * collection) in some representational format (HTML, JSON, XML, ...). Normally, a GET request must only present
     * data and not alter them in any way.
     *
     * E.g. GET /books
     *      GET /book/{id}
     *
     * @param string $url
     * @param callable | array $callback
     * @param array $acceptedFormats
     * @param array $authorizationRules
     */
    public function get(string $url, callable|array $callback, array $acceptedFormats = [ContentType::ANY], array $authorizationRules = []): void
    {
        $route = new RouteDefinition($url);
        $route->setCallback($callback);
        $route->setAcceptedContentTypes($acceptedFormats);
        $route->setAuthorizationRules($authorizationRules);
        $this->addRoute(HttpMethod::GET, $route);
    }

    /**
     * Adds a new POST route for the application. The POST method must be used to create a new entry in a collection. It
     * is rarely used on a specific resource.
     *
     * E.g. POST /books
     *
     * @param string $url
     * @param callable | array $callback
     * @param array $acceptedFormats
     * @param array $authorizationRules
     */
    public function post(string $url, callable|array $callback, array $acceptedFormats = [ContentType::ANY], array $authorizationRules = []): void
    {
        $route = new RouteDefinition($url);
        $route->setCallback($callback);
        $route->setAcceptedContentTypes($acceptedFormats);
        $route->setAuthorizationRules($authorizationRules);
        $this->addRoute(HttpMethod::POST, $route);
    }

    /**
     * Adds a new PUT route for the application. The PUT method must be used to update a specific resource or
     * collection and must be considered idempotent.
     *
     * E.g. PUT /book/{id}
     *
     * @param string $url
     * @param callable | array $callback
     * @param array $acceptedFormats
     * @param array $authorizationRules
     */
    public function put(string $url, callable|array $callback, array $acceptedFormats = [ContentType::ANY], array $authorizationRules = []): void
    {
        $route = new RouteDefinition($url);
        $route->setCallback($callback);
        $route->setAcceptedContentTypes($acceptedFormats);
        $route->setAuthorizationRules($authorizationRules);
        $this->addRoute(HttpMethod::PUT, $route);
    }

    /**
     * Adds a new PATCH route for the application. The PATCH method must be used to update a specific resource or
     * collection and must be considered idempotent. Should be used instead of PUT when it is possible to update only
     * given fields to update and not the entire resource.
     *
     * E.g. PATCH /book/{id}
     *
     * @param string $url
     * @param callable | array $callback
     * @param array $acceptedFormats
     * @param array $authorizationRules
     */
    public function patch(string $url, callable|array $callback, array $acceptedFormats = [ContentType::ANY], array $authorizationRules = []): void
    {
        $route = new RouteDefinition($url);
        $route->setCallback($callback);
        $route->setAcceptedContentTypes($acceptedFormats);
        $route->setAuthorizationRules($authorizationRules);
        $this->addRoute(HttpMethod::PATCH, $route);
    }

    /**
     * Adds a new DELETE route for the application. The DELETE method must be used only to delete a specific resource or
     * collection and must be considered idempotent.
     *
     * E.g. DELETE /book/{id}
     *      DELETE /books
     *
     * @param string $url
     * @param callable | array $callback
     * @param array $acceptedFormats
     * @param array $authorizationRules
     */
    public function delete(string $url, callable|array $callback, array $acceptedFormats = [ContentType::ANY], array $authorizationRules = []): void
    {
        $route = new RouteDefinition($url);
        $route->setCallback($callback);
        $route->setAcceptedContentTypes($acceptedFormats);
        $route->setAuthorizationRules($authorizationRules);
        $this->addRoute(HttpMethod::DELETE, $route);
    }

    /**
     * Adds a new route for the application. Make sure to create the adequate structure with corresponding parameters
     * regex pattern if needed. If the callback comes from a controller, it must be given as an array form [instance,
     * 'methodName'].
     *
     * @param HttpMethod $method
     * @param RouteDefinition $route
     */
    public function addRoute(HttpMethod $method, RouteDefinition $route): void
    {
        $this->routes[$method->value][] = $route;
    }
}