Problem/Motivation
Not all installed extensions, and their respective namespaces, are properly registered within the container.
This issue is, primarily, relevant to themes and theme engines as modules and profiles are currently hardcoded.
Certain requests only bootstrap so far. If a theme provides a static callback (in the form of #pre_render, #ajax, or #lazy_builder) to a method in one of its classes, this lives in a render array which can become cached. Because of this caching, the entire theme system does not necessarily need to be loaded just to execute a callback. Thus, the namespaces are never registered and the classes cannot be found.
It is a common misnomer that extension namespaces are registered on install.
Currently, the ThemeExtensionList is what instantiates and registers all installed theme namespaces.
DrupalKernel::compileContainer automatically registers module namespaces, simply because they can provide services (since themes cannot). However, these namespaces are done after the service providers have been determined and effectively cut all other installed extension types out of being properly registered.
There are also no significant tests to ensure that these namespaces are registered properly and whether or not they would in fact break contrib.
Proposed resolution
Register all installed extensions and their namespaces as separate container parameters:
container.namespaces.core- Drupal specific['Core', 'Component']namespaces.container.namespaces.modulecontainer.namespaces.profilecontainer.namespaces.themecontainer.namespaces.theme_enginecontainer.namespaces.all- all of the above namespaces
For BC reasons, the existing container.namespaces parameter/service needs to be aliased to the following namespaces:
container.namespaces.corecontainer.namespaces.modulecontainer.namespaces.profile
And then deprecate the use of container.namespaces when #2954562: [PP-2] Create provider based plugin managers makes it way in to allow plugin managers to choose which namespaces to allow.
Remaining tasks
- Create tests
- Create a patch
User interface changes
None
API changes
- Adds new service:
installed_extensions- Will be used in #3023131: [PP-1] Extension System, Part IV: ExtensionHandler and ExtensionHandlerInterface
- Adds new parameters:
container.namespaces.core- Drupal specific['Core', 'Component']namespaces.container.namespaces.modulecontainer.namespaces.profilecontainer.namespaces.themecontainer.namespaces.theme_enginecontainer.namespaces.all- all of the above namespaces
Data model changes
None
| Comment | File | Size | Author |
|---|---|---|---|
| #75 | 2941757_75.patch | 7.11 KB | mile23 |
| #63 | 2941757-63.patch | 89.94 KB | markhalliwell |
| #63 | interdiff-2941757-61-63.txt | 599 bytes | markhalliwell |
| #61 | 2941757-61-tests-only.patch | 6.59 KB | markhalliwell |
Issue fork drupal-2941757
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
Comment #2
markhalliwellComment #3
almaudoh commentedIt might be better if #2659940: Extension System, Part III: ThemeExtensionList and ThemeEngineExtensionList is committed first so we would have a clean
ThemeExtensionListAPI to use in fixing this issue.Comment #4
markhalliwellSure, that makes sense.
Comment #5
markhalliwellHopefully #2659940: Extension System, Part III: ThemeExtensionList and ThemeEngineExtensionList will make its way in soon and then I'll start tackling this.
Comment #6
markhalliwellComment #7
markhalliwellComment #8
markhalliwellComment #9
markhalliwellComment #10
markhalliwellComment #11
markhalliwellComment #12
markhalliwellComment #13
dawehnerWell, one complexity we always need to keep in mind is the following. Depending on the current request you can have different kind of active themes. Given that we cannot just make the namespaces available on boot-time. Code might do things conditionally whether a specific theme is loaded or not.
This point is also the reason why themes shouldn't have a
.services.ymlfileI'm curios, what blocks us from expanding
\Drupal\Core\Theme\ThemeInitialization::loadActiveThemeto register the theme and all its parents namespaces?Comment #14
markhalliwellCorrect, but this is becoming less and less of an “issue” due to the OO advancements we’re implementing.
Yes we can.
This issue isn’t about actually loading any classes. That is well out of scope. It’s about just ensuring that their namespaces are registered in the autoloader so, when they are needed, they can be found.
No, the reason themes shouldn’t have services is because then they’d just be modules (and because themes weren’t installable before).
Because, as the IS states, this is too late in the process and, quite frankly, overkill. We shouldn’t have to “boot up the entire theme system” just so namespaces can be registered.
The main reason this needs to be done in the kernel is because certain requests only bootstrap so far. If a theme provides a static #pre_render, #ajax, or #lazy_builder callback to a method in one of its classes, this lives in a render array which can become cached. Because of this caching, the entire theme system does not neccessarily need to be loaded just to execute a callback. Thus, the namespaces are never registered and the classes cannot be found.
Comment #15
dawehnerPlease put that into the issue summary so everyone can quickly see the problem.
Comment #16
markhalliwellComment #17
markhalliwellPretty sure it is implied, but I suppose it needs to be stated.
Comment #19
andypostUnblocked
Comment #20
mile23OK, so based on #14, the problem is that we have a cached render array, and it has callbacks to class methods in the theme extension that are not available because they are not autoloadable.
So we have to somehow autoload the theme(s) to which the cache belongs so that it can render to HTML, but we also want to remain performant by not bootstrapping resources we don't need for this to occur.
Since the proposed resolution suggests making a service called
container.namespaces.theme, let's just assume that we could somehow enable autoloading for all enabled themes early in the boot process. That process entails scanning for extensions and then callingaddPsr4()on the autoloader.In that case, we don't need those services. We need to discover the themes at some layer similar to
DrupalKernel::handle()just afterintializeSettings(). This lets the autoloader find classes that get used by the theme. I'm not sure ifExtensionDiscoveryis suitable for this, because it might need some services. But assuming it is suitable, since we know where the src/ directory is within extensions, we can then just tell the autoloader per theme and then we're done.We could also do this for all enabled extensions, since we're touching the filesystem already.
In this case the issue should rescope to: Register autoloading for extensions early in boot process.
Note that if it were our policy to require the use of Composer to add extensions, this issue would not exist. The autoload mapping would always be present in autoload.php, because we'd register it so early in the boot process it happened during deploy. :-)
Comment #21
markhalliwellNo. This issue isn't about bootstrapping/loading anything, let alone the theme system (that's way out of scope).
That's what this issue already is: "Properly register installed extension namespaces in container"
Comment #22
markhalliwellCleaned up IS to remove verbose project specific info.
Comment #23
markhalliwellNeed to look at #2021959-33: Refactor module handling responsibilities out of DrupalKernel to see what can be salvaged from that issue/patch (similar goal to this issue).
Comment #24
markhalliwellThis is a catch-22.
To get
ExtensionHandlerworking requires installed extensions registered with the container, to get registered extensions requires ExtensionList/ExtensionHandler.Both this issue and #3023131: [PP-1] Extension System, Part IV: ExtensionHandler and ExtensionHandlerInterface must happen at the same time.
Comment #25
markhalliwellComment #26
markhalliwellTurns out we can do this is steps after all.
Comment #27
markhalliwellOk... an initial stab at this.
Doesn't include any new tests yet... but I think it should pass all existing tests (had to modify a couple existing ones slightly).
Comment #29
markhalliwellGuess the installed extensions instance really needs to be added in the constructor. Had some worries about the class loader not yet being initialized (properly), but it seems to be working ok.
Comment #31
markhalliwellOk... let's try this.
Comment #32
markhalliwellComment #34
markhalliwellComment #36
markhalliwellSigh...
Comment #38
markhalliwellAlmost there (I think)...
Comment #40
markhalliwellComment #41
markhalliwellOk, I think this is finally it. At least it should fix the majority of all the "major" test failures. There may still be a few left.
Comment #44
markhalliwellAwesome! There's only a few left. Maybe someone else wants to take a look and see what I may have missed.
Comment #45
markhalliwellComment #47
markhalliwellHad to do a lot of local debugging to sort through some of this mess. Just FYI, the kernel/extensions are extremely fragile... hence why this is a great first step. Ultimately I think this should just be a wrapper around ExtensionList instances, but we're not there yet.
Anyway, I'm hoping this fixes the remaining tests... 🤞
Comment #49
andypostNice step ahead! some nits from skiming
if this API going to be public makes sense to change arguments and describe them
would be great to get rid of it
Comment #50
markhalliwell#49.1: This method already exists and is public. I really don't like this method and want to deprecate it and replace it with something better, but I think that may be out of scope for now. My goal is to simply get this working.
#49.2: Same as above, this already existed. I'm not entirely sure this is the most appropriate issue to tackle removing this.
---
On a side note: I've been doing even more local testing to try and figure out what's going on with the last remaining failures.
The best I can guess is that somehow normal procedural functions/constants (that aren't namespaced) can't be found.
In fact, I did a little searching and found #1912474: Unable to pre-populate db in settings.php.
That issue describes almost verbatim the same error messages I'm running into here now.
First I get:
Found #3015752: Properly deprecate USERNAME_MAX_LENGTH and USER_REGISTER_* constants, but then I run into:
That issue was closed as a dup of #2155701: Installer starts with fatal error/exception "table 'semaphore' not found" if settings.php contains $databases already, but I fail to see what exactly it did to rectify the warnings.
Could use some help here if someone is more familiar with
\Drupal\Core\Installer\InstallerKerneland\Drupal\Core\Test\TestRunnerKernel.---
Attaching patch of where I left off of solving a minor bug with cross profile modules and duplicate namespace registration.
Comment #52
markhalliwellIt seems any pre-emptive extension setting must happen in
initializeContainer.I still cannot, for the life of me, figure out the errors in #50.
Any direction would be helpful.
---
edit: this includes a change/addition of
InstallerKernel::isConfigEmptysince it really shouldn't be changing the scope ofgetConfigStorageto public when it's just used in one place.Comment #54
markhalliwellOk, I think I may actually have figured out the issues; at least I was able to get \Drupal\FunctionalTests\Installer\StandardInstallerTest::testInstaller passing locally anyway.
I think this was the main issue. The original code intersected
$this->module_list, which is the currently set "install" list... not what is stored in config.I also moved said "initial list" from config back to
DrupalKernel, which I think had another small part in the failures.I'll get started on the new tests if/when this passes the existing ones (with some minor modifications).
🤞
Comment #56
markhalliwellAwesome! This patch should now pass 😁
Comment #58
markhalliwellI really hate run-tests.sh...
Comment #59
markhalliwellw00t!!!! 🎉
Comment #60
markhalliwellHere are the initial tests. Still needs a bit more work so I'll reassign this issue to myself as I'll likely be working on them this week.
Comment #61
markhalliwellOk, this should complete the necessary tests. Patch is officially "done", barring review(s).
Comment #63
markhalliwellUgh, c&p fail...
Comment #65
dawehnerMay I ask a question which might be obvious to answer. Reading up the issue summary it seems to have an assumption that themes need to have complex logic, written in an OOP style.
I wonder though why themes can't use a corresponding module instead and put the logic in there.
Themes are this weird system as they are loaded dynamically. Putting them into the container might create the assumption they should be autoloadable at any given point in time, but as you know, that's not true. If they aren't the active theme, they cannot be loaded.
I'm confused about this. Isn't that the same as the Extension list conceptually?
Comment #66
markhalliwellI don't really see how describing a legitimate bug is a generalization/assumption for "all themes". In fact, the IS describes a very advanced use case where callbacks are cached in a render array. Advanced or not, it's still an issue.
Given that the overall goal is to move everything to OO, themes should be no exception.
Themes don't "need to have" complex logic, but when they do (especially when dealing with an external framework like Bootstrap)... the system should be able to handle it.
#474684: Allow themes to declare dependencies on modules
I'm still baffled by this way of thinking though. If this were the case, why have themes at all? Why couldn't we just put everything in a module?
The average [sub-]theme will likely not be as complex, but base themes like Drupal Bootstrap are.
Also, what type of "logic" we're referring to here? Complex or not, presentation logic should still live within a theme, not a module. This presentation logic can still be cached in a render array referencing a theme class callback.
Ultimately, I'd like to see #2869859: [PP-1] Refactor theme hooks/registry into plugin managers make its way in which would allow [base-]themes the ability to properly intercept the entire discovery (registry) and rendering process without the need of any "additional module" or antiquated theme special "hook/alter".
Just because things have been the way they have been for so long doesn't mean it has to stay that way.
That's an extremely heavy burden to put on the kernel.
Does the kernel know which blocks should be added to a page?
No. This kind of logic should be left to whatever mechanism is in place that is responsible for determining this kind of logic.
We don't know when or where a theme may be used (at this step), but we do know that it is installed.
Installed extension namespaces should be registered with the autoloader, plain and simple.
It's up to other subsystems (i.e. elements, render api, render cache, theme registry, etc.) to ensure that if their logic depends on which "active" theme that they build necessary caches based on said "active" theme; which they already do and we have tests for.
Yes/no.
I perhaps should have explained a little more in #24 and #26.
This code is literally just putting what was already in
DrupalKernelinto a separate service/class.Currently
ExtensionListhas a dependency onModuleHandler. There are also a lot of other servicesExtensionListcurrently relies on.I originally went down the path of "let's just use
ExtensionList", but given the required dependencies and circular references it would create, I decided to create a separate encapsulating service/class was a needed first step in this process.That is why I marked
InstalledExtensionsas@internal. Ultimately, I don't see this class sticking around. We'll probably remove it in #3023131: [PP-1] Extension System, Part IV: ExtensionHandler and ExtensionHandlerInterface or if we don't clean up theExtensionListduring that issue, an issue shortly after that.I didn't want to create a massive patch that no one could review. That's why I created
InstalledExtensionsas a way to allow us to make this change and introduce tests without breaking all of core (which you can see above, is very easy to do at this level).After speaking with @alexpott in slack, I think that ultimately this entire process in the kernel will become a
ServiceProviderdecoration of the container and we can just let the variousExtensionListservices handle it.Until we decouple
ExtensionListfromModuleHandlerthough, this step is needed.Comment #67
markhalliwellThis was already registering all installed themes; not just the "active" theme. However, this is too late in the process and requires the theme system to be initialized which may not happen during a slimmed down bootstrap (hence this issue).
Comment #68
markhalliwellBump...
Comment #70
psf_ commentedI applied the #63 patch trying to solve the issue https://www.drupal.org/project/jsonapi_extras/issues/3045087 but it breaks all the site, before that the issue #3045087 breaks only with clear cache and a few more actions.
The issue #3045087 is that the class FieldItemNormalizerImpostor it's not found in some installations. That class live in jsonapi_extras module but have jsonapi namespace. I have two cloned servers, in one work and in the other not, I think that maybe for different update process steps.
Comment #71
psf_ commentedComment #72
markhalliwellThat issue is a byproduct of that module doing some weird namespacing hackery (that doesn't follow PSR-4 btw):
https://git.drupalcode.org/project/jsonapi_extras/blob/8.x-3.x/src/Jsona...
It doesn't really have anything to do with this issue.
Comment #73
markhalliwellComment #74
markhalliwellBump... again
Comment #75
mile23Bump duly noted. :-)
It occurs to me that this discovery stuff is similar to what’s needed for #2242947: Integrate Symfony Console component to natively support command line operations, and so let’s look at it here, too.
Reply to @dawehner in #66:
OK, so the goal is to autoload extensions that are enabled, not autoload extensions that are not enabled, and do it all in one pass so that the autoloader can find anything that would be needed during rendering without special-case loading for themes etc.
This means we need a bunch of services, such as all the dependencies of config (namely a backend, probably a database).
So since all of these dependencies are services, we can’t add the extension namespaces to the class loader until after the container has been compiled. We don’t want to instantiate any services until after that phase. So, you know… never mind what I said in #20. :-)
We already have a
class_loadersynthetic service. This service is used throughout core, and is just the classloader object. This would be a great place to implement our own superclass ofClassLoader, which knows how to look for extension namespaces. However, we don’t have a way to generate that. Composer makes this class loader for us, and we’re supposed to add prefixes and that's it.So we can make a factory service. This service consumes all the services necessary to find autoload info within extensions, and then adds that info to the existing classloader. Let’s call it
class_loader.factory.The next question is: How do we inject the classloader into the factory object? In this regard, we must look at
CoreServiceProvider. We must inject the classloader intoCoreServiceProvider, which then injects it into a compile pass object.The compile pass object
ExtensionNamespaceCompilerPasscan then define theclass_loader.factoryservice, using the injected classloader object, as well as whatever other service references we need in order to add the extension autoloading info.This factory is the
ExtensionNamespaceClassLoaderFactoryclass.We change the
class_loaderservice so that it is not synthetic, and instead generated by our factory.Then, finally, when some code somewhere dereferences the
class_loaderservice, this factory object and the associated services will be used to add all the PSR-4 or whatever from all the extensions.I experimented with the container configurator pattern and for that you still need an existing service to be configured. In our case, that means doing all the same stuff to make sure the class loader object is injected into the service that’s created, so it would be essentially this patch plus a configurator object.
This patch is a proof-of-concept that puts all this class loading infrastructure in place, and should behave normally. The only cheating it does is to force
DrupalKernel::allowDumpingto always beFALSE. This makes for a longer run time, but if the concept is sound and there is some consensus, we can begin to work on teasing out the usages that break this. It’s probably something simple that I managed to overlook.This patch does not solve the problem of loading the extensions, but you can see where you should put that code in
ExtensionNamespaceClassLoaderFactory.Different approach, so no interdiff.
Comment #76
mile23PHP Fatal error: Uncaught TypeError: Argument 1 passed to Drupal\Core\CoreServiceProvider::__construct() must be an instance of Composer\Autoload\ClassLoader, instance of Symfony\Component\ClassLoader\ApcClassLoader given, called in /var/www/html/core/lib/Drupal/Core/DrupalKernel.php on line 619 and defined in /var/www/html/core/lib/Drupal/Core/CoreServiceProvider.php:54Yah, why can't there be an interface?
Comment #77
markhalliwellWhile I understand the end goal/desire of #75, wouldn't it make more sense to create a follow-up to do that?
If this issue is any indication (#27-#63), this is a complicated area and doing this in steps would probably be best.
Comment #79
daffie commentedComment #80
daffie commentedThe patch does not need a reroll. It just needs work. :)
Comment #88
catchComment #89
andypostComment #90
dpiCreated a autoloading-related issue for ensuring classes are added to the autoloader before container cache is unserialized @ #3522410: Using instances of classes defined by modules in service container parameters causes fatal errors
Comment #92
nicxvan commentedI deleted my comment it was meant for a different issue.
Comment #93
nicxvan commentedI think I want to work on the theme namespaces.
Comment #94
nicxvan commentedWe now register theme namespaces on container build which happens during theme install.
I'm not clear why we wanted them in different container parameters in this issue.
Postponing for an update with the new information if this is still relevant.