Skip to content

Instantly share code, notes, and snippets.

@Arifursdev
Last active January 21, 2026 12:31
Show Gist options
  • Select an option

  • Save Arifursdev/4d64f2385f932a4ab50d0d2251e70d2d to your computer and use it in GitHub Desktop.

Select an option

Save Arifursdev/4d64f2385f932a4ab50d0d2251e70d2d to your computer and use it in GitHub Desktop.
WordPress Router Class
// https://example.com/app/
// https://example.com/app/index
// https://example.com/app/author/123
// https://example.com/app/create
// https://example.com/app/update
// https://example.com/app/delete/123

WPRoutes::namespace( 'app' )
    ->get( '/', function( $request ) {
        echo 'Root';
    } )
    ->get( 'index', function( $request ) {
        echo 'Index';
    } )
    ->get( 'author/{id}', function( $request ) {
        $id = $request['params']['id'];
        echo 'Author ID: ' . $id;
    } )
    ->post( 'create', function( $request ) {
        echo 'Create via POST';
        var_dump( $request );
    } )
    ->put( 'update', function( $request ) {
        echo 'Update via PUT';
        var_dump( $request );
    } )
    ->delete( 'delete/{id}', function( $request ) {
        $id = $request['params']['id'];
        echo 'Delete by ID: ' . $id;
    } );

Remember to flush the routes after defining or modifying them once.

WPRoutes::flush();
<?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();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment