We are working on larger system, which is based on WordPress, but WordPress is used only for “static” content, the main part of the system should be consuming the external API and displaying the data.
My point is: am I able to somehow tell URL rewriting not to use the WordPress internal system for some URLs and use something else?
E.g. mysite.com/news/news-title
would show the post, but mysite.com/customcontent/anotherlink
would call some mechanism to load data from API and display it.
I don’t know if this is even possible with WordPress.. Thanks for your points.
Yes, it is possible.
WordPress frontend workflow can be summarized like so:
- A url is visited
- By checking current url against all the defaults and custom rewrite rules the url is “converted” to a set of arguments for
WP_Query
. This is done by the parse_request
method of an instance of the WP
class stored in the global $wp
variable
- An instance of
WP_Query
(saved in the $wp_query
global variable) is used to query database and getting the posts related to the arguments retrieved at point #2. This is referenced as the “main query”
- Based on the query arguments, a template is choosen according to template hierarchy and it is loaded and used to display results
So, even if you register a set of custom rewrite rules, they will be used to query posts.
However, if you look at the source of parse_request
method, in the very first lines you see this:
if ( ! apply_filters( 'do_parse_request', true, $this, $extra_query_vars ) )
return;
So by attacching a callback to the ‘do_parse_request’ filter, you’ll be able to stop the WordPress parsing process and do whatever you need.
There are different way to do this task, here for sake of simplicity, I’ll give you a rough example.
As said, we need to make a custom url match to… something, probably a callback that retrieve some data and a view to display it.
To make code reusable, we can use a class that accept custom url settings via a filter, and use that custom urls settings to show whatever we need, using a callback to obtain some data and a view to display them.
class MyCustomUrlParser {
private $matched = array();
/**
* Run a filter to obtain some custom url settings, compare them to the current url
* and if a match is found the custom callback is fired, the custom view is loaded
* and request is stopped.
* Must run on 'do_parse_request' filter hook.
*/
public function parse( $result ) {
if ( current_filter() !== 'do_parse_request' ) {
return $result;
}
$custom_urls = (array) apply_filters( 'my_custom_urls', array() );
if ( $this->match( $custom_urls ) && $this->run() ) {
exit(); // stop WordPress workflow
}
return $result;
}
private function match( Array $urls = array() ) {
if ( empty( $urls ) ) {
return FALSE;
}
$current = $this->getCurrentUrl();
$this->matched = array_key_exists( $current, $urls ) ? $urls[$current] : FALSE;
return ! empty( $this->matched );
}
private function run() {
if (
is_array( $this->matched )
&& isset( $this->matched['callback'] )
&& is_callable( $this->matched['callback'] )
&& isset( $this->matched['view'] )
&& is_readable( $this->matched['view'] )
) {
$GLOBALS['wp']->send_headers();
$data = call_user_func( $this->matched['callback'] );
require_once $this->matched['view'];
return TRUE;
}
}
private function getCurrentUrl() {
$home_path = rtrim( parse_url( home_url(), PHP_URL_PATH ), "https://wordpress.stackexchange.com/" );
$path = rtrim( substr( add_query_arg( array() ), strlen( $home_path ) ), "https://wordpress.stackexchange.com/" );
return ( $path === '' ) ? "https://wordpress.stackexchange.com/" : $path;
}
}
This is a rough class that let users set custom url settings via a filter (‘my_custom_urls’). Custom url settings must be an array where keys are relative urls and every value is an array containing two keyed values: one with key ‘callback’ and one with key ‘view’.
The callback is a callable (anything for which is_callable
returns true) and the view is a file used to render the data returned by the callable and accessible in the view file in the $data
variable.
Here an example how to use the class.
// first of all let's set custom url settings
add_filter( 'my_custom_urls', 'set_my_urls' );
function set_my_urls( $urls = array() ) {
$my_urls = array(
'/customcontent/alink' => array(
'callback' => 'my_first_content_callback',
'view' => get_template_directory() . '/views/my_first_view.php'
),
'/customcontent/anotherlink' => array(
'callback' => 'my_second_content_callback',
'view' => get_template_directory() . '/views/my_second_view.php'
)
);
return array_merge( (array) $urls, $my_urls );
}
// require the file that contain the MyCustomUrlParser class
require '/path/to/MyCustomUrlParser';
// attach MyCustomUrlParser::parse() method to 'do_parse_request' filter hook
add_filter( 'do_parse_request', array( new MyCustomUrlParser, 'parse' ) );
Of course, we need to write my_first_content_callback
and my_second_content_callback
and also my_first_view.php
and my_second_view.php
.
As example, the callback would be something like this:
function my_first_content_callback() {
$content = get_transient( 'my_first_content' );
if ( empty( $content ) ) {
$api = a_method_to_get_an_external_api_object();
$json = $api->fetch_some_json_data();
$content = json_decode( $json );
// cache content for 1 hour
set_transient( 'my_first_content', $content, HOUR_IN_SECONDS );
}
return $content;
}
Note that whatever callback returns is stored in the $data
variable accessible in the view. In facts, a view file would appear something like this:
<?php get_header(); ?>
<h1>My Content from API</h1>
<div>
<pre><?php print_r( $data ); ?></pre>
</div>
<?php get_footer() ?>
This works and is pretty reusable, you can set all the custom urls you want by using the 'my_custom_urls'
filter.
However, there is downside: all the matching against the current url is done via an exact match, but using a regex matching system it would be a lot better for large projects because you can use the urls to pass some variables to callbacks.
E.g. if you have an url settings like this:
'/customcontent/alink/page/{page}' => array(
'callback' => 'my_first_content_callback',
'view' => get_template_directory() . '/views/my_first_view.php'
)
using a regex system is possible to make the {page}
part variable and the matched value can be passed to the callback to retrieve different data.
This is what is commonly called a routing system, and there are some useful PHP libraries like FastRoute, Pux and Symfony Routing Component that can help you to use the workflow I explained here and build your own regex-based routing system in WordPress.
If you have PHP 5.4+ (that is really recommended) there is a plugin I wrote called Cortex that implements Symfony Routing Component and make it usable inside WordPress both for standard WordPress queries (showing posts) or even for custom content like you need.