Retrieve field group (tab) label name in ACF - wordpress

I'm running Advanced Custom Fields in Wordpress. In ACF, you can group items in Tabs, making it easier to navigate to create a post/page content.
I'm wanting to retrieve the name of the tab, and it's content, programmatically.
Is it possible? I can't find any documentation regarding this.

Not sure if you got an answer for your question,
There is no way through php code to get field/subfield data as there is no relationship between tabs and fields under them.
ACF Tabs are only for admin layout to organise fields(hide/show etc) and is done completely through JS/CSS. If you have ACF PRO check the functions in this folder - plugins/advanced-custom-fields-pro/assets/js/acf-input.js which explains how tabs work in ACF
If you want to get fields which are not in repeater/flexible layouts in an array then, You can get an entire set of fields data, which is explained in this ACF GET FIELDS IN A GROUP

I was faced with this problem today, figured I'd post my solution. Based on my research, there's no way to access the tab field directly except to find it in the field group and iterate backwards until a field of type tab is found.
Class ACFHelper {
/**
* get_field_group_by_name: gets the first matching field group for a given title
*
* #param $title the title of the acf field group
* #return Array the fields of the field group or false if not found
*
*/
public static function get_field_group_by_name($title) {
$field_group_post = get_posts([
'numberposts' => 1,
'post_type' => 'acf-field-group',
's' => $title
]);
return ( !empty($field_group_post[0]) ? acf_get_fields( $field_group_post[0]->ID ):false );
}
/**
* get_tab_by_field_name: gets the tab of a specified field by iterating backwards through the field group
*
* #param $field_group_title the title of the acf field group
* #param $field_name the name of the field for which we want the tab it's under
* #return Array the field group of the tab or false if not found
*
*/
public static function get_tab_by_field_name($field_group_title, $field_name) {
$field_group = Self::get_field_group_by_name($field_group_title);
if($field_group) {
$field_index = array_search($field_name, array_column($field_group, 'name'));
if($field_index) {
for($field_index; $field_index > -1; $field_index--) {
if( $field_group[$field_index]['type'] == 'tab' )
return $field_group[$field_index];
}
}
}
return false;
}
}
So now if I have a field strawberry under the fruits tab, I can find my fruits tab in my foods field group like:
$tab = ACFHelper::get_tab_by_field('foods', 'strawberry');

Related

How to set the value of an entity reference field with the value of another entity reference field in a hook function - D9

I am using a hook function to create a commerce product when a node type of PL is created.
I have 2 Entity Reference fields:
Name of Project - Which references content
Type of Use - Which references a taxonomy term
Everything works and the title field is set correctly except for the 2 field values, field_name_of_project and field_type_of_use.
What could I be doing wrong.
Here's my code:
<?php
/**
* #file
* file for the PL Product Creation module, which creates a product and product variations when a Pl node is created.
*
*/
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\commerce_product\Entity\ProductVariation;
use Drupal\commerce_product\Entity\Product;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Implementation of hook_entity_insert().
*/
function pl_product_creation_entity_insert(Drupal\Core\Entity\EntityInterface $entity) {
if ($entity->getEntityTypeId() == 'node' && $entity->bundle() == 'pl_node') {
// Get the pl node properties
$name_of_project = $entity->field_name_of_project->entity->getTitle();
$type_of_use = $entity->field_type_of_use->entity->getName();
// Load the product
$pl_product_title = 'Products at ' . $name_of_project;
// Load the product storage.
$entity_type_manager = \Drupal::service('entity_type.manager');
$product_storage = $entity_type_manager->getStorage('commerce_product');
// Load the product by title.
$pl_product = $product_storage->loadByProperties(['title' => $pl_product_title]);
// Check if the Pl product exists
if (!empty($pl_product)) {
// Load the Pl product
$pl_product = reset($pl_product);
}
else {
// Create a new Pl product
$pl_product = Product::create([
'type' => 'pl',
]);
// Set the title field.
$pl_product->setTitle($pl_product_title);
// Set the values of the custom fields.
$pl_product->set('field_name_of_project', $name_of_project);
$pl_product->set('field_type_of_use', $type_of_use);
// Save the product entity.
$pl_product->save();
}
}
}
I was finally able to figure it out. Posting here in case someone needs this in the future.
// Set the values of the custom fields.
$pl_product->set('field_name_of_project', $entity->field_name_of_project->entity);
$pl_product->set('field_type_of_use', $entity->field_type_of_use->entity);

wpallimport - how do I replace sku with product id?

I use a feed from a manufacturer to upload to a WooCommerce custom field.
The custom field expects a product id as input, but the feed is not aware of this as it's created by WooCommerce.
I use SKU as the key to compare existing products with the feed.
The imported field is called {metaparent_sku[1]} and the content has several values separated by | , for example:
BDCD7 | BDAUX | BDCA1 | ADUP89
These are unique product sku’s referring to products already in WooCommerce.
What I’d like to do is replace the sku value with the corresponding product ID value in as it exists already in WooCommerce.
I’ve been trying to use str_replace but can’t figure out how to set it up, if that's possible at all. Or should I use lookup?
If anyone could help with some sample code I'd be very grateful.
To find the product ID by a sku field.
[wc_get_product_id_by_sku({sku[1]})]
So in your case that would be:
[wc_get_product_id_by_sku({metaparent_sku[1]})]
For when you have multiple sku's in one field make a custom function.
Place the following function in the Function Editor.
/**
* Find product id(s) by sku(s)
* #param string $sku
* #return string with product id(s)
*/
function wp_all_import_get_product_ids_by_sku($sku, $seperator = "|") {
// define empty $product_ids
$product_ids = array();
// remove spaces from sku
$sku = str_replace(" ", "", $sku);
// convert to array
$skus = explode($seperator, $sku);
// find product ids
foreach ($skus as $sku) {
$product_ids[] = wc_get_product_id_by_sku($sku);
}
// return product ids
return implode(" | ", $product_ids);
}
Then use it like this
[wp_all_import_get_product_ids_by_sku({metaparent_sku[1]})]

Dynamic number of Contact Form 7 recipients

I am working a on a form with, at it's core, two fieldset: main and "other recipient"; at the end of of the "other recipient" fieldset, I have a "add another recipient" link.
Here's who needs what:
Main recipient: everything
Other recipient: the "other recipient" fieldset;
Sub-sequent recipients: Respective fieldsets
So far, I've been looking at the Documentation but not much luck there, not that I expected any, either.
Edit
I think this is unclear, so I will be a little more explicit as to what is the context. My form is a registration where we can sign up multiple people; one of the fields is labeled "Your email". Since we can register more than one person at once, I need to duplicate the fieldset containing "Your email".
Edit 2
To help clarify, imagine that we are signing up kids for a summer camp. The first fieldset is general, say the parent's billing information, and the second fieldset is the child's information. The parent needs to be able to fill out a single form and dynamically add as many children as the parent desires.
In each of the children's fieldset, their email is required and they receive the information relevant to this child, where the email would be similar to:
Hello {children's name},
You've been registered to StackOverflow Summer Camp. Here's the information you need to know:
[...]
Thanks for being a good sport!
Hope this helps.
When you've got a specific use case like this, shoehorning the functionality into peripherally related plugins often results in frustration. That being said - there are times where you're married to a specific plugin or approach, and just have to build on top of it.
With that caveat out of the way, I think you should approach this from the angle of creating a new fieldtype for Contact Form 7. This way you have control over rendering the field's HTML, the data validation, among other things. It might also provide an launch point for DB storage and sending reminders, later, as you've mentioned in a comment on another answer.
Here's this approach in action:
The new fieldtype is called emailplus, and you include it into a form like this:
<div class="cf7-duplicable-recipients">
<label>Main Recipient (required)</label>
[emailplus emails]
[submit "Sign Up"]
</div>
Additionally, I've set the recipient under the "mail" panel in the form's settings to [emails].
If an emailsplus field is set as the recipient, the class overrides the default wpcf7 behaviour and sends mail to each value in the email array, substituting any [emails] placeholders in the body of the message on a per-email basis.
The emailplus fieldtype is encapsulated here in a class, and commented liberally:
<?php
class WPCF7_Duplicable_Email
{
/**
* The emails this form's field is addressed to
*
* #var array
*/
public $emails = array();
/**
* The name of the tag bearing the emailplus type
*
* #var string
*/
public $tag_name;
/**
* Instantiate the class & glom onto the wpcf7 hooks
*
* #return void
*/
public function __construct()
{
add_action('wpcf7_init', array($this, 'add_emailplus_tag'));
add_action('wpcf7_form_tag', array($this, 'assign_values_to_field'));
add_filter('wpcf7_validate_emailplus', array($this, 'validate'), 2);
add_action('wpcf7_before_send_mail', array($this, 'send_email'));
add_action('wp_enqueue_scripts', array($this, 'emailplus_scripts'));
}
/**
* Add our the [emailplus __YOUR_FIELD_NAME__] shortcode for use in wpcf7 forms.
*
* #uses wpcf7_add_shortcode
*
* #return void
*/
public function add_emailplus_tag()
{
wpcf7_add_shortcode(
array('emailplus'),
array($this, 'shortcode_handler'),
true
);
}
/**
* Renders the HTML for the [emailplus] shortcode
*
* Referenced from wpcf7_text_shortcode_handler in wp-content/plugins/contact-form-7/modules/text.php
*
* #param array $tag The data relating to the emailplus form field.
*
* #uses wpcf7_get_validation_error
* #uses wpcf7_form_controls_class
*
* #return string
*/
public function shortcode_handler(array $tag) {
$tag = new WPCF7_Shortcode($tag);
if (true === empty($tag->name)) {
return '';
}
$validation_error = wpcf7_get_validation_error($tag->name);
$class = wpcf7_form_controls_class($tag->type);
if ($validation_error) {
$class .= ' wpcf7-not-valid';
}
$atts = array(
'class' => $tag->get_class_option($class),
'id' => $tag->get_id_option(),
'tabindex' => $tag->get_option('tabindex', 'int', true),
'aria-invalid' => $validation_error ? 'true' : 'false',
'type' => 'email',
'name' => $tag->name.'[]', // <-- Important! the trailing [] Tells PHP this is an array of values
'value' => $tag->get_default_option()
);
$atts = wpcf7_format_atts($atts);
$html = sprintf(
'<div class="emailplus-wrapper %1$s"><input %2$s />%3$s</div>',
sanitize_html_class($tag->name),
$atts,
$validation_error
);
// We identify the container that will hold cloned fields with the [data-wpcf7-duplicable-email] attr
return '<div class="wpcf7-form-control-wrap %1$s" data-wpcf7-duplicable-email>'.$html.'</div>';
}
/**
* Validates the value of the emailplus tag.
*
* Must be handled separately from other text-based form inputs,
* since the submitted POST value is an array.
*
* We can safely assume emailplus while creating the WPCF7_Shortcode,
* because we know we hooked this function onto 'wpcf7_validate_emailplus'
*
* #uses wpcf7_is_email
* #uses WPCF7_Validation::invalidate
*
* #param WPCF7_Validation $result The validation helper class from wpcf7.
* #param array $tag The array of values making up our emailplus tag
*
*/
public function validate(WPCF7_Validation $result, $tag)
{
$tag = new WPCF7_Shortcode(
array(
'basename' => 'emailplus',
'name' => $this->tag_name,
'raw_values' => $this->emails
)
);
// Check each value of the form field.
// Emails must be validated individually.
foreach($tag->raw_values as $email) {
if (false === wpcf7_is_email($email)) {
$result->invalidate($tag, wpcf7_get_message('invalid_email'));
}
}
return $result;
}
/**
* For completeness' sake, manually assign the value to the emailplus fieldtype.
*
* Wpcf7 doesn't know how to treat our fieldtype's value by default.
*
* As a side effect, this sets the email addresses that are up for delivery.
*
* #param array $scanned_tag The tag that wpcf7 is scanning through, and processing.
*
* #return $array;
*/
public function assign_values_to_field($scanned_tag)
{
if ($scanned_tag['type'] !== 'emailplus') {
return $scanned_tag;
}
$this->tag_name = $scanned_tag['name'];
if (key_exists($scanned_tag['name'], $_POST)) {
// Stores the emails on a class property for use later.
$this->emails = $_POST[$scanned_tag['name']];
$scanned_tag['raw_values'] = $this->emails;
$scanned_tag['values'] = $this->emails;
}
return $scanned_tag;
}
/**
* Performs a substitution on the emailplus field's fieldname, on a per-value basis.
*
* Ex. in two parts
* 1 - The shortcode [emailsplus emails] renders into <input type="email" name="emails[]" value="" >
* Which the user clones and submits, processing into something like
* ['test1#gmail.com', 'test2#gmail.com'].
* 2 - The user has set [emails] as the recipient in the "mail" panel for the form.
*
* Because wpcf7 isn't aware of how to process a field w/ multiple values when emailing,
* we loop over the values of [emails], replace the tag, and invoke WPCF7_Mail::send()
* for each value.
*
* #param WPCF7_ContactForm $form The contact form object.
*
* #uses WPCF7_Mail::send
*
* #return void
*/
public function send_email(WPCF7_ContactForm $form)
{
$placeholder = '['.$this->tag_name.']';
if (false === strpos($form->prop('mail'), $placeholder)) {
return;
}
foreach ($this->emails as $email) {
$template = $form->prop('mail');
$template['recipient'] = str_replace($placeholder, $email, $template['recipient']);
$template['body'] = str_replace($placeholder, $email, $template['body']);
WPCF7_Mail::send($template);
}
// Tell cf7 to skip over the default sending behaviour in WPCF7_Submission->mail()
$form->skip_mail = true;
}
/**
* Adds our js that will clone the emailplus field.
*
* Could be optimized with a conditional that checks if there is a form with the [emailplus]
* fieldtype somewhere on the page
*
* #return void
*/
public function emailplus_scripts()
{
wp_enqueue_script(
'cf7-duplication',
get_template_directory_uri() . '/js/cf7-duplication.js',
array('jquery'),
'20161006',
true
);
}
}
$wpcf7DuplicableEmail = new WPCF7_Duplicable_Email();
And, the .js file that handles the cloning. It should live in /path/to/your/theme/js/cf7-duplication.js'.
(function($) {
$(document).ready(function() {
var addEmailField = function(inputRow, container) {
inputRow.find('input[type=email]').val('');
var removeButton = $('×')
.click(function(e) {
e.preventDefault();
inputRow.remove();
});
inputRow.append(removeButton);
inputRow.insertAfter(container.find('.emailplus-wrapper').last());
}
$.each($('[data-wpcf7-duplicable-email]'), function(i, el) {
var container = $(el);
var inputRow = container.find('.emailplus-wrapper');
var addButton = $('Add another email');
addButton.click(function(e) {
e.preventDefault();
var newEmailField = inputRow.clone();
addEmailField(newEmailField, container);
});
container.append(addButton);
});
});
})(jQuery);
Last, but not least, if you'd like the form to fadeout when it's valid and the emails have gone out, add this to the "additional settings" panel.
on_sent_ok: "jQuery('.cf7-duplicable-recipients').fadeOut();"
This approach is best for CF7's AJAX submissions. If you want to extend it to handle vanilla POST requests, you could update the shortcode handler to render multiple <input> fields where you need to preserve the value attr on invalid submissions.
Options
1) Under the Mail tap in the setup menu, after you click Mail(2), in the TO: field add this line. Should the parent have less than the number of kids stated, the extra email addresses should do nothing. [email],[email] is the basic format.
[parents-email], [kid-email1], [kid-email2], [kid-email3], [kid-email4], [kid-email5], [kid-email6], [kid-email7]
Option 2) In the TO: field, just place 1 email address such as the parents. and in the Additional Headers: place the code below
CC: [kid-email1], [kid-email2], [kid-email3], [kid-email4], [kid-email5], [kid-email6], [kid-email7]
or
BCC: [kid-email1], [kid-email2], [kid-email3], [kid-email4], [kid-email5], [kid-email6], [kid-email7]
A possible problem that could arise: Many hosts block things like this to prevent spam. If theses do not work, then this is probably the case. You would need to contact your hosting provider about removing the block.
Contact form 7 has only one recipient field, however you can enter multiple email address with comma separated in that field, example "email1#domain.com,email2#domain.com,email3#domain.com".
So for your case, use JavaScript to add multiple duplicate recipient fields dynamically, and finally on form submit write a JavaScript function to concat all the recipient email addresses then keep that in main recipient field and submit the form. Hope you got my point.

Update a field after Linking / Unlinking Many-Many records in SilverStripe

I have created a Customer DataObject by extending Member. Customer has a many_many data relation with a Package DataObject.
I would like increment/decrement a Credits field in the Customer DataObject when a Package is linked / unlinked through the CMS based on the Limit field in the Package table.
Customer
class Customer extends Member {
private static $db = array(
'Gender' => 'Varchar(2)',
'DateOfBirth' => 'Date',
'Featured' => 'Boolean',
'Credits' => 'Int'
);
private static $many_many = array(
'Packages' => 'Package'
);
public function getCMSFields() {
$fields = new FieldList();
$config = GridFieldConfig_RelationEditor::create();
$config->removeComponentsByType('GridFieldAddNewButton');
$packageField = new GridField(
'Packages',
'Package',
$this->Packages(),
$config
);
$fields->addFieldToTab('Root.Package', $packageField);
Session::set('SingleID', $this->ID);
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
Package
class Package extends DataObject {
private static $db = array(
'Title' => 'Varchar(255)',
'Limit' => 'Int'
);
private static $belongs_many_many = array(
'Customers' => 'Customer'
);
}
When you create or delete many to many relationship just one record is modified in your database - the one in table which joins elements of both sides of the relationship. Therefore neither object the relationship is based on is updated. This is why methods like: onBeforeWrite, onAfterWrite, onBeforeDelete and onAfterDelete will not be called at all and you cannot use them to detect such change.
However, Silverstripe provides class ManyManyList which is responsible for all operations connected to many to many relationships. There are two methods which are of your interest: add and remove. You can override them and put inside action to do what you need. These methods are obviously called on each link or unlink operation no matter object types are, so you should make some filtering on classes you are particularly interested in.
The proper way to override the ManyManyList class is to use Injector mechanism, so as not to modify anything inside the framework or cms folder. The example below uses relationship between Members and Groups in Silverstripe but you can easily adopt it to your need (Customer -> Member; Package -> Group).
app.yml
Injector:
ManyManyList:
class: ManyManyListExtended
ManyManyListExtended.php
/**
* When adding or removing elements on a many to many relationship
* neither side of the relationship is updated (written or deleted).
* SilverStripe does not provide any built-in actions to get information
* that such event occurs. This is why this class is created.
*
* When it is uses together with SilverStripe Injector mechanism it can provide
* additional actions to run on many-to-many relations (see: class ManyManyList).
*/
class ManyManyListExtended extends ManyManyList {
/**
* Overwritten method for adding new element to many-to-many relationship.
*
* This is called for all many-to-many relationships combinations.
* 'joinTable' field is used to make actions on specific relation only.
*
* #param mixed $item
* #param null $extraFields
* #throws Exception
*/
public function add($item, $extraFields = null) {
parent::add($item, $extraFields);
if ($this->isGroupMembershipChange()) {
$memberID = $this->getMembershipID($item, 'MemberID');
$groupID = $this->getMembershipID($item, 'GroupID');
SS_Log::log("Member ($memberID) added to Group ($groupID)", SS_Log::INFO);
// ... put some additional actions here
}
}
/**
* Overwritten method for removing item from many-to-many relationship.
*
* This is called for all many-to-many relationships combinations.
* 'joinTable' field is used to make actions on specific relation only.
*
* #param DataObject $item
*/
public function remove($item) {
parent::remove($item);
if ($this->isGroupMembershipChange()) {
$memberID = $this->getMembershipID($item, 'MemberID');
$groupID = $this->getMembershipID($item, 'GroupID');
SS_Log::log("Member ($memberID) removed from Group ($groupID)", SS_Log::INFO);
// ... put some additional actions here
}
}
/**
* Check if relationship is of Group-Member type.
*
* #return bool
*/
private function isGroupMembershipChange() {
return $this->getJoinTable() === 'Group_Members';
}
/**
* Get the actual ID for many-to-many relationship part - local or foreign key value.
*
* This works both ways: make action on a Member being element of a Group OR
* make action on a Group being part of a Member.
*
* #param DataObject|int $item
* #param string $keyName
* #return bool|null
*/
private function getMembershipID($item, $keyName) {
if ($this->getLocalKey() === $keyName)
return is_object($item) ? $item->ID : $item;
if ($this->getForeignKey() === $keyName)
return $this->getForeignID();
return false;
}
}
The solution provided by 3dgoo should also work fine but IMO that code does much more "hacking" and that's why it is much less maintainable. It demands more modifications (in both classes) and needs to be multiplied if you would like to do any additional link/unlink managing, like adding custom admin module or some forms.
The problem is when adding or removing items on a many to many relationship neither side of the relationship is written. Therefore onAfterWrite and onBeforeWrite is not called on either object.
I've come across this problem before. The solution I used isn't great but it was the only thing that worked for me.
What we can do is set an ID list of Packages to a session variable when getCMSFields is called. Then when an item is added or removed on the grid field we refresh the CMS panel to call getCMSFields again. We then retrieve the previous list and compare it to the current list. If the lists are different we can do something.
Customer
class Customer extends Member {
// ...
public function getCMSFields() {
// Some JavaScript to reload the panel each time a package is added or removed
Requirements::javascript('/mysite/javascript/cms-customer.js');
// This is the code block that saves the package id list and checks if any changes have been made
if ($this->ID) {
if (Session::get($this->ID . 'CustomerPackages')) {
$initialCustomerPackages = json_decode(Session::get($this->ID . 'CustomerPackages'), true);
$currentCustomerPackages = $this->Packages()->getIDList();
// Check if the package list has changed
if($initialCustomerPackages != $currentCustomerPackages) {
// In here is where you put your code to do what you need
}
}
Session::set($this->ID . 'CustomerPackages', json_encode($this->Packages()->getIDList()));
}
$fields = parent::getCMSFields();
$config = GridFieldConfig_RelationEditor::create();
$config->removeComponentsByType('GridFieldAddNewButton');
$packageField = GridField::create(
'Packages',
'Package',
$this->Packages(),
$config
);
// This class needs to be added so our javascript gets called
$packageField->addExtraClass('refresh-on-reload');
$fields->addFieldToTab('Root.Package', $packageField);
Session::set('SingleID', $this->ID);
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
The if ($this->ID) { ... } code block is where all our session code happens. Also note we add a class to our grid field so our JavaScript refresh works $packageField->addExtraClass('refresh-on-reload');
As mentioned before, we need to add some JavaScript to reload the panel each time a package is added or removed from the list.
cms-customer.js
(function($) {
$.entwine('ss', function($){
$('.ss-gridfield.refresh-on-reload').entwine({
reload: function(e) {
this._super(e);
$('.cms-content').addClass('loading');
$('.cms-container').loadPanel(location.href, null, null, true);
}
});
});
})(jQuery);
Inside the if($initialCustomerPackages != $currentCustomerPackages) { ... } code block there are a number of things you can do.
You could use $this->Packages() to fetch all the current packages associated to this customer.
You could call array_diff and array_merge to get just the packages that have been added and removed:
$changedPackageIDs = array_merge(array_diff($initialCustomerPackages, $currentCustomerPackages), array_diff($currentCustomerPackages, $initialCustomerPackages));
$changedPackages = Package::get()->byIDs($changedPackageIDs);
The above code will add this functionality to the Customer side of the relationship. If you also want to manage the many to many relationship on the Package side of the relationship you will need to add similar code to the Package getCMSFields function.
Hopefully someone can come up with a nicer solution. If not, I hope this works for you.
note: Didn't actually check does the model work but by visually checking this should help you:
On the link you provided you are using
$customer = Customer::get()->Filter...
That returns a DataList of objects, not a singular object unless you specify what is the object you want from the DataList.
If you are filtering the Customers then you want to get a SPECIFIC customer from the DataList, e.g. the first one in this case.
$customer = Customer::get()->filter(array('ID' => $this->CustomerID))->first();
But You should be able to get the singular DataObject with:
$customer = $this->Customer();
As you are defining the Customer as "has_one". If the relation was a Has many, using () would get you a DataList of objects.
Protip:
You don't need to write our own debug files in SilverStripe. It has own functions for it. For example Debug::log("yay"); what writes the output to a file and Debug::dump("yay") that dumps it directly out.
Tip is that you can check what is the object that you accessing right. Debug::dump(get_class($customer)); would output only the class of the object.

OR operator in Drupal View Filters

I need to implement an OR operator between some filters in a Drupal View.
By default, Drupal AND's every filter together.
By using
hook_views_query_alter(&$view, &$query)
I can access the query ( var $query ) , and I can change either :
$query->where[0]['type']
to 'OR', or
$query->group_operator
to 'OR'
The problem is however, that I do not need OR's everywhere. I've tried changing both of them to OR seperately, and it doesn't yield the desired result.
It seems changing those values, puts OR's everywhere, while I need => ( filter 1 AND filter 2 ) OR ( filter 3 ), so just 1 OR.
I could just check the Query of the View, copy it, modify it, and run it through db_query, but that's just dirty ..
Any suggestions ?
Thx in advance.
If you are using Views 3 / Drupal 7 and looking for the answer to this question, it is baked right into Views. Where it says "add" next to filters, click the dropdown, then click "and/or; rearrange". It should be obvious from there.
Unfortunately this is still a missing feature in Views2. It has long been asked for and was promised a while ago, but seems to be a tricky piece of work and is now scheduled for Views3.
In the meantime you could try the Views Or module mentioned in that thread. As of today, it is still in dev status, but seems to be actively maintained and the issue queue does not look to bad, so you might want to give it a try.
if you want do it with view_query_alter hook, you should use $query->add_where() where you can specify if it's AND or OR. From views/include/query.inc
/**
* Add a simple WHERE clause to the query. The caller is responsible for
* ensuring that all fields are fully qualified (TABLE.FIELD) and that
* the table already exists in the query.
*
* #param $group
* The WHERE group to add these to; groups are used to create AND/OR
* sections. Groups cannot be nested. Use 0 as the default group.
* If the group does not yet exist it will be created as an AND group.
* #param $clause
* The actual clause to add. When adding a where clause it is important
* that all tables are addressed by the alias provided by add_table or
* ensure_table and that all fields are addressed by their alias wehn
* possible. Please use %d and %s for arguments.
* #param ...
* A number of arguments as used in db_query(). May be many args or one
* array full of args.
*/
function add_where($group, $clause)
I added it by concatenating the string.
It is relatively specific to the implementation - people would need to play with field to match for OR - node.title in the following code and the field to match it with - node_revisions.body in this case.
Extra piece of code to make sure that node_revisions.body is in the query.
/**
* Implementation of hook_views_api().
*/
function eventsor_views_api() { // your module name into hook_views_api
return array(
'api' => 2,
// might not need the line below, but in any case, the last arg is the name of your module
'path' => drupal_get_path('module', 'eventsor'),
);
}
/**
*
* #param string $form
* #param type $form_state
* #param type $form_id
*/
function eventsor_views_query_alter(&$view, &$query) {
switch ($view->name) {
case 'Events':
_eventsor_composite_filter($query);
break;
}
}
/**
* Add to the where clause.
* #param type $query
*/
function _eventsor_composite_filter(&$query) {
// If we see "UPPER(node.title) LIKE UPPER('%%%s%%')" - then add and to it.
if (isset($query->where)) {
$where_count = 0;
foreach ($query->where as $where) {
$clause_count = 0;
if (isset($where['clauses'])) {
foreach ($where['clauses'] as $clause) {
$search_where_clause = "UPPER(node.title) LIKE UPPER('%%%s%%')";
// node_data_field_long_description.field_long_description_value
$desirable_where_clause = "UPPER(CONCAT_WS(' ', node.title, node_revisions.body)) LIKE UPPER('%%%s%%')";
if ($clause == $search_where_clause) {
// $query->add_where('or', 'revisions.body = %s'); - outside of what we are looking for
$query->where[$where_count]['clauses'][$clause_count] = $desirable_where_clause;
// Add the field to the view, just in case.
if (!isset($query->fields['node_revisions_body'])) {
$query->fields['node_revisions_body'] = array(
'field' => 'body',
'table' => 'node_revisions',
'alias' => 'node_revisions_body'
);
}
}
$clause_count++;
}
}
$where_count++;
}
}
}

Resources