paketo-buildpacks / php-composer

Apache License 2.0
5 stars 3 forks source link

Composer autoloading not working #218

Open iainfogg opened 3 years ago

iainfogg commented 3 years ago

What happened?

I'm finding that autoloading is not working in applications using PHP and composer. Specifically, it's when the autoloading is attempting to autoload a class which is outside of the vendor folder.

For example, I have an public/index.php which the web server points to which requires a bootstrap.php file from the root folder of the application. That has the following code:

<?php
//set base dir
chdir(__dir__);

$loader = include 'vendor/autoload.php';

if (class_exists('My\\Namespace\\ClassName')) {
    echo "It exists";
} else {
    echo "Can't find it";
}
die;

The class ClassName lives in APPROOT/modules/namespace/ClassName.php

If I run that directly on my machine, using PHP 7.4, I get It exists in the browser.

With an image built with pack v. 0.19.0 using the command pack build my-pack --builder paketobuildpacks/builder:full and the following config, I get the following error.

Config:

php:
  version: 7.4.*
  webserver: php-server
  webdirectory: public
composer:
  version: 2.*
  install_options: ['--no-scripts']

Error:

[Fri Jun 25 15:15:35 2021] PHP 7.4.18 Development Server (http://0.0.0.0:8080) started
[Fri Jun 25 15:15:39 2021] 172.17.0.1:43176 Accepted
[Fri Jun 25 15:15:39 2021] PHP Warning:  include(/layers/paketo-buildpacks_php-composer/php-composer-packages/vendor/composer/../../modules/namespace/ClassName.php): failed to open stream: No such file or directory in /layers/paketo-buildpacks_php-composer/php-composer-packages/vendor/composer/ClassLoader.php on line 478
[Fri Jun 25 15:15:39 2021] PHP Warning:  include(): Failed opening '/layers/paketo-buildpacks_php-composer/php-composer-packages/vendor/composer/../../modules/namespace/ClassName.php' for inclusion (include_path='/layers/paketo-buildpacks_php-composer/php-composer-packages/vendor/phpunit/phpunit-selenium:/layers/paketo-buildpacks_php-dist/php/lib/php:/workspace/.') in /layers/paketo-buildpacks_php-composer/php-composer-packages/vendor/composer/ClassLoader.php on line 478

(the code is actually an anonymised version of the real code, but I believe I've corrected everything).

The issue seems to be that the file is at APPROOT/modules/namespace/ClassName.php, but because vendor is symlinked into the APPROOT folder but actually lives in /layers/paketo-buildpacks_php-composer/php-composer-packages/vendor, any folder references using within the vendor folder that get the folder location are actually using the real path, not the symlinked location. So, when they try to go up to the APPROOT folder using ../../ it actually ends up in /layers/paketo-buildpacks_php-composer/php-composer-packages instead of in APPROOT. Then when it tries to go down into the modules folder of APPROOT, it can't find it as it doesn't exist in /layers/paketo-buildpacks_php-composer/php-composer-packages.

I'm using the full buildpack.

The versions are:

paketo-buildpacks/ca-certificates 2.3.2
paketo-buildpacks/php-dist        0.3.1
paketo-buildpacks/php-composer    0.2.1
paketo-buildpacks/php-web         0.1.1

The full build output is here:

 pack build joy-pack --builder paketobuildpacks/builder:full                                                                                                           45s  2021-06-25 15:15
full: Pulling from paketobuildpacks/builder
Digest: sha256:7519e088ad834145d3f235b841e3ad2a30ffc7497811d73d295a0b7227067c68
Status: Image is up to date for paketobuildpacks/builder:full
full-cnb: Pulling from paketobuildpacks/run
Digest: sha256:eb91e04749e78b2828208d665a62a1876818a6ce27df68849afb5d92e890d814
Status: Image is up to date for paketobuildpacks/run:full-cnb
===> DETECTING
Warning: Warning: buildpack paketo-buildpacks/php-web has a "version" key. This key is deprecated in build plan requirements in buildpack API 0.3. "metadata.version" should be used instead
Warning: Warning: buildpack paketo-buildpacks/php-composer has a "version" key. This key is deprecated in build plan requirements in buildpack API 0.3. "metadata.version" should be used instead
4 of 9 buildpacks participating
paketo-buildpacks/ca-certificates 2.3.2
paketo-buildpacks/php-dist        0.3.1
paketo-buildpacks/php-composer    0.2.1
paketo-buildpacks/php-web         0.1.1
===> ANALYZING
Restoring metadata for "paketo-buildpacks/ca-certificates:helper" from app image
Restoring metadata for "paketo-buildpacks/php-dist:php" from app image
Restoring metadata for "paketo-buildpacks/php-composer:php-composer-packages" from app image
Restoring metadata for "paketo-buildpacks/php-composer:116fdf07cc926af646635a6abc92d88aff7b02a5dc36538f81c50a7d27366dbf" from cache
Restoring metadata for "paketo-buildpacks/php-composer:php-composer-cache" from cache
Restoring metadata for "paketo-buildpacks/php-web:php-web" from app image
===> RESTORING
Restoring data for "paketo-buildpacks/php-dist:php" from cache
Restoring data for "paketo-buildpacks/php-composer:116fdf07cc926af646635a6abc92d88aff7b02a5dc36538f81c50a7d27366dbf" from cache
Restoring data for "paketo-buildpacks/php-composer:php-composer-cache" from cache
===> BUILDING

Paketo CA Certificates Buildpack 2.3.2
  https://github.com/paketo-buildpacks/ca-certificates
  Launch Helper: Reusing cached layer
Paketo PHP Distribution Buildpack 0.3.1
  Resolving PHP version
    Candidate version sources (in priority order):
      buildpack.yml    -> "7.4.*"
      composer.lock    -> ">=7.4"
      default-versions -> "7.4.*"

    Selected PHP version (using buildpack.yml): 7.4.18

  Reusing cached layer /layers/paketo-buildpacks_php-dist/php

Paketo PHP Composer Buildpack 0.2.1
   2.0.13: Contributing to layer
    Reusing cached download from previous build
    Expanding to /layers/paketo-buildpacks_php-composer/composer
  PHP Composer Cache 96ebbb5c8694dd2c33b07ca6d40c10b0b670bc10176d2507d8b3b4a739d46f01: Reusing cached layer
Checking platform requirements for packages in the vendor dir
  PHP Composer 8b6fe1f4fbcca4929ec9ba7b2c43356708dae88bfba6a714233a2ac176890a38: Reusing cached layer

Paketo PHP Web Buildpack 0.1.1
  PHP Web 5d59e600d390f28fb58f29e6ca3a27fc2ed12215a229c09955aaee88bc73d6d8: Contributing to layer
  Configuring PHP Application
    Using feature -- PHP
    Writing PHPRC to shared
    Writing PHP_INI_SCAN_DIR to shared
    Using feature -- PHP Web Server
  Process types:
    task: php -S 0.0.0.0:$PORT -t /workspace/public
    web:  php -S 0.0.0.0:$PORT -t /workspace/public
===> EXPORTING
Reusing layer 'paketo-buildpacks/ca-certificates:helper'
Reusing layer 'paketo-buildpacks/php-dist:php'
Reusing layer 'paketo-buildpacks/php-composer:php-composer-packages'
Reusing layer 'paketo-buildpacks/php-web:php-web'
Adding 1/1 app layer(s)
Reusing layer 'launcher'
Reusing layer 'config'
Reusing layer 'process-types'
Adding label 'io.buildpacks.lifecycle.metadata'
Adding label 'io.buildpacks.build.metadata'
Adding label 'io.buildpacks.project.metadata'
Setting default process type 'web'
Saving joy-pack...
*** Images (5a721dc60c8a):
      joy-pack
Reusing cache layer 'paketo-buildpacks/php-dist:php'
Reusing cache layer 'paketo-buildpacks/php-composer:116fdf07cc926af646635a6abc92d88aff7b02a5dc36538f81c50a7d27366dbf'
Reusing cache layer 'paketo-buildpacks/php-composer:php-composer-cache'
Successfully built image joy-pack
iainfogg commented 3 years ago

PS in case it matters, that particular class is being loaded with a classmap in the composer autoloading config in composer.json, rather than using PSR-0 or PSR-4.

    "autoload" :
    {
        "classmap" :
        [
            "modules/namespace/ClassName.php"
        ]
    },
iainfogg commented 3 years ago

And if I had to guess why it's going wrong, it would be that these lines in vendor/autoload_classmap.php (and the other autoloading files that composer generates) are ending up with /layers/paketo-buildpacks_php-composer/php-composer-packages in $baseDir instead of APPROOT.

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

or if composer is using the autoload_static.php file, it's this bit towards the end of that file that's ending up with the wrong value:

    public static $classMap = array (
        'My\\Namespace\\ClassName' => __DIR__ . '/../..' . '/modules/namespace/ClassName.php',
    );
arjun024 commented 3 years ago

Could you provide a full app? We have a vendored app here that we test, but it doesn't seem to use classmap.

iainfogg commented 3 years ago

@arjun024 thanks, I just tried creating a super simple app that used a classmap, and it actually worked fine, so I'm going to need to try and figure out exactly what it was about the full application that's different and is causing this problem. I'll update you when I manage to identify it.

iainfogg commented 3 years ago

@arjun024 I've just spent some more time on this, and I think there's something more nuanced going on, potentially around caching.

The code that I said worked later stopped working, after I did some more work and rebuilding, and I couldn't work out why.

I then added a composer dependency (originally there were none, just some autoloading instructions), rebuilt and reran the code, and then it all started working again.

So I've got some demo code, but it either works or doesn't work and I can't figure out why.

Is there a way to force clearing any caches used in the process?

Or do you have any other ideas?

arjun024 commented 3 years ago

@iainfogg Interesting. With pack build, you can use the --clear-cache flag to always clear cache before build.

fg-j commented 2 years ago

Hey @iainfogg, it's been a little while since we last heard from you. Any update on the issue you're experiencing?

iainfogg commented 2 years ago

Hi, I'm sorry it's taken a long time to come back to you on this.

I've created a full application to demo the problem, which I've outlined fully in the README of the project, but I believe it's to do with having the vendor folder as a symlink to another location, rather than vendor existing as a concrete folder within the application. This seems to cause problems when Composer tries to move out of the vendor folder to autoload files within the application.

https://github.com/iainfogg/paketo-composer-demo

Hope that helps!

enumag commented 2 years ago

I'm facing the same issue. Simply put the problem is that /workspace/vendor ends up being a symbolic link to /layers/paketo-buildpacks_php-composer/php-composer-packages/vendor. This isn't supported by Composer and breaks autoloading for any class outside of the vendor directory because composer in the end generates for example vendor/composer/autoload_classmap.php like this:

<?php

// autoload_classmap.php @generated by Composer

$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);

return array(
    'App\\Kernel' => $baseDir . '/src/Kernel.php',
);

Here $baseDir ends up being /layers/paketo-buildpacks_php-composer/php-composer-packages while it should be /workspace meaning that $baseDir . '/src/Kernel.php' doesn't exist and the Kernel class can't be autoloaded.

sophiewigmore commented 2 years ago

cc @joshuatcasey

@enumag @iainfogg thank you for the details. We're in the process of rewriting this buildpack, so I'll make sure this issue is taken into consideration

enumag commented 2 years ago

Thinking about this further I believe there is actually a second issue lurking behind the first one. And that is composer's optimization --classmap-authoritative.

Based on the fact that you apparently prepare the vendor directory on a different layer (presumably for layer caching because composer dependencies don't change that often) I believe that the classmap won't contain classes outside of vendor. To fix this you need to run composer dump-autoload --classmap-authoritative after you copy the application files.