3 namespace Drupal\Core\Extension\Discovery;
6 * Filters a RecursiveDirectoryIterator to discover extensions.
8 * To ensure the best possible performance for extension discovery, this
9 * filter implementation hard-codes a range of assumptions about directories
10 * in which Drupal extensions may appear and in which not. Every unnecessary
11 * subdirectory tree recursion is avoided.
13 * The list of globally ignored directory names is defined in the
14 * RecursiveExtensionFilterIterator::$blacklist property.
16 * In addition, all 'config' directories are skipped, unless the directory path
17 * ends with 'modules/config', so as to still find the config module provided by
18 * Drupal core and still allow that module to be overridden with a custom config
21 * Lastly, ExtensionDiscovery instructs this filter to additionally skip all
22 * 'tests' directories at regular runtime, since just with Drupal core only, the
23 * discovery process yields 4x more extensions when tests are not ignored.
25 * @see ExtensionDiscovery::scan()
26 * @see ExtensionDiscovery::scanDirectory()
28 * @todo Use RecursiveCallbackFilterIterator instead of the $acceptTests
29 * parameter forwarding once PHP 5.4 is available.
31 class RecursiveExtensionFilterIterator extends \RecursiveFilterIterator {
34 * List of base extension type directory names to scan.
36 * Only these directory names are considered when starting a filesystem
37 * recursion in a search path.
41 protected $whitelist = [
48 * List of directory names to skip when recursing.
50 * These directories are globally ignored in the recursive filesystem scan;
51 * i.e., extensions (of all types) are not able to use any of these names,
52 * because their directory names will be skipped.
56 protected $blacklist = [
57 // Object-oriented code subdirectories.
69 // Legacy subdirectories.
71 // Test subdirectories.
73 // @todo ./tests/Drupal should be ./tests/src/Drupal
78 * Whether to include test directories when recursing.
82 protected $acceptTests = FALSE;
85 * Construct a RecursiveExtensionFilterIterator.
87 * @param \RecursiveIterator $iterator
88 * The iterator to filter.
89 * @param array $blacklist
90 * (optional) Add to the blacklist of directories that should be filtered
91 * out during the iteration.
93 public function __construct(\RecursiveIterator $iterator, array $blacklist = []) {
94 parent::__construct($iterator);
95 $this->blacklist = array_merge($this->blacklist, $blacklist);
99 * Controls whether test directories will be scanned.
102 * Pass FALSE to skip all test directories in the discovery. If TRUE,
103 * extensions in test directories will be discovered and only the global
104 * directory blacklist in RecursiveExtensionFilterIterator::$blacklist is
107 public function acceptTests($flag = FALSE) {
108 $this->acceptTests = $flag;
109 if (!$this->acceptTests) {
110 $this->blacklist[] = 'tests';
117 public function getChildren() {
118 $filter = parent::getChildren();
119 // Pass on the blacklist.
120 $filter->blacklist = $this->blacklist;
121 // Pass the $acceptTests flag forward to child iterators.
122 $filter->acceptTests($this->acceptTests);
129 public function accept() {
130 $name = $this->current()->getFilename();
131 // FilesystemIterator::SKIP_DOTS only skips '.' and '..', but not hidden
132 // directories (like '.git').
133 if ($name[0] == '.') {
136 if ($this->isDir()) {
137 // If this is a subdirectory of a base search path, only recurse into the
138 // fixed list of expected extension type directory names. Required for
139 // scanning the top-level/root directory; without this condition, we would
140 // recurse into the whole filesystem tree that possibly contains other
141 // files aside from Drupal.
142 if ($this->current()->getSubPath() == '') {
143 return in_array($name, $this->whitelist, TRUE);
145 // 'config' directories are special-cased here, because every extension
146 // contains one. However, those default configuration directories cannot
147 // contain extensions. The directory name cannot be globally skipped,
148 // because core happens to have a directory of an actual module that is
149 // named 'config'. By explicitly testing for that case, we can skip all
150 // other config directories, and at the same time, still allow the core
151 // config module to be overridden/replaced in a profile/site directory
152 // (whereas it must be located directly in a modules directory).
153 if ($name == 'config') {
154 return substr($this->current()->getPathname(), -14) == 'modules/config';
156 // Accept the directory unless the name is blacklisted.
157 return !in_array($name, $this->blacklist, TRUE);
160 // Only accept extension info files.
161 return substr($name, -9) == '.info.yml';