Problem/Motivation

We already inject Composer's autoloader into DrupalKernel, yet drupal_classloader() creates its own classloader with a hardcoded reference to Drupal's core/vendor/autolod.php. Therefore it is not possible to use a different autoloader simply by passing a different one into DrupalKernel.

Proposed resolution

Always use DrupalKernel's autoloader. For this purpose a getClassLoader() method is introduced on DrupalKernelInterface. This is consumed by drupal_classloader_register(). Alternatively, drupal_classloader_register() could be replaced directly by a new method on the kernel or the already existing classloaderAddMultiplePsr4(). In order to still support ApcClassLoader for PHP 5.3 a setClassLoader() method is introduced. This is (optionally) being called from index.php directly now.

With these changes Drupal can be run with a completely custom classloader (as long as that classloader contains the right classes, obviously).

Remaining tasks

User interface changes

API changes

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tstoeckler’s picture

Status: Active » Needs review
Issue tags: +Needs manual testing, +PHP 5.3
FileSize
13.56 KB

So I can install and run Drupal fine with this. I didn't try running tests, so let's see. I also checked that the ApcClassLoader stuff works, but I don't have PHP 5.3, so I could only verify that the correct classloader gets instantiated (it then fataled due to the missing apc extension). So someone with PHP 5.3 would need to try this patch and uncomment the corresponding lines in index.php.

dawehner’s picture

Added a related issue, which uses a different approach.

larowlan’s picture

We don't support 5.3 for drupal 8, fwiw

tstoeckler’s picture

Issue tags: -PHP 5.3

Oh right. Epic brainfail on my part :-)

So I really don't know why we ship with support for ApcClassLoader anymore, as that is pretty much PHP 5.3 only. I found a StackOverflow ticket which describes that it can be installed on PHP 5.4, but that seems rather complicated and not like something we need to support out of the box. And I couldn't get it to install at all on PHP 5.5.

Will open a follow-up.

Regarding #2023325: Add interface for classloader so it can be cleanly swapped: Thanks for the reference. I had seen that issue at some point, but forgotten about it. I still think this makes sense, though. As can be seen in this patch a ClassLoaderInterface would be quite nice to have regardless.

tstoeckler’s picture

So I misread http://stackoverflow.com/questions/9611676/is-apc-compatible-with-php-5-...

Apparently apc does work well on PHP 5.4. Still, there's not much point in using it (IMO) due to opcache being included in PHP 5.5. Thus, opened #2325305: Remove support for ApcClassLoader nonetheless.

Thoughts on the approach here, anyone? :-)

sun’s picture

Status: Needs review » Needs work
  1. +++ b/core/includes/bootstrap.inc
    @@ -1399,50 +1400,6 @@ function _current_path($path = NULL) {
    -function drupal_classloader($class_loader = NULL) {
    ...
    -  static $loader;
    

    Removing this static is welcome.

    At the same time, this construct did ensure that the class loader is only constructed once and not reconstructed from scratch more than once in a single process/request.

    However, given that this patch touches all instances, it looks like that situation did only occur in the setUp() of test base classes.

    Theoretically, constructing a new class loader for each test (method) is appropriate — however, a class loader is a native global state construct of PHP core.

    We need to ensure that (1) we're not stacking a dozen of class loaders on top of each other, and (2) due to how Simpletest works, we theoretically need to restore the original class loader (of the parent site/test runner) after tearing down a test when restoring the original environment, since the test might have manipulated the class loader.

  2. +++ b/core/includes/install.core.inc
    @@ -358,7 +358,8 @@ function install_begin_request(&$install_state) {
    +  $classloader = require __DIR__ . '/../vendor/autoload.php';
    

    This appears to be the only instance outside of tests that reconstructs a new classloader in the same process.

    We need to assign and inject the $loader from /core/install.php into install_drupal() and pass it forward into install_begin_request().

    Don't worry, we just need to change the signature of these two functions. I'd even suggest to inject it as the first argument, shifting the $settings / $install_state arguments by one position.

  3. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -410,6 +405,21 @@ public function getContainer() {
    +  public function getClassloader() {
    ...
    +  public function setClassloader($classloader) {
    

    These two methods are not necessary and should be removed.

    The class loader is exposed as synthetic service 'class_loader' on the service container; see DrupalKernel::attachSynthetic().

  4. +++ b/index.php
    @@ -18,6 +19,20 @@
    +  // $apc_loader = new ApcClassLoader('drupal.' . Settings::getHashSalt(), $autoloader);
    

    There's not really a reason for why we cannot do this from within DrupalKernel, instead of having to hack index.php.

    Ideally as a separate method, so that alternative kernels are able to override it.

    Alternatively, we could change the workflow and move this code directly into settings.php (commented out by default). To do so, we just need to forward the $classloader from DrupalKernel into Settings::initialize(). Just passing the argument is sufficient; the variable will be available in the local scope of settings.php.

    I'd be in favor of the latter approach, because it removes the indirection of the current settings variables, doesn't require to hack any front controllers, and allows you to use a different classloader/decorator.

tstoeckler’s picture

Thanks for the review!

Re 1. Composer itself keeps a static cache of it's loader. See ComposerAutoloaderInitDrupal8::getLoader(). . I'm not sure if the added require's themselves are a performance penalty, but that's only without opcache enabled, unless I'm missing something. So I'd say, that's OK ?!

Re 2. Yes, I had thought about that as well, but decided to pursue that in a follow-up, as it's going to break Drush. I'm not sure if it'll break the testbot, as they install Drupal through the UI IIRC, but yeah. Will do this now, though. I guess there's really not much difference whether we break Drush now or later.

Re 3. Thanks, that's a good tip! I had seen the 'class_loader' service but somehow didn't make the connection. Sounds great, I wasn't very pleased with those methods either.

Re 4. I like passing the classloader into settings.php will do that. Using settings.php is more inline with how we do things currently. I personally am of the opinion that people should own their front controllers and, thus, to me that's not hacking but as it stands, we do have index.php in the repo, so my point is pretty much moot sadly.

sun’s picture

re: 1) Interesting. Sorry, in that case, 2) is moot. And 1) can be ignored, since the behavior is effectively the same; the object is just retrieved from a different static. The additional requires are fine and not measurable (only a conditional file inclusion of a possibly non-existing file would present an issue).

Crell’s picture

+++ b/index.php
@@ -18,6 +19,20 @@
+  // By default, use the ClassLoader which is best for development, as it does
+  // not break when code is moved on the file system. However, as it is slow,
+  // the APC class loader can be used in production. To use the APC class loader
+  // simply uncomment the lines below. Note that ApcClassLoader decorates
+  // ClassLoader and only provides the findFile() method, but none of the others.
+  // The actual registry is still in ClassLoader.
+
+  // require_once __DIR__ . '/core/vendor/symfony/class-loader/Symfony/Component/ClassLoader/ApcClassLoader.php';
+  // $apc_loader = new ApcClassLoader('drupal.' . Settings::getHashSalt(), $autoloader);
+  // $autoloader->unregister();
+  // $apc_loader->register();
+  // $kernel->setClassloader($apc_loader);

Unlike Symfony, Drupal generally views index.php as its own file, not a particular app instance's file. That is, this is technically telling people to hack core to enable to APC class loader.

That said, I'm not adverse to it. I think in the circumstances it's probably for the best, but we should be conscious of what we're doing here.

I haven't dug into the rest of the implementation details so I won't comment on them.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
12.22 KB
18.35 KB

Here we go.

Fixed 2. from #6. I only know realize that you retrospectively declared that point moot (I previously thought you meant not doing something as it would break Drush would be moot). Do you want me to revert that part? I don't really care to be honest. I do think the change makes sense, but I don't care whether it's part of this issue, and also install.core.inc is all dusty and old anyway, so even if we don't end up changing it I don't really care.

Fixed 3. + 4.
4. should also fix the concern in #9 (although as stated in #7 I agree that having to "hack" index.php is not a bad thing, at least not inherently).

As you can see from the patch I couldn't get around leaving DrupalKernel::setClassLoader() in place, that would require a little bit more refactoring. Basically we would have to move the settings initialization out of createFromRequest() into an instance method. That makes sense in its own right, IMO, but again, not sure it should be this issue. If it is requested I will go for it, though.

Also would love to hear what you think about the settings.php solution. It's a little bit more work than the $class_loader = 'apc' we had before, but it's a lot more flexible, and I don't think it's that bad.

Let's see if this is still green.

Status: Needs review » Needs work

The last submitted patch, 10: 2325197-10-remove-drupal-classloader.patch, failed testing.

sun’s picture

leaving DrupalKernel::setClassLoader() in place

+++ b/core/lib/Drupal/Core/DrupalKernel.php
+    Settings::initialize($site_path, $class_loader);
+    $kernel->setClassLoader($class_loader);

Ah. Oh. Interesting challenge. ;)

That said, your patch seemingly implements the possible solution of making Settings::initialize() accept &$class_loader by reference, so the custom code in settings.php is able to replace the referenced variable itself (instead of the object reference) already.

That looks fully correct to me. What else needs to be refactored to fully propagate the variable? Can't be much, so I think we should.


what [do] you think about the settings.php solution. It's a little bit more work than the $class_loader = 'apc' we had before, but it's a lot more flexible, and I don't think it's that bad.

That was exactly my train of thought as well. The architectural concept of a string identifier indirection is artificially limited to a hard-coded set of string identifiers understood by Drupal core. By moving the code directly into settings.php, people can do whatever they want.

However, can we move the example code from within the PHPDoc comment into "more actual" PHP code that is commented out via # or /* ... */, please?


re: install.core.inc changes: Let's revert those for now, as they are not necessary.

Indeed, the initial installer invocation + front controller is pretty much one of the last major puzzle pieces of #1530756: [meta] Use a proper kernel for the installer

tstoeckler’s picture

Status: Needs work » Needs review
Issue tags: -Needs manual testing
FileSize
4.91 KB
8.62 KB
15.92 KB

OK, reverted the installer changes.

Removing setClassLoader() as is was not possible because even though $class_loader is being replaced fully (due to the reference), $kernel->classLoader is not. I found a way to fix this which is only a very small refactoring of DrupalKernel. Let me hear what you think.

Also removed all traces of XcacheClassLoader as that is currently incompatible with Drupal, as I found out during testing. See https://github.com/symfony/ClassLoader/pull/6

Also fixed the comment syntax in default.settings.php, and some other minor issues.

Since I apparently have you at hand here :-), @sun, do you have any thoughts on testing this? I didn't investigate this yet, but perhaps you have some upfront thoughts?

Edit: Sometimes I fail at life. Disregard the pull request above. Will submit a proper one and reference it here.

tstoeckler’s picture

I opened an issue for now: https://github.com/symfony/symfony/issues/11733 . Not sure whether this is considered a feature request or a bug report and the Symfony contributing notes are a bit intimidating in that regard.

tstoeckler’s picture

FileSize
2.21 KB
17.08 KB

Yeah, so the typehint was fairly pointless and also proof that our apc class-loading is completely borked currently.

dawehner’s picture

Using settings seems to be good enough approach. Still actual sites could still better control their front controller, but including this one
exception for logic inside settings.php feels okay.

  1. +++ b/core/modules/system/src/Tests/DrupalKernel/DrupalKernelTest.php
    @@ -59,7 +52,8 @@ protected function setUp() {
    +    $class_loader = require __DIR__ . '/../../../../../vendor/autoload.php';
    +    $kernel = DrupalKernel::createFromRequest($request, $class_loader, 'testing');
    
    +++ b/core/modules/system/src/Tests/System/IgnoreReplicaSubscriberTest.php
    @@ -32,7 +32,8 @@ function testSystemInitIgnoresSecondaries() {
    +    $class_loader = require __DIR__ . '/../../../../../vendor/autoload.php';
    

    Did we considered to just use DRUPAL_ROOT instead?

  2. +++ b/sites/default/default.settings.php
    @@ -377,11 +377,26 @@
    +
    +// Do not do anything if no hash salt is set.
    +if ($settings['hash_salt']) {
    +  // Wrap $class_loader with Symfony's APC class loader.
    +  $apc_loader = new ApcClassLoader('drupal.' . $settings['hash_salt'], $class_loader);
    +  $class_loader->unregister();
    +  $apc_loader->register();
    +  // Replace the $class_loader variable.
    +  $class_loader = $apc_loader;
    +}
    

    Just ensure that we don't accidentally enable APC support by default.

    Now that the hash_salt could be manually set here, we could get rid of having it as part of settings but rather be usual configuration.
    Instead use something like:

    // Change this to a site specific value, in case you have multiple sites running on the webserver.
    $apc_hash = 'apc_hash';
    $apc_loader = ...
    
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -220,20 +217,38 @@ public static function createFromRequest(Request $request, ClassLoader $class_lo
+  public function initializeSettings(Request $request) {

Is there any reason to make this methdo public? It seems rather pointless given that you would need createFromRequest anyway. At least there is no other external call to this function.

sun’s picture

any thoughts on testing this?

The initial bootstrap can only be tested in a PHPUnit test that runs in a separate and cleanly isolated process, and which uses the virtual site environment infrastructure for booting a kernel from #2304461: KernelTestBaseTNG™ — otherwise, all affected classes are loaded already and the class loader isn't triggered.


  1. +  public function initializeSettings(Request $request) {
    +    // This should not be called multiple times.
    +    if (isset($this->sitePath)) {
    +      throw new \BadMethodCallException('Do not initialize settings multiple times.');
    +    }
    

    I agree with @dawehner, let's make this method protected.

    Also, the new exception is a behavior change, doesn't appear to be relevant or related to this issue.

  2. +++ b/core/modules/system/src/Tests/DrupalKernel/DrupalKernelTest.php
    +    $class_loader = require __DIR__ . '/../../../../../vendor/autoload.php';
    

    In the affected test classes, we can use DRUPAL_ROOT. In all other non-test code that isn't deeply nested, I'd prefer to keep the relative include as in the patch.

tstoeckler’s picture

A. Unless people feel strongly, I would like to avoid DRUPAL_ROOT whereever possible. There a bunch of cases in core where we can't avoid it currently, but here it's easy enough, and just to avoid a couple '..' (which are admittedly not that nice) is not worth it IMO. I think we should rather look into avoiding so many of those calls and if possible centralizing them to a certain place. Again, if you insist min($dawehner, $sun) > $tstoeckler :-), no hard feelings.

B. I like not using settings for the APC prefix. I dug back and the usage of the hash salt dates all the way back to #1178246-34: Add Symfony2 HttpFoundation library to core, sadly without any explanation why this specific value was chosen. I don't see any reason why this should be any more random than a site-specific value, as you mention. Good idea!

C. Since the method is called "from the outside" I was pretty sure it was not possible to make initializeSettings() protected, but it in fact is. Either I'm weird, or PHP is weird... That also makes the Exception pointless, so removed that as well.

Will provide updated patch probably on Monday, though :-/

sun’s picture

Happy to help out.

I'd recommend to move the 'hash_salt' (APCu prefix) discussion into a separate issue, since that is an unrelated change in behavior, and needs more discussion, because we still need a unique value, so as to not clash with other Drupal installations that may run on the same machine/process/memory.

tstoeckler’s picture

Awesome, thanks a lot. Two minor fix-ups:

  1. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -182,15 +182,17 @@
    +   * @param \Composer\Autoload\ClassLoader $class_loader
    

    Let's drop the typehint from the docs, as it's not correct and it's also not in the code (for that reason).

  2. +++ b/sites/default/default.settings.php
    @@ -371,32 +371,23 @@
    + * it does not break when code is moved in the file system. You can wrap the
    

    Since you replaced "wrap" with "decorate" above, let's also do that here.

RTBC anyone?

dawehner’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +WSCCI
+++ b/core/includes/bootstrap.inc
@@ -1451,7 +1408,7 @@ function drupal_classloader($class_loader = NULL) {
 function drupal_classloader_register($name, $path) {
-  $loader = drupal_classloader();
+  $loader = \Drupal::service('class_loader');
   $loader->addPsr4('Drupal\\' . $name . '\\', DRUPAL_ROOT . '/' . $path . '/src');
 }

drupal_classloader_register is indeed just used in system_list() which requires a properly booted classloader already.

tstoeckler’s picture

Awesome, thanks!

Added two draft change notices.

webchick’s picture

I read the issue + the patch, but despite the thorough comments this is totally going to be a "close my eyes and commit it" type patch. ;) Since tests are passing I'm assuming either a) things aren't horribly broken, or b) since this touches the very guts of Drupal if anything IS broken it'll get found out pretty quick. ;)

I do have one concern before doing that, though, which is that catch was the creator of #2023325: Add interface for classloader so it can be cleanly swapped and since these two issues overlap (and since he's had a lot of very strong opinions about class loading in general), ideally he would be the one to review this. The problem with that is he's not back until next week, which would mean this sits here for a few days.

Tobias, since this is primarily blocking your "composer all the things" work (which dawehner showed a preview of in IRC and it looks awesome!), how urgent do you need this in this week? It's only a normal, so can it wait a few days for catch?

tstoeckler’s picture

Thanks for the feedback.

To provide some context for people following at home:
You can already pull in Drupal's core directory via Composer using https://github.com/tstoeckler/drupal-core/ (with or without this patch), if you want to control your own index.php, e.g. for using a different class loader, i.e. the one generated by Composer in your parent project and not the one shipped with Drupal core. But even if you do that, Drupal will not use Composer's generated auto loader, instead it hardcodedly uses the class loader it ships with (core/vendor/autoload.php. Thus, what this patch enables is: Additionally pulling in other code via Composer and having that loaded during a Drupal bootstrap. The prime example for such "other code" would be a repository of utility classes you or your company maintains for various projects. Think of Ctools in D7, except that with all the refactoring for Drupal 8 and the power of Composer you don't need to make it a module but instead it's simply always there (TM) and you just use the provided classes when you need them.

I'm doing exactly that both for a personal project as well as for a company project, so the sooner this goes in the awesomer, but we're not on a tight schedule (how could we with D8 ;-)), so a few days wouldn't hurt. Furthermore, I think it's a great idea to have @catch take a look at this (not that I wouldn't trust you to make an informed decision about this patch, though!).

webchick’s picture

Assigned: Unassigned » catch

Ok, great. Thanks so much for your understanding. Assigning to catch for a look-see. Sounds like a cool project though!

catch’s picture

Status: Reviewed & tested by the community » Fixed

OK read through this and discussed briefly with tstoeckler . This removes the bug part of #2023325: Add interface for classloader so it can be cleanly swapped (so I've retitled it and changed it to a task) - which will allow for experimentation with more performant class loaders.

Patch itself looks fine, I'm not clear how useful the "hack index.php to change the location of autoload.php" use case will be if we stop checking in composer stuff to the repo, but it's not going to make that worse anyway.

Committed/pushed to 8.0.x, thanks!

  • catch committed 6aeb7e9 on 8.0.x
    Issue #2325197 by tstoeckler, sun: Remove drupal_classloader().
    

Status: Fixed » Closed (fixed)

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

znerol’s picture