Contextual Complexity Vexes a Texan; or Non-contextual Views Filters, Contextually
Ok fine, I'm not a Texan.
But if you, like me, have ever needed a Drupal View's contextual filters to be combined with an "or" operator, you have probably been vexed. Contextual Filters are always combined with an "and" operator. Whereas standard Views filters can be combined into nested "and/or" groups, if you want to do so contextually, passing url or other arguments to the filter, you are headed upstream in a certain stinky creek, paddleless.
Until now, dear reader.
Let's set up a sample use case. We're making a blog network site, where registered users can post blogs on a variety of topics. A user can indicate which authors and topics she's interested in. We want to set up a user dashboard where she can see all blog posts by the authors, and about the topics, in which she's interested.
If we tried to set up a contextual filter for each of topics and author, the view would only show posts that are by an author she's interested in AND are about a topic she is interested in. What to do?
Hook_views_query_alter would seem like a good candidate. Drupal's Database API is reasonably simple and clean, surely we can just use the condition method in conjunction with db_or() to get what we need. Unfortunately, the query object that is passed to hook_views_query_alter is not the same kind of query object as a standard query. It's subclassed from views_plugin_query and accepts these methods. Certainly an acceptable way to proceed, but it's not documented at all, and I can offer a simple alternative.
Values of standard views filters can be set programmatically, in hook_views_pre_view, using views_db_object::set_item_option.
All we have to do is set up the standard filters, arranged into "and/or" groups, and leave the values empty. So we would make a filter for Content Author uid, leaving Usernames blank:
And we'd also make a filter for the topics taxonomy, leaving the terms blank like so:
We'd probably also only want to include published nodes, so the final filter setup would look like this:
Next we declare hook_views_pre_view in a module and set up a conditional so that we're only targeting the appropriate view. Here's what the whole block looks like. Let's say for simplicity that users have already indicated their author and topic interest via reference fields attached to their user objects.
function mymodule_views_pre_view(&$view, &$display_id, &$args){
if ($view->name == '[my_dashboard_view]'){
//grab the user object from the globals
global $user;
//load the whole user object with all fields
$full_user = user_load($user->uid);
$uids = $tids = array();
//loop through field values and make a simple array of tids and uids
foreach($full_user->field_author_follow[LANGUAGE_NONE] as $authors){
$uids[] = $authors['uid'];
}
foreach($full_user->field_topic_follow[LANGUAGE_NONE] as $topics){
$tids[] = $topics['tid'];
}
/*
finally, set the filters. the params of of set_item_option are:
display,
what view option to set, in this case 'filter',
the machine name of the filter, which can be found by looking at the $view object using dsm or similar,
the string 'value' because that's what we're setting
and finally an array that represents the value or values.
*/
$view->set_item_option($view->current_display, 'filter', 'uid', 'value', $uids);
$view->set_item_option($view->current_display, 'filter', 'field_topics_tid', 'value', $tids);
}
}
That about does it. Since the $view object is so unfathomably large and recursive, it can be difficult to find the machine name of the filter, but keep looking, it's nested in there somewhere.
Also note that single values must be nested as the only value in an array.