This issue/patch advances on #2185559: Extension System, Part I: SystemListing clean-up + performance; remove SystemListingInfo + drupal_system_listing() — we either skip that issue and directly go with this, or this patch can be easily rebased against 8.x with that patch.

Part I + II of #2186491: [meta] D8 Extension System: Discovery/Listing/Info

Problem

  1. SystemListingInfo::scan() triggers an infinite recursion to itself when scanning for installation profiles, because it tries to retrieve the profile directories to be scanned, which makes no sense.
  2. The installer scans for available installation profiles, but does not prime the static filepath lookup cache of drupal_get_filename(), which causes the first of call to drupal_get_path() from other code to trigger yet another filesystem scan.
  3. The results of SystemListing are not cached, even though a subsequent scan will yield the identical results.
  4. The results of InfoParser are not cached, even though a subsequent info file parsing will yield the identical results.
  5. hook_system_theme_info() only exists to allow modules to ship with test themes, whereas discovering themes is and should be the sole responsibility of the extension discovery process.

Proposed solution

  1. Replace SystemListing[Info] with a highly performance-optimized ExtensionDiscovery:

    Bench: 10 scans for 'modules'
    SystemListing (HEAD): 6.825 seconds
    ExtensionDiscovery:   0.391 seconds
    
    1. Require .info.yml files for all extension types.
    2. Instead of scanning a particular base/origin directory (e.g., /sites/all) for the requested extension type only, scan it once for all extensions (of all types).
    3. Use a highly optimized filter for the recursive filesystem scan, 100% tailored to the specific task of discovering Drupal extensions.
    4. Do not recurse into 'tests' directories at regular runtime (~400% performance increase on its own).
    5. Return proper Extension objects for all discovered extensions.

    → Substantial extension discovery performance increase. Installer responds like a breeze.

  2. Fix the installer to prime drupal_get_filename() after scanning for installation profiles to resolve the infinite recursion problem.
  3. Statically cache all discovered extensions.
  4. Statically cache the InfoParser results to prevent the same info files from being re-parsed again.
  5. Replace all instances of drupal_system_listing() with ExtensionDiscovery.

API changes

  1. All Drupal extension types (profiles, modules, themes, and also theme engines) have to supply an .info.yml file in order to be discovered.

    1. The info file must specify at least the 'type', 'core', and 'name' properties.
    2. The 'type' value for theme engines is theme_engine.
    3. The 'type' property should ideally be declared first (for file parsing performance reasons only).

    Example:

    type: theme_engine
    name: Twig
    core: 8.x
    version: VERSION
    package: Core
    
  2. drupal_system_listing() has been replaced with Drupal\Core\Extension\ExtensionDiscovery.

    Drupal 7

    $available_modules = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules');
    

    Drupal 8

    $listing = new ExtensionDiscovery();
    $available_modules = $listing->scan('module');
    // Note that the returned entries are Extension objects, offering access to
    // SplFileInfo of the .info.yml file.
    // @see http://php.net/manual/class.splfileinfo.php
    // For example:
    $filemtime = $available_modules['node']->getMTime();
    
  3. hook_system_theme_info() has been removed. Additional (test) themes provided by modules are discovered natively now.

  4. (8.x-only) SystemListing and SystemListingInfo have been removed.

Follow-up issues

  1. Remove /sites/all, duplicates top-level directories.
  2. Consider to remove core compatibility check in ExtensionDiscovery, obsolete with Migrate in core?
  3. Convert Database drivers into a new + regular extension type?
  4. Move 'type' properties in .info.yml files to be defined first for performance (lead by example).
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

sun’s picture

Issue summary: View changes
sun’s picture

Status: Needs work » Needs review
FileSize
103.63 KB
23.61 KB
  1. Fixed stale code from Part I and tests.
  2. Replaced obsolete infinite recursion protection with explicit profile directory overrides.
  3. Fixed and polished lots of docs.
  4. Updated system_rebuild_module_data() + system_rebuild_theme_data() for new ExtensionDiscovery.
  5. Updated + simplified drupal_get_filename() for latest HEAD and new ExtensionDiscovery.
sun’s picture

Status: Needs work » Needs review
FileSize
103.88 KB
1.66 KB

Fixed drupal_get_filename() is called in special test environment conditions.

sun’s picture

Status: Needs work » Needs review
FileSize
111.2 KB
10.43 KB
  1. Removed stale/malformed serialization_test module from System module tests directory.
  2. Fixed ThemeTest::testInvalidTheme().
  3. Fixed EntityFilteringThemeTest.
  4. Fixed LocaleUpdateTest. → Static cache in system_rebuild_module_data() still has to be reset.
sun’s picture

Status: Needs work » Needs review
FileSize
111.21 KB

Rebased against HEAD.

sun’s picture

Status: Needs work » Needs review
FileSize
111.74 KB
550 bytes

Fixed UpdateManager::projectStorage() does not ensure that project list is an array (k/v can return NULL).

sun’s picture

FileSize
111.42 KB
16.52 KB

Great, we're green! :)

  1. Updated for \Drupal::hasService().
  2. Replaced legacy ::scan($directory) with ::scan($type).
  3. Polished some comments.
sun’s picture

FileSize
111.42 KB
693 bytes

Updated ThemeHandlerTest for the parameter rename accordingly.

Will cancel the previous test in a moment.

sun’s picture

FileSize
112.43 KB
15.68 KB
  1. Removed commented out code of Extension::$info magic getter overloading idea, since most likely obsolete.
  2. Renamed $infoPathname to $pathname and added proper Extension object getter methods.

This pretty much removes and/or resolves all remaining @todos that are within the scope of this issue.

All remaining @todos in this patch are only documenting and clarifying status quo of HEAD and will be addressed in separate child issues of the Extension System clean-up/conversion meta issue.

In other words, I think this patch is ready and resolves the first step of an ExtensionDiscovery in a very solid + performant way.

sun’s picture

Status: Needs work » Needs review
FileSize
112.88 KB
5.42 KB

Hm. I'm not able to reproduce those test failures locally, so I assume that was a testbot fluke for now.

Anyway, some further final polishing while waiting for reviews:

  1. Leverage new Extension object methods at least in the code that is touched here anyway already.
  2. Properly denote legacy properties slated for removal in Extension class.
sun’s picture

FileSize
113.16 KB
5.38 KB

While waiting for reviews, further polishing some minor aspects…

  1. Priming the pathnames of profiles causes drupal_get_filename() to not retrieve module pathnames from ModuleHandler::getModuleList().
  2. Improved regex for extracting/matching extension type in .info.yml files.
  3. Added comments for follow-up tasks to fully leverage the new ExtensionDiscovery.
larowlan’s picture

Code review, manual testing to come:

  1. +++ b/core/includes/common.inc
    @@ -3182,19 +3181,6 @@ function drupal_page_set_cache(Response $response, Request $request) {
    -function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) {
    

    I think policy dictates we will need to leave this around, as a wrapper and mark deprecated. But you'll need to ask a committer.

  2. +++ b/core/includes/install.inc
    @@ -122,22 +123,19 @@ function drupal_detect_database_types() {
    + * @todo Convert database drivers into a new + regular extension type.
    

    Out of scope, please open a new issue

  3. +++ b/core/includes/install.inc
    @@ -122,22 +123,19 @@ function drupal_detect_database_types() {
    -  // Allow any valid PHP identifier.
    -  // @see http://www.php.net/manual/en/language.variables.basics.php.
    -  $mask = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/';
    +  // The internal database driver name is any valid PHP identifier.
    +  $mask = '/^' . DRUPAL_PHP_FUNCTION_PATTERN . '$/';
    

    good cleanup, but out of scope - separate issue?

  4. +++ b/core/lib/Drupal/Core/Config/InstallStorage.php
    @@ -110,9 +112,14 @@ public function listAll($prefix = '') {
    +      $listing = new ExtensionDiscovery();
    ...
    +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -297,19 +298,28 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
    +      $listing = new ExtensionDiscovery();
    

    can you wrap this in an extensionDirectory() method like you have for the theme handler, so we can mock for phpunit

  5. +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
    @@ -0,0 +1,154 @@
    +  protected $blacklist = array(
    +    // 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',
    

    should we add .git here? I would regularly have a contrib module in it's own checkout, no need to traverse into the .git folder

  6. +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
    @@ -0,0 +1,154 @@
    +    if ($name[0] == '.') {
    

    nevermind :)

  7. +++ b/core/lib/Drupal/Core/Extension/Extension.php
    @@ -0,0 +1,218 @@
    +   * The relative pathname of the extension (e.g., 'core/modules/node/node.info.yml').
    ...
    +   * The relative pathname of the main extension file (e.g., 'core/modules/node/node.module').
    

    > 80 chars

  8. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -0,0 +1,395 @@
    +class ExtensionDiscovery {
    

    Should this have an interface and be a service (so can be injected etc), I realise its low-level, but there are high-level uses of drupal_system_listing() in contrib in D7

  9. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -0,0 +1,395 @@
    +   *   (optional) Whether to include test extensions. Defaults to FALSE when not
    ...
    +  public function scan($type, $include_tests = NULL) {
    

    Default mismatch, one says NULL, the other FALSE

  10. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -0,0 +1,395 @@
    +    // Search the core directory.
    +    $searchdirs[static::ORIGIN_CORE] = 'core';
    +
    +    // Search the sites/all directory (replaced by top-level directory in D8).
    +    $searchdirs[static::ORIGIN_SITES_ALL] = 'sites/all';
    +
    +    // Search for contributed and custom extensions in top-level directories.
    +    // The scan uses a whitelist to limit recursion to the expected extension
    +    // type specific directory names only.
    +    $searchdirs[static::ORIGIN_ROOT] = '';
    +
    +    // Search the site-specific directory.
    +    $searchdirs[static::ORIGIN_SITE] = conf_path();
    

    Should there be an ORIGIN_PROFILE here? if so we must be missing tests. If not, then I've missed something.

  11. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -0,0 +1,395 @@
    +        static::$files[$dir][$include_tests] = $this->scanDirectory($dir, $include_tests);
    

    fetching it all in once pass++

  12. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -0,0 +1,395 @@
    +    // 1 0 profiles/parent_profile/modules/parent_module/parent_module.module
    

    Parent profiles? Do we support that?

  13. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -0,0 +1,395 @@
    +   * @todo Move installation profiles into Settings (including directory paths).
    

    Out of scope?

  14. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -0,0 +1,395 @@
    +   * @param array $profileDirectories
    +   *   A list of installation profile directories to search for extensions.
    +   *
    +   * @return $this
    +   */
    +  public function setProfileDirectories(array $paths = NULL) {
    

    mismatch $profileDirectories Vs $paths

  15. +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php
    @@ -316,12 +295,10 @@ public function rebuildThemeData() {
    -      // Give the screenshot proper path information.
    

    out of scope?

  16. +++ b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php
    @@ -81,10 +81,10 @@ public function listInfo();
    +   * @return \Drupal\Core\Extension\Extension[]
    

    Yay for less arbitrary arrays, more domain objects

  17. +++ b/core/lib/Drupal/Core/Utility/ProjectInfo.php
    @@ -178,13 +178,11 @@ function processInfoList(&$projects, $list, $project_type, $status, $additional_
    -   *
    -   * @see system_get_files_database()
    

    note for other reviewers: this function is gone

  18. +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php
    @@ -29,10 +30,12 @@ public function __construct() {
    +      $listing = new ExtensionDiscovery();
    

    again, makes unit testing difficult, can we use a method like for ThemeHandler, given it extends InstallStorage, should be do-able

  19. +++ b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/GetFilenameUnitTest.php
    @@ -47,15 +47,6 @@ function testDrupalGetFilename() {
    -    $this->assertIdentical(drupal_get_filename('script', 'test'), 'core/scripts/test/test.script');
    

    Does anything use this functionality (using drupal_get_filename with random arguments), surely it was added for a reason (and with tests). Worth grepping the git logs?

  20. +++ b/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php
    @@ -7,12 +7,13 @@
    -class SystemListingTest extends WebTestBase {
    +class SystemListingTest extends DrupalUnitTestBase {
    

    Nice!

  21. +++ b/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php
    @@ -254,9 +254,12 @@ function testSwitchDefaultTheme() {
    -    $this->assertText(t('This theme requires the base theme @base_theme to operate correctly.', array('@base_theme' => 'not_real_test_basetheme')), 'Invalid base theme check succeeded.');
    -    $this->assertText(t('This theme requires the theme engine @theme_engine to operate correctly.', array('@theme_engine' => 'not_real_engine')), 'Invalid theme engine check succeeded.');
    +    $this->assertText(t('This theme requires the base theme @base_theme to operate correctly.', array('@base_theme' => 'not_real_test_basetheme')));
    +    $this->assertText(t('This theme requires the theme engine @theme_engine to operate correctly.', array('@theme_engine' => 'not_real_engine')));
    

    Out of scope?

larowlan’s picture

Manual testing as follows:

  • create a new module 'foo' in core/profiles/standard - ensure it shows up
  • override said module 'foo' in sites/modules - ensure overrides work

so my comments about ORIGIN_PROFILE don't apply here

sun’s picture

FileSize
113.06 KB
2.13 KB

Thanks for your detailed review, @larowlan! :-)

Not going to address each of your points individually (most of them are very minor anyway), so on the main points:

  1. drupal_system_listing() is removed here, because it allows to scan the full filesystem for arbitrary files, but only within extension directories, for which it cannot use ExtensionDiscovery anymore, because that is fully tailored and performance-optimized to specifically find extensions only.

    Modules should not actually perform raw filesystem scans in the first place. The public API idea of drupal_system_listing() was that you can find files of a certain pattern within all extensions, regardless of whether installed or not.

    This public API use-case should be replaced with a new extension list based file discovery facility on the upcoming ExtensionList/ExtensionHandler service, which limits the lookup to installed extensions.

    All of that being said, the edge-case of performing a raw filesystem scan across all available extensions is still possible and as easy as this:

    $subpathname = 'discover.me.txt'; # The file we want to discover in each extension.
    $listing = new ExtensionDiscovery();
    $files = array();
    foreach ($listing->scan('module') as $module) {
      if (file_exists($module->getPath() . '/' . $subpathname)) {
        $files[$module->getName()] = $module->getPath() . '/' . $subpathname;
      }
    }
    

    Given how simple this is, and given how much of an edge-case the use-case is in the first place, I actually think we should not provide any kind of helper function for it.

    The much more common use-case is the following anyway, based on installed modules (instead of all available):

    $subpathname = 'discover.me.txt'; # The file we want to discover in each extension.
    $files = array();
    foreach (\Drupal::moduleHandler()->getModuleList() as $name => $pathname) {
      if (file_exists(dirname($pathname) . '/' . $subpathname)) {
        $files[$name] = dirname($pathname) . '/' . $subpathname;
      }
    }
    

    And that's essentially the extension list based discovery mechanism that could/should exist on the upcoming ExtensionList/ExtensionHandler, because this exact concept is required pretty much all across Drupal core (cf. routing, theme templates, libraries, etc.pp.).

  2. I want to leave the minor changes to the database driver installer code in this patch, because database drivers apparently are an extension type that I and everyone else continues to forget about. → They are not even part of the meta issue summary, because I also forgot about them.

    Case in point: This new ExtensionDiscovery allows us to convert custom database drivers into regular extension types. This cuts it:

    $listing = new ExtensionDiscovery();
    $drivers = $listing->scan('driver');
    

    → scans all ./drivers directories in all origins for database driver extensions, and in turn:

    /sites/default/drivers/mssql.info.yml:
    type: driver
    core: 8.x
    name: Microsoft™ SQL Server
    

    → The minor cleanups and added @todo is here to raise awareness of the full potential and impact of this patch.

  3. The ExtensionDiscovery class should not be exposed as a service, because only a few subsystems in core should need and use it. In addition, the ExtensionDiscovery is a dependency of the service Container build process itself.

    All other subsystems, services, and modules should never have to discover extensions. They solely operate on the list of installed extensions only.

  4. Replacing the direct new ExtensionDiscovery() with class methods is out of scope here, since that only makes sense when phpunits for these classes are actually introduced.

  5. "Parent installation profile" is a concept that belongs to Simpletest (web) tests, as also outlined in the meta issue.

Status: Needs review » Needs work

The last submitted patch, 21: extension.disco_.21.patch, failed testing.

sun’s picture

sun’s picture

23: extension.disco_.23.patch queued for re-testing.

Status: Needs review » Needs work

The last submitted patch, 23: extension.disco_.23.patch, failed testing.

sun’s picture

Status: Needs work » Needs review
FileSize
112.96 KB

Merged HEAD.

damiankloip’s picture

Overall, I like this patch alot.

  1. +++ b/core/includes/theme.inc
    @@ -118,19 +119,9 @@ function drupal_theme_initialize() {
      *    An optional array of objects that represent the 'base theme' if the
    

    Do we use (optional) An array.. instead?

  2. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -297,19 +298,28 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
    +      $parent_profile = !empty($settings['parent_profile']) ? $settings['parent_profile'] : FALSE;
    

    This could be a much broader question/issue really. However, might as well bring it up here. Do you think this should return NULL and not FALSE. So we are not mixing bool/strings? (minor issue)

  3. +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
    @@ -0,0 +1,154 @@
    +  public function acceptTests($flag = NULL) {
    

    Pass a bool here are the default parameter value maybe.

  4. +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
    @@ -0,0 +1,154 @@
    +    $name = $this->current()->getFilename();
    ...
    +      if ($this->current()->getSubPath() == '') {
    ...
    +        return substr($this->current()->getPathname(), -14) == 'modules/config';
    

    Might make sense to store the value of $this->current() as a variable instead in this method?

  5. +++ b/core/modules/simpletest/simpletest.module
    @@ -542,23 +545,36 @@ function simpletest_test_get_all($module = NULL) {
    +    cache()->set($cid, $extensions);
    

    Could use \Drupal::cache() directly but don't really mind. Would be pretty nice to remove cache.inc soon though. There isn't much in there nowadays.

sun’s picture

FileSize
114.48 KB
3.83 KB
  1. Updated for #2194445: Web tests of modules in a site-specific directory can no longer be run, because module is not in test site directory
  2. Use NULL instead of FALSE in DrupalKernel::moduleData().
  3. Cleaned up obsolete NULL handling in acceptTests().

Usage of $this->current() is a common/standard coding practice for FilterIterator implementations, as far as I can tell from code I studied across the net.

I do not plan to address #27.1 and .5 here.

ryan.armstrong’s picture

This is looking awesome Sun. I have run into multiple issues with discovery of extensions, so to see this rewrite is great!

I tested the patch from 28. For each module, profile and theme that I created, I only added a .info.yml file as that should be the only requirement to detect the module/theme. Also, when I note a success I mean that the module was shown on the admin/extend page, the theme was shown on the admin/appearance page, or the profile was shown as an option when running install.php. I'm assuming that issues regarding actually enabling these modules, themes, and profiles, which only contain info.yml files is another issue.

Modules:

  • Created a module in /modules and it worked.
  • Created a module in /core/profiles/minimal/modules (The profile I used to install Drupal) and it worked.
  • Created a module in /core/profiles/standard/modules (A profile that was not installed) and nothing showed up. It worked.

Themes:

  • Created a theme in /themes and it worked.
  • Created a theme in /core/profiles/minimal/themes (The profile I used to install Drupal) and it worked.
  • Created a theme in /core/profiles/standard/themes (A profile that was not installed) and nothing showed up. It worked.

Profiles:

  • Created a profile in /profiles and it worked.

Thoughts

  • Themes required a cache clear before they would be detected/updated while modules did not. Is this expected behavior?
  • As noted, I only made sure that these discovered extensions showed up in the proper sections of the admin UI. They all had issues being enabled, which I am assuming is out of the context of this patch/issue, and that subsequent issues would need to be raised regarding the enabling of these extensions. Is this correct?
  • At the top of the issue you note that type should be at the top of the .info.yml file. I found that the extension was discovered regardless of where the type was located. Is having the type anywhere but at the top supposed to have an effect?

Is there anything else that needs to be tested? Otherwise I would consider this RTBC depending on the response to my questions. Again, awesome work Sun!

sun’s picture

Thanks for reviewing and testing, @ryan.armstrong! :-)

I agree that this is in RTBC quality. Regarding your additional thoughts on the test results:

Themes required a cache clear before they would be detected/updated while modules did not. Is this expected behavior?

Yeah, the Themes/Appearance page uses an additional database cache layer, so the extension discovery is not actually invoked when you visit that page.

That behavior is not changed here, but may be fixed by #1067408: Themes do not have an installation status — although the additional caching behavior always existed to my knowledge; I'm not sure why we made a difference between the modules page and the themes/appearance page...

I only made sure that these discovered extensions showed up [...] They all had issues being enabled

Yeah, that is sadly still expected behavior. This patch is the blocker for #340723: Make modules and installation profiles only require .info.yml files — only with that additional change, you will be able to install a profile/module by just creating the .info.yml file.

Strictly speaking, you actually discovered a bug by saying that, because the modules are exposed in the UI, even though they don't have a .module file. ;) But I don't think it makes sense to "fix" that, because once this patch is in, #340723: Make modules and installation profiles only require .info.yml files will be piece of cake. :-)

you note that type should be at the top of the .info.yml file. I found that the extension was discovered regardless of where the type was located. Is having the type anywhere but at the top supposed to have an effect?

As of now, the 'type' can be declared anywhere in an .info.yml file. The discovery performance is best if the 'type' is declared first, since every .info.yml file is parsed for its 'type'. — To avoid the overhead of YAML parsers, the 'type' property is manually parsed/extracted from the .infoyml file, whereas the file is read line by line until the 'type' declaration is found. Hence: If declared first, exactly one line is read only.

I don't think we need to enforce it to be declared first, but as a follow-up issue, I think Drupal core should simply lead by example and have it declared first in all .info.yml files in core. That is, because most people are simply following what core is doing.

ryan.armstrong’s picture

Status: Needs review » Reviewed & tested by the community

Awesome, thanks for the detailed response. I'm marking this as RTBC, let's get this committed! In addition to leading by example with core's modules, we should also make sure that the change record as well as the documentation also note this, as this is something that could easily be lost in the shuffle.

damiankloip’s picture

I do not plan to address #27.1 and .5 here.

27.1 - fair enough. I don't really care about that
27.5 - I was not meaning remove cache.inc in this issue. Here you are adding calls to cache() though, which is not preferable to \Drupal::cache().

This can be removed in another issues though, I don't mind either way. If you're really against changing it for whatever reason.

damiankloip’s picture

OK, just for reference, created #2198339: Remove cache.inc to remove cache.inc (Also see child issue that is postponed on).

sun’s picture

Issue summary: View changes

Created the change notice: https://drupal.org/node/2198695

sun’s picture

catch’s picture

The 'type' property should ideally be declared first (for file parsing performance reasons only).

Is this a requirement or optional?

ryan.armstrong’s picture

As of now its optional. You can actually put the type anywhere in the info.yml but you get a performance bump if you put it first.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll

extension.disco_.28.patch no longer applies.

error: patch failed: core/lib/Drupal/Core/Extension/UpdateModuleHandler.php:46
error: core/lib/Drupal/Core/Extension/UpdateModuleHandler.php: patch does not apply

sun’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs reroll
FileSize
114.47 KB

Simple diff context conflict. 8.x merged cleanly.

webchick’s picture

Assigned: sun » catch

This seems like it'd be good for catch to review.

damiankloip’s picture

The usages of the cache() function still need to be changed. This is now deprecated and would reintroduce it, meaning the only two usages in core again.

Not sure why you refused to do this in my previous feedback tbh ;)

damiankloip’s picture

Status: Reviewed & tested by the community » Needs work
Berdir’s picture

Status: Needs work » Needs review
FileSize
114.45 KB
709 bytes

Fixed those calls. No commit credit, this is all sun's doing :)

damiankloip’s picture

Status: Needs review » Reviewed & tested by the community

Thanks!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll

extension.disco_.43.patch no longer applies.

error: patch failed: core/includes/module.inc:5
error: core/includes/module.inc: patch does not apply

Berdir’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
114.43 KB

Trivial re-roll, only conflicted on a new use statement.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

extension.disco_.46.patch no longer applies.

error: patch failed: core/modules/system/system.module:1574
error: core/modules/system/system.module: patch does not apply

catch’s picture

Overall looks great, few things I noticed:

  1. +++ b/core/includes/bootstrap.inc
    @@ -655,7 +656,7 @@ function _drupal_request_initialize() {
    @@ -667,72 +668,31 @@ function drupal_get_filename($type, $name, $filename = NULL) {
    

    Something in me was hoping this function would completely die. It's great that it's a lot shorter, is there a part ? where this gets moved to the extension system proper? (Fine if there isn't for this patch, just wondering).

  2. +++ b/core/includes/install.inc
    @@ -123,22 +124,19 @@ function drupal_detect_database_types() {
    + * @todo Convert database drivers into a new + regular extension type.
    

    Not sure about this, we don't do the same for lots of other services and seems like this would only ever be used in the installer. Could we remove the @todo and add a follow-up issue instead? The discovery here is no worse than it already is, so doesn't need a @todo (which is more of an apology than an intention to do anything usually).

  3. +++ b/core/includes/module.inc
    @@ -297,14 +298,19 @@ function module_uninstall($module_list = array(), $uninstall_dependents = TRUE)
    +  $files = $listing->scan('module');
    

    purdy.

  4. +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
    @@ -0,0 +1,152 @@
    + * To ensure the best possible performance for an extension discovery, this
    

    No need for 'an'.

  5. +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
    @@ -0,0 +1,152 @@
    +  protected $blacklist = array(
    

    What if there's a module with the same name as one of these directories?

  6. +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
    @@ -0,0 +1,152 @@
    +  public function acceptTests($flag = FALSE) {
    

    Does this need to be specific to tests, or could we add a setting, then override that setting when running tests? Just thinking whether there's a use case for extending the blacklisted directories during normal operation.

  7. +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
    @@ -0,0 +1,152 @@
    +    $this->acceptTests = (bool) $flag;
    

    No need to cast to bool.

  8. +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
    @@ -0,0 +1,152 @@
    +      // named 'config'. By explicitly testing for that case, we can skip all
    

    Should we open a follow-up issue to rename 'config' module to 'configuration? I understand the optimization here but it's a bit nasty.

  9. +++ b/core/lib/Drupal/Core/Extension/Extension.php
    @@ -0,0 +1,218 @@
    +    $info = new \ReflectionObject($this);
    

    Really? Could at least do with a @todo, but I'm having trouble seeing why this is all necessary.

  10. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -0,0 +1,411 @@
    +    // Search the sites/all directory (replaced by top-level directory in D8).
    

    Not really replaced.

  11. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -0,0 +1,411 @@
    +        $info = $this->getInfoParser()->parse($file->getPathname());
    +        if (!isset($info['core']) || $info['core'] != \Drupal::CORE_COMPATIBILITY) {
    

    Might be worth putting in a dedicated method, we must be doing the same check elsewhere.

  12. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -0,0 +1,411 @@
    +    // include_paths will not be consulted). Backup the relative originating
    

    Not a backup, just 'store' or 'retain'?

  13. +++ b/core/modules/block/tests/modules/block_test/block_test.module
    @@ -6,14 +6,6 @@
    - * Implements hook_system_theme_info().
    

    Nice.

YesCT’s picture

I'm going to reroll this. then we can address the concerns of @catch.

YesCT’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
114.48 KB
114.47 KB
2.26 KB

auto merged with no conflicts. (patch 50)

I didn't answer catch's questions, but did fix #48 4, 7, 12 in patch 50b.

sun’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
114.4 KB
6.87 KB

Thanks for the detailed review, @catch! :-)

It looks like all the questions you raised are very minor, but I'm happy to address some of them in code, and clarify on the others:

re 1) Yes, I absolutely agree, drupal_get_filename() needs to die. The ultimate goal of the extensions system revamp effort is to get rid of drupal_get_filename() and drupal_get_path() and ... and [insert large dependency chain here]... ;-) The discovery patch can only simplify it, but not remove it yet, because the fallback on discovery is only the edge-case situation in that function.

re 2) I removed and cleaned up all @todos that do not have to live in code, except of those that required me to uglify the new code.

re 5) Most of the blacklist exists in HEAD already, see SystemListing::scanDirectory(). The only additions are "src" + "vendor" (complementing existing "lib" + preparing for PSR-4), "assets" + "files" + "images" + "misc" (complementing "templates"+"css"+"js"), "includes" + "fixtures". I think it is fair to expect and simply say that no extension can use any of these [directory] names. Every unnecessary subdirectory [tree] recursion costs performance.

re 6) Off-hand, I'm not able to see a use-case for customizing the additional blacklist. In any case, if such a use-case arises, we can consider to change the code in a separate issue.

re 8) When I first worked on a similar implementation 1-2 years ago, someone raised the idea of renaming it to config_ui (in line with field_ui + views_ui). We can consider to do that in a separate issue, but wouldn't be strictly required, as the special filter for "config" works excellently for now (and doesn't decrease performance).

re 9) Once the extension system cleanup is done, only two properties will be serialized, $type and $pathname. Everything else can be derived from those two values. — Not sure which comment you missed, but the public Extension class properties as well as the serialize() and unserialize() methods are cluttered with @todos already? ;) → Once this baseline is in, we are able to move forward on the higher layers, extension list + extension info. Especially the info layer is where various other functions are adding random properties to the discovered extension objects. Dire need of cleanup. That's why I'm working on this in the first place :-)

re 10) I've adjusted the comment, but I actually want to remove the /sites/all location altogether from D8, because it fully duplicates the top-level directories. Especially when the root /settings.php patch will have landed, that option really doesn't make any sense anymore. The only reason for why we originally kept it was "we're not sure yet."

re 11) I'm not going to add a helper method for the info parsing and core version compatibility check, because extension info handling is a separate concern that is not to be addressed by extension discovery; i.e., if you need to perform this check, then you will not look into ExtensionDiscovery. — The mere existence of this code in the ExtensionDiscovery class actually offends me to begin with. As a potential separate follow-up issue, I'd like to propose to remove it altogether. → Because with Migrate in core, the edge-case this code is conveniently catering for (a core module being overridden by a later path, but incompatible with current core, since originating from a previous version of core) should theoretically no longer exist.

sun’s picture

Issue summary: View changes

FWIW, I did not intend to delete @YesCT's re-rolled patches — yet another bug with d.o's issue update conflict resolution, it seems. :-/

After removing the @todos from the code, I'm adding them "back" to the issue summary now, so I can create those issues after this patch has landed. :)

sun’s picture

Hm. I'm blocked on some of the improvements of this patch in multiple other issues — any chance to move forward here?

catch’s picture

I've been struggling finding time to get back to this, but had another look through and no more complaints really - or they'll hopefully get resolved in the next set of issues.

However, this no longer applies.

catch’s picture

51: extension.disco_.49.patch queued for re-testing.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 51: extension.disco_.49.patch, failed testing.

sun’s picture

Assigned: catch » sun
Status: Needs work » Reviewed & tested by the community
FileSize
114.46 KB

Merged 8.x. Simple diff context conflict.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.x, thanks!

andypost’s picture

Can't install into multi-site folder
EDIT
sites.php - $sites['d8.l'] = 'd8.l';

LogicException: Cannot use SplFileObject with directories in SplFileInfo->openFile() (line 354 of /home/andypost/www/core8/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php).

SplFileInfo->openFile('r')
Drupal\Core\Extension\ExtensionDiscovery->scanDirectory('sites/d8.l', )
Drupal\Core\Extension\ExtensionDiscovery->scan('profile')
install_begin_request(Array)
install_drupal()
catch’s picture

Status: Fixed » Needs work

Reverted for now.

andypost’s picture

The problem was caused by custom theme that is sym-link'ed to site's theme folder

penyaskito’s picture

catch’s picture

andypost confirmed this was a regression introduced by this specific patch in irc.

andypost’s picture

@penyaskito No! it is introduced here, before this patch my theme and modules works fine.

So I see no reason to commit major issue and make another major to fix regression!

I've checked php 5.4 (fresh debian) and core fails too: I just placed symlink to theme folder

Additional uncaught exception thrown while handling exception.

Original

LogicException: Cannot use SplFileObject with directories in SplFileInfo->openFile() (line 354 of /var/www/drupal/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php).

SplFileInfo->openFile('r')
Drupal\Core\Extension\ExtensionDiscovery->scanDirectory('sites/default', )
Drupal\Core\Extension\ExtensionDiscovery->scan('profile')
install_begin_request(Array)
install_drupal()
Additional

LogicException: Cannot use SplFileObject with directories in SplFileInfo->openFile() (line 354 of /var/www/drupal/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php).

SplFileInfo->openFile('r')
Drupal\Core\Extension\ExtensionDiscovery->scanDirectory('sites/default', )
Drupal\Core\Extension\ExtensionDiscovery->scan('theme')
Drupal\Core\Extension\ThemeHandler->rebuildThemeData()
Drupal\Core\Extension\ThemeHandler->listInfo()
list_themes()
_drupal_maintenance_theme()
drupal_maintenance_theme()
_drupal_log_error(Array, 1)
_drupal_exception_handler(Object)
niko-’s picture

@sun This looks like we must use
http://ua1.php.net/manual/en/splfileinfo.getlinktarget.php
http://ua1.php.net/manual/en/splfileinfo.islink.php
for checking if dst is symbol link and get its real target in this case.

sun’s picture

Status: Needs work » Needs review
FileSize
114.58 KB
1.05 KB

:-/ It would have been much easier to resolve the symlink issue in a quick follow-up patch instead of reverting...

I'm developing on Windows, so I'm not actually able to reproduce the issue. (Windows has mklink, which allows symbolic directory links and junctions, but they are transparent to PHP; i.e., not interpreted as symlinks.)

Unless I'm terribly mistaken, attached patch should cut it. @andypost et al, can you test, please?

damiankloip’s picture

This fix looks good to me - let's let andypost confirm and RTBC.

andypost’s picture

Now it works, just re-roll because 66 applies with offset.
Also I found that installation of modules become a bit slower but seems this caused by yml parser

catch’s picture

Status: Reviewed & tested by the community » Fixed

Thanks! Committed/pushed to 8.x.

andypost’s picture

Issue tags: -Avoid commit conflicts

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Wim Leers’s picture

This also seems to have broken run-tests.sh on OS X: #2216695: run-tests.sh broken on OS X ("Too many open files").

chx’s picture

Even for Drupal 8 which is full of stupid ideas this is a spectacularly stupid idea -- how the hell are we supposed to debug test failures if we can't switch on a test module?? Incredible!

sun’s picture

donquixote’s picture

Here are new tests documenting the behavior of ExtensionDiscovery.
#2729711: Improve test coverage for ExtensionDiscovery

I wonder if all of this is the intended behavior, or if some of it is by accident.
E.g. I remember that in D7, files in parent directories had priority over those in subdirectories with same name. In D8 it is the opposite.
Also, there is nothing to prevent extensions of different type to have the same machine name.

donquixote’s picture

In aftermath of this issue, was it a good idea to let the Extension object pretend to also be the SplFileInfo object for its *.info.yml file via magic __call()?
Why did we do this?
#2959989: Deprecate Extension::__call() magic