humbug / php-scoper

🔨 Prefixes all PHP namespaces in a file/directory to isolate the code bundled in PHARs.
MIT License
700 stars 75 forks source link

Fatal error: Uncaught Error: Interface 'Stringable' not found in #1010

Open reinos opened 5 months ago

reinos commented 5 months ago

Bug report

Question Answer
PHP-Scoper version master branch due to this possible fix
PHP version 8.2
Platform with version MacOS

After running php-scoper it seems that some interfaces are no scoped.

See the source of file /vendor/symfony/polyfill-php80/PhpToken.php

namespace Symfony\Polyfill\Php80;

class PhpToken implements \Stringable
{
....
}

and file vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php

if (\PHP_VERSION_ID < 80000) {
    interface Stringable
    {
        /**
         * @return string
         */
        public function __toString();
    }
}

are scoped like

namespace ReinosMapsScoped\Symfony\Polyfill\Php80;

class PhpToken implements \Stringable <!-- NOTICE THIS, its not scoped
{
...
}
if (\PHP_VERSION_ID < 80000) {
    interface Stringable
    {
        /**
         * @return string
         */
        public function __toString();
    }
    \class_alias('ReinosMapsScoped\\Stringable', 'Stringable', \false);
}

The issue seems that class PhpToken implements \Stringable is not scoped to class PhpToken implements \ ReinosMapsScoped\Stringable

Now it throws an PHP error Fatal error: Uncaught Error: Interface 'Stringable' not found in..... and with adding manually the scoped namespace it fixed the error.

Is this a bug in PHPscoper or a configuration issue? 🤔

theofidry commented 5 months ago

It you're using the latest tagged version it is a known issue, if you're using the master branch however it should be fixed.

You notice you have a class alias statement added:

    \class_alias('ReinosMapsScoped\\Stringable', 'Stringable', \false);

You should also have a corresponding class_exists() statement in your scoper-autoload.php so \Stingable should be a registered symbol by the time PhpToken is loaded.

If you still have the problem on master please provide a reproducer so that I can look into it

reinos commented 4 months ago

Hi @theofidry, thanks for your reply!

So I did some digging and I found the issue.

The file scoper-autoload.php generate this for me.

humbug_phpscoper_expose_class('Reinos_maps_upd', 'ReinosMapsScoped\Reinos_maps_upd');
humbug_phpscoper_expose_class('Reinos_maps', 'ReinosMapsScoped\Reinos_maps');
humbug_phpscoper_expose_class('Reinos_maps_ext', 'ReinosMapsScoped\Reinos_maps_ext');
humbug_phpscoper_expose_class('Reinos_maps_ACT', 'ReinosMapsScoped\Reinos_maps_ACT');
humbug_phpscoper_expose_class('Reinos_maps_mcp', 'ReinosMapsScoped\Reinos_maps_mcp');
humbug_phpscoper_expose_class('ComposerAutoloaderInitc47e9d714b1dba70cb6cf7603e380961', 'ReinosMapsScoped\ComposerAutoloaderInitc47e9d714b1dba70cb6cf7603e380961');
humbug_phpscoper_expose_class('GooglePolylineBadInput', 'ReinosMapsScoped\GooglePolylineBadInput');
humbug_phpscoper_expose_class('GoogleTraitBadInputTest', 'ReinosMapsScoped\GoogleTraitBadInputTest');
humbug_phpscoper_expose_class('GooglePolylineHacking', 'ReinosMapsScoped\GooglePolylineHacking');
humbug_phpscoper_expose_class('GoogleTraitHackingTest', 'ReinosMapsScoped\GoogleTraitHackingTest');
humbug_phpscoper_expose_class('JsonException', 'ReinosMapsScoped\JsonException');
humbug_phpscoper_expose_class('PhpToken', 'ReinosMapsScoped\PhpToken');
humbug_phpscoper_expose_class('ValueError', 'ReinosMapsScoped\ValueError');
humbug_phpscoper_expose_class('Attribute', 'ReinosMapsScoped\Attribute');
humbug_phpscoper_expose_class('UnhandledMatchError', 'ReinosMapsScoped\UnhandledMatchError');
humbug_phpscoper_expose_class('Stringable', 'ReinosMapsScoped\Stringable');
humbug_phpscoper_expose_class('Normalizer', 'ReinosMapsScoped\Normalizer');

Indeed, Stringable is there and should be registered. However, the class PhpToken is called first and that class implements the Stringable interface. So at that time when PhpToken is registering, \Stringable is not yet created as alias.

By moving Stringable just before the PhpToken fixed the issue.

humbug_phpscoper_expose_class('Stringable', 'ReinosMapsScoped\Stringable');
humbug_phpscoper_expose_class('PhpToken', 'ReinosMapsScoped\PhpToken');

Any idea how we can fix this?

theofidry commented 4 months ago

I see, that's an annoying one... We could fix it for this specific case for now. Fixing this more generally looks a bit tricky, doable but tricky.

incraigulous commented 4 months ago

@theofidry , do you know of a workaround we could use until this can be fixed? I can confirm that it works if I reorder my exposed classes to this order:

humbug_phpscoper_expose_class( 'Stringable', 'CodeZone\Bible\Stringable' ); humbug_phpscoper_expose_class( 'PhpToken', 'CodeZone\Bible\PhpToken' );

incraigulous commented 4 months ago

@theofidry , what about a setting to allow for setting a priority of exposed classes?

'exposed-class-priority' => [
     'Stringable' => 0,
     'PhpToken' => 1
]

Just an idea. I'm sure there is a more elegant solution.

theofidry commented 4 months ago

I'm working on a PoC for it. In terms of effort having another setting is not exactly trivial either, so might as well try to fix it the proper way. I don't think it is that hard in the end, but I was on holiday so didn't get that much time to look into it, I hope to fix this within the week or next week though.

incraigulous commented 4 months ago

:pray: Thank you @theofidry!

theofidry commented 4 months ago

Ok I'll need some help here... Could you guys provide a super minimal reproducer?

I identified the source code changes that I need to do, it's not a crazy amount but for the tests it's another story. So I've been trying to implement an e2e test instead to validate my idea first before fixing all the other tests... And I can't make it work.

I did my changes in #1035. There is some source code changes but they actually have no impact yet (I'm tracking the dependencies of the class when aliasing a class, but I do nothing with them for now). The end to end test is in the fixtures/set041-exposed-symbols-hierarchy directory and executed with make e2e_041.

I've tried various things, but I cannot reproduce the issue you describe. I've even added a polyfill-80 structure like to try to reproduce it without having to run an old PHP version but this does not do the trick: for me the StringeableLike interface (which would be the Stringeable of your case) is present in the loader classmap (because it is registered in classmap in the composer.json).

I also notice that PhpToken is namespaced, so I don't get why yours is exposed this way.