Skip to content
Snippets Groups Projects
Commit d300a2c3 authored by Jeroen Opdebeeck's avatar Jeroen Opdebeeck
Browse files

Issue #3452682: Add keymatch functionality

parent 65024faf
Branches
Tags
1 merge request!11Resolve #3452682 "Add keymatch functionality"
...@@ -40,6 +40,13 @@ services: ...@@ -40,6 +40,13 @@ services:
tags: tags:
- { name: event_subscriber } - { name: event_subscriber }
elasticsearch_search_api.factory.keymatch_entry:
class: Drupal\elasticsearch_search_api\Factory\KeymatchEntryFactory
arguments: []
elasticsearch_search_api.keymatch_service:
class: Drupal\elasticsearch_search_api\KeymatchService
arguments: ['@config.factory', '@path.validator', '@elasticsearch_search_api.factory.keymatch_entry']
# #
# #
# #
......
<?php
namespace Drupal\elasticsearch_search_api\Factory;
use Drupal\elasticsearch_search_api\KeymatchEntry;
/**
* Class KeymatchEntryFactory.
*/
class KeymatchEntryFactory {
/**
* Creates a KeymatchEntry object.
*
* @param string $keymatch_entry
* Keymatch entry as a string.
*
* @return \Drupal\elasticsearch_search_api\KeymatchEntry
* Returns a KeymatchEntry object
*/
public function createEntry(string $keymatch_entry) {
return new KeymatchEntry($keymatch_entry);
}
}
<?php
namespace Drupal\elasticsearch_search_api\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Render\RendererInterface;
use Drupal\elasticsearch_search_api\KeymatchService;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class KeymatchForm.
*
* @package Drupal\general\Form
*/
class KeymatchForm extends ConfigFormBase {
const CONFIG_KEY = 'elasticsearch_search_api.keymatch';
/**
* The config instance.
*
* @var \Drupal\Core\Config\Config|\Drupal\Core\Config\ImmutableConfig
*/
protected $configInstance;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The keymatch service.
*
* @var \Drupal\elasticsearch_search_api\KeymatchService
*/
protected $keymatchService;
/**
* KeymatchForm constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config instance.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\elasticsearch_search_api\KeymatchService $keymatch_service
* The keymatch service.
*/
public function __construct(ConfigFactoryInterface $config_factory, RendererInterface $renderer, KeymatchService $keymatch_service) {
parent::__construct($config_factory);
$this->configInstance = $this->config(self::CONFIG_KEY);
$this->renderer = $renderer;
$this->keymatchService = $keymatch_service;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('renderer'),
$container->get('elasticsearch_search_api.keymatch_service')
);
}
/**
* Implements \Drupal\Core\Form\FormInterface::getFormID().
*/
public function getFormId() {
return 'elasticsearch_search_api_keymatch_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
self::CONFIG_KEY,
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['keymatch_fieldset'] = [
'#type' => 'fieldset',
'#collapsible' => FALSE,
'#collapsed' => FALSE,
'#title' => '',
];
$form['keymatch_fieldset']['keymatches'] = [
'#title' => t('Keymatches'),
'#type' => 'textarea',
'#default_value' => $this->configInstance->get('keymatches'),
'#rows' => 15,
];
$form['keymatch_fieldset']['keymatch_information'] = [
'#markup' => $this->getKeymatchInfo(),
];
return parent::buildForm($form, $form_state);
}
/**
* Provide help text for configuring keymatches.
*
* @return string
* Info message markup.
*
* @throws \Exception
*/
protected function getKeymatchInfo() {
$types_list = [
'#theme' => 'item_list',
'#items' => [
KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_TERM . ' (= ' . t('All terms, space delimited, occur anywhere in search query. Case-insensitive.') . ')',
KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_PHRASE . ' (= ' . t('Phrase occurs anywhere in search query. Case-insensitive.') . ')',
KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_EXACT . ' (= ' . t('Phrase/Term matches search query exactly. Case-sensitive.') . ')',
],
];
return '<div class="instruction-msg">' . t('Enter 1 entry per line (comma separated)') . ':<br />' .
'- ' . t('Search term') . ',' . t('{Type}') . ',' . t('URL for match') . ',' . t('Title for match') . '<br />' .
'- ' . 'Organisatie,TERM,https://www.vdab.be/vdab,Over VDAB <br />' .
'- ' . 'Organisatie Overheid,TERM,https://www.vdab.be/vdab,Over VDAB <br />' .
'- ' . 'VDAB-partners,EXACT,https://www.vdab.be/vdab,Over VDAB </div><br />' .
'<div class="instruction-tokens">' . t('{Type} can be one of the following') . ':' . $this->renderer->render($types_list) . '</div>';
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$config = $this->keymatchService->getKeymatchConfiguration($form_state->getValue('keymatches'));
$errors = '';
foreach ($config as $keymatch_entry) {
if (!$this->keymatchService->isValid($keymatch_entry, TRUE)) {
$errors .= '<li>Invalid entry: ' . $keymatch_entry . "</li>";
}
}
$errors = empty($errors) ? $errors : '<ul>' . $errors . '</ul>';
$errors = Markup::create($errors);
if (!empty($errors)) {
$message = $this->t('Not all provided keymatches were valid. @errors', ['@errors' => $errors]);
$form_state->setErrorByName('keymatches', $message);
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($form_state->cleanValues()->getValues() as $key => $formValue) {
$this->configInstance->set($key, $formValue);
}
$this->configInstance->save();
parent::submitForm($form, $form_state);
}
}
<?php
namespace Drupal\elasticsearch_search_api;
/**
* A KeymatchEntry Object.
*/
class KeymatchEntry {
/**
* The keymatch query.
*
* @var string
*/
private $query;
/**
* The keymatch url.
*
* @var string
*/
private $url;
/**
* The keymatch title.
*
* @var string
*/
private $title;
/**
* The keymatch type.
*
* @var string
*/
private $type;
/**
* KeymatchEntry constructor.
*
* @param string $keymatch_entry
* Keymatch entry as string.
*/
public function __construct($keymatch_entry) {
$parts = explode(',', $keymatch_entry);
$this->query = $parts[0];
$this->url = $parts[2];
$this->title = $parts[3];
$this->type = $parts[1];
}
/**
* Gets the query.
*
* @return string
* The keymatch query.
*/
public function getQuery() {
return $this->query;
}
/**
* Gets the url.
*
* @return string
* The keymatch url.
*/
public function getUrl() {
return $this->url;
}
/**
* Gets the title.
*
* @return string
* The keymatch title.
*/
public function getTitle() {
return $this->title;
}
/**
* Gets the type.
*
* @return string
* The keymatch type.
*/
public function getType() {
return $this->type;
}
}
<?php
namespace Drupal\elasticsearch_search_api;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\elasticsearch_search_api\Factory\KeymatchEntryFactory;
/**
* Keymatch Service class.
*/
class KeymatchService {
const VDAB_SEARCH_KEYMATCH_TYPE_TERM = 'TERM';
const VDAB_SEARCH_KEYMATCH_TYPE_PHRASE = 'PHRASE';
const VDAB_SEARCH_KEYMATCH_TYPE_EXACT = 'EXACT';
/**
* The config instance.
*
* @var \Drupal\Core\Config\Config|\Drupal\Core\Config\ImmutableConfig
*/
protected $configInstance;
/**
* The path validator.
*
* @var \Drupal\Core\Path\PathValidatorInterface
*/
protected $pathValidator;
/**
* The keymatch entry factory.
*
* @var \Drupal\elasticsearch_search_api\Factory\KeymatchEntryFactory
*/
protected $keymatchEntryFactory;
/**
* KeymatchForm constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config instance.
* @param \Drupal\Core\Path\PathValidatorInterface $path_validator
* The path validator.
* @param \Drupal\elasticsearch_search_api\Factory\KeymatchEntryFactory $keymatch_entry_factory
* The keymatchEntry factory.
*/
public function __construct(ConfigFactoryInterface $config_factory, PathValidatorInterface $path_validator, KeymatchEntryFactory $keymatch_entry_factory) {
$this->configInstance = $config_factory->get('elasticsearch_search_api.keymatch');
$this->pathValidator = $path_validator;
$this->keymatchEntryFactory = $keymatch_entry_factory;
}
/**
* Returns the available keymatch types.
*
* @return array
* Returns the types in an array.
*/
public static function allKeymatchTypes() {
return [
KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_TERM => KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_TERM,
KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_PHRASE => KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_PHRASE,
KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_EXACT => KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_EXACT,
];
}
/**
* Finds keymatches for the given query.
*
* @param string $search_query
* The search query to match keymatches with.
*
* @return KeymatchEntry[]
* Returns an array of KeymatchEntry objects.
*/
public function find($search_query) {
$keymatch_configuration = $this->getKeymatchConfiguration();
$ordered_keymatches = $this->getKeymatchEntriesByType($keymatch_configuration);
$keymatches_found = [];
if (isset($search_query)) {
foreach ($ordered_keymatches as $type => $keymatches) {
$type_uppercase = strtoupper($type);
/** @var KeymatchEntry $keymatch */
foreach ($keymatches as $keymatch) {
$matches = FALSE;
switch ($type_uppercase) {
case KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_EXACT:
$matches = $this->matchesExact($search_query, $keymatch->getQuery());
break;
case KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_TERM:
$matches = $this->matchesTerm($search_query, $keymatch->getQuery());
break;
case KeymatchService::VDAB_SEARCH_KEYMATCH_TYPE_PHRASE:
$matches = $this->matchesPhrase($search_query, $keymatch->getQuery());
break;
}
if ($matches) {
$keymatches_found[] = $keymatch;
}
}
}
}
return $keymatches_found;
}
/**
* Checks an exact keymatch.
*
* @param string $query
* The search query.
* @param string $keymatch
* The keymatch.
*
* @return bool
* TRUE if query matches on specified keymatch
*/
public function matchesExact($query, $keymatch) {
return $query === $keymatch;
}
/**
* Checks a keymatch with the type 'phrase'.
*
* @param string $query
* The search query.
* @param string $keymatch
* The keymatch.
*
* @return bool
* TRUE if query matches on specified keymatch
*/
public function matchesPhrase($query, $keymatch) {
$query_lower = strtolower($query);
$keymatch_lower = strtolower($keymatch);
$regex = '/\b' . $keymatch_lower . '\b/';
preg_match($regex, $query_lower, $match);
return isset($match) && !empty($match);
}
/**
* Checks a keymatch with the type 'term'.
*
* @param string $query
* The search query.
* @param string $keymatch
* The keymatch.
*
* @return bool
* TRUE if query matches on specified keymatch
*/
public function matchesTerm($query, $keymatch) {
$keymatch_terms = explode(' ', $keymatch);
if (!isset($keymatch_terms) || $keymatch_terms === FALSE) {
return FALSE;
}
foreach ($keymatch_terms as $term) {
if (!$this->matchesPhrase($query, $term)) {
return FALSE;
};
}
return TRUE;
}
/**
* Checks whether a keymatch string is valid or not.
*
* @param string $keymatch_entry
* Keymatch entry as a string, as it is stored in the config.
* @param bool $strict
* Check if url is external or valid internal path.
*
* @return bool
* Whether the keymatch entry is valid or not.
*/
public function isValid($keymatch_entry, $strict = FALSE) {
$parts = explode(',', $keymatch_entry);
if (!$parts || !is_array($parts) || empty($parts) || count($parts) < 4) {
return FALSE;
}
$trimmed_query = trim($parts[0]);
$trimmed_url = trim($parts[2]);
$trimmed_title = trim($parts[3]);
$valid_path = TRUE;
if ($strict !== FALSE) {
$valid_path = $this->pathValidator->isValid($trimmed_url);
}
return !empty($trimmed_query) && !empty($trimmed_title) && !empty($trimmed_url) && isset($this->allKeymatchTypes()[$parts[1]]) && $valid_path !== FALSE;
}
/**
* Creates an array of keymatches as strings, either from a value or config.
*
* @param string|null $saved_keymatches
* Uses this value if provided, otherwise it gets the value stored in
* config.
*
* @return array
* Returns an array of keymatch strings.
*/
public function getKeymatchConfiguration($saved_keymatches = NULL) {
if (!isset($saved_keymatches)) {
$saved_keymatches = $this->configInstance->get('keymatches');
}
// Convert newlines to array based on \r\n or \n without empty matches.
$saved_keymatches = preg_split('/(\r\n?|\n)/', $saved_keymatches, -1, PREG_SPLIT_NO_EMPTY);
// Filter out values with only spaces.
$saved_keymatches = array_filter($saved_keymatches,
function ($split) {
$split_safe = trim($split);
return !empty($split_safe);
}
);
// Make sure our array indexes are reset in case entries were skipped.
return array_values($saved_keymatches);
}
/**
* Creates KeymatchEntry objects and orders them by type.
*
* @param array $config
* An array of keymatch strings as returned by getKeymatchConfiguration.
*
* @return array
* Returns KeymatchEntry objects ordered by type.
*/
public function getKeymatchEntriesByType(array $config) {
$keymatches_by_type = [];
foreach ($config as $keymatch_entry) {
if ($this->isValid($keymatch_entry)) {
$entry = $this->keymatchEntryFactory->createEntry($keymatch_entry);
$keymatches_by_type[$entry->getType()][] = $entry;
}
}
return $keymatches_by_type;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment