Skip to content

Instantly share code, notes, and snippets.

@StryKaizer
Last active October 13, 2022 14:44
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save StryKaizer/ae1cb9abc4844a9e7ac12317a9d84a78 to your computer and use it in GitHub Desktop.
Save StryKaizer/ae1cb9abc4844a9e7ac12317a9d84a78 to your computer and use it in GitHub Desktop.
Exposed filters for entity references -> nodes. Port from TaxonomyIndexTid.php
<?php
namespace Drupal\yourmodule\Plugin\views\filter;
use Drupal\Core\Entity\Element\EntityAutocomplete;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeStorageInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\filter\ManyToOne;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Filter by node id.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("node_index_nid")
*/
class NodeIndexNid extends ManyToOne {
// Stores the exposed input for this filter.
public $validated_exposed_input = NULL;
/**
* NodeType storage handler.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeTypeStorage;
/**
* The node storage.
*
* @var \Drupal\node\NodeStorageInterface
*/
protected $nodeStorage;
/**
* Constructs a NodeIndexNid object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $node_type_storage
* The node storage.
* @param \Drupal\node\NodeStorageInterface $node_storage
* The node storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $node_type_storage, NodeStorageInterface $node_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->nodeTypeStorage = $node_type_storage;
$this->nodeStorage = $node_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.manager')->getStorage('node_type'),
$container->get('entity.manager')->getStorage('node')
);
}
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
if (!empty($this->definition['node'])) {
$this->options['bundle'] = $this->definition['node'];
}
}
public function hasExtraOptions() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getValueOptions() {
return $this->valueOptions;
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['type'] = array('default' => 'textfield');
$options['limit'] = array('default' => TRUE);
$options['bundle'] = array('default' => '');
$options['error_message'] = array('default' => TRUE);
return $options;
}
public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) {
$bundles = $this->nodeTypeStorage->loadMultiple();
$options = array();
foreach ($bundles as $bundle) {
$options[$bundle->id()] = $bundle->label();
}
if ($this->options['limit']) {
// We only do this when the form is displayed.
if (empty($this->options['bundle'])) {
$first_bundle = reset($bundles);
$this->options['bundle'] = $first_bundle->id();
}
if (empty($this->definition['bundle'])) {
$form['bundle'] = array(
'#type' => 'radios',
'#title' => $this->t('Bundle'),
'#options' => $options,
'#description' => $this->t('Select which bundle to show nodes for in the regular options.'),
'#default_value' => $this->options['bundle'],
);
}
}
$form['type'] = array(
'#type' => 'radios',
'#title' => $this->t('Selection type'),
'#options' => array(
'select' => $this->t('Dropdown'),
'textfield' => $this->t('Autocomplete')
),
'#default_value' => $this->options['type'],
);
}
protected function valueForm(&$form, FormStateInterface $form_state) {
$bundle = $this->nodeTypeStorage->load($this->options['bundle']);
if (empty($bundle) && $this->options['limit']) {
$form['markup'] = array(
'#markup' => '<div class="js-form-item form-item">' . $this->t('An invalid type is selected. Please change it in the options.') . '</div>',
);
return;
}
if ($this->options['type'] == 'textfield') {
$nodes = $this->value ? Node::loadMultiple(($this->value)) : array();
$form['value'] = array(
'#title' => $this->options['limit'] ? $this->t('Select nodes from bundle @bundle', array('@bundle' => $bundle->label())) : $this->t('Select content'),
'#type' => 'textfield',
'#default_value' => EntityAutocomplete::getEntityLabels($nodes),
);
if ($this->options['limit']) {
$form['value']['#type'] = 'entity_autocomplete';
$form['value']['#target_type'] = 'node';
$form['value']['#selection_settings']['target_bundles'] = array($bundle->id());
$form['value']['#tags'] = TRUE;
$form['value']['#process_default_value'] = FALSE;
}
}
else {
$options = array();
$query = \Drupal::entityQuery('node')
// @todo Sorting on bundle properties -
->sort('title')
->addTag('node_access');
if ($this->options['limit']) {
$query->condition('type', $bundle->id());
}
$nodes = Node::loadMultiple($query->execute());
foreach ($nodes as $node) {
$options[$node->id()] = \Drupal::entityManager()
->getTranslationFromContext($node)
->label();
}
$default_value = (array) $this->value;
if ($exposed = $form_state->get('exposed')) {
$identifier = $this->options['expose']['identifier'];
if (!empty($this->options['expose']['reduce'])) {
$options = $this->reduceValueOptions($options);
if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
$default_value = array();
}
}
if (empty($this->options['expose']['multiple'])) {
if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
$default_value = 'All';
}
elseif (empty($default_value)) {
$keys = array_keys($options);
$default_value = array_shift($keys);
}
// Due to #1464174 there is a chance that array('') was saved in the admin ui.
// Let's choose a safe default value.
elseif ($default_value == array('')) {
$default_value = 'All';
}
else {
$copy = $default_value;
$default_value = array_shift($copy);
}
}
}
$form['value'] = array(
'#type' => 'select',
'#title' => $this->options['limit'] ? $this->t('Select nodes from bundle @bundle', array('@bundle' => $bundle->label())) : $this->t('Select content'),
'#multiple' => TRUE,
'#options' => $options,
'#size' => min(9, count($options)),
'#default_value' => $default_value,
);
$user_input = $form_state->getUserInput();
if ($exposed && isset($identifier) && !isset($user_input[$identifier])) {
$user_input[$identifier] = $default_value;
$form_state->setUserInput($user_input);
}
}
if (!$form_state->get('exposed')) {
// Retain the helper option
$this->helper->buildOptionsForm($form, $form_state);
// Show help text if not exposed to end users.
$form['value']['#description'] = t('Leave blank for all. Otherwise, the first selected term will be the default instead of "Any".');
}
}
protected function valueValidate($form, FormStateInterface $form_state) {
// We only validate if they've chosen the text field style.
if ($this->options['type'] != 'textfield') {
return;
}
$tids = array();
if ($values = $form_state->getValue(array('options', 'value'))) {
foreach ($values as $value) {
$tids[] = $value['target_id'];
}
}
$form_state->setValue(array('options', 'value'), $tids);
}
public function acceptExposedInput($input) {
if (empty($this->options['exposed'])) {
return TRUE;
}
// We need to know the operator, which is normally set in
// \Drupal\views\Plugin\views\filter\FilterPluginBase::acceptExposedInput(),
// before we actually call the parent version of ourselves.
if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) {
$this->operator = $input[$this->options['expose']['operator_id']];
}
// If view is an attachment and is inheriting exposed filters, then assume
// exposed input has already been validated
if (!empty($this->view->is_attachment) && $this->view->display_handler->usesExposed()) {
$this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']];
}
// If we're checking for EMPTY or NOT, we don't need any input, and we can
// say that our input conditions are met by just having the right operator.
if ($this->operator == 'empty' || $this->operator == 'not empty') {
return TRUE;
}
// If it's non-required and there's no value don't bother filtering.
if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) {
return FALSE;
}
$rc = parent::acceptExposedInput($input);
if ($rc) {
// If we have previously validated input, override.
if (isset($this->validated_exposed_input)) {
$this->value = $this->validated_exposed_input;
}
}
return $rc;
}
public function validateExposed(&$form, FormStateInterface $form_state) {
if (empty($this->options['exposed'])) {
return;
}
$identifier = $this->options['expose']['identifier'];
// We only validate if they've chosen the text field style.
if ($this->options['type'] != 'textfield') {
if ($form_state->getValue($identifier) != 'All') {
$this->validated_exposed_input = (array) $form_state->getValue($identifier);
}
return;
}
if (empty($this->options['expose']['identifier'])) {
return;
}
if ($values = $form_state->getValue($identifier)) {
foreach ($values as $value) {
$this->validated_exposed_input[] = $value['target_id'];
}
}
}
protected function valueSubmit($form, FormStateInterface $form_state) {
// prevent array_filter from messing up our arrays in parent submit.
}
public function buildExposeForm(&$form, FormStateInterface $form_state) {
parent::buildExposeForm($form, $form_state);
if ($this->options['type'] != 'select') {
unset($form['expose']['reduce']);
}
$form['error_message'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Display error message'),
'#default_value' => !empty($this->options['error_message']),
);
}
public function adminSummary() {
// set up $this->valueOptions for the parent summary
$this->valueOptions = array();
if ($this->value) {
$this->value = array_filter($this->value);
$nodes = Node::loadMultiple($this->value);
foreach ($nodes as $node) {
$this->valueOptions[$node->id()] = \Drupal::entityManager()
->getTranslationFromContext($node)
->label();
}
}
return parent::adminSummary();
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$contexts = parent::getCacheContexts();
// The result potentially depends on term access and so is just cacheable
// per user.
// @todo See https://www.drupal.org/node/2352175.
$contexts[] = 'user';
return $contexts;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
$bundle = $this->nodeTypeStorage->load($this->options['bundle']);
$dependencies[$bundle->getConfigDependencyKey()][] = $bundle->getConfigDependencyName();
foreach ($this->nodeStorage->loadMultiple($this->options['value']) as $term) {
$dependencies[$term->getConfigDependencyKey()][] = $term->getConfigDependencyName();
}
return $dependencies;
}
}
@mattc321
Copy link

mattc321 commented Jan 1, 2019

You will need to add a schema file to declare the configuration property of this filter if you wish to save it with a default value. If not you will reach an exception when saving the view.

+/yourmodule/config/schema/yourmodule.schema.yml

# Schema definition for select filter.
views.filter.node_index_nid:
  type: views.filter.in_operator

@top-master
Copy link

Below is based on SO Post:

You need to place this file in a custom module at yourmodule/src/Plugin/views/filter/NodeIndexNid.php, and also implement hook_views_data_alter in your yourmodule.module file, like this:

/**
 * Implements hook_field_views_data_alter().
 *
 * Views integration for entity reference fields which reference nodes.
 * Adds a term relationship to the default field data.
 *
 * @see views_field_default_views_data()
 */
function yourmodule_field_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
  if ($field_storage->getType() == 'entity_reference' && $field_storage->getSetting('target_type') == 'node') {
    foreach ($data as $table_name => $table_data) {
      foreach ($table_data as $field_name => $field_data) {
        if (isset($field_data['filter']) && $field_name != 'delta') {
          $data[$table_name][$field_name]['filter']['id'] = 'node_index_nid';
        }
      }
    }
  }
}

@akishankar
Copy link

Hi,
I have done the suggested changes in the custom module as below, but could not manage to get the option of select list in the exposed filters (please note that I am also using BEF modules):

modules\custom\entitydropdown\config\schema\entitydropdown.schema.yml
modules\custom\entitydropdown\entitydropdown.info.yml
modules\custom\entitydropdown\entitydropdown.module
modules\custom\entitydropdown\src\Plugin\views\filter\NodeIndexNid.php

With respect to code level I have not changed anything and utilizing as is uploded in github (namespace/hook name has been updated with appropriate module name i.e. entitydropdown).

Are you able to suggest please if I am missing any configurations ?
Thanks

@aceraven777
Copy link

I have implemented @ehsan-yaqubi comment and I encountered an error when I use FieldStorageConfigInterface, the error is suggesting me to use FieldStorageConfig instead:

use Drupal\field\Entity\FieldStorageConfig;

/**
 * Implements hook_field_views_data_alter().
 *
 * Views integration for entity reference fields which reference nodes.
 * Adds a term relationship to the default field data.
 *
 * @see views_field_default_views_data()
 */
function custom_node_view_filter_field_views_data_alter(array &$data, FieldStorageConfig $field_storage) {
    if ($field_storage->getType() == 'entity_reference' && $field_storage->getSetting('target_type') == 'node') {
        foreach ($data as $table_name => $table_data) {
            foreach ($table_data as $field_name => $field_data) {
                if (isset($field_data['filter']) && $field_name != 'delta') {
                    $data[$table_name][$field_name]['filter']['id'] = 'node_index_nid';
                }
            }
        }
    }
}

@zagrad
Copy link

zagrad commented Oct 13, 2022

I too can't get it to work. I keep getting the following error message when trying to add a node reference field as exposed filter. I can't find how to fix it. It seems something is missing in the "definition", but I know too little about Views plugins to fix it at this point.

ErrorException: Notice: Undefined index: original_configuration in Drupal\views\Plugin\views\filter\Broken->buildOptionsForm() (line 56 of core/modules/views/src/Plugin/views/BrokenHandlerTrait.php).

Drupal\views_ui\Form\Ajax\ConfigHandler->buildForm(Array, Object)
call_user_func_array(Array, Array) (Line: 531)
Drupal\Core\Form\FormBuilder->retrieveForm('views_ui_config_item_form', Object) (Line: 278)
Drupal\Core\Form\FormBuilder->buildForm('Drupal\views_ui\Form\Ajax\ConfigHandler', Object) (Line: 215)
Drupal\views_ui\Form\Ajax\ViewsFormBase->Drupal\views_ui\Form\Ajax\{closure}() (Line: 564)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 217)
Drupal\views_ui\Form\Ajax\ViewsFormBase->ajaxFormWrapper('Drupal\views_ui\Form\Ajax\ConfigHandler', Object) (Line: 150)
Drupal\views_ui\Form\Ajax\ViewsFormBase->getForm(Object, 'index', 'ajax') (Line: 36)
Drupal\views_ui\Form\Ajax\AddHandler->getForm(Object, 'index', 'ajax', 'filter')
call_user_func_array(Array, Array) (Line: 123)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 564)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 124)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) (Line: 97)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 158)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 80)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 320)
Drupal\cleantalk\EventSubscriber\BootSubscriber->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 709)
Drupal\Core\DrupalKernel->handle(Object) (Line: 28)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment