This entry is part 2 of 3 in the WordPress Rewrite API Series
- WordPress Rewrite API – Part 1
- WordPress Rewrite API – Part 2
- WordPress Rewrite API – Part 3
Last time, we covered some of the basics and fundamentals of the Rewrite API in WordPress. In this tutorial, we’ll cover more about these APIs see how to use them to build something more practical.
A products store
We’ll be building a very simple store, and we will be using the rewrite API to setup our store, category, and product URLs.
The store has a number of products organized in categories. To make things easier and simpler, in this tutorial there won’t be sub-categories. As with any store, products should be accessed by a unique URL and display a page with information about the product. There should be a page for each category that lists its’ products, and certainly the main store page which lists the categories.
In a real world, the store data is stored in a database, files or accessed from a third-party service. A store has generally a hierarchical structure (top categories, sub-categories, and then products) and data is pulled from the database to populate PHP objects or arrays. For simplicity, our store data is a pre-defined multi-dimensional array.
It’s important to note here that this code is for educational purposes only. Loading all your store data in an array for every page request is not a good practice. It’s also important to note that when building an e-commerce store in WordPress it is probably much better to use custom post types and taxonomies, but doing it like this works very well for a demonstration of the rewrite API.
In our pre-defined array, I put three categories (computers, cellphones, and cameras). Each category has two products, and each product has a name, description and a price. Certainly, you can add, edit or remove categories/products from the array.
<?php $store = array( 'computers' => array( 'macbookair' => array( 'product_name' => 'MacBook Air', 'product_description' => 'The new MacBook Air is faster and more powerful than before, yet it\'s still incredibly thin and light. It\'s everything a notebook should be. And more.', 'product_price' => '1500' ), 'sony' => array( 'product_name' => 'Sony Vaio', 'product_description' => 'The Sony VAIO Z series features an 13" LED-backlit display with native resolution of 1600x768, coupled with Intel GMA...', 'product_price' => '2000' ) ), 'cellphones' => array( 'iphone' => array( 'product_name' => 'iPhone 4s', 'product_description' => 'iPhone 4S features Siri, the dual-core A5 chip, the 8MP camera with all-new optics, 1080p HD video recording and more.', 'product_price' => '799' ), 'nexusprime' => array( 'product_name' => 'Nexus Prime', 'product_description' => 'Galaxy Nexus. First phone with Android 4.0, Face Unlock, Android Beam, an amazing HD screen and 4G LTE fast.', 'product_price' => '599' ) ), 'cameras' => array( 'samsung' => array( 'product_name' => 'Samsung nx11', 'product_description' => ' The NX11 is fully compatible with Samsungs innovative i-Function lens, which means that the camera can be easily controlled without having to understand all the complex camera settings.', 'product_price' => '800' ), 'canon' => array( 'product_name' => 'CANON EOS 600D', 'product_description' => ' The EOS 600D is powered by an 18-megapixel high image quality CMOS sensor and the powerful DIGIC 4 image processor.', 'product_price' => '1200' ) ) ); |
The store URL Structure
The URL structure is what actually matters. Since we have a simple store architecture, our URL structure will be simple too. “mystore.com” is your website URL, it is probably “localhost” if you are running a local server.
http://mystore.com/store
Access the store home page which lists the store categories
http://mystore.com/store/category_name
Access a store category, and get a list of products in this category
http://mystore.com/store/category_name/product_name
Access a store product details.
As the array content changes, our WordPress blog will have less or more pages. These pages should be generated on the fly from our array.
Defining a query variables based structure
In the first part, we mentioned that WordPress loads pages based on the old-fashioned permalinks structure, which is the default when you freshly install it. So apart from the smart permalink structure that we defined earlier, we need to define a query variables based structure.
http://mystore.com/index.php?store=true
Access the store home page which lists the store categorieshttp://mystore.com/ index.php?store=true&category=category_name
Access a store category, and get a list of products in this categoryhttp://mystore.com/ index.php?store=true&category=category_name&product=product_name
Access a store product details.
Pretty good. Now, there are a couple of things our code should do
- Read the URL query variables
- Display the proper page
Reading a URL query variable is done through the PHP $_GET variable. You have probably already used this variable a handful times when developing with PHP. To display the page, we’ll hook into the “template_redirect” filter.
The $store variable is omitted here as it was mentioned earlier. Save this snippet to a file in your plugins directory and activate the plug-in.
<?php /* Plugin Name: My Store Plugin URI: http://mystore.com Description: A Simple Store with WordPress Author: Abid Omar Version: 1.0 */ add_filter( 'template_redirect', 'ao_redirect_temp'); function ao_redirect_temp() { $store = array(…); // replace this with your store products array if (isset($_GET['store']) && isset($_GET['category']) && isset($_GET['product'])) { $product = $store[$_GET['category']][$_GET['product']]; if (is_array($product)) { ao_display_product($product); } } } /** * Displays a product page * @param $product Array */ function ao_display_product($product) { global $ao_product; $ao_product = $product; // Filters add_filter('the_title', 'ao_display_product_title'); add_filter('the_content', 'ao_display_product_content'); // Limit the number of posts to 1 query_posts('posts_per_page=1'); // Call the page template include(get_page_template('page')); exit; } function ao_display_product_title($title) { global $id, $post, $ao_product; if ($id && $post) { return 'Product ' . $ao_product['product_name']; } else { return $title; } } function ao_display_product_content() { global $ao_product; $html = '<h3>Description</h3>'; $html .= '<p>'; $html .= $ao_product['product_description']; $html .= '</p>'; $html .= '<h3>Price: '; $html .= $ao_product['product_price']; $html .= '</h3>'; return $html; } |
The first thing we do here is to hook into the “template_redirect” filter. This filter has the advantage of calling our function before the template is loaded (and even before sending the template headers, so it’s useful for doing a 404 or external redirects)
Next, in our “ao_redirect_temp” function, we check that the three query variables (store, category and product) are set, and we look inside our array for a category and product match.
If the product is found, the “ao_display_product” function is called which accepts a $product argument. The function does 3 things
- Change the Post title by adding a filter to “the_title” filter. There is a condition in the filter function that checks that the filter is not applied to the navigation menu
- Change the Post content by adding a filter to the “the_content” filter. This is where the product details are displayed.
- Finally, load the page template of the theme. Also, just a line before that, we set to display only one post. This is just for compatibility across themes that have latest posts displayed on their home page, instead of a static page.
Notice the “exit;” command in the end. It terminates the script, and ensures that no additional code gets executed after our page is fully loaded.
Enabling Smart Permalinks
Our store is already functioning. The next step is to improve the URL structure using the “add_rewrite_rule” function which augments the rewrite rules in WordPress. The function accepts three arguments. The first is a regular expression to match against a requested rule. The second is the actual URL to rewrite, and the final argument is the rewrite rule position.
We’ll take as an example the longest URL format we have in the store, which is the URL that displays a product page
http://mystore.com/store/category_name/product_name
This URL fetches
http://mystore.com/index.php?store=true&category=category_name&product=product_name
To successfully map between the rule and the rewrite, we need to match 3 tags and respect their order: store, category name, and product name. The thing about the last two tags is that they are variables. You won’t certainly add a rewrite rule for every category, and product. This is when Regular Expressions come in handy.
- The rule: The regular expression should match first, ‘store/’ and then a valid category name followed by another ‘/’ and then a valid product name. The category and product names can be made from any set of characters except the slash character (otherwise the category name will be everything from ‘store/’ to the end of the URL and we won’t be able to match the product name)
- The rewrite URL: To insert the captured matches from the rule, you can use the $matches[] array.
That’s it! Well, here is the code:
<?php add_action( 'init', 'ao_store_rewrite_rule'); function ao_store_rewrite_rule() { add_rewrite_rule( '^store/([^/]*)/([^/]*)/?', 'index.php?&store=true&category=$matches[1]&product=$matches[2]', 'top'); flush_rewrite_rules(); } |
Add this code at the top of your WordPress plugin and try opening the page http://localhost/wp_blog/store/computers/macbookair. It should work, but it doesn’t (last time it did). Something is probably wrong.
Well, if you try to debug the plugin code (or just take another look at the code), you’ll find out that it is using the PHP $_GET variable. This variable is empty (try to use var_dump function before the conditional block), and that’s how it should be since there are no query variables in the requested URL.
WordPress addresses this issue with the $wp_query->query_vars array. This array is populated with all the query variables that WordPress uses (and you can certainly add yours). A quick look at this array reveals some familiar query variables
array 'error' => string '' (length=0) 'm' => int 0 'p' => int 0 'post_parent' => string '' (length=0) |
I stripped down most of it since it’s a long array. The array properties are empty; they are populated only when a relevant URL request is made. This is the content of the array when you make a page request.
array 'page' => int 0 'name' => string 'hello-world' (length=11) 'error' => string '' (length=0) 'm' => int 0 |
Notice that the name key is populated with a string of ‘hello-world’.
The $wp_query object
To make use of the query_vars array in the $wp_query object, you need to make WordPress aware of the query variables you want to use. This is different from the PHP $_GET array where variables do not need to be regisered. WordPress goes a bit further; query variables need to match a regular expression rule to be valid and get added.
<?php /* Plugin Name: My Store Plugin URI: http://mystore.com Description: A Simple Store with WordPress Author: Abid Omar Version: 1.0 */ add_action( 'init', 'ao_store_rewrite_rule'); function ao_store_rewrite_rule() { add_rewrite_tag('%store%','true'); add_rewrite_tag('%category%','([^&]+)'); add_rewrite_tag('%product%','([^&]+)'); add_rewrite_rule( '^store/([^/]*)/([^/]*)/?', 'index.php?store=true&category=$matches[1]&product=$matches[2]', 'top'); flush_rewrite_rules(); } add_filter( 'template_redirect', 'ao_redirect_temp'); function ao_redirect_temp() { $store = array(…); global $wp_query; if (isset($wp_query->query_vars['store']) && isset($wp_query->query_vars['category']) && isset($wp_query->query_vars['product'])) { $product = $store[$wp_query->query_vars['category']][$wp_query->query_vars['product']]; if (is_array($product)) { ao_display_product($product); } } } /** * Displays a product page * @param $product Array */ function ao_display_product($product) { global $ao_product; $ao_product = $product; // Filters add_filter('the_title', 'ao_display_product_title'); add_filter('the_content', 'ao_display_product_content'); // Limit the number of posts to 1 query_posts('posts_per_page=1'); // Call the page template include(get_page_template('page')); exit; } function ao_display_product_title($title) { global $id, $post, $ao_product; if ($id && $post) { return 'Product ' . $ao_product['product_name']; } else { return $title; } } function ao_display_product_content() { global $ao_product; $html = '<h3>Description</h3>'; $html .= '<p>'; $html .= $ao_product['product_description']; $html .= '</p>'; $html .= '<h3>Price: '; $html .= $ao_product['product_price']; $html .= '</h3>'; return $html; } |
Putting it all together
That’s all you need to know to put your custom store in WordPress. This is just an introduction to inspire you, just think of the possibilities. But before that, there are a few more things that I think are worth mentioning
- Flushing rewrite rules
- Categories and Store Pages
- Detecting the permalinks structure
Rewrite rules should be flashed once when the plug-in is activated, and another time when the plug-in is deactivated.
As mentioned in the beginning of the tutorial, a Store and categories pages are useful to have. I didn’t mention then in the previous code to keep it short.
Since category and store pages have links on them (generated automatically by our code); these links must follow the permalinks structure in our WordPress blog. To check if smart permalinks are enabled in WordPress, we need to evaluate the ‘permalink_structure’ option.
Our final code with the improvements mentioned above looks like this:
<?php /* Plugin Name: My Store Plugin URI: http://mystore.com Description: A Simple Store with WordPress Author: Abid Omar Version: 1.0 */ // Activation hook register_activation_hook(__FILE__, 'ao_activate_mystore'); function ao_activate_mystore() { add_rewrite_rule('^store/([^/]*)/([^/]*)/?', 'index.php?store=true&category=$matches[1]&product=$matches[2]', 'top'); add_rewrite_rule('^store/([^/]*)/?', 'index.php?store=true&category=$matches[1]', 'top'); add_rewrite_rule('store', 'index.php?store=true', 'top'); flush_rewrite_rules(); } // Deactivation hook register_deactivation_hook(__FILE__, 'ao_deactivate_mystore'); function ao_deactivate_mystore() { flush_rewrite_rules(); } // Add the rewrite tags add_action('init', 'ao_mystore_rewrite_tag'); function ao_mystore_rewrite_tag() { add_rewrite_tag('%store%', 'true'); add_rewrite_tag('%category%', '([^&]+)'); add_rewrite_tag('%product%', '([^&]+)'); } // Template redirect filter add_filter('template_redirect', 'ao_mystore_redirect'); function ao_mystore_redirect() { $store = array( 'computers' => array( 'macbookair' => array( 'product_name' => 'MacBook Air', 'product_description' => 'The new MacBook Air is faster and more powerful than before, yet it\'s still incredibly thin and light. It\'s everything a notebook should be. And more.', 'product_price' => '1500' ), 'sony' => array( 'product_name' => 'Sony Vaio', 'product_description' => 'The Sony VAIO Z series features an 13" LED-backlit display with native resolution of 1600x768, coupled with Intel GMA...', 'product_price' => '2000' ) ), 'cellphones' => array( 'iphone' => array( 'product_name' => 'iPhone 4s', 'product_description' => 'iPhone 4S features Siri, the dual-core A5 chip, the 8MP camera with all-new optics, 1080p HD video recording and more.', 'product_price' => '799' ), 'nexusprime' => array( 'product_name' => 'Nexus Prime', 'product_description' => 'Galaxy Nexus. First phone with Android 4.0, Face Unlock, Android Beam, an amazing HD screen and 4G LTE fast.', 'product_price' => '599' ) ), 'cameras' => array( 'samsung' => array( 'product_name' => 'Samsung nx11', 'product_description' => ' The NX11 is fully compatible with Samsungs innovative i-Function lens, which means that the camera can be easily controlled without having to understand all the complex camera settings.', 'product_price' => '800' ), 'canon' => array( 'product_name' => 'CANON EOS 600D', 'product_description' => ' The EOS 600D is powered by an 18-megapixel high image quality CMOS sensor and the powerful DIGIC 4 image processor.', 'product_price' => '1200' ) ) ); global $wp_query; if (isset($wp_query->query_vars['store']) && isset($wp_query->query_vars['category']) && isset($wp_query->query_vars['product'])) { // Display a product page $product = $store[$wp_query->query_vars['category']][$wp_query->query_vars['product']]; if (is_array($product)) { ao_display_product($product); } } else if (isset($wp_query->query_vars['store']) && isset($wp_query->query_vars['category'])) { // Display a category page $category = $store[$wp_query->query_vars['category']]; ao_display_category($category); } else if (isset($wp_query->query_vars['store'])) { // Display the store ao_display_store($store); } } /** * Displays a product page * @param $product Array */ function ao_display_product($product) { global $ao_product; $ao_product = $product; // Filters add_filter('the_title', 'ao_display_product_title'); add_filter('the_content', 'ao_display_product_content'); // Limit the number of posts to 1 query_posts('posts_per_page=1'); // Call the page template include(get_page_template('page')); exit; } function ao_display_product_title($title) { global $id, $post, $ao_product; if ($id && $post) { return 'Product ' . $ao_product['product_name']; } else { return $title; } } function ao_display_product_content() { global $ao_product; $html = '<h3>Description</h3>'; $html .= '<p>'; $html .= $ao_product['product_description']; $html .= '</p>'; $html .= '<h3>Price: '; $html .= $ao_product['product_price']; $html .= '</h3>'; return $html; } /** * Displays a category page * @param $category Array */ function ao_display_category($category) { global $ao_category; $ao_category = $category; // Filters add_filter('the_title', 'ao_display_category_title'); add_filter('the_content', 'ao_display_category_content'); // Limit the number of posts to 1 query_posts('posts_per_page=1'); // Call the page template include(get_page_template('page')); exit; } function ao_display_category_title($title) { global $id, $post, $wp_query; if ($id && $post) { return 'Category: ' . ucfirst($wp_query->query_vars['category']); } else { return $title; } } function ao_display_category_content() { global $ao_category; $html = '<h3>Products</h3><ul>'; for ($i = 0; $i < count($ao_category); $i++) { if (get_option('permalink_structure') === '') { $link = add_query_arg('product', key($ao_category)); } else { $link = key($ao_category); } $html .= '<li><h4><a href="' . $link . '">' . $ao_category[key($ao_category)]['product_name'] . '</a></h4></li>'; next($ao_category); } $html .= '</ul>'; return $html; } /** * Displays the store page * @param $store Array */ function ao_display_store($store) { global $ao_store; $ao_store = $store; // Filters add_filter('the_title', 'ao_display_store_title'); add_filter('the_content', 'ao_display_store_content'); // Limit the number of posts to 1 query_posts('posts_per_page=1'); // Call the page template include(get_page_template('page')); exit; } function ao_display_store_title($title) { global $id, $post; if ($id && $post) { return 'My Store'; } else { return $title; } } function ao_display_store_content() { global $ao_store; $html = '<ul>'; for ($i = 0; $i < count($ao_store); $i++) { if (get_option('permalink_structure') === '') { $link = add_query_arg('category', key($ao_store)); } else { $link = key($ao_store); } $html .= '<li><h4><a href="' . $link . '">' . ucfirst(key($ao_store)) . '</a></h4></li>'; next($ao_store); } $html .= '</ul>'; return $html; } |
You can drop this code into your custom store plugin, activate it, and view the effects when you visit http://yoursite.com/store/.
If the flush_rewrite_rules is included in my init action, I experience various DNS timeouts in the dashboard e.g; searching for new plugins. Adding just the following is enough to cause this:
add_action('init', 'sfs_foo');
function sfs_foo() {
flush_rewrite_rules();
}
Nevermind. Not solved, but is appears to be an issue with Role Scoper: http://wordpress.org/support/topic/flush_rewrite_rules-in-plugin-init-causes-dns-timeout
As long as the 3 add_rewrite_rule() and the flush_rewrite_rule() are called from add_action(‘init’) all works fine. But if I change it to run only on activation the hole story fails. And I tried with a completly fresh installation of WordPress 3.42. Of cause I did update my permalinks, but it does’nt help. Any ideas?
The `flush_rewrite_rules()` function should never be called on the `init` hook. This causes the permalinks to get flushed on every single page load.
Can you elaborate on how it fails?
Sorry, fail isn’t really meaningful. It says “Page not found”, like these xxx_rewrite_rules are never called. I’ll send you more information by email.
Sorry, I don’t understand the explanation about `add_rewrite_tag`. Is it a way of adding a `query_var`? Could you explain a bit more please?
Many thanks!
The rewrite API is deceptively complicated in that the wrong regex can trip you up. If you’re needing to modify this code (as I was), you might find https://regex101.com/ to be of help.
I’d love to see a follow up tutorial using `wp_remote_get` to an API as the content source.