Commit 9b843b43 authored by Mateu Aguiló Bosch's avatar Mateu Aguiló Bosch
Browse files

Issue #3313222 by e0ipso: Drop support for file system loading

parent e85701a8
Loading
Loading
Loading
Loading
+1 −3
Original line number Diff line number Diff line
@@ -3,12 +3,10 @@ services:
  Drupal\cl_components\Twig\TwigComponentLoader:
    arguments:
      - '@plugin.manager.cl_component'
      - '@module_handler'
      - '@theme_handler'
      - '@renderer'
      - '%twig.config%'
    tags:
      - { name: twig.loader, priority: 250 }
      - { name: twig.loader, priority: 5 }

  Drupal\cl_components\Twig\TwigExtension:
    arguments:
+1 −1
Original line number Diff line number Diff line
@@ -212,7 +212,7 @@ final class ComponentMetadata {
   * Parse the schema information.
   *
   * @param array $metadata_info
   *   The metadata information as decoded from "metadata.json".
   *   The metadata information as decoded from the component definition file.
   *
   * @throws \Drupal\cl_components\Exception\InvalidComponentException
   */
+92 −101
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ namespace Drupal\cl_components\Twig;
use Drupal\cl_components\ComponentPluginManager;
use Drupal\cl_components\Exception\ComponentNotFoundException;
use Drupal\cl_components\Exception\TemplateNotFoundException;
use Drupal\Component\Discovery\YamlDirectoryDiscovery;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
@@ -12,12 +13,13 @@ use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Template\Loader\FilesystemLoader;
use Twig\Error\LoaderError;
use Twig\Loader\LoaderInterface;
use Twig\Source;

/**
 * Lets you load templates using the component ID.
 */
class TwigComponentLoader extends FilesystemLoader {
class TwigComponentLoader implements LoaderInterface {

  /**
   * The plugin manager.
@@ -52,25 +54,16 @@ class TwigComponentLoader extends FilesystemLoader {
   *
   * @param \Drupal\cl_components\ComponentPluginManager $plugin_manager
   *   The plugin manager.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
   *   The theme handler.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer.
   * @param array $twig_config
   * @param array|null $twig_config
   *   The twig configuration.
   */
  public function __construct(ComponentPluginManager $plugin_manager, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, RendererInterface $renderer, ?array $twig_config) {
  public function __construct(ComponentPluginManager $plugin_manager, RendererInterface $renderer, ?array $twig_config) {
    $this->pluginManager = $plugin_manager;
    $twig_config = $twig_config ?: [];
    $this->twigDebug = (bool) ($twig_config['debug'] ?? FALSE);
    $this->renderer = $renderer;
    parent::__construct(
      '.',
      $module_handler,
      $theme_handler
    );
  }

  /**
@@ -80,14 +73,10 @@ class TwigComponentLoader extends FilesystemLoader {
   *   Thrown if a template matching $name cannot be found.
   */
  protected function findTemplate($name, $throw = TRUE) {
    [$id, $variant] = $this->parseIdAndVariant($name);
    $path = $name;
    try {
      [$id, $variant] = $this->parseIdAndVariant($name);
      $component = $this->pluginManager->find($id);
      // Validate if the variant is valid.
      if ($variant && !in_array($variant, $component->getVariants(), TRUE)) {
        $message = sprintf('Invalid variant "%s" for component "%s".', $variant, $id);
        throw new LoaderError($message);
      }
      $template = $component->getTemplateName($variant ?? '');
      $path = sprintf(
        '%s%s%s',
@@ -97,8 +86,10 @@ class TwigComponentLoader extends FilesystemLoader {
      );
    }
    catch (ComponentNotFoundException | TemplateNotFoundException  $e) {
      if ($throw) {
        throw new LoaderError($e->getMessage(), $e->getCode(), $e);
      }
    }
    if ($path || !$throw) {
      return $path;
    }
@@ -110,16 +101,6 @@ class TwigComponentLoader extends FilesystemLoader {
   * {@inheritdoc}
   */
  public function exists($name): bool {
    // Check the filesystem for the template.
    try {
      if (!parent::exists($name)) {
        return FALSE;
      }
    }
    catch (LoaderError $e) {
      return FALSE;
    }

    try {
      [$id, $variant] = $this->parseIdAndVariant($name);
      $component = $this->pluginManager->find($id);
@@ -133,17 +114,30 @@ class TwigComponentLoader extends FilesystemLoader {
  /**
   * {@inheritdoc}
   *
   * @throws \Drupal\cl_components\Exception\ComponentNotFoundException
   * @throws \Exception
   * @throws \Twig\Error\LoaderError
   */
  public function getSourceContext($name): Source {
    try {
      [$component_id, $variant] = $this->parseIdAndVariant($name);
      $component = $this->pluginManager->find($component_id);
    $source = parent::getSourceContext($name);
      $path = $component->getMetadata()->getPath() . DIRECTORY_SEPARATOR . $component->getTemplateName($variant);
    }
    catch (ComponentNotFoundException | TemplateNotFoundException $e) {
      return new Source('', $name, '');
    }
    $code = file_get_contents($path);
    $lib_build = ['#attached' => ['library' => [$component->getLibraryName()]]];
    try {
      // This is the main reason for the loader (other than finding the Twig
      // template). This will eventually add the auto-generated library into the
      // page.
      $this->renderer->render($lib_build);
    }
    catch (\Exception $e) {
      // Off-band library render failed.
    }
    $code = "{{ cl_components_additional_context('" . addcslashes($component_id, "'") . "', '" . addcslashes($variant ?: '', "'") . "', " . (int) ($this->twigDebug && $component->isDebugMode()) . ") }}"
      . PHP_EOL . $source->getCode();
      . PHP_EOL . $code;
    if ($this->twigDebug) {
      $code = "{# start cl_component $name #}" . PHP_EOL
        . "<!-- start cl_component $name -->" . PHP_EOL
@@ -153,87 +147,85 @@ class TwigComponentLoader extends FilesystemLoader {
    }
    if ($component->isDebugMode()) {
      $debug_lib_build = ['#attached' => ['library' => ['cl_components/cl_debug']]];
      try {
        $this->renderer->render($debug_lib_build);
      }
    return new Source($code, $source->getName(), $source->getPath());
      catch (\Exception $e) {
        // Off-band library render failed.
      }
    }
    return new Source($code, $name, $path);
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheKey($name): string {
    return 'cl-component::' . parent::getCacheKey($name);
    try {
      [$component_id, $variant] = $this->parseIdAndVariant($name);
      $component = $this->pluginManager->find($component_id);
    }
    catch (ComponentNotFoundException | TemplateNotFoundException $e) {
      throw new LoaderError('Unable to find component');
    }
    return sprintf(
      'cl-component--%s--%s--%s',
      $name,
      $variant,
      $component->getPluginDefinition()['provider'] ?? ''
    );
  }

  /**
   * Parse ID and variant from the template key.
   *
   * @param string $name
   *   The template name as provided in the include/embed.
   *
   * @return array [string, string]
   *   The component ID and variant.
   *
   * @throws \Twig\Error\LoaderError
   * {@inheritdoc}
   */
  public function parseIdAndVariant(string $name): array {
    if (isset($this->idVariantCache[$name])) {
      return $this->idVariantCache[$name];
  public function isFresh($name, $time) {
    $file_is_fresh = static fn (string $path) => filemtime($path) < $time;
    try {
      [$component_id, $variant] = $this->parseIdAndVariant($name);
      $component = $this->pluginManager->find($component_id);
    }
    $id_and_variant = $this->parseIdAndVariantByIdSyntax($name);
    if (!empty($id_and_variant)) {
      return $id_and_variant;
    catch (ComponentNotFoundException | TemplateNotFoundException $e) {
      throw new LoaderError('Unable to find component');
    }
    return $this->parseIdAndVariantByPath($name);
    // If any of the templates, or the component definition, are fresh. Then the
    // component is fresh.
    $metadata_path = $component->getPluginDefinition()[YamlDirectoryDiscovery::FILE_KEY];
    if ($file_is_fresh($metadata_path)) {
      return TRUE;
    }
    return array_reduce(
      array_map(
        static fn (string $name) => $component->getMetadata()->getPath() . DIRECTORY_SEPARATOR . $name,
        $component->getTemplates()
      ),
      static fn (bool $fresh, string $path) => $fresh || $file_is_fresh($path),
      FALSE
    );
  }

  /**
   * Parses the ID and variant from the component path.
   * Parse ID and variant from the template key.
   *
   * @param string $name
   *   The template name.
   *   The template name as provided in the include/embed.
   *
   * @return array [string, string]
   *   The component ID and variant.
   *
   * @throws \Twig\Error\LoaderError
   * @throws \Drupal\cl_components\Exception\ComponentNotFoundException
   */
  private function parseIdAndVariantByPath(string $name): array {
    $path = parent::findTemplate($name);
    // Check if there is a metadata.json in the same directory.
    $dir = dirname($path);
    $metadata_path = sprintf(
      '%s%smetadata.json',
      rtrim($dir, '/'),
      DIRECTORY_SEPARATOR
    );
    $error_message = sprintf('Unable to find a valid metadata.json for the component with template: "%s"', $name);
    if (!file_exists($metadata_path)) {
      throw new LoaderError($error_message);
    }
    $metadata_contents = file_get_contents($metadata_path);
    $metadata_parsed = Json::decode($metadata_contents ?: '{}');
    $id = $metadata_parsed['machineName'] ?? '';
    if (empty($id)) {
      throw new LoaderError($error_message);
    }
    // If requested, parse the variant.
    $variant = preg_replace(
    // Turn @name/my/path/to/component-id--variant.twig => variant.
      [
        '@\.twig$@',
        '@^.*--@',
      ],
      '',
      $name
    );
    $variant = preg_match('@.*/[^/]*--.*\.twig@', $name) ? $variant : '';
    $available_variants = $metadata_parsed['variants'] ?? [];
    if (empty($variant) || in_array($variant, $available_variants) === FALSE) {
      $variant = '';
  private function parseIdAndVariant(string $name): array {
    if (isset($this->idVariantCache[$name])) {
      return $this->idVariantCache[$name];
    }
    $this->idVariantCache[$name] = [$id, $variant];
    return [$id, $variant];
    $id_and_variant = $this->parseIdAndVariantByIdSyntax($name);
    if (empty($id_and_variant)) {
      $message = sprintf('Unable to find %s by component ID.', $name);
      throw new LoaderError($message);
    }
    return $id_and_variant;
  }

  /**
@@ -244,6 +236,8 @@ class TwigComponentLoader extends FilesystemLoader {
   *
   * @return array|null
   *   The ID and variant, or NULL if not found.
   *
   * @throws \Drupal\cl_components\Exception\ComponentNotFoundException
   */
  private function parseIdAndVariantByIdSyntax(string $name): ?array {
    // First check if we can parse the prefix and variant from the name.
@@ -252,18 +246,15 @@ class TwigComponentLoader extends FilesystemLoader {
    if (str_contains($name, '--')) {
      [$id, $variant] = explode('--', $id);
    }
    if (empty($id)) {
      return NULL;
    }
    try {
    $component = $this->pluginManager->find($id);
    }
    catch (ComponentNotFoundException $e) {
      return NULL;
    }
    $available_variants = $component->getVariants();
    if (!empty($variant) && !in_array($variant, $available_variants, TRUE)) {
      return NULL;
      $message = sprintf(
        'Variant "%s" not found in component "%s".',
        $variant,
        $component->getMetadata()->getName()
      );
      throw new ComponentNotFoundException($message);
    }
    $this->idVariantCache[$name] = [$id, $variant];
    return [$id, $variant];