3 namespace Drupal\views\Plugin\views;
5 use Drupal\Component\Plugin\DependentPluginInterface;
6 use Drupal\Component\Utility\Xss;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Language\LanguageInterface;
9 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
10 use Drupal\Core\Plugin\PluginBase as ComponentPluginBase;
11 use Drupal\Core\Render\Element;
12 use Drupal\Core\StringTranslation\TranslatableMarkup;
13 use Drupal\views\Plugin\views\display\DisplayPluginBase;
14 use Drupal\views\ViewExecutable;
15 use Symfony\Component\DependencyInjection\ContainerInterface;
18 * Base class for any views plugin types.
20 * Via the @Plugin definition the plugin may specify a theme function or
21 * template to be used for the plugin. It also can auto-register the theme
22 * implementation for that file or function.
23 * - theme: the theme implementation to use in the plugin. This may be the name
24 * of the function (without theme_ prefix) or the template file (without
25 * template engine extension).
26 * If a template file should be used, the file has to be placed in the
27 * module's templates folder.
28 * Example: theme = "mymodule_row" of module "mymodule" will implement
29 * mymodule-row.html.twig in the [..]/modules/mymodule/templates folder.
30 * - register_theme: (optional) When set to TRUE (default) the theme is
31 * registered automatically. When set to FALSE the plugin reuses an existing
32 * theme implementation, defined by another module or views plugin.
33 * - theme_file: (optional) the location of an include file that may hold the
34 * theme or preprocess function. The location has to be relative to module's
36 * - module: machine name of the module. It must be present for any plugin that
37 * wants to register a theme.
39 * @ingroup views_plugins
41 abstract class PluginBase extends ComponentPluginBase implements ContainerFactoryPluginInterface, ViewsPluginInterface, DependentPluginInterface {
44 * Include negotiated languages when listing languages.
46 * @see \Drupal\views\Plugin\views\PluginBase::listLanguages()
48 const INCLUDE_NEGOTIATED = 16;
51 * Include entity row languages when listing languages.
53 * @see \Drupal\views\Plugin\views\PluginBase::listLanguages()
55 const INCLUDE_ENTITY = 32;
58 * Query string to indicate the site default language.
60 * @see \Drupal\Core\Language\LanguageInterface::LANGCODE_DEFAULT
62 const VIEWS_QUERY_LANGUAGE_SITE_DEFAULT = '***LANGUAGE_site_default***';
65 * Options for this plugin will be held here.
72 * The top object of a view.
74 * @var \Drupal\views\ViewExecutable
79 * The display object this plugin is for.
81 * For display plugins this is empty.
83 * @todo find a better description
85 * @var \Drupal\views\Plugin\views\display\DisplayPluginBase
87 public $displayHandler;
90 * Plugins's definition
97 * Denotes whether the plugin has an additional options form.
101 protected $usesOptions = FALSE;
104 * Stores the render API renderer.
106 * @var \Drupal\Core\Render\RendererInterface
111 * Constructs a PluginBase object.
113 * @param array $configuration
114 * A configuration array containing information about the plugin instance.
115 * @param string $plugin_id
116 * The plugin_id for the plugin instance.
117 * @param mixed $plugin_definition
118 * The plugin implementation definition.
120 public function __construct(array $configuration, $plugin_id, $plugin_definition) {
121 parent::__construct($configuration, $plugin_id, $plugin_definition);
123 $this->definition = $plugin_definition + $configuration;
129 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
130 return new static($configuration, $plugin_id, $plugin_definition);
136 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
138 $this->setOptionDefaults($this->options, $this->defineOptions());
139 $this->displayHandler = $display;
141 $this->unpackOptions($this->options, $options);
145 * Information about options for all kinds of purposes will be held here.
147 * 'option_name' => array(
148 * - 'default' => default value,
149 * - 'contains' => (optional) array of items this contains, with its own
150 * defaults, etc. If contains is set, the default will be ignored and
151 * assumed to be array().
156 * Returns the options of this handler/plugin.
158 protected function defineOptions() { return []; }
161 * Fills up the options of the plugin with defaults.
163 * @param array $storage
164 * An array which stores the actual option values of the plugin.
165 * @param array $options
166 * An array which describes the options of a plugin. Each element is an
167 * associative array containing:
168 * - default: The default value of one option. Should be translated to the
169 * interface text language selected for page if translatable.
170 * - (optional) contains: An array which describes the available options
171 * under the key. If contains is set, the default will be ignored and
172 * assumed to be an empty array.
173 * - (optional) 'bool': TRUE if the value is boolean, else FALSE.
175 protected function setOptionDefaults(array &$storage, array $options) {
176 foreach ($options as $option => $definition) {
177 if (isset($definition['contains'])) {
178 $storage[$option] = [];
179 $this->setOptionDefaults($storage[$option], $definition['contains']);
182 $storage[$option] = $definition['default'];
190 public function filterByDefinedOptions(array &$storage) {
191 $this->doFilterByDefinedOptions($storage, $this->defineOptions());
195 * Do the work to filter out stored options depending on the defined options.
197 * @param array $storage
198 * The stored options.
199 * @param array $options
200 * The defined options.
202 protected function doFilterByDefinedOptions(array &$storage, array $options) {
203 foreach ($storage as $key => $sub_storage) {
204 if (!isset($options[$key])) {
205 unset($storage[$key]);
208 if (isset($options[$key]['contains'])) {
209 $this->doFilterByDefinedOptions($storage[$key], $options[$key]['contains']);
217 public function unpackOptions(&$storage, $options, $definition = NULL, $all = TRUE, $check = TRUE) {
218 if ($check && !is_array($options)) {
222 if (!isset($definition)) {
223 $definition = $this->defineOptions();
226 foreach ($options as $key => $value) {
227 if (is_array($value)) {
228 // Ignore arrays with no definition.
229 if (!$all && empty($definition[$key])) {
233 if (!isset($storage[$key]) || !is_array($storage[$key])) {
237 // If we're just unpacking our known options, and we're dropping an
238 // unknown array (as might happen for a dependent plugin fields) go
239 // ahead and drop that in.
240 if (!$all && isset($definition[$key]) && !isset($definition[$key]['contains'])) {
241 $storage[$key] = $value;
245 $this->unpackOptions($storage[$key], $value, isset($definition[$key]['contains']) ? $definition[$key]['contains'] : [], $all, FALSE);
247 elseif ($all || !empty($definition[$key])) {
248 $storage[$key] = $value;
256 public function destroy() {
257 unset($this->view, $this->display, $this->query);
263 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
264 // Some form elements belong in a fieldset for presentation, but can't
265 // be moved into one because of the $form_state->getValues() hierarchy. Those
266 // elements can add a #fieldset => 'fieldset_name' property, and they'll
267 // be moved to their fieldset during pre_render.
268 $form['#pre_render'][] = [get_class($this), 'preRenderAddFieldsetMarkup'];
274 public function validateOptionsForm(&$form, FormStateInterface $form_state) { }
279 public function submitOptionsForm(&$form, FormStateInterface $form_state) { }
284 public function query() { }
289 public function themeFunctions() {
290 return $this->view->buildThemeFunctions($this->definition['theme']);
296 public function validate() { return []; }
301 public function summaryTitle() {
302 return $this->t('Settings');
308 public function pluginTitle() {
309 // Short_title is optional so its defaults to an empty string.
310 if (!empty($this->definition['short_title'])) {
311 return $this->definition['short_title'];
313 return $this->definition['title'];
319 public function usesOptions() {
320 return $this->usesOptions;
326 public function globalTokenReplace($string = '', array $options = []) {
327 return \Drupal::token()->replace($string, ['view' => $this->view], $options);
331 * Replaces Views' tokens in a given string. The resulting string will be
332 * sanitized with Xss::filterAdmin.
335 * Unsanitized string with possible tokens.
337 * Array of token => replacement_value items.
341 protected function viewsTokenReplace($text, $tokens) {
342 if (!strlen($text)) {
343 // No need to run filterAdmin on an empty string.
346 if (empty($tokens)) {
347 return Xss::filterAdmin($text);
351 foreach ($tokens as $token => $replacement) {
352 // Twig wants a token replacement array stripped of curly-brackets.
353 // Some Views tokens come with curly-braces, others do not.
354 //@todo: https://www.drupal.org/node/2544392
355 if (strpos($token, '{{') !== FALSE) {
356 // Twig wants a token replacement array stripped of curly-brackets.
357 $token = trim(str_replace(['{{', '}}'], '', $token));
360 // Check for arrays in Twig tokens. Internally these are passed as
361 // dot-delimited strings, but need to be turned into associative arrays
363 if (strpos($token, '.') === FALSE) {
364 // We need to validate tokens are valid Twig variables. Twig uses the
365 // same variable naming rules as PHP.
366 // @see http://php.net/manual/language.variables.basics.php
367 assert('preg_match(\'/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/\', $token) === 1', 'Tokens need to be valid Twig variables.');
368 $twig_tokens[$token] = $replacement;
371 $parts = explode('.', $token);
372 $top = array_shift($parts);
373 assert('preg_match(\'/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/\', $top) === 1', 'Tokens need to be valid Twig variables.');
374 $token_array = [array_pop($parts) => $replacement];
375 foreach (array_reverse($parts) as $key) {
376 // The key could also be numeric (array index) so allow that.
377 assert('is_numeric($key) || (preg_match(\'/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/\', $key) === 1)', 'Tokens need to be valid Twig variables.');
378 $token_array = [$key => $token_array];
380 if (!isset($twig_tokens[$top])) {
381 $twig_tokens[$top] = [];
383 $twig_tokens[$top] += $token_array;
388 // Use the unfiltered text for the Twig template, then filter the output.
389 // Otherwise, Xss::filterAdmin could remove valid Twig syntax before the
390 // template is parsed.
393 '#type' => 'inline_template',
394 '#template' => $text,
395 '#context' => $twig_tokens,
397 function ($children, $elements) {
398 return Xss::filterAdmin($children);
403 // Currently you cannot attach assets to tokens with
404 // Renderer::renderPlain(). This may be unnecessarily limiting. Consider
405 // using Renderer::executeInRenderContext() instead.
406 // @todo: https://www.drupal.org/node/2566621
407 return (string) $this->getRenderer()->renderPlain($build);
410 return Xss::filterAdmin($text);
417 public function getAvailableGlobalTokens($prepared = FALSE, array $types = []) {
418 $info = \Drupal::token()->getInfo();
419 // Site and view tokens should always be available.
420 $types += ['site', 'view'];
421 $available = array_intersect_key($info['tokens'], array_flip($types));
423 // Construct the token string for each token.
426 foreach ($available as $type => $tokens) {
427 foreach (array_keys($tokens) as $token) {
428 $prepared[$type][] = "[$type:$token]";
441 public function globalTokenForm(&$form, FormStateInterface $form_state) {
444 foreach ($this->getAvailableGlobalTokens() as $type => $tokens) {
449 foreach ($tokens as $name => $info) {
450 $item['children'][$name] = "[$type:$name]" . ' - ' . $info['name'] . ': ' . $info['description'];
453 $token_items[$type] = $item;
456 $form['global_tokens'] = [
457 '#type' => 'details',
458 '#title' => $this->t('Available global token replacements'),
460 $form['global_tokens']['list'] = [
461 '#theme' => 'item_list',
462 '#items' => $token_items,
464 'class' => ['global-tokens'],
472 public static function preRenderAddFieldsetMarkup(array $form) {
473 foreach (Element::children($form) as $key) {
474 $element = $form[$key];
475 // In our form builder functions, we added an arbitrary #fieldset property
476 // to any element that belongs in a fieldset. If this form element has
477 // that property, move it into its fieldset.
478 if (isset($element['#fieldset']) && isset($form[$element['#fieldset']])) {
479 $form[$element['#fieldset']][$key] = $element;
480 // Remove the original element this duplicates.
490 public static function preRenderFlattenData($form) {
491 foreach (Element::children($form) as $key) {
492 $element = $form[$key];
493 if (!empty($element['#flatten'])) {
494 foreach (Element::children($element) as $child_key) {
495 $form[$child_key] = $form[$key][$child_key];
497 // All done, remove the now-empty parent.
508 public function calculateDependencies() {
515 public function getProvider() {
516 $definition = $this->getPluginDefinition();
517 return $definition['provider'];
521 * Makes an array of languages, optionally including special languages.
524 * (optional) Flags for which languages to return (additive). Options:
525 * - \Drupal\Core\Language::STATE_ALL (default): All languages
526 * (configurable and default).
527 * - \Drupal\Core\Language::STATE_CONFIGURABLE: Configurable languages.
528 * - \Drupal\Core\Language::STATE_LOCKED: Locked languages.
529 * - \Drupal\Core\Language::STATE_SITE_DEFAULT: Add site default language;
530 * note that this is not included in STATE_ALL.
531 * - \Drupal\views\Plugin\views\PluginBase::INCLUDE_NEGOTIATED: Add
532 * negotiated language types.
533 * - \Drupal\views\Plugin\views\PluginBase::INCLUDE_ENTITY: Add
534 * entity row language types. Note that these are only supported for
535 * display options, not substituted in queries.
536 * @param array|null $current_values
537 * The currently-selected options in the list, if available.
540 * An array of language names, keyed by the language code. Negotiated and
541 * special languages have special codes that are substituted in queries by
542 * PluginBase::queryLanguageSubstitutions().
543 * Only configurable languages and languages that are in $current_values are
544 * included in the list.
546 protected function listLanguages($flags = LanguageInterface::STATE_ALL, array $current_values = NULL) {
547 $manager = \Drupal::languageManager();
548 $languages = $manager->getLanguages($flags);
551 // The entity languages should come first, if requested.
552 if ($flags & PluginBase::INCLUDE_ENTITY) {
553 $list['***LANGUAGE_entity_translation***'] = $this->t('Content language of view row');
554 $list['***LANGUAGE_entity_default***'] = $this->t('Original language of content in view row');
557 // STATE_SITE_DEFAULT comes in with ID set
558 // to LanguageInterface::LANGCODE_SITE_DEFAULT.
559 // Since this is not a real language, surround it by '***LANGUAGE_...***',
560 // like the negotiated languages below.
561 if ($flags & LanguageInterface::STATE_SITE_DEFAULT) {
562 $name = $languages[LanguageInterface::LANGCODE_SITE_DEFAULT]->getName();
563 // The language name may have already been translated, no need to
564 // translate it again.
565 // @see Drupal\Core\Language::filterLanguages().
566 if (!$name instanceof TranslatableMarkup) {
567 $name = $this->t($name);
569 $list[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $name;
570 // Remove site default language from $languages so it's not added
571 // twice with the real languages below.
572 unset($languages[LanguageInterface::LANGCODE_SITE_DEFAULT]);
575 // Add in negotiated languages, if requested.
576 if ($flags & PluginBase::INCLUDE_NEGOTIATED) {
577 $types_info = $manager->getDefinedLanguageTypesInfo();
578 $types = $manager->getLanguageTypes();
579 // We only go through the configured types.
580 foreach ($types as $id) {
581 if (isset($types_info[$id]['name'])) {
582 $name = $types_info[$id]['name'];
583 // Surround IDs by '***LANGUAGE_...***', to avoid query collisions.
584 $id = '***LANGUAGE_' . $id . '***';
585 $list[$id] = $this->t('@type language selected for page', ['@type' => $name]);
588 if (!empty($current_values)) {
589 foreach ($types_info as $id => $type) {
590 $id = '***LANGUAGE_' . $id . '***';
591 // If this (non-configurable) type is among the current values,
592 // add that option too, so it is not lost. If not among the current
593 // values, skip displaying it to avoid user confusion.
594 if (isset($type['name']) && !isset($list[$id]) && in_array($id, $current_values)) {
595 $list[$id] = $this->t('@type language selected for page', ['@type' => $type['name']]);
601 // Add real languages.
602 foreach ($languages as $id => $language) {
603 $list[$id] = $language->getName();
610 * Returns substitutions for Views queries for languages.
612 * This is needed so that the language options returned by
613 * PluginBase::listLanguages() are able to be used in queries. It is called
614 * by the Views module implementation of hook_views_query_substitutions()
615 * to get the language-related substitutions.
618 * An array in the format of hook_views_query_substitutions() that gives
619 * the query substitutions needed for the special language types.
621 public static function queryLanguageSubstitutions() {
623 $manager = \Drupal::languageManager();
625 // Handle default language.
626 $default = $manager->getDefaultLanguage()->getId();
627 $changes[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $default;
629 // Handle negotiated languages.
630 $types = $manager->getDefinedLanguageTypesInfo();
631 foreach ($types as $id => $type) {
632 if (isset($type['name'])) {
633 $changes['***LANGUAGE_' . $id . '***'] = $manager->getCurrentLanguage($id)->getId();
641 * Returns the render API renderer.
643 * @return \Drupal\Core\Render\RendererInterface
645 protected function getRenderer() {
646 if (!isset($this->renderer)) {
647 $this->renderer = \Drupal::service('renderer');
650 return $this->renderer;