|
<?php
|
|
|
|
class WPRoutes {
|
|
|
|
/**
|
|
* @var $this[]
|
|
*/
|
|
private static $instances = [];
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private static $routes = [];
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
private static $initialized = false;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $namespace;
|
|
|
|
/*
|
|
** Constructor
|
|
*/
|
|
private function __construct( $namespace ) {
|
|
$this->namespace = trim( $namespace, '/' );
|
|
|
|
if ( !self::$initialized ) {
|
|
add_action( 'init', [self::class, 'register_rewrite_rules'] );
|
|
add_action( 'parse_request', [self::class, 'handle_request'] );
|
|
self::$initialized = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get or create a WPRoutes instance for a specific namespace
|
|
*
|
|
* @param string $namespace
|
|
* @return $this
|
|
*/
|
|
public static function namespace( $namespace ) {
|
|
if ( !isset( self::$instances[$namespace] ) ) {
|
|
self::$instances[$namespace] = new self( $namespace );
|
|
}
|
|
|
|
return self::$instances[$namespace];
|
|
}
|
|
|
|
/**
|
|
* Define a GET route
|
|
*
|
|
* @param string $path
|
|
* @param callable $callback
|
|
* @return $this
|
|
*/
|
|
public function get( $path, $callback ) {
|
|
return $this->add_route( 'GET', $path, $callback );
|
|
}
|
|
|
|
/**
|
|
* Define a POST route
|
|
*
|
|
* @param string $path
|
|
* @param callable $callback
|
|
* @return $this
|
|
*/
|
|
public function post( $path, $callback ) {
|
|
return $this->add_route( 'POST', $path, $callback );
|
|
}
|
|
|
|
/**
|
|
* Define a PUT route
|
|
*
|
|
* @param string $path
|
|
* @param callable $callback
|
|
* @return $this
|
|
*/
|
|
public function put( $path, $callback ) {
|
|
return $this->add_route( 'PUT', $path, $callback );
|
|
}
|
|
|
|
/**
|
|
* Define a DELETE route
|
|
*
|
|
* @param string $path
|
|
* @param callable $callback
|
|
* @return $this
|
|
*/
|
|
public function delete( $path, $callback ) {
|
|
return $this->add_route( 'DELETE', $path, $callback );
|
|
}
|
|
|
|
/**
|
|
* Add a route to the routing table
|
|
*
|
|
* @param string $method
|
|
* @param string $path
|
|
* @param callable $callback
|
|
* @return $this
|
|
*/
|
|
private function add_route( $method, $path, $callback ) {
|
|
$path = '/' . trim($path, '/');
|
|
|
|
$pattern = '#^' . preg_replace('/\{([^}]+)\}/', '([^\/]+)', $path) . '$#';
|
|
|
|
self::$routes[$method][] = [
|
|
'namespace' => $this->namespace,
|
|
'callback' => $callback,
|
|
'pattern' => $pattern,
|
|
'params' => $this->extract_param_names($path),
|
|
];
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Extract parameter names from a route path
|
|
*
|
|
* @param string $path
|
|
* @return array
|
|
*/
|
|
private function extract_param_names( $path ) {
|
|
preg_match_all( '/\{([^}]+)\}/', $path, $matches );
|
|
|
|
return $matches[1] ?? [];
|
|
}
|
|
|
|
/**
|
|
* Register rewrite rules for all defined routes
|
|
*/
|
|
public static function register_rewrite_rules() {
|
|
$namespaces = array_unique(array_column(array_merge(...array_values(self::$routes)), 'namespace'));
|
|
|
|
foreach ($namespaces as $ns) {
|
|
add_rewrite_tag("%{$ns}_route%", '(.+)');
|
|
add_rewrite_rule("^{$ns}/(.+)?", "index.php?{$ns}_route=\$matches[1]", 'top');
|
|
add_rewrite_rule("^{$ns}/?$", "index.php?{$ns}_route=", 'top');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle incoming requests and dispatch to the appropriate route callback
|
|
*/
|
|
public static function handle_request( $wp ) {
|
|
$method = self::get_method();
|
|
|
|
if ( !isset( self::$routes[$method] ) ) {
|
|
return;
|
|
}
|
|
|
|
foreach ( self::$routes[$method] as $route ) {
|
|
$ns_key = $route['namespace'] . '_route';
|
|
|
|
if ( !isset( $wp->query_vars[$ns_key] ) ) {
|
|
continue;
|
|
}
|
|
|
|
$requestPath = '/' . trim( $wp->query_vars[$ns_key], '/' );
|
|
|
|
if ( preg_match( $route['pattern'], $requestPath, $matches ) ) {
|
|
array_shift( $matches );
|
|
|
|
$params = array_combine( $route['params'], $matches );
|
|
$request = self::build_request( $params, $method );
|
|
|
|
$response = call_user_func( $route['callback'], $request );
|
|
self::send_response( $response );
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the HTTP method, considering method spoofing
|
|
*
|
|
* @return string
|
|
*/
|
|
private static function get_method() {
|
|
$method = strtoupper( $_SERVER['REQUEST_METHOD'] ?? 'GET' );
|
|
|
|
if ( $method === 'POST' ) {
|
|
$spoofed = strtoupper( $_POST['_method'] ?? $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ?? '' );
|
|
|
|
if ( in_array( $spoofed, ['PUT', 'DELETE', 'PATCH'], true ) ) {
|
|
return $spoofed;
|
|
}
|
|
}
|
|
|
|
return $method;
|
|
}
|
|
|
|
/**
|
|
* Build a request array
|
|
*
|
|
* @param array $params
|
|
* @return array
|
|
*/
|
|
private static function build_request( $params, $method ) {
|
|
$body = [];
|
|
|
|
if ( in_array( $method, ['POST', 'PUT', 'PATCH', 'DELETE'] ) ) {
|
|
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
|
|
|
if ( stripos( $contentType, 'application/json' ) !== false ) {
|
|
$rawInput = file_get_contents( 'php://input' );
|
|
$decoded = json_decode( $rawInput, true );
|
|
$body = is_array( $decoded ) ? $decoded : [];
|
|
} elseif ( stripos( $contentType, 'multipart/form-data' ) !== false ) {
|
|
$body = array_merge( $_POST, $_FILES );
|
|
} elseif ( stripos( $contentType, 'application/x-www-form-urlencoded' ) !== false ) {
|
|
$body = $_POST;
|
|
} else {
|
|
// Fallback: try to parse raw input as query string
|
|
parse_str( file_get_contents( 'php://input' ), $parsed );
|
|
$body = is_array( $parsed ) ? $parsed : [];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'params' => array_map( 'sanitize_text_field', $params ),
|
|
'query' => array_map( 'sanitize_text_field', $_GET ),
|
|
'body' => $body,
|
|
'method' => $method
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Send the response to the client
|
|
*
|
|
* @param mixed $response
|
|
*/
|
|
private static function send_response( $response ) {
|
|
if ( is_array( $response ) || is_object( $response ) ) {
|
|
wp_send_json( $response );
|
|
} else {
|
|
echo $response;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flush rewrite rules
|
|
*/
|
|
public static function flush() {
|
|
flush_rewrite_rules();
|
|
}
|
|
|
|
} |