It was suggested in #2503927: Convert ExtensionDiscovery to a service to turn ExtensionDiscovery into a service. I think it is not really suitable as such, and we should write something new instead.

The ExtensionDiscovery component has a number of issues. It is mutable (->setProfileDirectories()), it is big, it explicitly calls out to the environment for global state data, and it statically caches mutable objects.

Some of the problems can be fixed. E.g. adding the additional key [$this->root] to the static cache, as in #2605654-48: Modify Drupal\Core\Extension\ExtensionDiscovery to allow for scanning multiple file systems, enabling vfsStream testing.

But beyond this, we probably need to leave some of the crappy behavior in place for existing components that are already depending on it. One of the flaws is the existence of a setProfileDirectories() method, which makes this object mutable and thus not suitable as a service.

If a depending component already needs to be modified (to use a service instead of calling a constructor), it can as well use a different tool that does the job more reliably.

More details in the first comment, since this will change over time.

Comments

donquixote created an issue. See original summary.

donquixote’s picture

I experimented, and came up with an architecture where a lot of things can be swapped out for testing, and individual components can be tested individually. Here is what such a test can look like:
https://github.com/donquixote/drupal/blob/8.2.x-extension-libraries-2016...
This is far from perfect, and the git history is messy. Don't waste too much time looking at it.
But it does what it is designed for. And even runs measurably faster for me (10ms instead of 13ms).
It is not meant to be used as-is, but as a quarry to take parts from.
Some of it is redundant, providing more than one solution to a problem. Again this is to compare and then decide what's best.

Lower discovery layer:
The result of this layer is a grouped list of extensions or file paths per searchdir, extension type, subdir name and extension name.
SearchdirToFilesGroupedInterface
SearchdirToRawExtensionsGroupedInterface

It is possible to just take this part, and use it to replace the scanning and static cache in ExtensionDiscovery.
https://github.com/donquixote/drupal/commit/e7694f3781d9a694b41720bfb566...
Note that this is following a series of smaller fixes on ExtensionDiscovery
We can, however, leave ExtensionDiscovery unchanged and instead only use the new components directly.

Upper discovery layer:
The result of this layer is an array of unprocssed extension objects by extension type and extension name.
RawExtensionsByTypeInterface

  • Distinguish "raw extension" vs "extension" in class names and comments. Still the same Extension class, for BC reasons. But "Extension" has an ->info array added, whereas "raw extension" does not. Or more precisely, it is not guaranteed, because the same objects are passed around and modified.
  • Declare new public properties on the Extension class, that were previously anonymous.
  • The list of searchdir prefixes is swappable for testing.
    Maybe we no longer want this if we test with vfsStream.
    An alternative would be to optionally set an array
  • The active profile name is swappable for testing.
    For some this may look like too much of interfaces and swappability. And maybe they are right. If we can instead inject the literal profile name as a string.
  • The info parsing is swappable for testing, so we don't have to bother with the filesystem.
    Again, we could avoid this with vfsStream.
  • No more setProfileDirectories(), which made other extension lists depend on the profile extension list. Instead, only the profile name is required. See RawExtensionsByType_FromRawExtensionsGrouped.
  • The equivalent of this with file paths instead of extension objects: FilesByType_FromFilesGrouped.

Extension list layer:
The result of this is a list of extension objects with ->info array filled in, and a bunch of processing based on the extension type, which modules are enabled, etc.
To not nameclash with the planned ExtensionList classes, I named this one ExtensionsByType*.

General considerations:

  • Naming: Currently classes are named with a pattern that is unusal for Drupal but is what I got used to. We should figure this out later in the process, but I think it is ok for experimenting.
  • Every member variable is private, not protected. All the supposed benefits of protected member variables vanish, if your classes are sufficiently small. For the few remaining cases of inheritance, member variables of the parent class are intentionally concealed from the child class.
  • I think it totally matches all the letters of "SOLID".
    S (Single responsibility) - Classes and interfaces are really small. Some may say too small, and too many. We can always "collapse" some of them.
    O (Open/closed) - Small enough components that if we ever want to change something, we likely don't have to change any implementation, but only their composition.
    L (Liskov substitution) - This is part of the language, if we work with interfaces..
    I (Interface segregation) - Most of the interfaces have only one method. Not sure how the reset() method fits in, but that's ok.
    D (Dependency inversion) - "Depend upon abstractions". Yep. All the components depend on interfaces, not implementations. They do not create their own helper objects, but instead have stuff injected.

Fixes:

  • Info files in the searchdir itself, such as sites/default/foo.info.yml are no longer found. I think this was a misbehavior anyway.

Issues remaining or newly introduced:

  • For some this may seem like too many layers with explicit interfaces. Maybe some of it can be squashed, and maybe some of it can be made more generic and reusable. File streams may make some of the swapping out obsolete for testing. I just prefer to start with too many than too few interfaces.
  • Class names do not conform with usual practice in
  • Variable names are messed up, lowerCamel vs snake_case. This is not the time to discuss this!
    Overall this is not a time for code review, but architectural planning!
  • The "file cache" is somehow lost in the modified ExtensionDiscovery. I am sure it could be re-added as a decorator somewhere.
  • The ->reset() mechanic is incomplete. I'm not really sure how much of it is needed, and whether it should be part of the interfaces.

For BC reasons, the following was *not* changed:

donquixote’s picture

Btw I'm a bit undecided or confused about the semi-mutability of cached / buffered data providers.
But I think we can live with it.

dawehner’s picture

This is a hell lot of a long post. I don't have yet the time to read it all.
I'm sorry, but my brain cannot hold 30 files in my head when scanning files.

Can you describe why your system is faster? I would guess there should be a way to add those kind of optimizations to the existing system. ... Need to run

donquixote’s picture

This is a hell lot of a long post. I don't have yet the time to read it all.
I'm sorry, but my brain cannot hold 30 files in my head when scanning files.

The trick is to look at one component at a time, and stop trying to think too much about the context where it is used.
Also it helps to see the code in an IDE where you can jump around.

But as said further down, maybe we can squash some of the interfaces and classes. I find it a bit too much myself, especially if we need additional layers to ease the construction and composition of components.

The above list is a quarry. We can pick and choose.
But I think it can be useful to have a proof-of-concept implementation, instead of doing it all in theory.

Can you describe why your system is faster? I would guess there should be a way to add those kind of optimizations to the existing system. ... Need to run

I think it is faster by using good-old opendir() + readdir() instead of RecursiveDirectoryIterator. I was sceptical when I first saw the DirectoryIterator in ExtensionDiscovery. And I don't really think it helps readability, or anything else. I don't understand why this was introduced. Maybe it looks "modern".

The opendir() + readdir(), or even another method, can be applied to the existing ExtensionDiscovery system. But doing this on the existing system would mean to move all the logic from RecursiveExtensionFilterIterator into ExtensionDiscovery, making this class even more scary.

I would prefer if we have a standalone and separately testable directory scanning tool, which we can then optimize. E.g. I recently made some experiments where glob() was even faster than scandir() + readdir(). But this depends on the way it is used, and it is not clear whether the same optimization can be applied here.

-----

Otherwise, performance should be mostly the same in terms of performance, because it does the same steps.
The additional interfaces and indirection should be fairly cheap in performance terms, because they make one call per array of files (e.g. per searchdir, per extension type, etc), instead of a separate call per file.

So I think there is no structural reason why one system should be significantly faster than the other, if both are optimized in the best possible way. But this optimization would be easier in a new system, where the relevant components are standalone and can be individually tested.

dawehner’s picture

I think it is faster by using good-old opendir() + readdir() instead of RecursiveDirectoryIterator. I was sceptical when I first saw the DirectoryIterator in ExtensionDiscovery. And I don't really think it helps readability, or anything else. I don't understand why this was introduced. Maybe it looks "modern".

Please don't assume that this was done just because it looks modern. We came up with a highly optimized version in #2188661: Extension System, Part II: ExtensionDiscovery, see some of the examples. I still think we can improve things, but why we cannot do an incremental improvement is just not obvious for me

donquixote’s picture

We came up with a highly optimized version in #2188661: Extension System, Part II: ExtensionDiscovery

The optimization here was that we have dedicated logic for extension discovery, instead of a one-size-fits-all file_scan_directory().
I don't find any comment that talks about the difference between RecursiveDirectoryIterator vs opendir() + readdir() vs scandir() vs glob().
The numbers show that whatever we did with SystemListing[Info] was slower than what we now do with ExtensionDiscovery. They do not show that whatever we are doing now is the best possible.
Also I am a bit suspicious about the numbers. They are so far off, that maybe the SystemListing[Info] includes parsing the info file. This is obviously slower, but does not tell us much about the fastest way to discover the files.

Btw, in this issue my goal is not really performance. I just want to preemptively counter any concerns that a cleaned-up system might be slower.

I still think we can improve things, but why we cannot do an incremental improvement is just not obvious for me

Imo, to fix ExtensionDiscovery, or to make it suitable as a service, we have to change its public API and behavior, because this is part of the problem.

There are two ways to do this.

  • First option:
    (all in one patch/commit):
    1. Modify the class ExtensionDiscovery itself, in a BC-breaking way.
      Update all code that uses it, all instances at once.
      Possibly break contrib.
  • Second option:
    (in separate patches/commits):
    1. Leave ExtensionDiscovery as it is, with some minor non-BC-breaking fixes.
    2. Create an alternative class or mechanism that is more reliable, easier to test, suitable as a service, etc, overall just better. So far, no risk of breaking stuff, because we are not using it anywhere.
    3. Write tests to verify that the new thing produces the same result as the old thing, in those cases where we intend to use it.
    4. Slowly replace usages of ExtensionDiscovery with the new thing, and verify that nothing breaks.

The second option is a lot safer and BC-friendly.
The initial patch will be quite big, but it will be risk-free.
The risky patches can be done

Btw, this is exactly how I understand the "O" in "SOLID" (after a long time of confusion): Stop modifying the behavior of existing classes / components. Once a component is finished, with clearly defined specs, leave it alone ("close" it). Then write a new component for your changed requirements.
(Maybe I am still getting it wrong, and the "O" means something else. But at least this meaning makes sense to me and reflects the way I think and work.)

So far, this does not automatically imply a "total rewrite".
We could instead just copy ExtensionDiscovery to ExtensionDiscovery2, and apply gradual modifications. But this just means spending more time with fragile code, instead of focusing on something beautiful.

-------------=========-----------

Besides all of this:

I find the incremental approach, depending how it is executed, depressing. It kills creativity, it makes us blind for the "big picture", it prevents real architecture, and is possibly one of the reasons why some problems take so long to fix. (This can be a long discussion, where this issue is the wrong place.)

I need at least a vision and possibly a proof of concept how I want things to look like. Then I can go back to baby steps, but with an inspiring direction in mind.

I understand that we need to stay "shippable" and avoid BC breaks. But as illustrated above, I do not see a contradiction here. In fact, a rewrite can sometimes be less risky than changing the behavior of existing components.

dawehner’s picture

Well, I strongly disagree with the idea that incremental improvements doesn't help. Its the ONLY way forward in an opensource project to stay sane. In an opensource project you need to optimize on more parameters than simply the code:

* You need to get changes understood by people
* You need to reduce the long term maintenance cost on midterm and longterm which means you cannot just drop existing knowledge about components completely

From my point of view we should get #2208429: Extension System, Part III: ExtensionList, ModuleExtensionList and ProfileExtensionList in and improve things continuously. In general I cannot imagine anyone to get the installer working properly within a complete rewrite.

donquixote’s picture

We (@dawehner, @donquixote) agreed in IRC that we continue to follow different angles. This means, whatever we do here, we still continue to work on the existing ExtensionDiscovery, e.g. #2605654: Modify Drupal\Core\Extension\ExtensionDiscovery to allow for scanning multiple file systems, enabling vfsStream testing

Opinion

You need to reduce the long term maintenance cost on midterm and longterm which means you cannot just drop existing knowledge about components completely

I think we have different ideas what rewrite means.
Think of a city with a river, and a bridge connecting the two sides.
One day new requirements appear, and the existing bridge is no longer sufficient.
We can now refit the existing bridge, causing traffic jams or worse during construction time.
Or build a new bridge at a different place, and leave the old bridge in place. No traffic jams. We can leave the old bridge around as long as we want.

We do not "drop existing knowledge" this way.

Well, I strongly disagree with the idea that incremental improvements doesn't help. Its the ONLY way forward in an opensource project to stay sane.

How has this worked out for us so far? Where are we now, where will we be 2 years from now?
I'm afraid that a lot of the problems we have today will still be there in 2 years.

Proposal

But seeing that we disagree about this, here is what I propose:

We regard this code as "proof of concept", and we keep this issue for experimenting.
I might post a few patches based on the code, to test if this works for the installer and other places.
(spoiler: I already tried some of it, and it was ok so far)

At the same time, we continue working on the incremental improvements.
I have some ideas how we can do this without breaking BC of ExtensionDiscovery. Basically we turn this class into a transformer, which behaves awkward by default, but can turn into something sane with alternative constructor setup. We add static factory methods that give us the sane behavior by default.

Once this works, maybe the difference to what I am proposing here is not as big anymore.

EDIT: In the metaphor above, this means we build a demo bridge, then we refit the old bridge based on the demo bridge. But we never open the demo bridge for the public (= we never commit it).

donquixote’s picture

Title: ExtensionDiscovery rewrite, according to SOLID principles » Replacement for ExtensionDiscovery, following SOLID principles

Other example:
Imagine PHP that the behavior of array_multisort() is awkward.
So they have 3 options:
1) Change the existing behavior.
2) Add an additional parameter combo which now changes the behavior.
3) Provide a new function which does the job better.

I am changing the title to "replacement". Maybe it sounds less threatening :)

(The direction I proposed in #10 still applies nonwithstanding.)

donquixote’s picture

Nothing shall happen here before we have better test coverage.
#2729711: Improve test coverage for ExtensionDiscovery

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

smustgrave’s picture

Status: Active » Postponed (maintainer needs more info)
Issue tags: +stale-issue-cleanup

Thank you for creating this issue to improve Drupal.

We are working to decide if this task is still relevant to a currently supported version of Drupal. There hasn't been any discussion here for over 8 years which suggests that this has either been implemented or is no longer relevant. Your thoughts on this will allow a decision to be made.

Since we need more information to move forward with this issue, the status is now Postponed (maintainer needs more info). If we don't receive additional information to help with the issue, it may be closed after three months.

Thanks!

nicxvan’s picture

Status: Postponed (maintainer needs more info) » Needs work

I have been planning to work with Don to break some of these ideas into separate issues so we can track them.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.