Problem/Motivation

1. D7 => D8 WTF - Using e.g. sites/all/modules/memcache/memcache.inc did just work, but the same with classes does not - until the module is enabled, which can be very problematic to deploy production changes. (How do you sync the settings.php change with the enabling of the module without the site WSOD'ing in between?)

2. In a composer enabled world the expectation is that modules have a composer.json, which specifies the folders to autoload, which is then automatically loaded. Once a module starts using autoload: psr-4: { 'Drupal\\my_module\\': 'src/'} we get a huge behavior change for a disabled vs. an enabled module when that module is managed by composer or not.

3. Small DX problems, e.g. to override the container class with one from a module, explicitly the autoloader has to be used to addPsr4() for the module.

Proposed resolution

- Always load PSR-4 for all available modules.

This is a step in transitioning us to a static composer world - as the rest of the PHP world already uses.

Remaining tasks

- Discuss
- Create patch

User interface changes

- None

API changes

- Behavior change: Modules src/ directory classes are always available in the autoloader.
- Composer managed modules can add autoload: psr-4 safely as there is no change in behavior.

Data model changes

- None

Issue fork drupal-2536610

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

timmillwood’s picture

I hadn't thought of this very good point.

Taking the contrib module "monolog" as an example, all of the /modules/monolog/src and /core/vendor/monolog files will get loaded by Drupal even if the monolog module is not enabled or being used.

Crell’s picture

Didn't we just recently go to a lot of work to ensure this DIDN'T happen, as it did crazy things to plugins?

catch’s picture

@Crell: Plugin discovery used to use the actual namespaces registered to the classloader.

This is no longer the case however, plugin/annotation discovery gets a list of namespaces from the container/DrupalKernel, which in turn gets the module list from config.extension - at least that's what core does, nothing stops you putting something different than container.namespaces in a services.yml

So plugin discovery itself should be OK.

However...

@Fabianx:

D7 => D8 WTF - Using e.g. sites/all/modules/memcache/memcache.inc did just work

This didn't just work.

At least you had to do:

$conf['cache_backends'][] = 'sites/all/modules/memcache/memcache.inc';
$conf['cache_default_class'] = 'MemCacheDrupal';
Once a module starts using autoload: psr-4: { 'Drupal\\my_module\\': 'src/'}

Shouldn't they not do this?

This is a step in transitioning us to a static composer world - as the rest of the PHP world already uses.

There's no chance that 8.x will get there since we can't drop the module interface from core or the requirements that comes with.

 public function getFormId($form_arg, FormStateInterface &$form_state) {
    // If the $form_arg is the name of a class, instantiate it. Don't allow
    // arbitrary strings to be passed to the class resolver.
    if (is_string($form_arg) && class_exists($form_arg)) {
      $form_arg = $this->classResolver->getInstanceFromDefinition($form_arg);
    }

This opens up modules being able to specify classes from other modules for forms (quite a common use case), but then forget to add a dependency because it works as long as that module is in the file system. This seems like more of a gotcha than not adding autoload: psr-4 to contrib modules with a composer.json

fabianx’s picture

#3: Some great feedback! :)

Yes, agree 'container.namespaces' is how plugin discovery works and also not a problem for other things like *.yml, which is all based on the module list.

--

1. Yes, but that was a completely different world with almost no dependencies on e.g. cache backends and almost no autoloading.

2. Nothing is preventing them from doing that. Also modules will be re-used in different contexts and then someone will add PSR-4 loading.

e.g. currently strictly speaking all modules break the unit testing contract, I can't require module/X and use its Interface, because it is not auto-loaded.

Therefore modules unit test with the whole of core installed and enabled and using core's phpunit runner, which is okay, but a questionable practice.

Therefore rather sooner or later modules will start adding PSR-4, I can guarantee that.

(Edit: or in this case use PSR-0 http://cgit.drupalcode.org/monolog/tree/composer.json?id=84cdb36, thanks #1 - it has already started then ...)

And strictly speaking modules using composer _should_ be adding autoload: psr-4 to them.

3. We will definitely and 100% keep the module isolation and separation, but we should also ensure that our 'module world' does not conflict with the 'composer world'.

And this is one area of conflict, where composer has different assumptions, but we support that.

4. And then we have that problem anyway once modules start to specify PSR-4, so I would rather have a module_class_exists() function or service, which maps the namespace to the module if existing and denies it when its not loaded.

Rather than to wait for contrib to "break" this.

e.g. start with the right assumption when 8.0.x ships.

And especially not land in a situation where a module behaves different when installed with composer or not.

e.g. a contrib developer uses a composer based workflow for their local development, the class_exists() is true. Hence forgets to add the dependency.

Someone else downloads the module from drupal.org and wonders why the form cannot be found ...

etc.

Lets discuss some more in IRC today :).

fabianx’s picture

Okay, I did some research on other content management systems.

- CMS Made Simple extensions just push composer libraries with hard-coded vendor directories, the core does not support it at all. You FTP modules and the code base.

- Typo 3 is where it gets interesting and I am pretty sure we will end up with a similar solution as that is the most sane way and exactly what I thought about for the long-run (though I disagree a little with the specific implementation):

https://git.typo3.org/Packages/TYPO3.CMS.git/blob/refs/heads/master:/typ...

They parse the autoloading information of the composer.json for those extensions not covered by composer and register them in the same way.

So far as I can see they also have essentially a 'all-extensions-are-autoloaded' way in that.

Also adding the psr-4 information in here, makes a lot of sense:

- https://git.typo3.org/Packages/TYPO3.CMS.git/blob/refs/heads/master:/com...

and then again in each module: https://git.typo3.org/Packages/TYPO3.CMS.git/blob/refs/heads/master:/typ...

From their press announcement:

This has now been streamlined and the “old” class loading functionality was migrated and unified to be using composer, which is only triggered on installation of an extension and is cached separately.

(http://typo3.org/news/article/announcing-typo3-cms-73-more-stability-mor...)

Need to check some more other CMS, to see how they solved it.

fabianx’s picture

Title: Ensure all modules are autoloaded with PSR-4 regardless if enabled or not » Ensure all modules are autoloaded with PSR-4 only if enabled
Issue tags: +Needs issue summary update

Discussed in IRC:

Outcome was:

- Automatically loading modules is a bad idea - whether that happens by composer or by core. The contract of Drupal modules is that they are available once enabled, and not before that.

We should ensure that contract is always fulfilled - else chaos via class_exists() can happen.

Plan B is:

- Add a composer hook that removes all autoloading information from modules (type = drupal-module as per composers/installers)
- Add normal autoload: psr-4 to the modules composer.json itself, also do that for all core modules (follow-up), so core sets a good example
- Add 'library' type to drupal.org so that e.g. memcache can provide a library instead of a module and libraries are auto-loaded per definition and cannot be enabled. :)
- Remove the hard-coded addPsr4() from DrupalKernel and instead parse composer.json files and store the autoloading information in the system information / container. / fallback for modules that don't have composer.json to use PSR-4: Drupal\\module_name\\ => src/.

That way:

- We blacklist all autoloading information, but then take care of it while loading the module.
- We ensure a module is never available unless enabled; use a library if you need something auto-loaded.

That should cater to all use-cases and preserves the strong contract that modules have with core, while enabling them to be unit-tested, etc.

catch’s picture

Category: Bug report » Task

Given #6 I think this is a task now.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

mikeryan’s picture

Time to resurrect this issue? #2796953: [regression] Plugins extending from classes of uninstalled modules lead to fatal error during discovery would have been caught much sooner if phpunit only autoloaded classes from enabled modules (see #2797269: Improve MigrationPluginListTest).

mikeryan’s picture

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

A little late for 8.1.x...

fabianx’s picture

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

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.

dpi’s picture

Time has passed, new perspectives and such.

Following on a suggestion and discussion @ #3522410-7: Using instances of classes defined by modules in service container parameters causes fatal errors + #3522410-11: Using instances of classes defined by modules in service container parameters causes fatal errors

What if we did the inverse to what is currently proposed here, back to the original intent of the ticket before direction change in #2536610-6: Ensure all modules are autoloaded with PSR-4 regardless if enabled or not

Always have all modules available in autoloader.

Then, the concept of "installed" means whether a modules hooks/services/discovery/plugins etc etc is loaded.

Maybe there was more context in referenced IRC discussion, 10 years ago.

nicxvan’s picture

Component: base system » extension system
cmlara’s picture

Title: Ensure all modules are autoloaded with PSR-4 only if enabled » Ensure all modules are autoloaded with PSR-4 regardless if enabled or not

Always have all modules available in autoloader.

+1 from me as a contrib developer.

Yes as #6 calls out this will require contrib to do some work with the (hacky) use of class_exists() that has been used to determine if a module was enabled, however we now have a robust system with Drupal Rector that would allow automated updates by converting class_exists('\Drupal\some_module\some_class') to \Drupal::service('module_handler')->moduleExists('some_module') && class_exists('\Drupal\some_module\some_class')

cmlara’s picture

For DX: I have encountered this in a couple scenarios related to implementing interfaces:

Scenario 1:
Decorating a core service that may not always be enabled using autowire:
The decorator has to implement the service interface, however with the core module namespace not resolvable and the design of Symfony PHP will throw a fatal error that the interface was not found. If the interface was resolvable Symfony onInvalid: ContainerInterface::IGNORE_ON_INVALID_REFERENCE should allow for the service to remain autowired yet not registered in the container.

Scenario 2 (I'm working through this with contrib modules right now)
Plugin implementing interfaces from two modules, where one is optional additional features:
Module A exists in contrib and provides a plugin system.
Module B wishes to extend the features of module A, to do this it will provide an additional interface that third party plugins will implement that provides these features.
Module C implements a Module A plugin and also wishes to implement the enhancements of Module B.

In this scenario Module B must be enabled for Module C's plugins to load Module B's interface forcing the site to use Module B even if they do not desire the extra features. As above this would be due to PHP throwing a fatal error for an unknown interface. If Module B's namespace were resolvable it need only be a composer dependency not an install dependency.

Of course there are ways around both of these, such as with use of additional meta packages, custom ServiceProvider alters, not using autowiring, etc, however these push us away from aligning with upstream support and the general PHP ecosystem.

catch’s picture

Scenario #2 from #30 should be solved by providing two plugin classes, the second one implementing the interface from module C. If you wanted this to be automatically used by existing config specifying the first plugin, then the class could be swapped out by a plugin info alter.

Having a composer dependency on a module that may or may not be installed can introduce problems if module C isn't compatible with newer Drupal core versions - the site won't be able to update, and it also won't be able to composer remove because that would lead to the fatal error again.

cmlara’s picture

Having a composer dependency on a module that may or may not be installed can introduce problems if module C isn't compatible with newer Drupal core versions -

For clarity, I believe you mean Module B.

That really is no different than having a dependency on module that can be installed that isn't compatible (you can't PMU it if its a Drupal Dependency of Module C) or a dependency on any other composer library that would conflict with Drupal Core.

If you choose to implement a dependency or even install a package on your Drupal site you need to have vetted the developer to be sure they will continue to maintain the software (or be prepared to dump them should it be required).

should be solved by ...

For brevity I did not list all the scenarios that had been considered and failed an initial architectural review.

catch’s picture

If you choose to implement a dependency or even install a package on your Drupal site you need to have vetted the developer to be sure they will continue to maintain the software (or be prepared to dump them should it be required).

No it's not the same, because you're specifically talking about the case of a contrib developer making a deliberate choice to add a hard composer dependency on another contrib module, instead of making that dependency properly optional. If the dependency is genuinely optional, then version compatibility issues are easier to mitigate later. Saying 'it's the site owner's fault if they install a module' is not really applicable in the context of an issue against core specifically to make bad scenarios as described above more likely.

cmlara’s picture

No it's not the same, because you're specifically talking about the case of a contrib developer making a deliberate choice to add a hard composer dependency on another contrib module, instead of making that dependency properly optional.

Welcome to the PHP Language. Interfaces will/must be loaded by the parser to be implemented. This is no different than being dependent upon psr/log for the interfaces it provides.

Loading the namespace into the autolaoder makes the module optional, otherwise (one of) the (possible) solution is that Module C has to make Module B (the module that provides the interface) a hard dependency. Autoloading makes it so as long as the interface is properly formatted PHP it loads (Module B would then have config options to disable its features, but now it sits taking up space and processing time in all Drupal related module functions such as the hook dispatchers, updates checker, etc)

it's the site owner's fault if they install a module

To be clear, the primary fault was directed to the developer who failed to vet their dependencies. The site owner example was thrown in as secondary responsibility.

catch’s picture

Welcome to the PHP Language. Interfaces will/must be loaded by the parser to be implemented. This is no different than being dependent upon psr/log for the interfaces it provides.

The plugin system includes explicit support for discarding plugins that implement missing interfaces during discovery to support use cases like the one above - e.g. modules can ship plugins that will be discovered only when the module the plugin requires is installed. You're pretending as if this doesn't exist and no-one has thought about this before.

Symfony spent a good couple of years factoring out various interfaces it provides into a separate 'contracts' component so that people could use the interface without depending on the entire component, this was explicitly to avoid projects having to add dependencies on entire components to use one interface which they otherwise weren't using.

We had to workaround a PHP bug with missing traits to support attribute discovery in #3502913: Add a fallback classloader that can handle missing traits for attribute discovery, and contributed to it being fixed in PHP 8.5 https://github.com/php/php-src/issues/17959

Patronising comments about how PHP works won't cover a lack of research into the API under discussion.

Loading the namespace into the autolaoder makes the module optional

It doesn't make it optional in the code base you're arguing for the exact opposite here.

catch’s picture

There's also an existing use case now that #30 would break.

Module A implements e.g. a field formatter plugin that should only be discovered when module B is installed, because it relies on features in in module B. Let's say it depends on a formatter base clase or trait that module B provides.

With HEAD, the field formatter that module A provides will only be discovered by the plugin system when module B is installed.

With the change here, it would be discovered whenever module B is a composer dependency. However, the base class or trait may rely on other code in module B other than the trait/base class - so actually trying to use it when module B isn't installed could result in fatal errors or it otherwise not working at all.

It also means module A would behave differently in three ways rather than two - when module B is installed, when it's required by composer but not installed, and when it's not required by composer at all.

There is no proposal here for how to provide backwards compatibility for that case or otherwise how it would be addressed.

cmlara’s picture

it doesn't make it optional in the code base you're arguing for the exact opposite here.

Sorry if there is confusion around this point. I have been trying to say that yes the composer package is not optional in the code base, the Drupal Module is optional from the standpoint of Drupal core operations. (Module disabled, Drupal doesn't care that it is on disk, composer however can still access the libraries/interfaces).

There is no proposal here for how to provide backwards compatibility for that case or otherwise how it would be addressed.

Thank you for providing a technical concern that is rooted in a current logic instead of developer design choice. I don’t have an answer for that scenario at this moment and would need to take time to consider the impact.

I do wonder if this issue been acted upon sooner, or at least been in the roadmap for all new designs, might we have avoided those concerns as part of the attributes conversion. We can't change the past, and just have to push on, just want to annotate how we could possible work on additional coordination during design phases.

We had to workaround a PHP bug with missing traits to support attribute discovery in

Indeed. I believe I was monitoring a number of issues related to annotations conversions including that issue.

interfaces it provides into a separate 'contracts' component

FWIW that’s currently the leading contender for my project at the moment, though option #30-2 would IMO having been through the design evaluation also be significantly valid in this particular case.

Realistically Module B belongs inside Module A’s code base, however when maintainers don't act we look at the options.

You're pretending as if this doesn't exist and no-one has thought about this before.

There is significantly more in my designs evaluations than can be laid out here in short blurbs, especially since the majority is not directly related to this issue. I have noted previously some ideas you have suggested would work technically they failed the DX review which leads to the scenario proposed.

nicxvan’s picture

@cmlara can you share your full design concerns somewhere so we can evaluate?

Whether it affects the likelihood of this issue being resolved I cannot say for now but I think it would be informative to read what seems like a deep analysis as I consider issues in general.

nicxvan’s picture

Status: Active » Postponed (maintainer needs more info)

Postponing for a few reasons:
1. The question in 38
2. How will we handle all the conflicts because this is kind of a bedrock Drupal assumption
3. What edge cases are missing for discovery based on answers in 35

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.

nicxvan’s picture

Status: Postponed (maintainer needs more info) » Closed (works as designed)

I'm going to close this as works as designed for 35, 36 and 38.

If you can share more information we can explore further.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

andypost’s picture

alexpott’s picture

I would love to do this but there are a number of very large challenges which from my perspective are bigger than the plugin system stuff.

  • composer require drupal/extension can bring in sub modules for example, webform ships with loads
  • Install profiles / distributions change where you have to look for modules
  • Test module affect the class loader and discovery as well - based on the $settings['extension_discovery_scan_tests'] = TRUE; setting
  • Multi site also affects the class loader and discovery

Getting around all of these problems is not at simple. In order to do this I think we need to consider deprecating and then removing support for install profiles and mulit-site shipping their own modules as this would make this problem space considerably simpler.

alexpott’s picture

Status: Closed (works as designed) » Active

I'm going change this to active. I think this is worthwhile to pursue but we can descope sites/all and sites/* and install profile modules - ie. still support them with the kernel adding the class loader. The only complexity will be tests but that feels surmoutable.

alexpott’s picture

alexpott’s picture

I've done quite a bit of thinking about this and how we might tackle it. Here's where I'm at:

  1. We can tackle the plugin dependency issue by ensuring all the classes in the inherited change are for installed modules.
  2. We need to deprecate modules in profiles and sites/all and sites/SITE_PATH and remove it before because composer can not know all this and build portable autoload files. I thought maybe we could do this later and just sort out autoloading for core extensions and extensions managed by composer BUT we have a problem. The way ExtensionDiscovery deals with precedence - ie. if you have a node module in sites/SITE_NAME and that takes precedence over the core module. I know people have used that in the past to do interesting and wrong things. But this same capability allows you to move a site to a newer version of a module than than one in modules/ so that seems like a use-case that someone might depend on. One potential way to overcome this is to change \Drupal\Core\DrupalKernel::updateModules() to prepend there autoloader but only with paths that composer cannot handle.
  3. Test modules and sub module discovery complicates all of this.