Skip to content

Commit

Permalink
Endpoints
Browse files Browse the repository at this point in the history
Endpoints added + Code improvements
joanrodas authored Apr 12, 2022
2 parents 1080734 + 2463c37 commit 5183a1f
Showing 13 changed files with 452 additions and 43 deletions.
22 changes: 22 additions & 0 deletions PluboRoutes/Endpoint/DeleteEndpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
namespace PluboRoutes\Endpoint;

/**
* An endpoint with DELETE method.
*
*/
final class DeleteEndpoint extends Endpoint
{
/**
* Constructor.
*
* @param string $namespace
* @param string $path
* @param callable $config
*/
public function __construct(string $namespace, string $path, callable $config, callable $permission_callback = null)
{
parent::__construct($namespace, $path, $config, $permission_callback);
$this->method = \WP_REST_Server::DELETABLE;
}
}
133 changes: 133 additions & 0 deletions PluboRoutes/Endpoint/Endpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php
namespace PluboRoutes\Endpoint;

/**
* An Endpoint describes a route and its parameters.
*
*/
abstract class Endpoint implements EndpointInterface
{
/**
* The endpoint namespace.
*
* @var string
*/
private $namespace;

/**
* The URL path that the endpoint needs to match.
*
* @var string
*/
private $path;

/**
* The configuration of the endpoint.
*
* @var array
*/
private $config;

/**
* The permission callback of the endpoint.
*
* @var array
*/
private $permission_callback;

/**
* The method of the endpoint.
*
* @var string
*/
private $method;

/**
* Constructor.
*
* @param string $namespace
* @param string $path
* @param callable $config
* @param string $method
*/
public function __construct(string $namespace, string $path, callable $config, callable $permission_callback = null)
{
$this->namespace = $namespace;
$this->path = $path;
$this->config = $config;
$this->permission_callback = $permission_callback ?? '__return_true';
$this->args = array();
}

/**
* Get the namespace of the endpoint.
*
* @return string
*/
public function getNamespace()
{
return $this->namespace;
}

/**
* Get the path to be matched.
*
* @return string
*/
public function getPath()
{
return $this->path;
}

/**
* Get the config parameters of the endpoint.
*
* @return array
*/
public function getConfig()
{
return $this->config;
}

/**
* Get the method of the endpoint.
*
* @return string
*/
public function getMethod()
{
return $this->method;
}

/**
* Get the endpoint permission callback.
*
* @return callable
*/
public function getPermissionCallback()
{
return $this->permission_callback;
}

/**
* Serialize the endpoint.
*
* @return string
*/
public function serialize()
{
return serialize(array($this->path, $this->method));
}

/**
* Unserialize the endpoint.
*
* @param array
*/
public function unserialize($data)
{
$data = unserialize($data);
$this->path = $data['path'];
$this->method = $data['method'];
}
}
15 changes: 15 additions & 0 deletions PluboRoutes/Endpoint/EndpointInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
namespace PluboRoutes\Endpoint;

/**
* Route Interface.
*
*/
interface EndpointInterface extends \Serializable
{
public function getNamespace();
public function getPath();
public function getConfig();
public function getPermissionCallback();
public function getMethod();
}
22 changes: 22 additions & 0 deletions PluboRoutes/Endpoint/GetEndpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
namespace PluboRoutes\Endpoint;

/**
* An Endpoint with GET method.
*
*/
final class GetEndpoint extends Endpoint
{
/**
* Constructor.
*
* @param string $namespace
* @param string $path
* @param callable $config
*/
public function __construct(string $namespace, string $path, callable $config, callable $permission_callback = null)
{
parent::__construct($namespace, $path, $config, $permission_callback);
$this->method = \WP_REST_Server::READABLE;
}
}
22 changes: 22 additions & 0 deletions PluboRoutes/Endpoint/PostEndpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
namespace PluboRoutes\Endpoint;

/**
* An Endpoint with POST method.
*
*/
final class PostEndpoint extends Endpoint
{
/**
* Constructor.
*
* @param string $namespace
* @param string $path
* @param callable $config
*/
public function __construct(string $namespace, string $path, callable $config, callable $permission_callback = null)
{
parent::__construct($namespace, $path, $config, $permission_callback);
$this->method = \WP_REST_Server::CREATABLE;
}
}
22 changes: 22 additions & 0 deletions PluboRoutes/Endpoint/PutEndpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
namespace PluboRoutes\Endpoint;

/**
* An Endpoint with PUT method.
*
*/
final class PutEndpoint extends Endpoint
{
/**
* Constructor.
*
* @param string $namespace
* @param string $path
* @param callable $config
*/
public function __construct(string $namespace, string $path, callable $config, callable $permission_callback = null)
{
parent::__construct($namespace, $path, $config, $permission_callback);
$this->method = 'PUT';
}
}
58 changes: 42 additions & 16 deletions PluboRoutes/Helpers/RegexHelper.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
namespace PluboRoutes\Helpers;

class RegexHelper
abstract class RegexHelper
{
const DIGIT = '([0-9])';
const NUMBER = '([0-9]+)';
@@ -15,21 +15,47 @@ class RegexHelper
const SLUG = '([a-z0-9-]+)';
const EMAIL = '([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}';

public static function getRegex($type)
const AVAILABLE_REGEX = array(
'number' => self::NUMBER,
'word' => self::WORD,
'date' => self::DATE,
'slug' => self::SLUG,
'digit' => self::DIGIT,
'year' => self::YEAR,
'month' => self::MONTH,
'day' => self::DAY,
'jwt' => self::JWT,
'email' => self::EMAIL,
'ip' => self::IP
);

/**
* Get translated Regex path for an endpoint route.
*
* @param string $path
*/
public static function getRegexMatches(string $regex_path)
{
$available_regex = array(
'number' => self::NUMBER,
'word' => self::WORD,
'date' => self::DATE,
'slug' => self::SLUG,
'digit' => self::DIGIT,
'year' => self::YEAR,
'month' => self::MONTH,
'day' => self::DAY,
'jwt' => self::JWT,
'email' => self::EMAIL,
'ip' => self::IP
);
return array_key_exists($type, $available_regex) ? $available_regex[$type] : $type;
preg_match_all('#\{(.+?)\}#', $regex_path, $matches);
return $matches;
}

/**
* Return trimmed path.
*
* @param string $path
* @return string
*/
public static function cleanPath(string $path)
{
return ltrim(trim($path), '/');
}

/**
* Return regex of path.
*
* @param mixed $type
* @return string
*/
abstract public static function getRegex($type): string;
}
11 changes: 11 additions & 0 deletions PluboRoutes/Helpers/RegexHelperEndpoints.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
namespace PluboRoutes\Helpers;

class RegexHelperEndpoints extends RegexHelper
{
public static function getRegex($type): string
{
$regex_code = array_key_exists($type[1], self::AVAILABLE_REGEX) ? self::AVAILABLE_REGEX[$type[1]] : $type[1];
return "(?P<$type[0]>$regex_code)";
}
}
10 changes: 10 additions & 0 deletions PluboRoutes/Helpers/RegexHelperRoutes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
namespace PluboRoutes\Helpers;

class RegexHelperRoutes extends RegexHelper
{
public static function getRegex($type): string
{
return array_key_exists($type, self::AVAILABLE_REGEX) ? self::AVAILABLE_REGEX[$type] : $type;
}
}
54 changes: 45 additions & 9 deletions PluboRoutes/PluboRoutesProcessor.php
Original file line number Diff line number Diff line change
@@ -57,8 +57,10 @@ public static function init()
$self = new self(new Router());
add_action('init', array($self, 'addRoutes'));
add_action('parse_request', array($self, 'matchRouteRequest'));
add_action('rest_api_init', array($self, 'addEndpoints'));
add_action('template_redirect', array($self, 'doRouteActions'));
add_action('template_include', array($self, 'includeRouteTemplate'));
add_filter('body_class', array($self, 'addBodyClasses'));
}

/**
@@ -70,27 +72,48 @@ public function addRoutes()
foreach ($routes as $route) {
$this->router->addRoute($route);
}
$this->router->compile();
$routes_hash = md5(serialize($routes));
if ($routes_hash != get_option('plubo-routes-hash')) {
$this->router->compileRoutes();
$this->maybeFlushRewriteRules($routes, 'plubo-routes-hash');
}

/**
* Step 1 alt: Register all our endpoints into WordPress. Flush rewrite rules if the endpoints changed.
*/
public function addEndpoints()
{
$endpoints = apply_filters('plubo/endpoints', array());
foreach ($endpoints as $endpoint) {
$this->router->addEndpoint($endpoint);
}
$this->router->compileEndpoints();
$this->maybeFlushRewriteRules($endpoints, 'plubo-endpoints-hash');
}

/**
* Flush if needed.
*/
public function maybeFlushRewriteRules(array $values, string $option_name)
{
$hash = md5(serialize($values));
if ($hash != get_option($option_name)) {
flush_rewrite_rules();
update_option('plubo-routes-hash', $routes_hash);
update_option($option_name, $hash);
}
}

/**
* Step 2: Attempts to match the current request to an added route.
*
* @param WP $wp
* @param WP $env
*/
public function matchRouteRequest(\WP $wp)
public function matchRouteRequest(\WP $env)
{
$found_route = $this->router->match($wp->query_vars);
$found_route = $this->router->match($env->query_vars);
if ($found_route instanceof RouteInterface) {
$found_args = array();
$args_names = $found_route->getArgs();
foreach ($args_names as $arg_name) {
$found_args[$arg_name] = $wp->query_vars[$arg_name] ?? false;
$found_args[$arg_name] = $env->query_vars[$arg_name] ?? false;
}
$this->matched_route = $found_route;
$this->matched_args = $found_args;
@@ -160,8 +183,21 @@ public function includeRouteTemplate($template)
$template_func = $this->matched_route->getTemplate();
$template = call_user_func($template_func, $this->matched_args);
} else {
$template = apply_filters('plubo/template', locate_template($this->matched_route->getTemplate()));
$template = locate_template(apply_filters('plubo/template', $this->matched_route->getTemplate()));
}
return $template;
}

/**
* Filter: If a route was found, add name as body tag.
*/
public function addBodyClasses($classes)
{
if ($this->matched_route instanceof Route) {
$route_name = $this->matched_route->getName();
$classes["route-$route_name"];
$classes = apply_filters('plubo/body_classes', $classes, $route_name, $this->matched_args);
}
return $classes;
}
}
10 changes: 10 additions & 0 deletions PluboRoutes/Route/RouteTrait.php
Original file line number Diff line number Diff line change
@@ -69,11 +69,21 @@ public function getArgs()
return $this->args;
}

/**
* Serialize the route.
*
* @return string
*/
public function serialize()
{
return serialize(array($this->path, $this->args));
}

/**
* Unserialize the route.
*
* @param array
*/
public function unserialize($data)
{
$data = unserialize($data);
104 changes: 86 additions & 18 deletions PluboRoutes/Router.php
Original file line number Diff line number Diff line change
@@ -2,7 +2,9 @@
namespace PluboRoutes;

use PluboRoutes\Route\RouteInterface;
use PluboRoutes\Helpers\RegexHelper;
use PluboRoutes\Endpoint\EndpointInterface;
use PluboRoutes\Helpers\RegexHelperRoutes;
use PluboRoutes\Helpers\RegexHelperEndpoints;

/**
* The Router manages routes using the WordPress rewrite API.
@@ -13,10 +15,17 @@ class Router
/**
* All registered routes.
*
* @var Route[]
* @var RouteInterface[]
*/
private $routes;

/**
* All registered endpoints.
*
* @var EndpointInterface[]
*/
private $endpoints;

/**
* Query variable used to identify routes.
*
@@ -31,6 +40,7 @@ class Router
public function __construct()
{
$this->routes = array();
$this->endpoints = array();
$this->route_variable = apply_filters('plubo/route_variable', 'route_name');
}

@@ -44,17 +54,42 @@ public function addRoute(RouteInterface $route)
$this->routes[] = $route;
}

/**
* Add an endpoint to the router.
*
* @param EndpointInterface $route
*/
public function addEndpoint(EndpointInterface $endpoint)
{
$this->endpoints[] = $endpoint;
}

/**
* Compiles the router into WordPress rewrite rules.
*/
public function compile()
public function compileRoutes()
{
add_rewrite_tag('%'.$this->route_variable.'%', '(.+)');
foreach ($this->routes as $route) {
$this->addRule($route);
}
}

/**
* Compiles the router into WordPress endpoints.
*/
public function compileEndpoints()
{
foreach ($this->endpoints as $endpoint) {
$path = $this->getEndpointPath($endpoint->getPath());
register_rest_route($endpoint->getNamespace(), $path, array(
'methods' => $endpoint->getMethod(),
'callback' => $endpoint->getConfig(),
'permission_callback' => $endpoint->getPermissionCallback()
));
}
}

/**
* Tries to find a matching route using the given query variables. Returns the matching route
* or a WP_Error.
@@ -85,27 +120,60 @@ public function match(array $query_variables)
*/
private function addRule(RouteInterface $route, $position = 'top')
{
$regex_path = $this->cleanPath($route->getPath());
$regex_path = RegexHelperRoutes::cleanPath($route->getPath());
$matches = RegexHelperRoutes::getRegexMatches($regex_path);
$index_string = 'index.php?' . $this->route_variable . '=' . $route->getName();
if (preg_match_all('#\{(.+?)\}#', $regex_path, $matches)) {
foreach ($matches[1] as $key => $pattern) {
$pattern = explode(':', $pattern);
if (count($pattern) > 1) {
$name = $pattern[0];
$num_arg = $key+1;
$regex_code = RegexHelper::getRegex($pattern[1]);
$regex_path = str_replace($matches[0][$key], $regex_code, $regex_path);
add_rewrite_tag("%$name%", $regex_code);
$index_string .= "&$name=\$matches[$num_arg]";
$route->addArg($name);
}
if (!$matches) {
return;
}
foreach ($matches[1] as $key => $pattern) {
$pattern = explode(':', $pattern);
if (count($pattern) > 1) {
$name = $pattern[0];
$num_arg = $key+1;
$regex_code = RegexHelperRoutes::getRegex($pattern[1]);
$regex_path = str_replace($matches[0][$key], $regex_code, $regex_path);
add_rewrite_tag("%$name%", $regex_code);
$index_string .= "&$name=\$matches[$num_arg]";
$route->addArg($name);
}
}
add_rewrite_rule("^$regex_path$", $index_string, $position);
}

private function cleanPath($path)
/**
* Get translated Regex path for an endpoint route.
*
* @param string $path
*/
private function getEndpointPath(string $path)
{
return ltrim(trim($path), '/');
$regex_path = RegexHelperEndpoints::cleanPath($path);
$matches = RegexHelperEndpoints::getRegexMatches($regex_path);
if ($matches) {
foreach ($matches[1] as $key => $pattern) {
$regex_path = $this->getEndpointPatternPath($regex_path, $key, $pattern, $matches);
}
}
return $regex_path;
}

/**
* Get translated Regex path for an endpoint pattern.
*
* @param string $path
* @param int $key
* @param string $pattern
* @param array $matches
* @return string $regex_path
*/
private function getEndpointPatternPath(string $path, int $key, string $pattern, array $matches)
{
$pattern = explode(':', $pattern);
if (count($pattern) > 1) {
$regex_code = RegexHelperEndpoints::getRegex($pattern);
$path = str_replace($matches[0][$key], $regex_code, $path);
}
return $path;
}
}
12 changes: 12 additions & 0 deletions plubo-routes.php
Original file line number Diff line number Diff line change
@@ -51,3 +51,15 @@ function () {
);
return $routes;
});

add_filter('plubo/endpoints', function ($endpoints) {
$endpoints[] = new PluboRoutes\Endpoint\GetEndpoint(
'plubo/v1',
'client/{client_id:number}',
function ($request) {
$params = $request->get_params();
return array('client', $params['client_id']);
}
);
return $endpoints;
});

0 comments on commit 5183a1f

Please sign in to comment.