Commit 3704ea07 authored by borisson_'s avatar borisson_ Committed by borisson_

Issue #2637594 by borisson_, StryKaizer: Make it easier to switch the url processor

parent 436f55bc
......@@ -14,3 +14,6 @@ facets.facet_source.*:
filterKey:
type: string
label: 'Filter key'
url_processor:
type: string
label: 'Url processor'
<?php
/**
* @file
* Contains Drupal\facets_query_processor\Plugin\facets\url_processor\DummyQuery.
*/
namespace Drupal\facets_query_processor\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 = "dummy_query",
* label = @Translation("Dummy query"),
* description = @Translation("Dummy for testing.")
* )
*/
class DummyQuery 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 $url_alias;
/**
* 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) {
// Create links for all the values.
// First get the current list of get parameters.
$get_params = $this->request->query;
// Set the url alias from the the facet object.
$this->url_alias = $facet->getUrlAlias();
// No results are found for this facet, so don't try to create urls.
if (empty($results)) {
return [];
}
/** @var \Drupal\facets\Result\ResultInterface $result */
foreach ($results as &$result) {
$filter_string = $this->url_alias . 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->url_alias = $facet->getUrlAlias();
// Get the filter key of the facet.
if (isset($this->activeFilters[$this->url_alias])) {
foreach ($this->activeFilters[$this->url_alias] as $value) {
$facet->setActiveItem(trim($value, '"'));
}
}
}
/**
* Initialize 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;
}
}
}
}
......@@ -3,6 +3,11 @@ facets_processor:
plugin_manager_service_id: plugin.manager.facets.processor
plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
facets_url_processor:
label: Facets URL processor
plugin_manager_service_id: plugin.manager.facets.url_processor
plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
facets_facet_source:
label: Facets source
plugin_manager_service_id: plugin.manager.facets.facet_source
......
......@@ -11,6 +11,9 @@ services:
plugin.manager.facets.processor:
class: Drupal\facets\Processor\ProcessorPluginManager
arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@string_translation']
plugin.manager.facets.url_processor:
class: Drupal\facets\UrlProcessor\UrlProcessorPluginManager
parent: default_plugin_manager
facets.manager:
class: Drupal\facets\FacetManager\DefaultFacetManager
arguments:
......
<?php
/**
* @file
* Contains \Drupal\facets\Annotation\FacetsUrlProcessor.
*/
namespace Drupal\facets\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Facets URL Processor annotation.
*
* @see \Drupal\facets\Processor\ProcessorPluginManager
* @see plugin_api
*
* @ingroup plugin_api
*
* @Annotation
*/
class FacetsUrlProcessor extends Plugin {
/**
* The URL processor plugin id.
*
* @var string
*/
public $id;
/**
* The human-readable name of the URL processor plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The URL processor description.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
}
......@@ -9,6 +9,7 @@ namespace Drupal\facets\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\facets\FacetSourceInterface;
use Drupal\facets\UrlProcessor\UrlProcessorInterface;
/**
* Defines the facet source configuration entity.
......@@ -36,7 +37,8 @@ use Drupal\facets\FacetSourceInterface;
* config_export = {
* "id",
* "name",
* "filterKey"
* "filterKey",
* "urlProcessor"
* },
* links = {
* "canonical" = "/admin/config/search/facets/facet-sources/",
......@@ -67,6 +69,13 @@ class FacetSource extends ConfigEntityBase implements FacetSourceInterface {
*/
protected $filterKey;
/**
* The url processor name.
*
* @var string
*/
protected $urlProcessor = 'query_string';
/**
* {@inheritdoc}
*/
......@@ -95,4 +104,18 @@ class FacetSource extends ConfigEntityBase implements FacetSourceInterface {
return $this->filterKey;
}
/**
* {@inheritdoc}
*/
public function setUrlProcessor($processor_name) {
$this->urlProcessor = $processor_name;
}
/**
* {@inheritdoc}
*/
public function getUrlProcessorName() {
return $this->urlProcessor;
}
}
......@@ -38,4 +38,20 @@ interface FacetSourceInterface extends ConfigEntityInterface {
*/
public function setFilterKey($filter_key);
/**
* Set the processor name to be used.
*
* @param string $processor_name
* Plugin name of the url processor.
*/
public function setUrlProcessor($processor_name);
/**
* Returns a string version of the url processor.
*
* @return string
* The url processor to be used as a string.
*/
public function getUrlProcessorName();
}
......@@ -14,6 +14,7 @@ use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\Processor\ProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginManager;
use Drupal\facets\UrlProcessor\UrlProcessorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\facets\Widget\WidgetPluginManager;
use Drupal\facets\Processor\WidgetOrderProcessorInterface;
......@@ -219,7 +220,7 @@ class FacetDisplayForm extends EntityForm {
),
);
foreach ($all_processors as $processor_id => $processor) {
if (!($processor instanceof WidgetOrderProcessorInterface)) {
if (!($processor instanceof WidgetOrderProcessorInterface) && !($processor instanceof UrlProcessorInterface)) {
$clean_css_id = Html::cleanCssIdentifier($processor_id);
$form['facet_settings'][$processor_id]['status'] = array(
'#type' => 'checkbox',
......
......@@ -8,8 +8,11 @@
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.
......@@ -19,11 +22,33 @@ use Drupal\facets\Entity\FacetSource;
*/
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.
*/
public function __construct() {
$facet_source_storage = \Drupal::entityTypeManager()->getStorage('facets_facet_source');
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.
......@@ -81,6 +106,21 @@ class FacetSourceEditForm extends EntityForm {
),
];
$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['urlProcessor'] = [
'#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);
}
......
<?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{
/** @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);
}
}
......@@ -2,31 +2,26 @@
/**
* @file
* Contains Drupal\facets\Plugin\facets\url_processor\UrlProcessorQueryString.
* Contains Drupal\facets\Plugin\facets\url_processor\QueryString.
*/
namespace Drupal\facets\Plugin\facets\processor;
namespace Drupal\facets\Plugin\facets\url_processor;
use Drupal\Core\Url;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\UrlProcessorPluginBase;
use Drupal\facets\UrlProcessor\UrlProcessorPluginBase;
use Symfony\Component\HttpFoundation\Request;
/**
* The basic url processor, uses query strings.
* Query string URL processor.
*
* @FacetsProcessor(
* @FacetsUrlProcessor(
* id = "query_string",
* label = @Translation("Query string url processor"),
* description = @Translation("Most simple url processor which uses the query sting."),
* stages = {
* "pre_query" = 50,
* "build" = 15,
* },
* locked = true
* 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 QueryStringUrlProcessor extends UrlProcessorPluginBase {
class QueryString extends UrlProcessorPluginBase {
/**
* A string that separates the filters in the query string.
......@@ -59,7 +54,7 @@ class QueryStringUrlProcessor extends UrlProcessorPluginBase {
/**
* {@inheritdoc}
*/
public function build(FacetInterface $facet, array $results) {
public function buildUrls(FacetInterface $facet, array $results) {
// Create links for all the values.
// First get the current list of get parameters.
$get_params = $this->request->query;
......@@ -74,7 +69,7 @@ class QueryStringUrlProcessor extends UrlProcessorPluginBase {
/** @var \Drupal\facets\Result\ResultInterface $result */
foreach ($results as &$result) {
$filter_string = $this->url_alias . ':' . $result->getRawValue();
$filter_string = $this->url_alias . self::SEPARATOR . $result->getRawValue();
$result_get_params = clone $get_params;
$filter_params = $result_get_params->get($this->filterKey, [], TRUE);
......@@ -108,7 +103,7 @@ class QueryStringUrlProcessor extends UrlProcessorPluginBase {
/**
* {@inheritdoc}
*/
public function preQuery(FacetInterface $facet) {
public function setActiveItems(FacetInterface $facet) {
// Set the url alias from the the facet object.
$this->url_alias = $facet->getUrlAlias();
......
......@@ -29,11 +29,17 @@ class FacetSourceTest extends FacetWebTestBase {
$this->clickLink($this->t('Configure'));
// Test the edit page.
$edit = array(
'filterKey' => 'fq',
'urlProcessor' => 'query_string',
);
$this->assertField('filterKey');
$this->drupalPostForm(NULL, array('filterKey' => 'fq'), $this->t('Save'));
$this->assertField('urlProcessor');
$this->drupalPostForm(NULL, $edit, $this->t('Save'));
// Test that saving worked.
$this->assertField('filterKey');
$this->assertField('urlProcessor');
$this->assertRaw('fq');
}
......
<?php
/**
* @file
* Contains \Drupal\facets\Tests\UrlIntegrationTest.
*/
namespace Drupal\facets\Tests;
use \Drupal\facets\Tests\WebTestBase as FacetWebTestBase;
/**
* Tests the overall functionality of the Facets admin UI.
*
* @group facets
*/
class UrlIntegrationTest extends FacetWebTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'views',
'node',
'search_api',
'search_api_test_backend',
'facets',
'search_api_test_views',
'block',
'facets_search_api_dependency',
'facets_query_processor',
];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
$this->insertExampleContent();
$this->assertEqual($this->indexItems($this->indexId), 5, '5 items were indexed.');
}
/**
* Tests various url integration things.
*/
public function testUrlIntegration() {
$id = 'facet';
$name = '&^Facet@#1';
$facet_add_page = 'admin/config/search/facets/add-facet';
$this->drupalGet($facet_add_page);
$form_values = [
'id' => $id,
'status' => 1,
'url_alias' => $id,
'name' => $name,
'facet_source_id' => 'search_api_views:search_api_test_views_fulltext:page_1',
'facet_source_configs[search_api_views:search_api_test_views_fulltext:page_1][field_identifier]' => 'entity:entity_test/type',
];
$this->drupalPostForm(NULL, ['facet_source_id' => 'search_api_views:search_api_test_views_fulltext:page_1'], $this->t('Configure facet source'));
$this->drupalPostForm(NULL, $form_values, $this->t('Save'));
// Go to the only enabled facet source's config.
$this->drupalGet('admin/config/search/facets');
$this->clickLink($this->t('Configure'));
$edit = [
'filterKey' => 'y',
'urlProcessor' => 'dummy_query',
];
$this->drupalPostForm(NULL, $edit, $this->t('Save'));
/** @var \Drupal\facets\FacetInterface $facet */
$facet = \Drupal::service('entity_type.manager')->getStorage('facets_facet')->load($id);
$block_values = [
'region' => 'footer',
'id' => str_replace('_', '-', $id),
'context_mapping' => [
'facet' => '@facets.facet_context:' . $facet->uuid(),
],
];
$this->drupalPlaceBlock('facet_block', $block_values);
$this->drupalGet('search-api-test-fulltext');
$this->assertResponse(200);
$this->assertLink('item');
$this->assertLink('article');
$this->clickLink('item');
$this->assertResponse(200);
$this->assertLink('(-) item');
$this->assertNoLink('article');
$this->assertUrl('search-api-test-fulltext?y[0]=facet||item');
}
}
<?php
/**
* @file
* Contains Drupal\facets\Processor\UrlProcessorInterface.
* Contains Drupal\facets\UrlProcessor\UrlProcessorInterface.
*/
namespace Drupal\facets\Processor;
namespace Drupal\facets\UrlProcessor;
use Drupal\facets\FacetInterface;
/**
* Interface UrlProcessorInterface.
......@@ -18,7 +20,30 @@ namespace Drupal\facets\Processor;
*
* @package Drupal\facets\UrlProcessor
*/
interface UrlProcessorInterface extends PreQueryProcessorInterface, BuildProcessorInterface {
interface UrlProcessorInterface {
/**
* Adds urls to the results.
*
* @param \Drupal\facets\FacetInterface $facet
* The facet.
* @param \Drupal\facets\Result\ResultInterface[] $results
* An array of results.
*
* @return \Drupal\facets\Result\ResultInterface[]
* An array of results with added urls.
*/
public function buildUrls(FacetInterface $facet, array $results);
/**
* Sets active items.
*
* Makes sure that the items that are already set in the current request are
* remembered when building the facet's urls.
*
* @param \Drupal\facets\FacetInterface $facet
*/
public function setActiveItems(FacetInterface $facet);
/**
* Returns the filter key.
......
<?php
/**
* @file
* Contains Drupal\facets\Processor\UrlProcessorPluginBase.
* Contains Drupal\facets\UrlProcessor\UrlProcessorPluginBase.
*/
namespace Drupal\facets\Processor;
namespace Drupal\facets\UrlProcessor;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\facets\Exception\InvalidProcessorException;
use Drupal\facets\Processor\ProcessorPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
......@@ -56,7 +57,7 @@ abstract class UrlProcessorPluginBase extends ProcessorPluginBase implements Url
$this->request = $request;
if (!isset($configuration['facet'])) {
throw new InvalidProcessorException();
throw new InvalidProcessorException("The url processor doesn't have the required 'facet' in the configuration array.");
}
/** @var \Drupal\facets\FacetInterface $facet */
......
<?php
/**
* @file
* Contains \Drupal\facets\Processor\UrlProcessorPluginManager.
*/
namespace Drupal\facets\UrlProcessor;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**