toolshed-8.x-1.x-dev/src/Twig/ToolshedTwigExtension.php

src/Twig/ToolshedTwigExtension.php
<?php

namespace Drupal\toolshed\Twig;

use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Pager\PagerManagerInterface;
use Drupal\Core\Render\Element\FormElementInterface;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Template\TwigEnvironment;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Twig\TwigTest;

/**
 * Add empty checks and render functions for Twig.
 *
 * Adds a cleaner implementation in Twig for:
 *   + Checking for empty, with checks for Twig debug comments.
 *   + Is a view empty (has no results).
 *   + Render if a designated child item is not empty.
 */
class ToolshedTwigExtension extends AbstractExtension {

  /**
   * The Twig environment service.
   *
   * @var \Drupal\Core\Template\TwigEnvironment
   */
  protected TwigEnvironment $twig;

  /**
   * The render element plugin manager.
   *
   * @var \Drupal\Core\Render\ElementInfoManagerInterface
   */
  protected ElementInfoManagerInterface $elementManager;

  /**
   * Drupal active renderer and render context.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected RendererInterface $renderer;

  /**
   * Current Twig configurations.
   *
   * @var array
   */
  protected array $twigOpts;

  /**
   * Create a new instance of the twig extensions for adding Toolshed utilities.
   *
   * @param \Drupal\Core\Template\TwigEnvironment $twig
   *   The Twig environment and setttings.
   * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info_manager
   *   Plugin manager for maintaining element definitions.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   Drupal renderer services. Transforms Drupal render arrays and objects
   *   into HTML.
   * @param array $twig_options
   *   Twig options passed to the twig environment.
   */
  public function __construct(TwigEnvironment $twig, ElementInfoManagerInterface $element_info_manager, RendererInterface $renderer, array $twig_options) {
    $this->twig = $twig;
    $this->elementManager = $element_info_manager;
    $this->renderer = $renderer;
    $this->twigOpts = $twig_options;
  }

  /**
   * Get the pager manager service.
   *
   * @return \Drupal\Core\Pager\PagerManagerInterface
   *   The pager manager service.
   */
  protected static function getPagerManager(): PagerManagerInterface {
    return \Drupal::service('pager.manager');
  }

  /**
   * {@inheritdoc}
   */
  public function getName(): string {
    return 'toolshed.twig_utils';
  }

  /**
   * {@inheritdoc}
   */
  public function getFunctions(): array {
    return [
      new TwigFunction('render_if_child', $this->renderIfChild(...), ['is_safe' => ['html']]),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getTests(): array {
    return [
      new TwigTest('element_empty', $this->isElementEmpty(...)),
      new TwigTest('view_empty', $this->isViewEmpty(...)),
      new TwigTest('block_empty', $this->isContentEmpty(...)),
    ];
  }

  /**
   * Checks the visible access of an element to see if it will be rendered.
   *
   * @param array $element
   *   Element to check visibility access for.
   *
   * @return bool
   *   TRUE if the element is seen to have visibility access, and will be
   *   rendered.
   */
  protected function isVisible(array $element): bool {
    $access = $element['#access'] ?? NULL;

    return !isset($access)
      || $access === TRUE
      || ($access instanceof AccessResultInterface && $access->isAllowed());
  }

  /**
   * Check if nested render arrays are all empty.
   *
   * @param array $children
   *   An array of children renderables. This can actually be the element itself
   *   since all the attributes of the pass array are checked.
   *
   * @return bool
   *   TRUE if the children elements are empty, otherwise returns FALSE if any
   *   children are known to have renderable content.
   */
  protected function areChildrenEmpty(array $children = []): bool {
    // Iterate through the render array children are either invisible
    // due to element access of empty.
    foreach ($children as $key => $child) {
      if ((!is_string($key) || $key[0] !== '#') && is_array($child)) {
        if (!$this->isElementEmpty($child)) {
          return FALSE;
        }
      }
    }

    return TRUE;
  }

  /**
   * Determine if an element is empty in terms of rendered content.
   *
   * @param array|null $element
   *   Element to examine to check for empty value. NULL is possible coming
   *   from a Twig template, allowing for this makes it easier to write
   *   the your template files with a lot of extra checks for NULL.
   *
   * @return bool
   *   If element can be considered empty or not. TRUE implies that
   *   the content is considered empty.
   */
  public function isElementEmpty(?array $element): bool {
    // Remove common render properties that do not contribute to the content
    // having renderable content. This is similar to
    // \Drupal\Core\Render\Element::isEmpty() checking excluding #cache.
    unset($element['#cache'], $element['#attributes']);

    if (empty($element) || !$this->isVisible($element)) {
      return TRUE;
    }

    // It is not possible to know if a placeholder or lazy builder will result
    // in visible content at this point, because rendering is being deferred.
    if (!empty($element['#lazy_builder']) || !empty($element['#placeholder'])) {
      return FALSE;
    }

    $type = $element['#type'] ?? @$element['#theme'];

    // If the type / theme is empty, this array is just a container for nested
    // render arrays. Check for markup or empty children.
    //
    // This also captures render elements that use "theme_wrappers".
    if (!$type) {
      return $this->isContentEmpty($element);
    }

    switch ($type) {
      case 'fieldset':
      case 'details':
        if (!empty($element['#title']) || !empty($element['#description'])) {
          return FALSE;
        }

      case 'container':
      case 'form_element':
        // These are containers for other elements, they are empty if their
        // children are all empty.
        return $this->areChildrenEmpty($element);

      case 'field':
        // \Drupal\Core\Field\FieldListInterface::isEmpty() will indicate if
        // a field is empty or not. Typically fields generate field render
        // arrays if the field was empty to start with, but we'll still test for
        // instances where that behavior are overridden.
        return empty($element['#items']) || $element['#items']->isEmpty();

      case 'value':
        // Form element which does not need to render any output.
        return TRUE;

      case 'table':
        return empty($element['#rows']) && $this->areChildrenEmpty($element);

      case 'form':
      case 'link':
      case 'file':
      case 'more_link':
      case 'iconset_icon':
        // These render elements that we'll consider as always rendering.
        return FALSE;

      case 'links':
      case 'dropbutton':
      case 'operstaions':
        return empty($element['#links']) && empty($element['#header']);

      case 'item_list':
        return empty($element['#items']);

      case 'view':
        return $this->isViewEmpty($element);

      case 'block':
        return $this->isContentEmpty($element);

      case 'pager':
        if (isset($element['#element'])) {
          /** @var \Drupal\Core\Pager\PagerManagerInterface $pagerManager */
          $pagerManager = static::getPagerManager();

          if ($pager = $pagerManager->getPager()) {
            return (bool) !$pager->getTotalItems();
          }
        }

        return TRUE;

      case 'processed_text':
        // @todo Also check if text only consists of white-space tags
        // such as "<p>", "<br>", and "&nbsp;".
        return $this->isMarkupEmpty($element['#text']);

      default:
        $elementDef = isset($element['#type']) ? $this->elementManager->getDefinition($type, FALSE) : NULL;

        // Consider form elements to always have content (with the exception of
        // "value" but already handled in the switch statement).
        if ($elementDef && is_a($elementDef['class'], FormElementInterface::class, TRUE)) {
          return FALSE;
        }

        // Is this just a markup element?
        foreach (['#markup', '#plain_text'] as $idx) {
          if (isset($element[$idx])) {
            return $this->isMarkupEmpty($element[$idx]);
          }
        }

        // Unfortunately we can't tell what this theme or type is, and therfore
        // cannot tell if it is actually empty or not. We can only take a
        // shortcut and exit if it has non-empty children.
        if (!$this->areChildrenEmpty($element)) {
          return FALSE;
        }

        // If we are not able to determine this information easily, we fallback
        // to the hammer approach of rendering, and checking for empty.
        //
        // @todo Chip away at this approach for better tests for empty content.
        $str = $this->renderer->render($element);

        // If the Twig debug flag is enabled, there are comments that need to be
        // removed from the output before checking for empty.
        if (!empty($this->twigOpts['debug'])) {
          // @todo strip comments.
        }

        return empty(trim($str));
    }
  }

  /**
   * Determine if a view should be considered empty (has no results).
   *
   * @param mixed $view
   *   Either a ViewExecutable object, or an array or string that can be used
   *   to determine and load the correct views object.
   *
   * @return bool
   *   TRUE if the view query is returning empty results. This filter ignores
   *   if the view is configure to return an empty message or still expected
   *   to render even if empty.
   *
   *   @todo Add check for empty plugin behavior configured for view display.
   *   A view that is configured to display an empty message might not be
   *   considered empty, and is intended to still display in most cases.
   */
  public function isViewEmpty($view): bool {
    if (empty($view)) {
      return TRUE;
    }

    if (!$view instanceof ViewExecutable) {
      if (is_string($view)) {
        [$viewId, $displayId] = explode(':', $view);
        $args = func_get_args();
        array_shift($args);
      }
      elseif (is_array($view)) {
        if (isset($view['#type']) && $view['#type'] === 'view') {
          $viewId = $view['#name'];
          $displayId = $view['#display_id'];
          $args = !empty($view['#arguments']) ? $view['#arguments'] : [];
        }
        else {
          [$viewId, $displayId] = $view;
          $args = func_get_args();
          array_shift($args);
        }
      }

      // If either of these are missing, skip and consider the view empty.
      if (empty($viewId) || empty($displayId)) {
        return TRUE;
      }

      // Try to load and set the view information.
      $view = Views::getView($viewId);
      if (!$view || !$view->setDisplay($displayId)) {
        return TRUE;
      }

      if (!empty($args)) {
        $view->setArguments($args);
      }
    }

    // Execute the view and check if there are any results. No results
    // means that the view is empty.
    return !$view->execute() || empty($view->result);
  }

  /**
   * Check if a we're able to determine if a block should be considered empty.
   *
   * @param array $element
   *   Renderable array representing the block content.
   *
   * @return bool
   *   TRUE if the block content is empty. FALSE if otherwise.
   */
  public function isContentEmpty(array $element): bool {
    // First check for markup, this takes precedence over child render items.
    foreach (['#markup', '#plain_text'] as $idx) {
      if (isset($element[$idx])) {
        return $this->isMarkupEmpty($element[$idx]);
      }
    }

    // If not just a simple text block.
    return $this->areChildrenEmpty($element);
  }

  /**
   * Render if child is considered not empty.
   *
   * @param array $element
   *   Render array to render if the child is not empty.
   * @param array $parents
   *   Array of parent keys to the child relative to $element.
   *
   * @return \Drupal\Component\Render\MarkupInterface|null
   *   Render array to render. Will be the value of element if the child is
   *   not empty, but an empty array (render nothing) if child is determined
   *   to be empty. Empty is determined by static::isElementEmpty().
   */
  public function renderIfChild(array $element, array $parents): ?MarkupInterface {
    $child = &NestedArray::getValue($element, $parents);

    if ($child && !$this->isElementEmpty($child)) {
      return $this->renderer->render($element);
    }

    return NULL;
  }

  /**
   * Detect if a string or MarkupInterface object is considered empty.
   *
   * @param \Drupal\Component\Render\MarkupInterface|string $text
   *   Either a renderable markup object or a string of markup to output.
   *
   * @return bool
   *   TRUE if the passed markup should be considered empty or, FALSE if the
   *   markup contains anything.
   */
  public function isMarkupEmpty(MarkupInterface|string $text): bool {
    if ($text instanceof MarkupInterface) {
      return !($text instanceof \Countable ? $text->count() : strlen((string) $text));
    }

    return empty(trim($text));
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc