Skip to content
......@@ -22,7 +22,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
class FacetForm extends EntityForm {
/**
* The facet storage controller.
* The facet storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
......@@ -74,22 +74,6 @@ class FacetForm extends EntityForm {
return new static($entity_type_manager, $facet_source_plugin_manager, $processor_plugin_manager);
}
/**
* Gets the form entity.
*
* The form entity which has been used for populating form element defaults.
* This method is defined on the \Drupal\Core\Entity\EntityFormInterface and
* has the same contents there, we only extend to add the correct return type,
* this makes IDE's smarter about the other places where we use
* $this->getEntity().
*
* @return \Drupal\facets\FacetInterface
* The current form facet entity.
*/
public function getEntity() {
return $this->entity;
}
/**
* Retrieves the facet storage controller.
*
......@@ -173,6 +157,18 @@ class FacetForm extends EntityForm {
],
];
$form['url_alias'] = [
'#type' => 'machine_name',
'#title' => $this->t('The name of the facet for usage in URLs'),
'#default_value' => $facet->getUrlAlias(),
'#maxlength' => 50,
'#required' => TRUE,
'#machine_name' => [
'exists' => [$this->getFacetStorage(), 'load'],
'source' => ['name'],
],
];
$facet_sources = [];
foreach ($this->getFacetSourcePluginManager()->getDefinitions() as $facet_source_id => $definition) {
$facet_sources[$definition['id']] = !empty($definition['label']) ? $definition['label'] : $facet_source_id;
......@@ -222,9 +218,10 @@ class FacetForm extends EntityForm {
}
/**
* Form submission handler for the facet source subform.
* Handles form submissions for the facet source subform.
*/
public function submitAjaxFacetSourceConfigForm($form, FormStateInterface $form_state) {
$form_state->setValue('id', NULL);
$form_state->setRebuild();
}
......@@ -247,10 +244,10 @@ class FacetForm extends EntityForm {
$facet_source_id = $this->getEntity()->getFacetSourceId();
if (!is_null($facet_source_id) && $facet_source_id !== '') {
/** @var \Drupal\facets\FacetSource\FacetSourceInterface $facet_source */
$facet_source = $this->getFacetSourcePluginManager()->createInstance($facet_source_id);
/** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */
$facet_source = $this->getFacetSourcePluginManager()->createInstance($facet_source_id, ['facet' => $this->getEntity()]);
if ($config_form = $facet_source->buildConfigurationForm([], $form_state, $this->getEntity(), $facet_source)) {
if ($config_form = $facet_source->buildConfigurationForm([], $form_state)) {
$form['facet_source_configs'][$facet_source_id]['#type'] = 'container';
$form['facet_source_configs'][$facet_source_id]['#title'] = $this->t('%plugin settings', ['%plugin' => $facet_source->getPluginDefinition()['label']]);
$form['facet_source_configs'][$facet_source_id] += $config_form;
......@@ -263,6 +260,13 @@ class FacetForm extends EntityForm {
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$facet_source_id = $form_state->getValue('facet_source_id');
if (!is_null($facet_source_id) && $facet_source_id !== '') {
/** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */
$facet_source = $this->getFacetSourcePluginManager()->createInstance($facet_source_id, ['facet' => $this->getEntity()]);
$facet_source->validateConfigurationForm($form, $form_state);
}
}
/**
......@@ -277,7 +281,6 @@ class FacetForm extends EntityForm {
if ($is_new) {
// On facet creation, enable all locked processors by default, using their
// default settings.
$initial_settings = [];
$stages = $this->getProcessorPluginManager()->getProcessingStages();
$processors_definitions = $this->getProcessorPluginManager()->getDefinitions();
......@@ -289,39 +292,38 @@ class FacetForm extends EntityForm {
$weights[$stage_id] = $processor['stages'][$stage_id];
}
}
$initial_settings[$processor_id] = array(
$facet->addProcessor([
'processor_id' => $processor_id,
'weights' => $weights,
'settings' => [],
);
]);
}
}
$facet->setOption('processors', $initial_settings);
// Set a default widget for new facets.
$facet->setWidget('links');
// Set default empty behaviour.
$facet->setOption('empty_behavior', ['behavior' => 'none']);
$facet->setEmptyBehavior(['behavior' => 'none']);
$facet->setOnlyVisibleWhenFacetSourceIsVisible(TRUE);
}
// Make sure the field identifier is copied from within the facet source
// config to the facet object and saved there.
$facet_source = $form_state->getValue('facet_source_id');
$field_identifier = $form_state->getValue('facet_source_configs')[$facet_source]['field_identifier'];
$facet->setFieldIdentifier($field_identifier);
$facet_source_id = $form_state->getValue('facet_source_id');
if (!is_null($facet_source_id) && $facet_source_id !== '') {
/** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */
$facet_source = $this->getFacetSourcePluginManager()->createInstance($facet_source_id, ['facet' => $this->getEntity()]);
$facet_source->submitConfigurationForm($form, $form_state);
}
$facet->save();
// Ensure that the caching of the view display is disabled, so the search
// correctly returns the facets. This is a temporary fix, until the cache
// metadata is correctly stored on the facet block. Only apply this when the
// facet source type is actually something this is related to views.
list($type,) = explode(':', $facet_source);
list($type,) = explode(':', $facet_source_id);
if ($type === 'search_api_views') {
list(, $view_id, $display) = explode(':', $facet_source);
list(, $view_id, $display) = explode(':', $facet_source_id);
}
if (isset($view_id)) {
......@@ -334,13 +336,14 @@ class FacetForm extends EntityForm {
if ($is_new) {
if (\Drupal::moduleHandler()->moduleExists('block')) {
$message = $this->t('Facet %name has been created. Go to the <a href=":block_overview">Block overview page</a> and add a new "Facet block". If this is your first and only facet, just adding that block make it link to this facet, if you have addded more facets already, please make sure to select the correct Facet to render.', ['%name' => $facet->getName(), ':block_overview' => \Drupal::urlGenerator()->generateFromRoute('block.admin_display')]);
$message = $this->t('Facet %name has been created. Go to the <a href=":block_overview">Block overview page</a> to place the new block in the desired region.', ['%name' => $facet->getName(), ':block_overview' => \Drupal::urlGenerator()->generateFromRoute('block.admin_display')]);
drupal_set_message($message);
$form_state->setRedirect('entity.facets_facet.display_form', ['facets_facet' => $facet->id()]);
}
}
else {
drupal_set_message(t('Facet %name has been updated.', ['%name' => $facet->getName()]));
$form_state->setRedirect('entity.facets_facet.edit_form', ['facets_facet' => $facet->id()]);
}
return $facet;
......
<?php
/**
* @file
* Contains \Drupal\facets\Form\FacetSourceEditForm.
*/
namespace Drupal\facets\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\Entity\FacetSource;
use Drupal\facets\UrlProcessor\UrlProcessorPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for editing facet sources.
*
* Configuration saved trough this form is specific for a facet source and can
* be used by all facets on this facet source.
*/
class FacetSourceEditForm extends EntityForm {
/**
* The plugin manager for URL Processors.
*
* @var \Drupal\facets\UrlProcessor\UrlProcessorPluginManager
*/
protected $urlProcessorPluginManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = $container->get('entity_type.manager');
/** @var \Drupal\facets\UrlProcessor\UrlProcessorPluginManager $url_processor_plugin_manager */
$url_processor_plugin_manager = $container->get('plugin.manager.facets.url_processor');
return new static($entity_type_manager, $url_processor_plugin_manager);
}
/**
* Constructs a FacetSourceEditForm.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\facets\UrlProcessor\UrlProcessorPluginManager $url_processor_plugin_manager
* The url processor plugin manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, UrlProcessorPluginManager $url_processor_plugin_manager) {
$facet_source_storage = $entity_type_manager->getStorage('facets_facet_source');
$this->urlProcessorPluginManager = $url_processor_plugin_manager;
// Make sure we remove colons from the source id, those are disallowed in
// the entity id.
$source_id = $this->getRequest()->get('source_id');
$source_id = str_replace(':', '__', $source_id);
$facet_source = $facet_source_storage->load($source_id);
if ($facet_source instanceof FacetSource) {
$this->setEntity($facet_source);
}
else {
// We didn't have a facet source config entity yet for this facet source
// plugin, so we create it on the fly.
$facet_source = new FacetSource(
[
'id' => $source_id,
'name' => $this->getRequest()->get('source_id'),
],
'facets_facet_source'
);
$facet_source->save();
$this->setEntity($facet_source);
}
$this->setModuleHandler(\Drupal::moduleHandler());
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'facet_source_edit_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
/** @var \Drupal\facets\FacetSourceInterface $facet_source */
$facet_source = $this->getEntity();
$form['filter_key'] = [
'#type' => 'textfield',
'#title' => $this->t('Filter key'),
'#size' => 20,
'#maxlength' => 255,
'#default_value' => $facet_source->getFilterKey(),
'#description' => $this->t(
'The key used in the url to identify the facet source.
When using multiple facet sources you should make sure each facet source has a different filter key.'
),
];
$url_processors = array();
$url_processors_description = array();
foreach ($this->urlProcessorPluginManager->getDefinitions() as $definition) {
$url_processors[$definition['id']] = $definition['label'];
$url_processors_description[] = $definition['description'];
}
$form['url_processor'] = [
'#type' => 'radios',
'#title' => $this->t('URL Processor'),
'#options' => $url_processors,
'#default_value' => $facet_source->getUrlProcessorName(),
'#description' => $this->t(
'The URL Processor defines the url structure used for this facet source.') . '<br />- ' . implode('<br>- ', $url_processors_description),
];
// The parent's form build method will add a save button.
return parent::buildForm($form, $form_state);
}
}
......@@ -3,40 +3,43 @@
/**
* @file
* Contains Drupal\facets\Plugin\Block\FacetBlock.
*
* NOTE: There should be a facetblock or settings for the facets later.
*/
namespace Drupal\facets\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\facets\FacetManager\DefaultFacetManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'FacetBlock' block.
* Exposes a facet rendered as a block.
*
* @Block(
* id = "facet_block",
* admin_label = @Translation("Facet block"),
* context = {
* "facet" = @ContextDefinition("entity:facets_facet", label=@Translation("Facet"))
* }
* id = "facet_block",
* deriver = "Drupal\facets\Plugin\Block\FacetBlockDeriver"
* )
*/
class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The facet_manager plugin manager.
* The facet manager.
*
* @var DefaultFacetManager
* @var \Drupal\facets\FacetManager\DefaultFacetManager
*/
protected $facetManager;
/**
* Construct.
* The entity storage used for facets.
*
* @var \Drupal\Core\Entity\EntityStorageInterface $facetStorage
*/
protected $facetStorage;
/**
* Construct a FacetBlock instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
......@@ -45,10 +48,13 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface {
* @param string $plugin_definition
* The plugin implementation definition.
* @param \Drupal\facets\FacetManager\DefaultFacetManager $facet_manager
* The facet manager service.
* The facet manager.
* @param \Drupal\Core\Entity\EntityStorageInterface $facet_storage
* The entity storage used for facets.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, DefaultFacetManager $facet_manager) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, DefaultFacetManager $facet_manager, EntityStorageInterface $facet_storage) {
$this->facetManager = $facet_manager;
$this->facetStorage = $facet_storage;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
......@@ -56,15 +62,17 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface {
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
/** @var \Drupal\facets\FacetManager\DefaultFacetManager $facet_manager */
$facet_manager = $container->get('facets.manager');
/** @var \Drupal\Core\Entity\EntityStorageInterface $facet_storage */
$facet_storage = $container->get('entity_type.manager')->getStorage('facets_facet');
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$facet_manager
$facet_manager,
$facet_storage
);
}
......@@ -72,35 +80,57 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface {
* {@inheritdoc}
*/
public function build() {
/** @var Facet $facet */
$facet = $this->getContextValue('facet');
// The id saved in the configuration is in the format of
// base_plugin:facet_id. We're splitting that to get to the facet id.
$facet_mapping = $this->configuration['id'];
$facet_id = explode(PluginBase::DERIVATIVE_SEPARATOR, $facet_mapping)[1];
/** @var \Drupal\facets\FacetInterface $facet */
$facet = $this->facetStorage->load($facet_id);
// Let the facet_manager build the facets.
$build = $this->facetManager->build($facet);
// Add contextual links only when we have results.
if (!empty($build)) {
$build['#contextual_links']['facets_facet'] = array(
'route_parameters' => array('facets_facet' => $facet->id()),
);
$build['#contextual_links']['facets_facet'] = [
'route_parameters' => ['facets_facet' => $facet->id()],
];
}
return $build;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
parent::blockSubmit($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// Makes sure a facet block is never cached.
// @TODO Make blocks cacheable, see: https://www.drupal.org/node/2581629
// A facet block cannot be cached, because it must always match the current
// search results, and Search API gets those search results from a data
// source that can be external to Drupal. Therefore it is impossible to
// guarantee that the search results are in sync with the data managed by
// Drupal. Consequently, it is not possible to cache the search results at
// all. If the search results cannot be cached, then neither can the facets,
// because they must always match.
// Fortunately, facet blocks are rendered using a lazy builder (like all
// blocks in Drupal), which means their rendering can be deferred (unlike
// the search results, which are the main content of the page, and deferring
// their rendering would mean sending an empty page to the user). This means
// that facet blocks can be rendered and sent *after* the initial page was
// loaded, by installing the BigPipe (big_pipe) module.
//
// When BigPipe is enabled, the search results will appear first, and then
// each facet block will appear one-by-one, in DOM order.
// See https://www.drupal.org/project/big_pipe.
//
// In a future version of Facet API, this could be refined, but due to the
// reliance on external data sources, it will be very difficult if not
// impossible to improve this significantly.
//
// Note: when using Drupal core's Search module instead of the contributed
// Search API module, the above limitations do not apply, but for now it is
// not considered worth the effort to optimize this just for Drupal core's
// Search.
return 0;
}
......@@ -108,22 +138,15 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface {
* {@inheritdoc}
*/
public function calculateDependencies() {
$facet_context_mapping = $this->configuration['context_mapping']['facet'];
$facet_id = explode(':', $facet_context_mapping)[1];
$em = \Drupal::getContainer()->get('entity_type.manager');
// The ID saved in the configuration is of the format
// 'base_plugin:facet_id'. We're splitting that to get to the facet ID.
$facet_mapping = $this->configuration['id'];
$facet_id = explode(PluginBase::DERIVATIVE_SEPARATOR, $facet_mapping)[1];
/** @var \Drupal\facets\FacetInterface $facet */
$facets = $em->getStorage('facets_facet')
->loadByProperties(['uuid' => $facet_id]);
$keys = array_keys($facets);
$facet = $facets[$keys[0]];
$config_name = $facet->getConfigDependencyName();
$facet = $this->facetStorage->load($facet_id);
return ['config' => [$config_name]];
return ['config' => [$facet->getConfigDependencyName()]];
}
}
<?php
/**
* @file
* Contains Drupal\facets\Plugin\Block\FacetBlockDeriver.
*/
namespace Drupal\facets\Plugin\Block;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* This deriver creates a block for every facet that has been created.
*/
class FacetBlockDeriver implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* List of derivative definitions.
*
* @var array
*/
protected $derivatives = [];
/**
* The entity storage used for facets.
*
* @var \Drupal\Core\Entity\EntityStorageInterface $facetStorage
*/
protected $facetStorage;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
$deriver = new static($container, $base_plugin_id);
$deriver->facetStorage = $container->get('entity_type.manager')->getStorage('facets_facet');
return $deriver;
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
$derivatives = $this->getDerivativeDefinitions($base_plugin_definition);
return isset($derivatives[$derivative_id]) ? $derivatives[$derivative_id] : NULL;
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$base_plugin_id = $base_plugin_definition['id'];
if (!isset($this->derivatives[$base_plugin_id])) {
$plugin_derivatives = [];
/** @var \Drupal\facets\FacetInterface[] $all_facets */
$all_facets = $this->facetStorage->loadMultiple();
foreach ($all_facets as $facet) {
$machine_name = $facet->id();
$plugin_derivatives[$machine_name] = [
'id' => $base_plugin_id . PluginBase::DERIVATIVE_SEPARATOR . $machine_name,
'label' => $this->t('Facet: :facet', [':facet' => $facet->getName()]),
'admin_label' => $facet->getName(),
'description' => $this->t('Facet'),
] + $base_plugin_definition;
$sources[] = $this->t('Facet: :facet', [':facet' => $facet->getName()]);
}
$this->derivatives[$base_plugin_id] = $plugin_derivatives;
}
return $this->derivatives[$base_plugin_id];
}
}
<?php
/**
* @file
* Contains \Drupal\facets\Plugin\Condition\OtherFacet.
*/
namespace Drupal\facets\Plugin\Condition;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Block\BlockManager;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\facets\FacetManager\DefaultFacetManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides an 'other facet' condition.
*
* This adds a condition plugin to make sure that facets can depend on other
* facet's or their values. The facet value is a freeform textfield and works on
* both raw and display values of the results.
*
* @Condition(
* id = "other_facet",
* label = @Translation("Other facet"),
* )
*/
class OtherFacet extends ConditionPluginBase implements ContainerFactoryPluginInterface {
/**
* The facet entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $facetStorage;
/**
* The block plugin manager.
*
* @var \Drupal\Core\Block\BlockManager
*/
protected $blockManager;
/**
* The user that's currently logged in.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* The facet manager service.
*
* @var \Drupal\facets\FacetManager\DefaultFacetManager
*/
protected $facetManager;
/**
* Creates a new instance of the condition.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
* The entity storage.
* @param \Drupal\Core\Block\BlockManager $block_manager
* The block plugin manager.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The currently logged in user.
* @param \Drupal\facets\FacetManager\DefaultFacetManager $facet_manager
* The default facet manager class.
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*/
public function __construct(EntityStorageInterface $entity_storage, BlockManager $block_manager, AccountProxyInterface $current_user, DefaultFacetManager $facet_manager, array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->facetStorage = $entity_storage;
$this->blockManager = $block_manager;
$this->currentUser = $current_user;
$this->facetManager = $facet_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('entity_type.manager')->getStorage('facets_facet'),
$container->get('plugin.manager.block'),
$container->get('current_user'),
$container->get('facets.manager'),
$configuration,
$plugin_id,
$plugin_definition
);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$options = [];
// Loop over all defined blocks and filter them by provider, this builds an
// array of blocks that are provided by the facets module.
foreach ($this->blockManager->getDefinitions() as $definition) {
if ($definition['provider'] == 'facets') {
$options[$definition['id']] = $definition['label'];
}
}
$form['facets'] = [
'#title' => $this->t('Other facet blocks'),
'#type' => 'radios',
'#options' => $options,
'#default_value' => $this->configuration['facets'],
];
$form['facet_value'] = [
'#title' => $this->t('Facet value'),
'#description' => $this->t('Only applies when a facet is already selected.'),
'#type' => 'textfield',
'#default_value' => $this->configuration['facet_value'],
];
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['facets'] = $form_state->getValue('facets');
$this->configuration['facet_value'] = $form_state->getValue('facet_value');
parent::submitConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function summary() {
return $this->t(
'The facet is @facet also rendered on the same page.',
['@facet' => $this->configuration['facets']]
);
}
/**
* {@inheritdoc}
*/
public function evaluate() {
$allowed_facet_value = $this->configuration['facet_value'];
$allowed_facets = $this->configuration['facets'];
// Return as early as possible when there are no settings for allowed
// facets.
if (empty($allowed_facets)) {
return TRUE;
}
/** @var \Drupal\facets\Plugin\Block\FacetBlock $block_plugin */
$block_plugin = $this->blockManager->createInstance($allowed_facets);
// Allowed facet value is not set, so we only have to check if the block is
// shown here by running the access method on the block plugin with the
// currently logged in user.
if (empty($allowed_facet_value)) {
return $block_plugin->access($this->currentUser);
}
// The block plugin id is saved in the schema: BasePluginID:FacetID. This
// means we can explode the ID on ':' and the facet id is in the last part
// of that result.
$block_plugin_id = $block_plugin->getPluginId();
$facet_id = explode(PluginBase::DERIVATIVE_SEPARATOR, $block_plugin_id)[1];
/** @var \Drupal\facets\FacetInterface $facet */
$facet = $this->facetStorage->load($facet_id);
$this->facetManager->setFacetSourceId($facet->getFacetSourceId());
$facet = $this->facetManager->returnProcessedFacet($facet_id);
foreach ($facet->getResults() as $result) {
$is_value = $result->getRawValue() == $allowed_facet_value || $result->getDisplayValue() == $allowed_facet_value;
if ($is_value && $result->isActive()) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$config = ['facets' => FALSE, 'facet_value' => FALSE];
return $config + parent::defaultConfiguration();
}
}
......@@ -11,9 +11,7 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\facets\Exception\InvalidQueryTypeException;
use Drupal\facets\FacetInterface;
use Drupal\search_api\Backend\BackendInterface;
use Drupal\facets\FacetSource\FacetSourceInterface;
use Drupal\facets\FacetSource\FacetSourcePluginBase;
use Drupal\search_api\FacetsQueryTypeMappingInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -66,7 +64,7 @@ abstract class SearchApiBaseFacetSource extends FacetSourcePluginBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet, FacetSourceInterface $facet_source) {
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['field_identifier'] = [
'#type' => 'select',
......@@ -74,7 +72,7 @@ abstract class SearchApiBaseFacetSource extends FacetSourcePluginBase {
'#title' => $this->t('Facet field'),
'#description' => $this->t('Choose the indexed field.'),
'#required' => TRUE,
'#default_value' => $facet->getFieldIdentifier(),
'#default_value' => $this->facet->getFieldIdentifier(),
];
return $form;
......@@ -85,7 +83,7 @@ abstract class SearchApiBaseFacetSource extends FacetSourcePluginBase {
*/
public function getFields() {
$indexed_fields = [];
$fields = $this->index->getFields(TRUE);
$fields = $this->index->getFields();
foreach ($fields as $field) {
$indexed_fields[$field->getFieldIdentifier()] = $field->getLabel();
}
......@@ -100,16 +98,17 @@ abstract class SearchApiBaseFacetSource extends FacetSourcePluginBase {
// identifier.
$field_id = $facet->getFieldIdentifier();
// Get the Search API Server.
$server = $this->index->getServer();
$server = $this->index->getServerInstance();
// Get the Search API Backend.
$backend = $server->getBackend();
$fields = $this->index->getFields(TRUE);
$fields = $this->index->getFields();
foreach ($fields as $field) {
if ($field->getFieldIdentifier() == $field_id) {
return $this->getQueryTypesForDataType($backend, $field->getType());
}
}
throw new InvalidQueryTypeException($this->t("No available query types were found for facet @facet", ['@facet' => $facet->getName()]));
}
......
......@@ -75,9 +75,7 @@ class SearchApiViewsPage extends SearchApiBaseFacetSource {
public function getPath() {
$view = Views::getView($this->pluginDefinition['view_id']);
$view->setDisplay($this->pluginDefinition['view_display']);
$view->execute();
return $view->getDisplay()->getOption('path');
return $view->getDisplay()->getPath();
}
/**
......
<?php
/**
* @file
* Contains Drupal\facets\Plugin\facets\processor\UrlProcessorHandler.
*/
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\facets\Exception\InvalidProcessorException;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\PreQueryProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
/**
* The URL processor handler triggers the actual url processor.
*
* The URL processor handler allows managing the weight of the actual URL
* processor per Facet. This handler will trigger the actual.
*
* @FacetsUrlProcessor, which can be configured on the Facet source.
*
* @FacetsProcessor(
* id = "url_processor_handler",
* label = @Translation("URL handler"),
* description = @Translation("Triggers the URL processor, which is set in the Facet source configuration."),
* stages = {
* "pre_query" = 50,
* "build" = 15,
* },
* locked = true
* )
*/
class UrlProcessorHandler extends ProcessorPluginBase implements BuildProcessorInterface, PreQueryProcessorInterface {
/**
* The actual url processor used for handing urls.
*
* @var \Drupal\facets\UrlProcessor\UrlProcessorInterface
*/
protected $processor;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
if (!isset($configuration['facet']) || !$configuration['facet'] instanceof FacetInterface) {
throw new InvalidProcessorException("The UrlProcessorHandler doesn't have the required 'facet' in the configuration array.");
}
/** @var \Drupal\facets\FacetInterface $facet */
$facet = $configuration['facet'];
/** @var \Drupal\facets\FacetSourceInterface $fs */
$fs = $facet->getFacetSourceConfig();
$url_processor_name = $fs->getUrlProcessorName();
$manager = \Drupal::getContainer()->get('plugin.manager.facets.url_processor');
$this->processor = $manager->createInstance($url_processor_name, ['facet' => $facet]);
}
/**
* {@inheritdoc}
*/
public function build(FacetInterface $facet, array $results) {
return $this->processor->buildUrls($facet, $results);
}
/**
* {@inheritdoc}
*/
public function preQuery(FacetInterface $facet) {
$this->processor->setActiveItems($facet);
}
}
......@@ -29,7 +29,7 @@ use Drupal\facets\Result\Result;
class SearchApiString extends QueryTypePluginBase {
/**
* Holds the backend's native query object.
* The backend's native query object.
*
* @var \Drupal\search_api\Query\QueryInterface
*/
......@@ -41,11 +41,33 @@ class SearchApiString extends QueryTypePluginBase {
public function execute() {
$query = $this->query;
// Alter the query here.
if (!empty($query)) {
$options = &$query->getOptions();
$unfiltered_results = [];
// Only alter the query when there's an actual query object to alter.
if (!empty($query)) {
$operator = $this->facet->getQueryOperator();
$field_identifier = $this->facet->getFieldIdentifier();
$exclude = $this->facet->getExclude();
// Copy the query object so we can do an unfiltered query. We need to have
// this unfiltered results to make sure that the count of a facet is
// correct. The unfiltered results get returned to the facet manager, the
// facet manager will save it on facet::unfiltered_results.
$unfiltered_query = $query;
$unfiltered_options = &$unfiltered_query->getOptions();
$unfiltered_options['search_api_facets'][$field_identifier] = array(
'field' => $field_identifier,
'limit' => 50,
'operator' => 'and',
'min_count' => 0,
'missing' => FALSE,
);
$unfiltered_results = $unfiltered_query
->execute()
->getExtraData('search_api_facets');
// Set the options for the actual query.
$options = &$query->getOptions();
$options['search_api_facets'][$field_identifier] = array(
'field' => $field_identifier,
'limit' => 50,
......@@ -56,25 +78,36 @@ class SearchApiString extends QueryTypePluginBase {
// Add the filter to the query if there are active values.
$active_items = $this->facet->getActiveItems();
if (count($active_items)) {
$filter = $query->createConditionGroup($operator);
foreach ($active_items as $value) {
$filter = $query->createFilter();
$filter->condition($this->facet->getFieldIdentifier(), $value);
$query->filter($filter);
$filter->addCondition($this->facet->getFieldIdentifier(), $value, $exclude ? '<>' : '=');
}
$query->addConditionGroup($filter);
}
}
return $unfiltered_results;
}
/**
* {@inheritdoc}
*/
public function build() {
$query_operator = $this->facet->getQueryOperator();
if (!empty($this->results)) {
$facet_results = array();
foreach ($this->results as $result) {
if ($result['count']) {
$facet_results[] = new Result(trim($result['filter'], '"'), trim($result['filter'], '"'), $result['count']);
foreach ($this->results as $key => $result) {
if ($result['count'] || $query_operator == 'OR') {
$count = $result['count'];
if ($query_operator === 'OR') {
$count = $this->facet->getUnfilteredResults()[$this->facet->getFieldIdentifier()][$key]['count'];
}
$result = new Result(trim($result['filter'], '"'), trim($result['filter'], '"'), $count);
$facet_results[] = $result;
}
}
$this->facet->setResults($facet_results);
......
<?php
/**
* @file
* Contains Drupal\facets\Plugin\facets\url_processor\QueryString.
*/
namespace Drupal\facets\Plugin\facets\url_processor;
use Drupal\Core\Url;
use Drupal\facets\FacetInterface;
use Drupal\facets\UrlProcessor\UrlProcessorPluginBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Query string URL processor.
*
* @FacetsUrlProcessor(
* id = "query_string",
* label = @Translation("Query string"),
* description = @Translation("Query string is the default Facets URL processor, and uses GET parameters, e.g. ?f[0]=brand:drupal&f[1]=color:blue")
* )
*/
class QueryString extends UrlProcessorPluginBase {
/**
* A string that separates the filters in the query string.
*/
const SEPARATOR = ':';
/**
* A string of how to represent the facet in the url.
*
* @var string
*/
protected $urlAlias;
/**
* An array of active filters.
*
* @var string[]
* An array containing the active filters
*/
protected $activeFilters = [];
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $request);
$this->initializeActiveFilters();
}
/**
* {@inheritdoc}
*/
public function buildUrls(FacetInterface $facet, array $results) {
// No results are found for this facet, so don't try to create urls.
if (empty($results)) {
return [];
}
// First get the current list of get parameters.
$get_params = $this->request->query;
// Set the url alias from the the facet object.
$this->urlAlias = $facet->getUrlAlias();
/** @var \Drupal\facets\Result\ResultInterface $result */
foreach ($results as &$result) {
$filter_string = $this->urlAlias . self::SEPARATOR . $result->getRawValue();
$result_get_params = clone $get_params;
$filter_params = $result_get_params->get($this->filterKey, [], TRUE);
// If the value is active, remove the filter string from the parameters.
if ($result->isActive()) {
foreach ($filter_params as $key => $filter_param) {
if ($filter_param == $filter_string) {
unset($filter_params[$key]);
}
}
}
// If the value is not active, add the filter string.
else {
$filter_params[] = $filter_string;
}
$result_get_params->set($this->filterKey, $filter_params);
$request = $this->request;
if ($facet->getFacetSource()->getPath()) {
$request = Request::create('/' . $facet->getFacetSource()->getPath());
}
$url = Url::createFromRequest($request);
$url->setOption('query', $result_get_params->all());
$result->setUrl($url);
}
return $results;
}
/**
* {@inheritdoc}
*/
public function setActiveItems(FacetInterface $facet) {
// Set the url alias from the the facet object.
$this->urlAlias = $facet->getUrlAlias();
// Get the filter key of the facet.
if (isset($this->activeFilters[$this->urlAlias])) {
foreach ($this->activeFilters[$this->urlAlias] as $value) {
$facet->setActiveItem(trim($value, '"'));
}
}
}
/**
* Initializes the active filters.
*
* Get all the filters that are active. This method only get's all the
* filters but doesn't assign them to facets. In the processFacet method the
* active values for a specific facet are added to the facet.
*/
protected function initializeActiveFilters() {
$url_parameters = $this->request->query;
// Get the active facet parameters.
$active_params = $url_parameters->get($this->filterKey, array(), TRUE);
// Explode the active params on the separator.
foreach ($active_params as $param) {
list($key, $value) = explode(self::SEPARATOR, $param);
if (!isset($this->activeFilters[$key])) {
$this->activeFilters[$key] = [$value];
}
else {
$this->activeFilters[$key][] = $value;
}
}
}
}
......@@ -7,10 +7,13 @@
namespace Drupal\facets\Plugin\facets\widget;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\facets\FacetInterface;
use Drupal\facets\Widget\WidgetInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* The checkbox / radios widget.
......@@ -21,18 +24,10 @@ use Drupal\facets\Widget\WidgetInterface;
* description = @Translation("A configurable widget that shows a list of checkboxes"),
* )
*/
class CheckboxWidget implements WidgetInterface {
class CheckboxWidget implements WidgetInterface, FormInterface {
use StringTranslationTrait;
/**
* The link generator.
*
* @var \Drupal\Core\Utility\LinkGeneratorInterface $linkGenerator
* The link generator.
*/
protected $linkGenerator;
/**
* {@inheritdoc}
*/
......@@ -44,33 +39,14 @@ class CheckboxWidget implements WidgetInterface {
* {@inheritdoc}
*/
public function build(FacetInterface $facet) {
/** @var \Drupal\facets\Result\Result[] $results */
$results = $facet->getResults();
$items = [];
$configuration = $facet->get('widget_configs');
$show_numbers = (bool) $configuration['show_numbers'];
$form_builder = \Drupal::getContainer()->get('form_builder');
foreach ($results as $result) {
if ($result->getCount()) {
// Get the link.
$text = $result->getDisplayValue();
if ($show_numbers) {
$text .= ' (' . $result->getCount() . ')';
}
if ($result->isActive()) {
$text = '(-) ' . $text;
}
$link = $this->linkGenerator()->generate($text, $result->getUrl());
$items[] = $link;
}
}
$build = [
'#theme' => 'item_list',
'#items' => $items,
];
// The form builder's getForm method accepts 1 argument in the interface,
// the form ID. Extra arguments get passed into the form states addBuildInfo
// method. This way we can pass the facet to the ::buildForm method, it uses
// FormState::getBuildInfo to get the facet out.
$build = $form_builder->getForm(static::class, $facet);
$build['#prefix'] = $this->t('Checkboxes');
return $build;
}
......@@ -102,16 +78,102 @@ class CheckboxWidget implements WidgetInterface {
}
/**
* Gets the link generator.
*
* @return \Drupal\Core\Utility\LinkGeneratorInterface
* The link generator.
* {@inheritdoc}
*/
protected function linkGenerator() {
if (!isset($this->linkGenerator)) {
$this->linkGenerator = \Drupal::linkGenerator();
public function getFormId() {
return 'facets_checkbox_widget';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
/** @var \Drupal\facets\FacetInterface $facet */
// Get the facet form the build info, see the remark in ::build to know
// where this comes from.
$build_info = $form_state->getBuildInfo();
$facet = $build_info['args'][0];
/** @var \Drupal\facets\Result\Result[] $results */
$results = $facet->getResults();
$configuration = $facet->getWidgetConfigs();
$show_numbers = (bool) (isset($configuration['show_numbers']) ? $configuration['show_numbers'] : FALSE);
$form[$facet->getFieldAlias()] = [
'#type' => 'checkboxes',
'#title' => $facet->getName(),
];
$options = array();
foreach ($results as $result) {
$text = $result->getDisplayValue();
if ($show_numbers) {
$text .= ' (' . $result->getCount() . ')';
}
$options[$result->getRawValue()] = $text;
if ($result->isActive()) {
$form[$facet->getFieldAlias()]['#default_value'][] = $result->getRawValue();
}
}
$form[$facet->getFieldAlias()]['#options'] = $options;
$form[$facet->id() . '_submit'] = [
'#type' => 'submit',
'#value' => 'submit',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
/** @var \Drupal\facets\FacetInterface $facet */
$build_info = $form_state->getBuildInfo();
$facet = $build_info['args'][0];
$result_link = FALSE;
$active_items = [];
foreach ($values[$facet->getFieldAlias()] as $key => $value) {
if ($value !== 0) {
$active_items[] = $value;
}
}
return $this->linkGenerator;
foreach ($facet->getResults() as $result) {
if (in_array($result->getRawValue(), $active_items)) {
$result_link = $result->getUrl();
}
}
// We have an active item, so we redirect to the page that has that facet
// selected. This should be an absolute link because RedirectResponse is a
// symfony class that requires a full URL.
if ($result_link instanceof Url) {
$result_link->setAbsolute();
$form_state->setResponse(new RedirectResponse($result_link->toString()));
return;
}
// The form was submitted but nothing was active in the form, we should
// still redirect, but the url for the new page can't come from a result.
// So we're redirecting to the facet source's page.
$link = Url::fromUri($facet->getFacetSource()->getPath());
$link->setAbsolute();
$form_state->setResponse(new RedirectResponse($link->toString()));
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\facets\Plugin\facets\widget;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\facets\FacetInterface;
use Drupal\facets\Widget\WidgetInterface;
......@@ -25,14 +26,6 @@ class LinksWidget implements WidgetInterface {
use StringTranslationTrait;
/**
* The link generator.
*
* @var \Drupal\Core\Utility\LinkGeneratorInterface $linkGenerator
* The link generator.
*/
protected $linkGenerator;
/**
* {@inheritdoc}
*/
......@@ -48,32 +41,36 @@ class LinksWidget implements WidgetInterface {
$results = $facet->getResults();
$items = [];
$configuration = $facet->get('widget_configs');
$show_numbers = (bool) $configuration['show_numbers'];
$configuration = $facet->getWidgetConfigs();
$show_numbers = empty($configuration['show_numbers']) ? FALSE : (bool) $configuration['show_numbers'];
foreach ($results as $result) {
if ($result->getCount()) {
// Get the link.
$text = $result->getDisplayValue();
if ($show_numbers) {
$text .= ' (' . $result->getCount() . ')';
}
if ($result->isActive()) {
$text = '(-) ' . $text;
}
if (is_null($result->getUrl())) {
$items[] = $text;
}
else {
$items[] = $this->linkGenerator()->generate($text, $result->getUrl());
}
// Get the link.
$text = $result->getDisplayValue();
if ($show_numbers) {
$text .= ' (' . $result->getCount() . ')';
}
if ($result->isActive()) {
$text = '(-) ' . $text;
}
if (is_null($result->getUrl())) {
$items[] = $text;
}
else {
$items[] = new Link($text, $result->getUrl());
}
}
$build = [
'#theme' => 'item_list',
'#items' => $items,
'#cache' => [
'contexts' => [
'url.path',
'url.query_args',
],
],
];
return $build;
}
......@@ -107,17 +104,4 @@ class LinksWidget implements WidgetInterface {
return $query_types['string'];
}
/**
* Gets the link generator.
*
* @return \Drupal\Core\Utility\LinkGeneratorInterface
* The link generator.
*/
protected function linkGenerator() {
if (!isset($this->linkGenerator)) {
$this->linkGenerator = \Drupal::linkGenerator();
}
return $this->linkGenerator;
}
}
......@@ -16,7 +16,7 @@ use Drupal\facets\FacetInterface;
interface BuildProcessorInterface extends ProcessorInterface {
/**
* Processor runs before the renderable array is created.
* Runs before the renderable array is created.
*
* @param \Drupal\facets\FacetInterface $facet
* The facet being changed.
......
......@@ -14,7 +14,7 @@ namespace Drupal\facets\Processor;
interface PostQueryProcessorInterface extends ProcessorInterface {
/**
* Processor runs after the query was executed.
* Runs after the query was executed.
*
* Uses the query results and can alter those results, for example a
* ValueCallbackProcessor.
......
......@@ -15,7 +15,7 @@ use Drupal\facets\FacetInterface;
interface PreQueryProcessorInterface extends ProcessorInterface {
/**
* Processor runs before the query is executed.
* Runs before the query is executed.
*
* Uses the queryType and the facetSource implementation to make sure the
* alteration to the query was added before the query is executed in the
......
......@@ -28,8 +28,7 @@ class ProcessorPluginBase extends PluginBase implements ProcessorInterface {
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
}
public function validateConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {}
/**
* {@inheritdoc}
......
......@@ -11,7 +11,7 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetInterface;
/**
* A base class for plugins that implements most of the boilerplate.
* A base class for plugins that implements some boilerplate for a widget order.
*/
abstract class WidgetOrderPluginBase extends ProcessorPluginBase implements WidgetOrderProcessorInterface {
......
......@@ -13,7 +13,7 @@ namespace Drupal\facets\Processor;
interface WidgetOrderProcessorInterface extends BuildProcessorInterface {
/**
* Order results and return the new order of results.
* Orders results and return the new order of results.
*
* @param \Drupal\facets\Result\Result[] $results
* An array containing results.
......
......@@ -12,12 +12,15 @@ namespace Drupal\facets\QueryType;
interface QueryTypeInterface {
/**
* Add facet info to the query using the backend native query object.
* Adds facet info to the query using the backend native query object.
*
* @return array
* Returns an array of unfiltered results
*/
public function execute();
/**
* Build the facet information, so it can be rendered.
* Builds the facet information, so it can be rendered.
*/
public function build();
......
......@@ -28,7 +28,7 @@ abstract class QueryTypePluginBase extends PluginBase implements QueryTypeInterf
}
/**
* Holds the backend native query object.
* The backend native query object.
*
* @var \Drupal\search_api\Query\Query $query
*/
......@@ -42,7 +42,7 @@ abstract class QueryTypePluginBase extends PluginBase implements QueryTypeInterf
protected $facet;
/**
* Holds the results for the facet.
* The results for the facet.
*
* @var \Drupal\facets\Result\ResultInterface[]
*/
......