jetski/jetski-router

A lightning fast, easy to use router for WordPress


License
GPL-3.0

Documentation

Jetski Router - WordPress Routing, Fast & Easy

Code Climate Build Status

This WordPress plugin adds a regular expression based router on top of the WordPress standard routing system. Any route declared through this router will take priority over standard WordPress urls.

Installation

Note: I am still in the process of adding this plugin to the WordPress repository.

As soon as the Jetski Router makes it into the WordPress plugins repository you will be able to download it directly from there and also through WordPress Packagist if you use composer to manage plugins as dependencies.

For now, you have 2 other methods to use the Jetski Router.

Download zip

You can download a zip of the latest stable release of the project and upload it to your WordPress website’s plugins directory. Make sure to run composer install from the plugin's directory if you choose this method.

Composer

If your setup uses Composer for proper dependency management (check out the awesome Bedrock project if you are interested in running WordPress as a 12 factor app) installing the Jetski Router is quite easy.

Add this package to the "repositories" property of your composer json definition (make sure the version specified is the one you want to install):

{
  "repositories": [
    {
      "type": "package",
      "package": {
        "name": "jetski/jetski-router",
        "type": "wordpress-plugin",
        "version": "0.1",
        "dist": {
          "type": "zip",
          "url": "https://github.com/sformisano/jetski-router/releases/download/v0.1/jetski-router-v0.1.zip",
          "reference": "v0.1"
        },
        "autoload": {
          "classmap": ["."]
        }
      }
    }
  ]
}

Once that's done, add the Jetski Router in the composer require property (the version has to match the package version):

{
  "require":{
    "jetski/jetski-router": "0.1"
  }
}

Finally, make sure to specify the correct path for the "wordpress-plugin" type we assigned to the Jetski Router package in the definition above (note: if you are using Bedrock this is already taken care of).

{
  "extra": {
    "installer-paths": {
      "path/to/your/wordpress/plugins/{$name}/": ["type:wordpress-plugin"],
    }
  }
}

Run composer update from your composer directory and you're good to go!

Usage

Initialization & Configuration

In your theme’s functions.php file or somewhere in your plugin:

// Import the Jetski Router
use JETSKI\Router\Router;

// Router config
$config = [];

// Create the router instance
$r = Router::create($config);

Available router configuration properties:

  • namespace: if defined, the namespace prefixes all routes, e.g. if you set the namespace to “my-api” and then define a route as “comments/published”, the actual endpoint will be “/my-api/comments/published/“.
  • outputFormat: determines the routes handlers output format. Available values: auto, json, html.

The router automatically hooks itself to WordPress, so once you’ve created the router instance with the configuration that suits your needs, all that is left to do is adding routes. After that, the router is ready to dispatch requests.

Pro tip on the namespace

If you use the Jetski Router to build a json/data api, or if you simply don’t mind having a namespace before your urls, having one will speed up standard WordPress requests, especially if you have many dynamic routes.

This is because, if the router has a namespace, but the http request’s path does not begin with that namespace (like it would be the case with all WordPress urls), the router won’t even try to dispatch the request.

Without a namespace, on the other hand, the router will try to dispatch all requests, building the dynamic routes array every time. (Note: caching the generated dynamic routes is on my todo list.)

Adding Routes

The basics

The basic method to add routes is addMethod. Example:

$r->addRoute('GET', 'some/resource/path', 'the_route_name', function(){
  // the callback fired when a request matches this route
});

The addRoute method has aliases for each http method (get, post, put, patch, delete, head, options). Example:

$r->get(‘users/new’, ‘route_name’, function(){
  // new user form view
});

$r->post('users', 'create_user', function(){
  // create user in database
});

Dynamic routes

Use curly braces to outline parameters, then declare them as arguments of the callback function and the router will take care of everything else. Example:

$r->get('users/{username}', 'get_user_by_username', function($username){
  echo "Welcome back, $username!";
});

$->get('schools/{school}/students/{year}', 'get_school_students_by_year', function($school, $year){
  echo "Welcome, $school students of $year!";
});

Parameters regular expressions

By default, all parameters are matched against a very permissive regular expression:

/**
  * One or more characters that is not a '/'
  */
  const DEFAULT_PARAM_VALUE_REGEX = '[^/]+';

You can use your own regular expressions by adding a colon after the parameter name and the regex right after it. Example:

$r->get('schools/{school}/students/{year:[0-9]+}', 'get_school_students_by_year', function($school, $year){
  // If $year is not an integer an exception will be thrown
});

You can also use one of these regex shortcuts for convenience:

private $regexShortcuts = [
  ':i}' => ':[0-9]+}',           // integer
  ':a}' => ':[a-zA-Z0-9]+}',     // alphanumeric
  ':s}' => ':[a-zA-Z0-9_\-\.]+}' // alphanumeric, "_", "-" and ".""
];

If, for example, we wanted to write the same get_school_students_by_year route by using the :i shortcut, this is how we would do it:

$r->get('schools/{school}/students/{year:i}', 'get_school_students_by_year', function($school, $year){
  // If $year is not an integer an exception will be thrown
});

Optional parameters

You can make a parameter optional by adding a question mark after the closing curly brace. Example:

$r->get('schools/{school}/students/{year}?’, 'get_school_students_by_year', function($school, $year){
  // $year can be empty!
});

You can make all parameters optionals, even when they are not the last segment in a route. Example:

$r->get('files/{filter_name}?/{filter_value}?/latest', 'get_latest_files', function($filter_name, $filter_value){
  // get files and work with filters if they have a value
});

All the example requests below would match the route defined above:

'/files/latest'
'/files/type/pdf/latest'
'/files/author/sformisano/latest'

This is not necessarily the best use of optional parameters. The examples here are just to show that, if you have a valid use case, the Jetski Router will allow you to setup your routes this way.

Reverse Routing

Reverse routing is done through the same router object for simplicity’s sake.

Given this simple example route:

$r->addRoute('GET', 'posts/popular', 'popular_posts', function(){});

To print out the full path you use the thePath method:

$r->thePath('popular_posts'); // prints /posts/popular/

You can also get the value returned, rather than printed out, by using the getThePath method (trying to follow some WordPress conventions with this):

$r->getThePath('popular_posts'); // returns /posts/popular/

Dynamic routes reverse routing

Dynamic routes work in the exact same way, you just pass the values of the parameters after the route name in the same order they are defined in when you created the route.

For example, given this route:

$r->get('schools/{school}/students/{year}?’, 'get_school_students_by_year', function($school, $year){
});

This is how you would print out a full path to this route:

$r->thePath('get_school_students_by_year', 'caltech', 2005); // prints  /schools/caltech/students/2005/

As the last parameter is optional you can simply omit it:

$r->thePath('get_school_students_by_year', 'mit'); // prints  /schools/mit/students/

If the optional parameter you need to omit is not the last parameter, you can simply pass null as a value and it will be ignored. Example:

$r->get('{school}?/students/{year}’, 'get_students', function($school, $year){
});

$r->thePath('get_students', null, 1999); // prints /students/1999/

I prefer this approach to the more common associative array for arguments as it makes everything simpler (less typing in the vast majority of scenarios) and more explicit (a missing optional parameter is still defined as null, so there’s a 1:1 match between formal parameters and arguments).

Why I built the Jetski Router

  • I wanted something fast, easy to setup and with a minimal API. The Jetski Router is setup with one line of code ($r = Router::create();) and all functionality is readily available through that object.

  • The routers I am aware of, even the ones I credited below, are conceived to be used as the core routing system of a website/webapp. This does not make them the best fit for the use case of a WordPress extra routing layer, e.g. they will throw an exception if a route is not found or if there’s a path match but the http method of the request is different. Because this router is an extra layer on top of WordPress, it needs to fail silently in most of these scenarios and give control back to WordPress. WordPress can then dispatch those requests matching, return a 404 or do whatever else it’s supposed to do.

Credits

  • This router implements Nikita Popov’s group position based, non-chunked approach to regex matching for lightning fast routing. Learn more about it by reading this great post by Nikita himself.

  • Joe Green and his phroute project (also loosely based on Nikita’s work) are to be thanked for:

    • the regex used to capture dynamic routes parameters
    • the regex shortcuts (a simple yet very elegant idea)
    • the basics of the optional parameters and reverse routing implementation

Go check both these authors and their projects, they are awesome.