Commit 32c74e52 authored by StryKaizer's avatar StryKaizer Committed by borisson_

Issue #2861586 by borisson_, StryKaizer, dcam, dragos-dumi: Make it easier to...

Issue #2861586 by borisson_, StryKaizer, dcam, dragos-dumi: Make it easier to programmatically generate facet links
parent bd3de438
......@@ -29,6 +29,10 @@ services:
class: Drupal\facets\Utility\FacetsDateHandler
arguments:
- '@date.formatter'
facets.utility.url_generator:
class: Drupal\facets\Utility\FacetsUrlGenerator
arguments:
- '@plugin.manager.facets.url_processor'
facets.event_subscriber:
class: Drupal\facets\EventSubscriber
arguments: ['@plugin.manager.block']
......
......@@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\url_processor;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Url;
use Drupal\facets\FacetInterface;
use Drupal\facets\UrlProcessor\UrlProcessorPluginBase;
......@@ -25,19 +26,11 @@ class QueryString extends UrlProcessorPluginBase {
*/
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);
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $request, $entity_type_manager);
$this->initializeActiveFilters();
}
......@@ -89,7 +82,13 @@ class QueryString extends UrlProcessorPluginBase {
$filter_string = $this->urlAlias . $this->getSeparator() . $result->getRawValue();
$result_get_params = clone $get_params;
$filter_params = $result_get_params->get($this->filterKey, [], TRUE);
$filter_params = [];
foreach ($this->getActiveFilters() as $facet_id => $values) {
foreach ($values as $value) {
$filter_params[] = $this->getUrlAliasByFacetId($facet_id, $facet->getFacetSourceId()) . ":" . $value;
}
}
// If the value is active, remove the filter string from the parameters.
if ($result->isActive()) {
foreach ($filter_params as $key => $filter_param) {
......@@ -162,32 +161,18 @@ class QueryString extends UrlProcessorPluginBase {
}
/**
* {@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.
* Initializes the active filters from the request query.
*
* 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.
* Get all the filters that are active by checking the request query and store
* them in activeFilters which is an array where key is the facet id and value
* is an array of raw values.
*/
protected function initializeActiveFilters() {
$url_parameters = $this->request->query;
// Get the active facet parameters.
$active_params = $url_parameters->get($this->filterKey, [], TRUE);
$facet_source_id = $this->configuration['facet']->getFacetSourceId();
// When an invalid parameter is passed in the url, we can't do anything.
if (!is_array($active_params)) {
......@@ -197,7 +182,8 @@ class QueryString extends UrlProcessorPluginBase {
// Explode the active params on the separator.
foreach ($active_params as $param) {
$explosion = explode($this->getSeparator(), $param);
$key = array_shift($explosion);
$url_alias = array_shift($explosion);
$facet_id = $this->getFacetIdByUrlAlias($url_alias, $facet_source_id);
$value = '';
while (count($explosion) > 0) {
$value .= array_shift($explosion);
......@@ -205,13 +191,61 @@ class QueryString extends UrlProcessorPluginBase {
$value .= $this->getSeparator();
}
}
if (!isset($this->activeFilters[$key])) {
$this->activeFilters[$key] = [$value];
if (!isset($this->activeFilters[$facet_id])) {
$this->activeFilters[$facet_id] = [$value];
}
else {
$this->activeFilters[$key][] = $value;
$this->activeFilters[$facet_id][] = $value;
}
}
}
/**
* Gets the facet id from the url alias & facet source id.
*
* @param string $url_alias
* The url alias.
* @param string $facet_source_id
* The facet source id.
*
* @return bool|string
* Either the facet id, or FALSE if that can't be loaded.
*/
protected function getFacetIdByUrlAlias($url_alias, $facet_source_id) {
$mapping = &drupal_static(__FUNCTION__);
if (!isset($mapping[$facet_source_id][$url_alias])) {
$storage = $this->entityTypeManager->getStorage('facets_facet');
$facet = current($storage->loadByProperties(['url_alias' => $url_alias, 'facet_source_id' => $facet_source_id]));
if (!$facet) {
return NULL;
}
$mapping[$facet_source_id][$url_alias] = $facet->id();
}
return $mapping[$facet_source_id][$url_alias];
}
/**
* Gets the url alias from the facet id & facet source id.
*
* @param string $facet_id
* The facet id.
* @param string $facet_source_id
* The facet source id.
*
* @return bool|string
* Either the url alias, or FALSE if that can't be loaded.
*/
protected function getUrlAliasByFacetId($facet_id, $facet_source_id) {
$mapping = &drupal_static(__FUNCTION__);
if (!isset($mapping[$facet_source_id][$facet_id])) {
$storage = $this->entityTypeManager->getStorage('facets_facet');
$facet = current($storage->loadByProperties(['id' => $facet_id, 'facet_source_id' => $facet_source_id]));
if (!$facet) {
return FALSE;
}
$mapping[$facet_source_id][$facet_id] = $facet->getUrlAlias();
}
return $mapping[$facet_source_id][$facet_id];
}
}
......@@ -34,8 +34,8 @@ interface UrlProcessorInterface {
/**
* Sets active items.
*
* Makes sure that the items that are already set in the current request are
* remembered when building the facet's urls.
* Is called after the url processor is ready retrieving and altering the
* active filters to let the facet know about the active items.
*
* @param \Drupal\facets\FacetInterface $facet
* The facet that is edited.
......@@ -58,4 +58,25 @@ interface UrlProcessorInterface {
*/
public function getSeparator();
/**
* Returns the active filters.
*
* @return array
* An array containing the active filters with key being the facet id and
* value being an array of raw values.
*/
public function getActiveFilters();
/**
* Set active filters.
*
* Allows overriding the active filters, which initially are set by the url
* processor logic, to build custom urls.
*
* @param array $active_filters
* An array containing the active filters with key being the facet id and
* value being an array of raw values.
*/
public function setActiveFilters(array $active_filters);
}
......@@ -2,8 +2,10 @@
namespace Drupal\facets\UrlProcessor;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\facets\Exception\InvalidProcessorException;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
......@@ -36,6 +38,22 @@ abstract class UrlProcessorPluginBase extends ProcessorPluginBase implements Url
*/
protected $request;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityTypeManager;
/**
* An array of active filters.
*
* @var array
* An array containing the active filters with key being the facet id and
* value being an array of raw values.
*/
protected $activeFilters = [];
/**
* {@inheritdoc}
*/
......@@ -61,12 +79,15 @@ abstract class UrlProcessorPluginBase extends ProcessorPluginBase implements Url
* The plugin implementation definition.
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object for the current request.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The Entity Type Manager.
*
* @throws \Drupal\facets\Exception\InvalidProcessorException
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->request = clone $request;
$this->entityTypeManager = $entity_type_manager;
if (!isset($configuration['facet'])) {
throw new InvalidProcessorException("The url processor doesn't have the required 'facet' in the configuration array.");
......@@ -96,8 +117,35 @@ abstract class UrlProcessorPluginBase extends ProcessorPluginBase implements Url
$configuration,
$plugin_id,
$plugin_definition,
$container->get('request_stack')->getMasterRequest()
$container->get('request_stack')->getMasterRequest(),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getActiveFilters() {
return $this->activeFilters;
}
/**
* {@inheritdoc}
*/
public function setActiveFilters(array $active_filters) {
$this->activeFilters = $active_filters;
}
/**
* {@inheritdoc}
*/
public function setActiveItems(FacetInterface $facet) {
// Get the filter key of the facet.
if (isset($this->activeFilters[$facet->id()])) {
foreach ($this->activeFilters[$facet->id()] as $value) {
$facet->setActiveItem(trim($value, '"'));
}
}
}
}
<?php
namespace Drupal\facets\Utility;
use Drupal\facets\Entity\Facet;
use Drupal\facets\Result\Result;
use Drupal\facets\UrlProcessor\UrlProcessorPluginManager;
/**
* Facets Url Generator service.
*/
class FacetsUrlGenerator {
/**
* The url processor plugin manager.
*
* @var \Drupal\facets\UrlProcessor\UrlProcessorPluginManager
*/
protected $urlProcessorPluginManager;
/**
* Constructs a new instance of the FacetsUrlGenerator.
*
* @param \Drupal\facets\UrlProcessor\UrlProcessorPluginManager $urlProcessorPluginManager
* The url processor plugin manager.
*/
public function __construct(UrlProcessorPluginManager $urlProcessorPluginManager) {
$this->urlProcessorPluginManager = $urlProcessorPluginManager;
}
/**
* Returns an URL object for a facet path.
*
* Example implementations:
* @code
* // Example to generate URL for 1 facet with 1 value.
* \Drupal::service('facets.utility.url_generator')->getUrl(['tags' => [7]]);
* // Example with multiple active filters.
* $active_filters = ['tags' => [5, 7], 'color' => ['blue']];
* \Drupal::service('facets.utility.url_generator')->getUrl($active_filters);
* @endcode
*
* @param array $active_filters
* An array containing the active filters with key being the facet id and
* value being an array of raw values.
* @param bool $keep_active
* TRUE if the currently active facets should be included to the URL or
* FALSE if they should be discarded. Defaults to TRUE.
*
* @return \Drupal\Core\Url|null
* A Url object for the given facet/value combination or null if no Result
* was returned by the UrlProcessor.
*
* @throws \InvalidArgumentException
*/
public function getUrl(array $active_filters, $keep_active = TRUE) {
// We use the first defined facet to load the url processor. As all facets
// should be from the same facet source, this is fine.
// This is because we don't support generating a url over multiple facet
// sources.
$facet_id = key($active_filters);
$facet = Facet::load($facet_id);
if ($facet === NULL) {
throw new \InvalidArgumentException("The Facet $facet_id could not be loaded.");
}
if (!is_array($active_filters[$facet_id])) {
throw new \InvalidArgumentException("The active filters passed in are invalid. They should look like: [$facet_id => ['value1', 'value2']]");
}
// We need one raw value to build a Result. If we have the raw value in the
// already active filters, it will be removed in the final result. So
// instead we copy the value into a variable and unset it from the list.
$raw_value = $active_filters[$facet_id][0];
unset($active_filters[$facet_id][0]);
/** @var \Drupal\facets\UrlProcessor\UrlProcessorInterface $url_processor */
$url_processor = $this
->urlProcessorPluginManager
->createInstance($facet->getFacetSourceConfig()
->getUrlProcessorName(), ['facet' => $facet]);
if ($keep_active) {
$active_filters = array_merge_recursive($active_filters, $url_processor->getActiveFilters());
}
$url_processor->setActiveFilters($active_filters);
// Use the url processor to create a result and return that item's url.
$results = [new Result($facet, $raw_value, '', 0)];
$processed_results = $url_processor->buildUrls($facet, $results);
$result = reset($processed_results);
if ($result) {
return $result->getUrl();
}
return NULL;
}
}
<?php
namespace Drupal\facets_query_processor\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Link;
/**
* Class DisplayGeneratedLinkBlock.
*
* @Block(
* id = "display_generated_link",
* admin_label = @Translation("Display Generated Link"),
* )
*/
class DisplayGeneratedLinkBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
/** @var \Drupal\facets\Utility\FacetsUrlGenerator $urlGeneratorService */
$urlGeneratorService = \Drupal::service('facets.utility.url_generator');
$url = $urlGeneratorService->getUrl(['owl' => ['item']], \Drupal::state()->get('facets_url_generator_keep_active', FALSE));
$link = new Link('Link to owl item', $url);
return $link->toRenderable() + ['#cache' => ['max-age' => 0]];
}
}
<?php
namespace Drupal\Tests\facets\Functional;
use Drupal\facets\Entity\Facet;
/**
* Class FacetsUrlGeneratorTest.
*
* @group facets
* @coversDefaultClass \Drupal\facets\Utility\FacetsUrlGenerator
*/
class FacetsUrlGeneratorTest extends FacetsTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'facets',
'facets_search_api_dependency',
'facets_query_processor',
'search_api',
'search_api_db',
'search_api_test_db',
'search_api_test_example_content',
'views',
'rest',
'serialization',
];
/**
* The FacetsUrlGenerator service.
*
* @var \Drupal\facets\Utility\FacetsUrlGenerator
*/
protected $urlGenerator;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->urlGenerator = \Drupal::service('facets.utility.url_generator');
$this->setUpExampleStructure();
$this->insertExampleContent();
$this->assertEquals(5, $this->indexItems($this->indexId), '5 items were indexed.');
}
/**
* @covers ::getUrl
*/
public function testCreateUrl() {
/** @var \Drupal\facets\FacetInterface $entity */
$entity = Facet::create([
'id' => 'test_facet',
'name' => 'Test facet',
]);
$entity->setWidget('links');
$entity->setEmptyBehavior(['behavior' => 'none']);
$entity->setUrlAlias('owl');
$entity->setFacetSourceId('search_api:views_page__search_api_test_view__page_1');
$entity->save();
$url = $this->urlGenerator->getUrl(['test_facet' => ['fuzzy']]);
$this->assertEquals('route:view.search_api_test_view.page_1;arg_0&arg_1&arg_2?f%5B0%5D=owl%3Afuzzy', $url->toUriString());
}
/**
* Tests that passing an invalid facet ID throws an InvalidArgumentException.
*
* @covers ::getUrl
*/
public function testInvalidFacet() {
$this->setExpectedException(\InvalidArgumentException::class, 'The Facet imaginary could not be loaded.');
$this->urlGenerator->getUrl(['imaginary' => ['unicorn']]);
}
/**
* Tests that passing an invalid facet ID throws an InvalidArgumentException.
*
* @covers ::getUrl
*/
public function testInvalidArguments() {
$entity = Facet::create([
'id' => 'test_facet',
'name' => 'Test facet',
]);
$entity->setWidget('links');
$entity->setEmptyBehavior(['behavior' => 'none']);
$entity->save();
$this->setExpectedException(\InvalidArgumentException::class, 'The active filters passed in are invalid. They should look like: [test_facet => [\'value1\', \'value2\']]');
$this->urlGenerator->getUrl(['test_facet' => 'unicorn']);
}
/**
* @covers ::getUrl
*/
public function testWithAlreadySetFacet() {
$this->drupalPlaceBlock('display_generated_link');
$this->createFacet('Owl', 'owl');
$this->createFacet('Llama', 'llama', 'keywords');
$facet = Facet::load('owl');
$facet->setUrlAlias('donkey');
$facet->save();
$url = $this->urlGenerator->getUrl(['owl' => ['foo']]);
$this->assertEquals('route:view.search_api_test_view.page_1;arg_0&arg_1&arg_2?f%5B0%5D=donkey%3Afoo', $url->toUriString());
// This won't work without it being in the request, so we need to do this
// from a block. We first click the link, check that the "orange" facet is
// active as expected and that the output from the custom block is shown.
// Then we click the item from the custom block and check that the orange is
// no longer active, but item is.
$this->drupalGet('search-api-test-fulltext');
$this->clickLink('orange');
$this->checkFacetIsActive('orange');
$this->checkFacetIsNotActive('item');
$this->assertSession()->pageTextContains('Link to owl item');
$this->clickLink('Link to owl item');
$this->checkFacetIsActive('item');
$this->checkFacetIsNotActive('orange');
// This won't work without it being in the request, so we need to do this
// from a block. We first click the link, check that the "orange" facet is
// active as expected and that the output from the custom block is shown.
// Then we click the item from the custom block and check that the orange is
// still active, but item is.
\Drupal::state()->get('facets_url_generator_keep_active', TRUE);
$this->drupalGet('search-api-test-fulltext');
$this->clickLink('orange');
$this->checkFacetIsActive('orange');
$this->checkFacetIsNotActive('item');
$this->assertSession()->pageTextContains('Link to owl item');
$this->clickLink('Link to owl item');
$this->checkFacetIsActive('item');
$this->checkFacetIsActive('orange');
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment