You are here

ComponentDiscovery.php in Decoupled Blocks 8

Namespace

Drupal\pdb

File

src/ComponentDiscovery.php
View source
<?php

namespace Drupal\pdb;

use Drupal\pdb\Discovery\PdbRecursiveExtensionFilterIterator;
use Drupal\pdb\Event\PdbDiscoveryEvent;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Extension\InfoParserInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Site\Settings;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Discovery service for front-end components provided by modules and themes.
 *
 * Components (anything whose info file 'type' is 'pdb') are treated as Drupal
 * extensions unto themselves.
 */
class ComponentDiscovery extends ExtensionDiscovery implements ComponentDiscoveryInterface {

  /**
   * The event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $eventDispatcher;

  /**
   * The info parser.
   *
   * @var \Drupal\Core\Extension\InfoParserInterface
   */
  protected $infoParser;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * Flag to indicate if the discovery is global or fixed to given dirs.
   *
   * @var bool
   */
  protected $globalDiscovery = FALSE;

  /**
   * ComponentDiscovery constructor.
   *
   * @param string $root
   *   The root directory of the Drupal installation.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher.
   * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
   *   The info parser.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   */
  public function __construct($root, EventDispatcherInterface $event_dispatcher, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler) {
    parent::__construct($root);
    $this->eventDispatcher = $event_dispatcher;
    $this->infoParser = $info_parser;
    $this->moduleHandler = $module_handler;
  }

  /**
   * {@inheritdoc}
   */
  public function getComponents() {

    // Find components.
    $components = $this
      ->scan('pdb');

    // Set defaults for module info.
    $defaults = [
      'dependencies' => [],
      'description' => '',
      'version' => NULL,
    ];

    // Read info files for each module.
    foreach ($components as $key => $component) {

      // Look for the info file.
      $component->info = $this->infoParser
        ->parse($component
        ->getPathname());

      // Merge in defaults and save.
      $components[$key]->info = $component->info + $defaults;
    }
    $this->moduleHandler
      ->alter('component_info', $components);
    return $components;
  }

  /**
   * {@inheritdoc}
   *
   * Extends to provide user defined paths to look for components.
   */
  public function scan($type, $include_tests = NULL) {

    // Try to get search dirs from settings.php.
    $search_dirs = Settings::get('pdb_search_dirs', []);
    if (is_string($search_dirs)) {
      $search_dirs = [
        $search_dirs,
      ];
    }

    // Try to get search dirs from subscribers.
    $event = new PdbDiscoveryEvent($search_dirs);
    $this->eventDispatcher
      ->dispatch(PdbDiscoveryEvent::SEARCH_DIRS, $event);

    // Get the updated dicovery path from subscribers.
    $search_dirs = $event
      ->getDirs();

    // If user is not defining any custom path, then do a global discovery
    // by following parent's approach.
    if (empty($search_dirs)) {
      $this->globalDiscovery = TRUE;
      return parent::scan($type, $include_tests);
    }

    // Based on parent::scan().
    $files = [];
    foreach ($search_dirs as $dir) {

      // Discover all extensions in the directory, unless we did already.
      if (!isset(static::$files[$this->root][$dir][$include_tests])) {
        static::$files[$this->root][$dir][$include_tests] = $this
          ->scanDirectory($dir, $include_tests);
      }

      // Only return extensions of the requested type.
      if (isset(static::$files[$this->root][$dir][$include_tests][$type])) {
        $files += static::$files[$this->root][$dir][$include_tests][$type];
      }
    }

    // If applicable, filter out extensions that do not belong to the current
    // installation profiles.
    $files = $this
      ->filterByProfileDirectories($files);

    // Sort the discovered extensions by their originating directories.
    $origin_weights = array_flip($search_dirs);
    $files = $this
      ->sort($files, $origin_weights);

    // Process and return the list of extensions keyed by extension name.
    return $this
      ->process($files);
  }

  /**
   * {@inheritdoc}
   *
   * This is mostly extended to be able to use a different
   * RecursiveExtensionFilterIterator class when searching custom user dirs.
   */
  protected function scanDirectory($dir, $include_tests) {

    // If it is a global discovery, then follow parent's approach.
    if ($this->globalDiscovery) {
      return parent::scanDirectory($dir, $include_tests);
    }

    // Based on parent::scanDirectory().
    $files = [];

    // In order to scan top-level directories, absolute directory paths have to
    // be used (which also improves performance, since any configured PHP
    // include_paths will not be consulted). Retain the relative originating
    // directory being scanned, so relative paths can be reconstructed below
    // (all paths are expected to be relative to $this->root).
    $dir_prefix = $dir == '' ? '' : "{$dir}/";
    $absolute_dir = $dir == '' ? $this->root : $this->root . "/{$dir}";
    if (!is_dir($absolute_dir)) {
      return $files;
    }

    // Use Unix paths regardless of platform, skip dot directories, follow
    // symlinks (to allow extensions to be linked from elsewhere), and return
    // the RecursiveDirectoryIterator instance to have access to getSubPath(),
    // since SplFileInfo does not support relative paths.
    $flags = \FilesystemIterator::UNIX_PATHS;
    $flags |= \FilesystemIterator::SKIP_DOTS;
    $flags |= \FilesystemIterator::FOLLOW_SYMLINKS;
    $flags |= \FilesystemIterator::CURRENT_AS_SELF;
    $directory_iterator = new \RecursiveDirectoryIterator($absolute_dir, $flags);

    // Allow directories specified in settings.php to be ignored. You can use
    // this to not check for files in common special-purpose directories. For
    // example, node_modules and bower_components. Ignoring irrelevant
    // directories is a performance boost.
    $ignore_directories = Settings::get('file_scan_ignore_directories', []);

    // Filter the recursive scan to discover extensions only.
    // Important: Without a RecursiveFilterIterator, RecursiveDirectoryIterator
    // would recurse into the entire filesystem directory tree without any kind
    // of limitations.
    $filter = new PdbRecursiveExtensionFilterIterator($directory_iterator, $ignore_directories);
    $filter
      ->acceptTests($include_tests);

    // The actual recursive filesystem scan is only invoked by instantiating the
    // RecursiveIteratorIterator.
    $iterator = new \RecursiveIteratorIterator($filter, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD);
    foreach ($iterator as $key => $fileinfo) {

      // All extension names in Drupal have to be valid PHP function names due
      // to the module hook architecture.
      if (!preg_match(static::PHP_FUNCTION_PATTERN, $fileinfo
        ->getBasename('.info.yml'))) {
        continue;
      }
      $extension_arguments = $this->fileCache ? $this->fileCache
        ->get($fileinfo
        ->getPathName()) : FALSE;

      // Ensure $extension_arguments is an array. Previously, the Extension
      // object was cached and now needs to be replaced with the array.
      if (empty($extension_arguments) || !is_array($extension_arguments)) {

        // Determine extension type from info file.
        $type = FALSE;
        $file = $fileinfo
          ->openFile('r');
        while (!$type && !$file
          ->eof()) {
          preg_match('@^type:\\s*(\'|")?(\\w+)\\1?\\s*$@', $file
            ->fgets(), $matches);
          if (isset($matches[2])) {
            $type = $matches[2];
          }
        }
        if (empty($type)) {
          continue;
        }
        $name = $fileinfo
          ->getBasename('.info.yml');
        $pathname = $dir_prefix . $fileinfo
          ->getSubPathname();

        // Determine whether the extension has a main extension file.
        // For theme engines, the file extension is .engine.
        if ($type == 'theme_engine') {
          $filename = $name . '.engine';
        }
        else {
          $filename = $name . '.' . $type;
        }
        if (!file_exists($this->root . '/' . dirname($pathname) . '/' . $filename)) {
          $filename = NULL;
        }
        $extension_arguments = [
          'type' => $type,
          'pathname' => $pathname,
          'filename' => $filename,
          'subpath' => $fileinfo
            ->getSubPath(),
        ];
        if ($this->fileCache) {
          $this->fileCache
            ->set($fileinfo
            ->getPathName(), $extension_arguments);
        }
      }
      $extension = new Extension($this->root, $extension_arguments['type'], $extension_arguments['pathname'], $extension_arguments['filename']);

      // Track the originating directory for sorting purposes.
      $extension->subpath = $extension_arguments['subpath'];
      $extension->origin = $dir;
      $files[$extension_arguments['type']][$key] = $extension;
    }
    return $files;
  }

}

Classes

Namesort descending Description
ComponentDiscovery Discovery service for front-end components provided by modules and themes.