@ -2,15 +2,27 @@
namespace Drupal\solr_facet_field_formatter\Plugin\Field\FieldFormatter;
namespace Drupal\solr_facet_field_formatter\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Url;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Link;
use Drupal\search_api_solr\Utility;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\facets\FacetSource\FacetSourcePluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
/**
* Plugin implementation of the 'Facet_Field_Formatter' formatter.
* Plugin implementation of the 'Facet_Field_Formatter' formatter.
*
*
* Renders a text or string field value as a link into a Search API / Facets
* search page, pre-filtered by a configured facet. The search page, filter key
* and facet are all configured per field instance so the formatter is not tied
* to any particular site structure.
*
* @FieldFormatter(
* @FieldFormatter(
* id = "solr_facet_text_field_formatter",
* id = "solr_facet_text_field_formatter",
* module = "solr_facet_field_formatter",
* module = "solr_facet_field_formatter",
@ -24,14 +36,133 @@ use Drupal\search_api_solr\Utility;
* }
* }
* )
* )
*/
*/
class SolrFacetTextFieldFormatter extends FormatterBase {
class SolrFacetTextFieldFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The facet source plugin manager.
*
* @var \Drupal\facets\FacetSource\FacetSourcePluginManager
*/
protected $facetSourceManager;
/**
* {@inheritdoc}
*/
public function __construct($plugin_id, $plugin_definition, $field_definition, array $settings, $label, $view_mode, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, FacetSourcePluginManager $facet_source_manager) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->entityTypeManager = $entity_type_manager;
$this->facetSourceManager = $facet_source_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('entity_type.manager'),
$container->get('plugin.manager.facets.facet_source')
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
// The facet source plugin id (e.g. the view display) that renders the
// search results page the link points to.
'facet_source' => '',
// The machine name of the facet to filter on.
'facet_id' => '',
// Whether to reset an exposed fulltext filter on the target page so the
// link produces a clean, facet-only search.
'reset_fulltext' => TRUE,
// The query parameter used by the exposed fulltext filter.
'fulltext_key' => 'search_api_fulltext',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$form = parent::settingsForm($form, $form_state);
$form['facet_source'] = [
'#type' => 'select',
'#title' => $this->t('Search page (facet source)'),
'#description' => $this->t('The search results page the link points to. This determines both the URL path and the facet filter key.'),
'#options' => $this->getFacetSourceOptions(),
'#default_value' => $this->getSetting('facet_source'),
'#empty_option' => $this->t('- Select a facet source -'),
'#required' => TRUE,
];
$form['facet_id'] = [
'#type' => 'select',
'#title' => $this->t('Facet'),
'#description' => $this->t('The facet whose value should be set to this field value. Choose the facet configured for this field on the selected search page.'),
'#options' => $this->getFacetOptions(),
'#default_value' => $this->getSetting('facet_id'),
'#empty_option' => $this->t('- Select a facet -'),
'#required' => TRUE,
];
$form['reset_fulltext'] = [
'#type' => 'checkbox',
'#title' => $this->t('Reset fulltext search'),
'#description' => $this->t('Add an empty fulltext query parameter so following the link starts a fresh, facet-only search.'),
'#default_value' => $this->getSetting('reset_fulltext'),
];
$form['fulltext_key'] = [
'#type' => 'textfield',
'#title' => $this->t('Fulltext parameter'),
'#description' => $this->t('The query parameter used by the exposed fulltext filter on the search page.'),
'#default_value' => $this->getSetting('fulltext_key'),
'#states' => [
'visible' => [
':input[name$="[settings_edit_form][settings][reset_fulltext]"]' => ['checked' => TRUE],
],
],
];
return $form;
}
/**
/**
* {@inheritdoc}
* {@inheritdoc}
*/
*/
public function settingsSummary() {
public function settingsSummary() {
$summary = [];
$summary = [];
$summary[] = $this->t('Format string or text fields as links to a facet search.');
$facet_id = $this->getSetting('facet_id');
$facet_source = $this->getSetting('facet_source');
if ($facet_id & & $facet_source) {
$summary[] = $this->t('Links to facet %facet on %source.', [
'%facet' => $this->getFacetLabel($facet_id),
'%source' => $this->getFacetSourceLabel($facet_source),
]);
}
else {
$summary[] = $this->t('Not configured: select a facet source and facet.');
}
return $summary;
return $summary;
}
}
@ -41,15 +172,230 @@ class SolrFacetTextFieldFormatter extends FormatterBase {
public function viewElements(FieldItemListInterface $items, $langcode) {
public function viewElements(FieldItemListInterface $items, $langcode) {
$element = [];
$element = [];
$facet_source = $this->getSetting('facet_source');
$facet_id = $this->getSetting('facet_id');
$path = $facet_source & & $facet_id ? $this->getFacetSourcePath($facet_source) : NULL;
// Without a configured, resolvable target there is nothing to link to; fall
// back to rendering the plain value.
if ($path === NULL) {
foreach ($items as $delta => $item) {
$element[$delta] = ['#plain_text' => $this->getFacetValue($item)];
}
return $element;
}
$filter_key = $this->getFilterKey($facet_source);
$url_alias = $this->getFacetUrlAlias($facet_id);
// Facets uses a colon separator between the alias and the raw value.
$separator = ':';
foreach ($items as $delta => $item) {
foreach ($items as $delta => $item) {
$field = $item->getFieldDefinition();
$value = $this->getFacetValue($item);
// TODO: more robust fieldname parsing
if ($value === NULL || $value === '') {
$fieldLabel = str_replace(' ', '_', $field->getLabel());
continue;
$link = Link::fromTextAndUrl($item->value, Url::fromroute('view.herbarium_search.page_1',
}
[], ['query' => ['search_api_fulltext' => '','herbarium-search[0]' => $fieldLabel . ':' . $item->value]]));
$query = [];
if ($this->getSetting('reset_fulltext')) {
$query[$this->getSetting('fulltext_key')] = '';
}
// Produces filter_key[0]=alias:value, matching the query string built by
// the Facets module itself.
$query[$filter_key] = [$url_alias . $separator . $value];
$url = Url::fromUserInput($path, ['query' => $query]);
$link = Link::fromTextAndUrl($value, $url);
$element[$delta] = [$link->toRenderable()];
$element[$delta] = [$link->toRenderable()];
}
}
return $element;
return $element;
}
}
/**
* Extracts the value to filter the facet by from a field item.
*
* For reference fields (entity_reference, typed_relation, …) the referenced
* entity's label is used, so the facet is filtered by the readable value
* rather than the internal entity id. For all other field types the field's
* main property value is used.
*
* @param \Drupal\Core\Field\FieldItemInterface $item
* The field item.
*
* @return string|null
* The facet value, or NULL if none could be determined.
*/
protected function getFacetValue(FieldItemInterface $item) {
if ($item instanceof EntityReferenceItem) {
$entity = $item->entity;
if ($entity instanceof EntityInterface) {
return $entity->label();
}
}
$main_property = $item->getFieldDefinition()
->getFieldStorageDefinition()
->getMainPropertyName();
if ($main_property !== NULL & & isset($item->{$main_property})) {
return $item->{$main_property};
}
// Last resort: the item's string representation.
$string = $item->getString();
return $string === '' ? NULL : $string;
}
/**
* Builds the list of facet source options for the settings form.
*
* @return array
* An array of facet source labels keyed by plugin id.
*/
protected function getFacetSourceOptions() {
$options = [];
foreach ($this->facetSourceManager->getDefinitions() as $id => $definition) {
$options[$id] = !empty($definition['label']) ? $definition['label'] : $id;
}
natcasesort($options);
return $options;
}
/**
* Builds the list of facet options for the settings form.
*
* @return array
* An array of facet labels keyed by facet machine name.
*/
protected function getFacetOptions() {
$options = [];
/** @var \Drupal\facets\FacetInterface $facet */
foreach ($this->loadFacets() as $facet) {
$options[$facet->id()] = $this->t('@label (@source)', [
'@label' => $facet->label(),
'@source' => $facet->getFacetSourceId(),
]);
}
natcasesort($options);
return $options;
}
/**
* Loads all configured facet entities.
*
* @return \Drupal\facets\FacetInterface[]
* The facet entities, or an empty array if the facets module is not
* available.
*/
protected function loadFacets() {
if (!$this->entityTypeManager->hasDefinition('facets_facet')) {
return [];
}
return $this->entityTypeManager->getStorage('facets_facet')->loadMultiple();
}
/**
* Loads a single facet entity.
*
* @param string $facet_id
* The facet machine name.
*
* @return \Drupal\facets\FacetInterface|null
* The facet entity, or NULL if it does not exist.
*/
protected function loadFacet($facet_id) {
if (!$this->entityTypeManager->hasDefinition('facets_facet')) {
return NULL;
}
return $this->entityTypeManager->getStorage('facets_facet')->load($facet_id);
}
/**
* Returns the human readable label for a facet.
*/
protected function getFacetLabel($facet_id) {
$facet = $this->loadFacet($facet_id);
return $facet ? $facet->label() : $facet_id;
}
/**
* Returns the human readable label for a facet source.
*/
protected function getFacetSourceLabel($facet_source) {
$definitions = $this->facetSourceManager->getDefinitions();
if (isset($definitions[$facet_source]['label']) & & !empty($definitions[$facet_source]['label'])) {
return $definitions[$facet_source]['label'];
}
return $facet_source;
}
/**
* Returns the URL alias used in facet queries for a facet.
*
* @param string $facet_id
* The facet machine name.
*
* @return string
* The facet URL alias, falling back to the facet id.
*/
protected function getFacetUrlAlias($facet_id) {
$facet = $this->loadFacet($facet_id);
return $facet ? $facet->getUrlAlias() : $facet_id;
}
/**
* Resolves the search page path for a facet source.
*
* @param string $facet_source
* The facet source plugin id.
*
* @return string|null
* The internal path (e.g. "/search"), or NULL if it cannot be resolved.
*/
protected function getFacetSourcePath($facet_source) {
if (!$this->facetSourceManager->hasDefinition($facet_source)) {
return NULL;
}
try {
/** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $plugin */
$plugin = $this->facetSourceManager->createInstance($facet_source);
$path = $plugin->getPath();
if ($path === NULL || $path === '') {
return NULL;
}
// Url::fromUserInput() requires a leading slash.
return ($path[0] === '/') ? $path : '/' . $path;
}
catch (\Exception $e) {
return NULL;
}
}
/**
* Resolves the facet filter key for a facet source.
*
* Mirrors the logic in the Facets module: the facet source config entity id
* is the plugin id with colons replaced by double underscores, and the
* default filter key is "f" when no configuration exists.
*
* @param string $facet_source
* The facet source plugin id.
*
* @return string
* The query string filter key.
*/
protected function getFilterKey($facet_source) {
if ($this->entityTypeManager->hasDefinition('facets_facet_source')) {
$source_id = str_replace(':', '__', $facet_source);
/** @var \Drupal\facets\FacetSourceInterface|null $config */
$config = $this->entityTypeManager->getStorage('facets_facet_source')->load($source_id);
if ($config & & $config->getFilterKey()) {
return $config->getFilterKey();
}
}
return 'f';
}
}
}