RecursiveExtensionFilterIterator.php 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. <?php
  2. namespace Drupal\Core\Extension\Discovery;
  3. /**
  4. * Filters a RecursiveDirectoryIterator to discover extensions.
  5. *
  6. * To ensure the best possible performance for extension discovery, this
  7. * filter implementation hard-codes a range of assumptions about directories
  8. * in which Drupal extensions may appear and in which not. Every unnecessary
  9. * subdirectory tree recursion is avoided.
  10. *
  11. * The list of globally ignored directory names is defined in the
  12. * RecursiveExtensionFilterIterator::$blacklist property.
  13. *
  14. * In addition, all 'config' directories are skipped, unless the directory path
  15. * ends with 'modules/config', so as to still find the config module provided by
  16. * Drupal core and still allow that module to be overridden with a custom config
  17. * module.
  18. *
  19. * Lastly, ExtensionDiscovery instructs this filter to additionally skip all
  20. * 'tests' directories at regular runtime, since just with Drupal core only, the
  21. * discovery process yields 4x more extensions when tests are not ignored.
  22. *
  23. * @see ExtensionDiscovery::scan()
  24. * @see ExtensionDiscovery::scanDirectory()
  25. *
  26. * @todo Use RecursiveCallbackFilterIterator instead of the $acceptTests
  27. * parameter forwarding once PHP 5.4 is available.
  28. * https://www.drupal.org/node/2532228
  29. */
  30. class RecursiveExtensionFilterIterator extends \RecursiveFilterIterator {
  31. /**
  32. * List of base extension type directory names to scan.
  33. *
  34. * Only these directory names are considered when starting a filesystem
  35. * recursion in a search path.
  36. *
  37. * @var array
  38. */
  39. protected $whitelist = [
  40. 'profiles',
  41. 'modules',
  42. 'themes',
  43. ];
  44. /**
  45. * List of directory names to skip when recursing.
  46. *
  47. * These directories are globally ignored in the recursive filesystem scan;
  48. * i.e., extensions (of all types) are not able to use any of these names,
  49. * because their directory names will be skipped.
  50. *
  51. * @var array
  52. */
  53. protected $blacklist = [
  54. // Object-oriented code subdirectories.
  55. 'src',
  56. 'lib',
  57. 'vendor',
  58. // Front-end.
  59. 'assets',
  60. 'css',
  61. 'files',
  62. 'images',
  63. 'js',
  64. 'misc',
  65. 'templates',
  66. // Legacy subdirectories.
  67. 'includes',
  68. // Test subdirectories.
  69. 'fixtures',
  70. // @todo ./tests/Drupal should be ./tests/src/Drupal
  71. 'Drupal',
  72. ];
  73. /**
  74. * Whether to include test directories when recursing.
  75. *
  76. * @var bool
  77. */
  78. protected $acceptTests = FALSE;
  79. /**
  80. * Construct a RecursiveExtensionFilterIterator.
  81. *
  82. * @param \RecursiveIterator $iterator
  83. * The iterator to filter.
  84. * @param array $blacklist
  85. * (optional) Add to the blacklist of directories that should be filtered
  86. * out during the iteration.
  87. */
  88. public function __construct(\RecursiveIterator $iterator, array $blacklist = []) {
  89. parent::__construct($iterator);
  90. $this->blacklist = array_merge($this->blacklist, $blacklist);
  91. }
  92. /**
  93. * Controls whether test directories will be scanned.
  94. *
  95. * @param bool $flag
  96. * Pass FALSE to skip all test directories in the discovery. If TRUE,
  97. * extensions in test directories will be discovered and only the global
  98. * directory blacklist in RecursiveExtensionFilterIterator::$blacklist is
  99. * applied.
  100. */
  101. public function acceptTests($flag = FALSE) {
  102. $this->acceptTests = $flag;
  103. if (!$this->acceptTests) {
  104. $this->blacklist[] = 'tests';
  105. }
  106. }
  107. /**
  108. * {@inheritdoc}
  109. */
  110. public function getChildren() {
  111. $filter = parent::getChildren();
  112. // Pass on the blacklist.
  113. $filter->blacklist = $this->blacklist;
  114. // Pass the $acceptTests flag forward to child iterators.
  115. $filter->acceptTests($this->acceptTests);
  116. return $filter;
  117. }
  118. /**
  119. * {@inheritdoc}
  120. */
  121. public function accept() {
  122. $name = $this->current()->getFilename();
  123. // FilesystemIterator::SKIP_DOTS only skips '.' and '..', but not hidden
  124. // directories (like '.git').
  125. if ($name[0] == '.') {
  126. return FALSE;
  127. }
  128. if ($this->isDir()) {
  129. // If this is a subdirectory of a base search path, only recurse into the
  130. // fixed list of expected extension type directory names. Required for
  131. // scanning the top-level/root directory; without this condition, we would
  132. // recurse into the whole filesystem tree that possibly contains other
  133. // files aside from Drupal.
  134. if ($this->current()->getSubPath() == '') {
  135. return in_array($name, $this->whitelist, TRUE);
  136. }
  137. // 'config' directories are special-cased here, because every extension
  138. // contains one. However, those default configuration directories cannot
  139. // contain extensions. The directory name cannot be globally skipped,
  140. // because core happens to have a directory of an actual module that is
  141. // named 'config'. By explicitly testing for that case, we can skip all
  142. // other config directories, and at the same time, still allow the core
  143. // config module to be overridden/replaced in a profile/site directory
  144. // (whereas it must be located directly in a modules directory).
  145. if ($name == 'config') {
  146. return substr($this->current()->getPathname(), -14) == 'modules/config';
  147. }
  148. // Accept the directory unless the name is blacklisted.
  149. return !in_array($name, $this->blacklist, TRUE);
  150. }
  151. else {
  152. // Only accept extension info files.
  153. return substr($name, -9) == '.info.yml';
  154. }
  155. }
  156. }