views_autocomplete_api-8.x-1.x-dev/src/Service/ViewsAutocompleteApiManager.php
src/Service/ViewsAutocompleteApiManager.php
<?php
namespace Drupal\views_autocomplete_api\Service;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Render\Markup;
use Drupal\Core\Render\Renderer;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\views\Entity\View;
use Drupal\views\ViewExecutable;
use Drupal\views\ViewExecutableFactory;
/**
* Views Autocomplete Api Manager class.
*
* @package Drupal\views_autocomplete_api\Service
*/
class ViewsAutocompleteApiManager {
/**
* The executable views object.
*
* @var \Drupal\views\ViewExecutableFactory
*/
protected $viewsExecute;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\Renderer
*/
protected $renderer;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactory
*/
protected $configFactory;
/**
* The logger channel.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
use LoggerChannelTrait;
/**
* ViewsAutocompleteApiController constructor.
*
* @param \Drupal\views\ViewExecutableFactory $views_execute
* The object views execute.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current user.
* @param \Drupal\Core\Render\Renderer $renderer
* The renderer service.
* @param \Drupal\Core\Config\ConfigFactory $config_factory
* The config factory.
*/
public function __construct(ViewExecutableFactory $views_execute, AccountProxyInterface $current_user, Renderer $renderer, ConfigFactory $config_factory) {
$this->viewsExecute = $views_execute;
$this->currentUser = $current_user;
$this->renderer = $renderer;
$this->configFactory = $config_factory;
$this->logger = $this->getLogger('views_auto_complete_api');
}
/**
* Extract display id, built it if not send.
*
* @param string $display_id
* All display ids of all views.
* @param int $count_view
* The number of views.
*
* @return array
* An array of display_id of all views.
*/
public function getViewsDisplayId($display_id, $count_view) {
if (!empty($display_id)) {
$display_id = explode(',', $display_id);
if (count($display_id) !== $count_view) {
$this->logger->warning('Number of display are different from number of views, calling in parameters controller.');
}
return $display_id;
}
$display_id = [];
for ($i = 0; $i < $count_view; $i++) {
$display_id[$i] = 'default';
}
return $display_id;
}
/**
* Prepare the arguments of views.
*
* @param string $views_arguments
* All arguments of all views.
* @param int $count_view
* The number of views.
*
* @return array
* An array of all arguments of all views.
*/
public function prepareArgumentViews($views_arguments, $count_view) {
// Prepare arguments.
$args_views = [];
if (!empty($views_arguments) || trim($views_arguments) == "") {
return $args_views;
}
$args = explode(',', $views_arguments);
if (count($args) !== $count_view) {
$this->logger->warning(
'Number of views arguments are different from number of views, calling in parameters controller.'
);
}
foreach ($args as $arg) {
$args_views[] = explode('&', $arg);
}
return $args_views;
}
/**
* Execute the views.
*
* @param \Drupal\views\Entity\View $view
* The view entity to execute.
* @param string $display_id
* The display id of view.
* @param string $search
* The words searched.
* @param array $view_data
* The view data.
* @param array $args_views
* Arguments of views.
*
* @throws \Exception
*/
public function executeViews(View $view, $display_id, $search, array &$view_data, array $args_views = []) {
// Init views.
$view_execute = $this->initViews($view, $display_id, $args_views);
$this->setFilter($view_execute, $search);
// Execute the view to get results.
$view_execute->executeDisplay();
// Gets the current style plugin object.
/** @var \Drupal\views\Plugin\views\style\DefaultStyle $currentStylePlugin */
$currentStylePlugin = $view_execute->getStyle();
$rendered_fields = [];
if (!empty($view_execute->result && !empty($view_execute->field))) {
foreach (array_keys($view_execute->result) as $index) {
foreach (array_keys($view_execute->field) as $field_name) {
$rendered_fields[$index][$field_name] = $currentStylePlugin->getField(
$index,
$field_name
);
}
}
}
if ($data = $this->getData($rendered_fields, $search)) {
// @todo catch display even if no results for header and footer.
// Add Header if exist.
if (!empty($view_execute->display_handler->options['header'])) {
$header = $this->formatSpecialRow(
'header',
$view_execute->display_handler->renderArea('header'),
$search
);
// Insert header.
if (!empty($header)) {
array_unshift($data, implode(PHP_EOL, $header));
}
}
// Add Header if exist.
if (!empty($view_execute->display_handler->options['footer'])) {
$footer = $this->formatSpecialRow(
'footer',
$view_execute->display_handler->renderArea('footer'),
$search
);
if (!empty($footer)) {
$data[] = implode(PHP_EOL, $footer);
}
}
$view_data = array_merge($view_data, $data);
}
elseif (!empty($view_execute->display_handler->options['empty'])) {
$empty = $this->formatSpecialRow(
'empty',
$view_execute->display_handler->renderArea('empty'),
$search
);
if (!empty($empty)) {
$view_data = array_merge($view_data, [implode(PHP_EOL, $empty)]);
}
}
}
/**
* Init the views and return the executable object.
*
* @param \Drupal\views\Entity\View $view
* The view entity.
* @param string $display_id
* The display id of view.
* @param array $args_views
* Arguments of views.
*
* @return bool|\Drupal\views\ViewExecutable
* False if error, or the views executable object.
*/
private function initViews(View $view, $display_id, array $args_views = []) {
$view_execute = $this->viewsExecute->get($view);
/** @var \Drupal\views\ViewExecutable $view */
if (!$view_execute) {
$this->logger->error('Can\'t load views "%view_name"', ['%view_name' => $view->id()]);
return FALSE;
}
// Set display.
if ($view_execute->setDisplay($display_id) == FALSE) {
$this->logger->error('No display "%display_name" found for the views "%view_name"', [
'%display_name' => $display_id,
'%view_name' => $view->id(),
]);
return FALSE;
}
// Check permission to the view display default(master).
if (!$view_execute->access($display_id) && !$this->currentUser->hasPermission('administer views')) {
$this->logger->warning('Access denied for the views "%view_name"', ['%view_name' => $view->id()]);
return FALSE;
}
if (!empty($args_views)) {
$view_execute->setArguments($args_views);
}
return $view_execute;
}
/**
* Set filter on views executable.
*
* @param \Drupal\views\ViewExecutable $view_execute
* The view executable object.
* @param string $search
* The word searched.
*/
private function setFilter(ViewExecutable $view_execute, $search) {
// Loop on each exposed filter.
$display_handler = $view_execute->getDisplay();
$options_filter = $display_handler->getOption('filters');
foreach ($options_filter as &$options) {
if (!empty($options['exposed']) && $options['exposed'] === TRUE && !empty($options['expose']['identifier'])) {
$options['value'] = $search;
}
}
$display_handler->overrideOption('filters', $options_filter);
}
/**
* Get header of the view if exist.
*
* @param string $type
* Type request i.e header or footer.
* @param array $data_views
* The header of the view.
* @param string $search
* The search text.
*
* @return array
* An array of views data header.
*
* @throws \Exception
*/
protected function formatSpecialRow($type, array $data_views, $search) {
if (empty($data_views)) {
return [];
}
$view_data_formatted = [];
$row = '';
foreach ($data_views as $area) {
$value = $this->renderer->render($area);
if ($value instanceof Markup) {
$value = $value->__toString();
}
if (!empty($value) && strpos($value, '[autocomplete]')) {
$value = str_replace('[autocomplete]', $search, $value);
}
$row .= $value;
}
$element = [
'#theme' => 'views_autocomplete_api_special_row',
'#type_group' => $type,
'#row' => $row,
];
$view_data_formatted[] = $this->renderer->render($element);
return $view_data_formatted;
}
/**
* Re-format the views data rendered.
*
* @param array $rendered_fields
* An array of rendered field.
* @param string $search
* The search text.
*
* @return array
* An array with views data (the last and before last row).
*/
protected function getData(array $rendered_fields, $search) {
$view_data_formatted = [];
// The String Which search for.
foreach ($rendered_fields as $row) {
// Content of rendered fields.
$row_values = array_values($row);
$count = count($row_values);
$key = $rendered = $row_values[count($row) - 2];
// Take the last field to allow to call more that one and
// "Rewrite field" and call them all.
if ($count > 1) {
$rendered = $row_values[$count - 1];
}
// We doesn't allow html for key input.
$viewData['value'] = strip_tags($key);
// Highlight search word.
if ($this->configFactory->get('views_autocomplete_api.settings')
->get('highlight') == TRUE) {
$rendered = $this->highlightStr($rendered, $search);
}
$viewData['label'] = $rendered;
$view_data_formatted[] = $viewData;
}
return $view_data_formatted;
}
/**
* Highlight string searched.
*
* @param string|string[]|null $haystack
* The haystack to search.
* @param string|string[]|null $needle
* The needle to find.
*
* @return string|string[]|null
* The haystack returned with highlighted needles.
*/
public function highlightStr($haystack, $needle) {
// Return $haystack if there is no highlight color or strings given,
// nothing to do.
if (empty($haystack) || empty($needle)) {
return $haystack;
}
$patterns = $replacements = [];
// Old regex : "/(?![^<]*>)$needle+/i".
// First replacement.
// [] = "'(?!((<.*?)|(<a.*?)))($needle)(?!(([^<>]*?)>)|([^>]*?</a>))'si";.
$patterns[] = "/(?![^<]*>)$needle+/i";
$element = [
'#theme' => 'views_autocomplete_api_highlight',
'#search_word' => $needle,
];
$replacements[] = $this->renderer->render($element);
// Check translated search query.
$transliterated_match = $this->removeAccents($needle);
if ($needle != $transliterated_match) {
// Old regex : "/(?![^<]*>)$transliterated_match+/i".
// [] = "'(?!((<.*?)|(<a.*?)))($transliterated_match)(?!(([^<>]*?)>)|([^>]*?</a>))'si";.
$patterns[] = "/(?![^<]*>)$transliterated_match+/i";
$element = [
'#theme' => 'views_autocomplete_api_highlight',
'#search_word' => $transliterated_match,
];
$replacements[] = $this->renderer->render($element);
}
// Replace for highlighting.
$haystack = preg_replace($patterns, $replacements, $haystack);
return $haystack;
}
/**
* Delete accent from string.
*
* @param string $str
* String to convert.
* @param string $encoding
* Format encoding.
*
* @return string
* The modified string.
*
* @todo find way to get list on all encoding et put it in config module.
*/
public function removeAccents($str, $encoding = 'utf-8') {
// Convert all applicable characters to HTML entities.
$str = htmlentities($str, ENT_NOQUOTES, $encoding);
// Replace the html entities, to get just the first letter without accent.
// Example : "&ecute;" => "e", "&Ecute;" => "E", "Ã " => "a" ...
$str = preg_replace(
'#&([A-za-z])(?:acute|grave|cedil|circ|orn|ring|slash|th|tilde|uml);#',
'\1',
$str
);
// Replace ligatures as : Œ, Æ ...
// Example "Å“" => "oe".
$str = preg_replace('#&([A-za-z]{2})(?:lig);#', '\1', $str);
// Delete other special character.
$str = preg_replace('#&[^;]+;#', '', $str);
return $str;
}
}
