I ran into a similar scenario. I've seen a lot of questions, answers, and articles that recommend various methods of passing hierarchical or nested URL patterns into views arguments or contextual filters, but they were all too generic or didn't work for my situation, which I feel is a very common one.
I'm working with Drupal Commerce products and hierarchical taxonomy terms and I wanted URL structures like this:
Taxonomy:
store/parent-category
store/parent-category/child-category
store/parent-category/child-category/grandchild-category
Product Display:
store/parent-category/child-category/grandchild-category/product-title
In PathAuto I defined the following patterns:
Product Display paths:
store/[node:field-product-display-category:parents:join-path]/[node:field-product-display-category:name]/[node:title]
Product Taxonomy paths:
store/[term:parents:join-path]/[term:name]
The field-product-display-category
mentioned in the token above is coming from a Term Reference field on my Product Display content type.

So a default taxonomy page would have the path store/parent-category/child-category/grandchild-category
. Each one of those nested taxonomy pages needed to show all the products referenced by child terms. So store/parent-category
would need to show all products belonging to child-category
and grandchild-category
.
I also needed the taxonomy names in the URL instead of the taxonomy term IDs. I created a view page with a Content: Has taxonomy term ID (with depth)
contextual filter because I might have any number of possible nested taxonomies and I wanted the parent taxonomy to show all results for the child taxonomies.

I tried to use the contextual filter validation method of converting the term name in the url into its corresponding term id and replace spaces with dashes, but my category names have special characters like /
, &
, +
, and -
, which meant decoding the url didn't always match the original term name.

To get around this I created a custom views contextual filter validator, which could correctly parse the term name out of the url and figure out the corresponding term id by querying the url_alias
table. After querying a particular path once, the result is cached for better performance.
To create the custom contextual filter validator I followed this example: http://www.joetsuihk.com/en/en/custom_views_argument_validator_views_plugin_argument_validate
The info file:
name = Example Module
package = "Example"
description = Example Module custom views argument validator
core = 7.x
files[] = plugins/example_module_plugin_argument_validate_url.inc
The module:
/**
* @file example_module/example_module.module
* Implement hook_views_api - load up the custom views include files
*/
function example_module_views_api() {
return array(
'api' => 3.0,
'path' => drupal_get_path('module', 'example_module'),
);
}
The views include file:
/**
* @file example_module/example_module.views.inc
* Defines a custom contextual filter validator
*/
function example_module_views_plugins() {
return array(
'argument validator' => array(
'taxonomy_url_alias' => array(
'title' => t('Taxonomy URL Alias'),
'handler' => 'example_module_plugin_argument_validate_url',
'path' => drupal_get_path('module', 'example_module') . '/plugins',
),
),
);
}
The contextual filter handler:
/**
* @file example_module/plugins/example_module_plugin_argument_validate_url.inc
* Contains the taxonomy url alias contextual filter validation plugin.
*/
/**
* Validate whether an argument (contextual filter) is a valid
* @ingroup views_argument_validate_plugins
*/
class example_module_plugin_argument_validate_url extends views_plugin_argument_validate {
private $intAddUnixTime = 604800;
private $error = FALSE;
function validate_argument($argument) {
$view = &$this->view;
$handler = &$this->argument;
if (!empty($view->args)) {
//reconstruct the url from the views argument array
$strPath = implode("/", $view->args);
//get the taxonomy term id based on the url, not the term name
$objAlias = $this->getTermId($strPath);
//split the url_alias's source column up into an array.
//the source column contains the term id at the end of a path like 'taxonomy/term/123'.
$arrTermData = explode("/", $objAlias->source);
$tid = end($arrTermData);
$term = taxonomy_term_load($tid);
//manually assign the term id to be the view's argument
$handler->argument = $term->tid;
return true;
}
else {
return false;
}
}
/**
* Get the taxonomy term id from either the url_alias table or from persistent cache
* @param string $strPath like 'parent-category/child-category/grandchild-category'
* @return object containing the result of the url_alias table or persistent cache lookup
*/
private function getTermId($strPath) {
$objReturn = NULL;
//attempt to get the taxonomy term id from cached url_alias table result object
$objReturn = $this->getCache($strPath);
//if cache is not set, do a select query
if ($objReturn == NULL) {
try {
if(!$this->error) {
$objReturn = $this->_dbSelect($strPath);
//cache the response
$this->setCache($strPath, $objReturn);
}
else {
//log some error because $this->error was true
}
}
catch (Exception $ex) {
$this->error = TRUE;
//log an exception
}
}
return $objReturn;
}
/**
* Run a query to look up the taxonomy term id based on the url alias path
* @param string $strPath url alias like 'parent-category/child-category/grandchild-category'
* @return object results of the query
*/
private function _dbSelect($strPath) {
$result = NULL;
try {
//The alias stored in the url_alias table is going to be prepended with 'store/' because of my pathauto taxonomy pattern
$result = db_select("url_alias", "ua")
->fields('ua', array('pid','source','alias'))
->condition('alias', 'store/'.$strPath, '=')
->condition('source', 'taxonomy/term%', 'LIKE')
->range(0,1)
->execute()
->fetchObject();
}
catch(Exception $ex) {
//log an exception
}
//$result is an object like:
//$result->pid //'12345'
//$result->source //'taxonomy/term/123'
//$result->alias //'store/parent-category/child-category/grandchild-category'
//$result->uid //1
return $result;
}
/**
* Get an object from cache
* @param string $strPath used as part of the cache id
* 'cache_taxonomy_url_alias' is a custom cache bin
* @return object data from the persistent cache
*/
private function getCache($strPath) {
$objCache = cache_get('example_module_plugin_argument_validate_url' . '::' . 'store/'.$strPath, 'cache_taxonomy_url_alias');
if (isset($objCache->data)) {
return $objCache->data;
}
else {
return NULL;
}
}
/**
* Store an object in cache
* @param string $strPath used as part of the cache id
* @param object $objData contains the url alias table results
* @return void
*/
private function setCache($strPath, $objData) {
cache_set('example_module_plugin_argument_validate_url' . '::' . 'store/'.$strPath, $objData, 'cache_taxonomy_url_alias', time() + $this->intAddUnixTime);
}
}
After that I selected my custom contextual filter validator:

The next issue I ran into was that my views path was the same as my taxonomy pattern. I visited store/parent-category/child-category
and expected the view to handle the display, but instead it went to the default taxonomy page. Worst of all, it didn't display any products as belonging to the child-category
because the products actually belong to the grandchild-category
taxonomy nested underneath it. I was seeing the 'There is currently no content classified with this term' message.
To overcome this, I just gave my view a different path like store-view/%
.
This still doesn't satisfy my needs because I really need the view to be displayed with other blocks on a panel. To solve this I created a content pane display type on the view to be embedded in a panel page.
I set up the pane settings to use the panel path:

And to use the first panel argument as the view's argument data source:

Then for the content pane's contextual filter validation, I selected the custom Taxonomy URL Alias validator:

Then tell it to link to the other view's Categories Page display that I created earlier, which is also using the Taxonomy URL Alias validator:

Then I created a panel page with a different path to contain the views content pane I just created:

I configured the argument to be a String instead of a Taxonomy Term ID

In the argument settings, select Get all arguments after this one because I needed to send the whole path parent-category/child-category/grandchild-category
into the view content pane, not just the first argument parent-category
:

That's it, now navigating to 'panel-page/parent-category/child-category' looks up the term id for 'child-category' and displays all the products belonging to the 'child-category' and 'grandchild category', since the contextual filter is configured for a term id (with depth).