A quick look into WP Emerge’s new route query API

by Atanas Angelov | Nov 15th, 2018 | Tutorials

WP Emerge v0.11.0 comes with a new Route::query() API for routes which replaces the old Route::rewrite() one which, as you probably guessed, was based on WordPress’ Rewrite API. This change was done due to several reasons:

  • rewrite() did little more than just call add_rewrite_rule().
  • rewrite() only allowed us to declare direct mapping from one url to another.
  • query() works without having to refresh permalinks.
  • query() gives us imperative control over the rewriting – this allows us to do things like filter our parameters, add conditional logic etc.
  • query() gives us full access to the current WP_Query arguments so we can filter them as needed and event use arguments that are not publicly queryable under normal circumstances. Be careful with the last part though.

Setup

In case you don’t have WP Emerge installed or you are not using the WP Emerge Starter Theme, here are the quickstart steps to have WP Emerge running in your theme:

  1. Make sure you cover the requirements listed in the documentation.
  2. Open your terminal of choice and run the following in your theme directory:
    composer require htmlburger/wpemerge
    
  3. Make sure you’ve included the generated autoload.php file inside your functions.php file
    require_once( 'vendor/autoload.php' );
    
  4. Add the following immediately after:
    add_action( 'after_setup_theme', function() {
        WPEmerge::boot();
    
        // All code references in the examples section would go here.
        // For organization purposes, you may want to create a
        // separate file and require it here instead.
    } );
    

Breaking it down

Here’s the full example, as seen in the article thumbnail:

Router::get( '/custom/{taxonomy}/url/for/{term}/', '[email protected]' )
    ->query( function ( $query_vars, $taxonomy, $term ) {
        $taxonomy = get_taxonomy( sanitize_text_field( $taxonomy ) );

        if ( ! $taxonomy || ! $taxonomy->publicly_queryable ) {
            return $query_vars;
        }

        return [
            $taxonomy->query_var => sanitize_text_field( $term ),
        ];
    } );

Let’s break it down and explore what each part does:

Router::get( ... )

Our first step in declaring a new route is to specify the HTTP method we want to match, The Router class comes with a number of tools to match against HTTP methods which are shown here. For simplicity’s sake we will go with GET.

'/custom/{taxonomy}/url/for/{term}/'

Next, we specify the URL pattern we wish to match. The special {taxonomy} and {term} parameter tokens specify that we wish to accept any user input in these segments – we’ll get to use their values later on.

'[email protected]'

One of the core concepts in WP Emerge are Controllers (the same controllers as in the MVC design pattern) – they handle the actual response when a route matches. Most commonly, we will specify a controller class name and a method of that class which WP Emerge should use to handle the request – this comes in the [email protected]_NAME format as seen in the example. Check out the Controllers and Route Handlers sections of the documentation for more information.

If we do not have any special handling logic we can omit this argument and let normal behavior resume:
Router::get( '/custom/{taxonomy}/url/for/{term}/' ).

->query( function ( $query_vars, $taxonomy, $term ) { ... } );

Here we call query() to signify we wish to filter the main WordPress query and pass an anonymous function that will do the filtering. The first argument passed to our function holds the current WordPress query args and subsequent arguments are the values of the special parameter tokens we used in the URL pattern earlier.

$taxonomy = get_taxonomy( sanitize_text_field( $taxonomy ) );

if ( ! $taxonomy || ! $taxonomy->publicly_queryable ) {
    return $query_vars;
}

return [
    $taxonomy->query_var => sanitize_text_field( $term ),
];

Finally, due to our example’s needs, we throw away any query vars that WordPress intended to use and just return an array of custom query arguments that we want to use instead. Don’t forget to sanitize user input!

More examples

Show a landing page to anonymous users instead of the normal homepage

Match:
/

Route:

Router::get( '/' )
    ->query( function ( $query_vars ) {
        if ( ! is_user_logged_in() ) {
            return [
                'post_type' => 'page',
                'name'      => 'landing-page',
            ];
        }

        return $query_vars;
    } );

Clean URL search filter based on taxonomies

Match:
/cars/BMW/7-series/2019/

Route:

Router::get( '/cars/{make}/{model}/{year}/' )
    ->query( function ( $query_vars, $make, $model, $year ) {
        return [
            'car_make'  => sanitize_text_field( $make ),
            'car_model' => sanitize_text_field( $model ),
            'car_year'  => sanitize_text_field( $year ),
        ];
    } );

Custom endpoint to existing post type

Match:
/files/2018-yearly-report/download/csv/
/files/2018-yearly-report/download/xls/

Route:

Router::get( '/files/{file}/download/{format:(csv|xls)}/', '[email protected]' )
    ->query( function ( $query_vars, $file, $format ) {
        // Assuming we handle the download format logic in our controller, we
        // only want to force WordPress to render the single page of the
        // file post even though we have extra parameters added to
        // the URL.
        return [
            'post_type' => 'file',
            'name'      => sanitize_text_field( $file ),
        ];
    } );

Reach out

Let me know your thoughts about the new query API in the comment section and feel free to join me for a chat in the Gitter Lobby!