Verified Commit c3431950 authored by Dave Long's avatar Dave Long
Browse files

Issue #2532228 by Spokje, andypost, quietone, dawehner, Berdir, smustgrave,...

Issue #2532228 by Spokje, andypost, quietone, dawehner, Berdir, smustgrave, longwave: Use RecursiveCallbackFilterIterator instead of the $acceptTests parameter forwarding
parent f890b7ff
Loading
Loading
Loading
Loading
+139 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Core\Extension\Discovery;

/**
 * Filters a RecursiveDirectoryIterator to discover extensions.
 *
 * To ensure the best possible performance for extension discovery, this
 * filter implementation hard-codes a range of assumptions about directories
 * in which Drupal extensions may appear and in which not. Every unnecessary
 * subdirectory tree recursion is avoided.
 *
 * The list of globally ignored directory names is defined in the
 * RecursiveExtensionFilterCallback::$skippedFolders property.
 *
 * In addition, all 'config' directories are skipped, unless the directory path
 * ends with 'modules/config', to still find the config module provided by
 * Drupal core and still allow that module to be overridden with a custom config
 * module.
 *
 * Lastly, ExtensionDiscovery instructs this filter to additionally skip all
 * 'tests' directories at regular runtime, since just with Drupal core only, the
 * discovery process yields 4x more extensions when tests are not ignored.
 *
 * @see ExtensionDiscovery::scan()
 * @see ExtensionDiscovery::scanDirectory()
 *
 * @internal
 */
class RecursiveExtensionFilterCallback {

  /**
   * List of base extension type directory names to scan.
   *
   * Only these directory names are considered when starting a filesystem
   * recursion in a search path.
   *
   * @var string[]
   */
  protected array $allowedExtensionTypes = [
    'profiles',
    'modules',
    'themes',
  ];

  /**
   * List of directory names to skip when recursing.
   *
   * These directories are globally ignored in the recursive filesystem scan;
   * i.e., extensions (of all types) are not able to use any of these names,
   * because their directory names will be skipped.
   *
   * @var string[]
   */
  protected array $skippedFolders = [
    // Object-oriented code subdirectories.
    'src',
    'lib',
    'vendor',
    // Front-end.
    'assets',
    'css',
    'files',
    'images',
    'js',
    'misc',
    'templates',
    // Legacy subdirectories.
    'includes',
    // Test subdirectories.
    'fixtures',
    // @todo ./tests/Drupal should be ./tests/src/Drupal
    'Drupal',
  ];

  /**
   * Construct a RecursiveExtensionFilterCallback.
   *
   * @param array $skipped_folders
   *   (optional) Add to the list of directories that should be filtered out
   *   during the iteration.
   * @param bool $accept_tests
   *   (optional) Pass FALSE to skip all test directories in the discovery. If
   *   TRUE, extensions in test directories will be discovered and only the
   *   global directory skip list in
   *   RecursiveExtensionFilterCallback::$skippedFolders and $skipped_folders
   *   are applied.
   */
  public function __construct(array $skipped_folders = [], bool $accept_tests = FALSE) {
    $this->skippedFolders = array_merge($this->skippedFolders, $skipped_folders);
    if (!$accept_tests) {
      $this->skippedFolders[] = 'tests';
    }
  }

  /**
   * Checks whether a given filesystem directory is acceptable.
   *
   * @param \RecursiveDirectoryIterator $filesystem_directory
   *   The filesystem directory to check.
   *
   * @return bool
   *   TRUE if the given filesystem directory is acceptable, otherwise FALSE.
   */
  public function accept(\RecursiveDirectoryIterator $filesystem_directory): bool {
    $name = $filesystem_directory->getFilename();
    // FilesystemIterator::SKIP_DOTS only skips '.' and '..', but not hidden
    // directories (like '.git').
    if ($name[0] === '.') {
      return FALSE;
    }
    if ($filesystem_directory->isDir()) {
      // If this is a subdirectory of a base search path, only recurse into the
      // fixed list of expected extension type directory names. Required for
      // scanning the top-level/root directory; without this condition, we would
      // recurse into the whole filesystem tree that possibly contains other
      // files aside from Drupal.
      if ($filesystem_directory->getSubPath() === '') {
        return in_array($name, $this->allowedExtensionTypes, TRUE);
      }
      // 'config' directories are special-cased here, because every extension
      // contains one. However, those default configuration directories cannot
      // contain extensions. The directory name cannot be globally skipped,
      // because core happens to have a directory of an actual module that is
      // named 'config'. By explicitly testing for that case, we can skip all
      // other config directories, and at the same time, still allow the core
      // config module to be overridden/replaced in a profile/site directory
      // (whereas it must be located directly in a modules directory).
      if ($name === 'config') {
        return str_ends_with($filesystem_directory->getPathname(), 'modules/config');
      }
      // Accept the directory unless the folder is skipped.
      return !in_array($name, $this->skippedFolders, TRUE);
    }
    // Only accept extension info files.
    return str_ends_with($name, '.info.yml');
  }

}
+5 −3
Original line number Diff line number Diff line
@@ -25,9 +25,10 @@
 * @see ExtensionDiscovery::scan()
 * @see ExtensionDiscovery::scanDirectory()
 *
 * @todo Use RecursiveCallbackFilterIterator instead of the $acceptTests
 *   parameter forwarding once PHP 5.4 is available.
 *   https://www.drupal.org/node/2532228
 * @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use
 *   \Drupal\Core\Extension\Discovery\RecursiveExtensionFilterCallback instead.
 *
 * @see https://www.drupal.org/node/3343023
 */
class RecursiveExtensionFilterIterator extends \RecursiveFilterIterator {

@@ -92,6 +93,7 @@ class RecursiveExtensionFilterIterator extends \RecursiveFilterIterator {
   *   during the iteration.
   */
  public function __construct(\RecursiveIterator $iterator, array $skipped_folders = []) {
    @trigger_error('The ' . __NAMESPACE__ . '\RecursiveExtensionFilterIterator is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Extension\Discovery\RecursiveExtensionFilterCallback instead. See https://www.drupal.org/node/3343023', E_USER_DEPRECATED);
    parent::__construct($iterator);
    $this->skippedFolders = array_merge($this->skippedFolders, $skipped_folders);
  }
+4 −4
Original line number Diff line number Diff line
@@ -4,7 +4,7 @@

use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator;
use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterCallback;
use Drupal\Core\Site\Settings;
use Symfony\Component\HttpFoundation\Request;

@@ -388,7 +388,7 @@ protected function process(array $all_files) {
   *   are associative arrays of \Drupal\Core\Extension\Extension objects, keyed
   *   by absolute path name.
   *
   * @see \Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator
   * @see \Drupal\Core\Extension\Discovery\RecursiveExtensionFilterCallback
   */
  protected function scanDirectory($dir, $include_tests) {
    $files = [];
@@ -424,8 +424,8 @@ protected function scanDirectory($dir, $include_tests) {
    // Important: Without a RecursiveFilterIterator, RecursiveDirectoryIterator
    // would recurse into the entire filesystem directory tree without any kind
    // of limitations.
    $filter = new RecursiveExtensionFilterIterator($directory_iterator, $ignore_directories);
    $filter->acceptTests($include_tests);
    $callback = new RecursiveExtensionFilterCallback($ignore_directories, $include_tests);
    $filter = new \RecursiveCallbackFilterIterator($directory_iterator, [$callback, 'accept']);

    // The actual recursive filesystem scan is only invoked by instantiating the
    // RecursiveIteratorIterator.
+13 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@
namespace Drupal\Tests\Core\Extension;

use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Tests\UnitTestCase;
@@ -203,4 +204,16 @@ protected function addFileToFilesystemStructure(array &$filesystem_structure, ar
    }
  }

  /**
   * Tests deprecated iterator.
   *
   * @covers \Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator
   * @group legacy
   */
  public function testDeprecatedIterator(): void {
    $this->expectDeprecation('The Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Extension\Discovery\RecursiveExtensionFilterCallback instead. See https://www.drupal.org/node/3343023');
    $recursive_extension_filter_iterator = new RecursiveExtensionFilterIterator(new \RecursiveDirectoryIterator('.'));
    $this->assertInstanceOf(RecursiveExtensionFilterIterator::class, $recursive_extension_filter_iterator);
  }

}