Skip to content

Weird behaviour when a file is autoloaded in assignment of a constant  #10232

Closed

Description

@fasterforward

Description

The following files:

test.php

<?php

error_reporting(E_ALL);
ini_set('display_errors', 1);

class Loader
{
    public function __construct()
    {
        spl_autoload_register([$this, 'include'], true);
    }
    public function include()
    {
        require 'test/constant.php';
    }
}

new Loader();
class B
{
    public const A = constant::TEST;
}

new B();
echo 'here';

test/constant.php

<?php

declare(strict_types=1);
require_once 'required.php';

class constant
{
    public const TEST = 'test';
}

test/required.php

<?php

Resulted in this output:

Warning: require_once(required.php): Failed to open stream: No such file or directory in /mnt/fasterforward/web01/elements/src/httpdocs/test/test.php on line 21 Call Stack: 0.0183 365472 1. {main}() /mnt/fasterforward/web01/elements/src/httpdocs/test/test.php:0 0.1607 397352 2. Loader->include('constant') /mnt/fasterforward/web01/elements/src/httpdocs/test/test.php:24 0.1611 397752 3. require_once('/mnt/fasterforward/web01/elements/src/httpdocs/test/test.php') /mnt/fasterforward/web01/elements/src/httpdocs/test/test.php:11 Fatal error: Uncaught Error: Failed opening required 'required.php' (include_path='.:/usr/share/pear:/usr/share/php:/usr/share/pear:/usr/share/php') in /mnt/fasterforward/web01/elements/src/httpdocs/test/test.php on line 21 Error: Failed opening required 'required.php' (include_path='.:/usr/share/pear:/usr/share/php:/usr/share/pear:/usr/share/php') in /mnt/fasterforward/web01/elements/src/httpdocs/test/test.php on line 21 Call Stack: 0.0183 365472 1. {main}() /mnt/fasterforward/web01/elements/src/httpdocs/test/test.php:0 0.1607 397352 2. Loader->include('constant') /mnt/fasterforward/web01/elements/src/httpdocs/test/test.php:24 0.1611 397752 3. require_once('/mnt/fasterforward/web01/elements/src/httpdocs/test/test.php') /mnt/fasterforward/web01/elements/src/httpdocs/test/test.php:11 

But I expected this output instead:

here

For some reason in when a file is autoloaded in assignment of a constant, the requires there do not get checked based on the location of that file.
When I put the autoloader outside of a class, require the file directly or specify constant as a type instead it works fine
In PHP 8.1.8 and PHP 7.4.30 with very simular php.ini's it works fine and the file gets required properly.
I thought it might be opcache but when I disable it the same problem happens.

PHP Version

PHP 8.2

Operating System

Red Hat Enterprise Linux release 8.7

Activity

  1. cmb69 commented on Jan 5, 2023

    @cmb69
    Member

    I don't think this could ever have worked, unless you set include_path properly. You claim that constant.php and required.php are in the same directory, but the includes don't match that structure, and there is no chdir() happening.

    So, have you set include_path for the other PHP versions? If not, which SAPI do you use? Is test.php really supposed to be a level above the other files, or actually stored in the same directory? And from which directory are you running the test (or what is the docroot, respectively).

  2. fasterforward commented on Jan 5, 2023

    @fasterforward
    Author

    I see I made a tiny mistake in the name of the file "test/required.php" just fixed that.

    But yes test.php is in the location as indicated one level above the files included. And constant.php and required.php are in the same directory.
    The include_path is not modified and is '.:/usr/share/pear:/usr/share/php:/usr/share/pear:/usr/share/php' for all versions.
    The SAPI is "FPM/FastCGI' for all versions as well.
    The docroot is ""/mnt/fasterforward/web01/elements/src/httpdocs/" in this case, made a seperate folder for testing called ``test` but you can replicate this behaviour from the command-line as well.

    Reading through the documentation and seems that you are right it seems that requiring like this is not supposed to work, but it still does mostly work and did completely work for the previous versions.
    I just tested it, and this behavior is working in a brand new installation of PHP 8.2.1 for windows.

  3. fasterforward commented on Jan 13, 2023

    @fasterforward
    Author

    Retested this bug on PHP 8.2.1. with FPM/FastCGI and Oracle Linux 8 same result

  4. ranvis commented on Jan 16, 2023

    @ranvis
    Contributor

    I think I have confirmed the issue on PHP 8.2.1 CLI.

    From the document of include():

    If the file isn't found in the include_path, include will finally check in the calling script's own directory and the current working directory before failing.

    And the issue is about a misidentification of the script's directory.
    What I tried follows:

     mkdir empty first second
     cat <<'END' > main.php
    <?php
    chdir(__DIR__ . '/empty');
    set_include_path(getcwd());
    
    spl_autoload_register(function () {
        require_once __DIR__ . '/second/SecondClass.php';
    }, true);
    $case = (int)$argv[1];
    require_once __DIR__ . '/first/FirstClass.php';
    
    new FirstClass();
    END
     cat <<'END' > first/FirstClass.php
    <?php
    if ($case == 1): // fails
        class FirstClass { public const VALUE = SecondClass::VALUE; }
    elseif ($case == 2): // fails
        class FirstClass { public $value = SecondClass::VALUE; }
    elseif ($case == 3):
        class FirstClass { public function __construct() { SecondClass::VALUE; } }
    elseif ($case == 4):
        class FirstClass {}
        SecondClass::VALUE;
    endif;
    END
     cat <<'END' > second/SecondClass.php
    <?php
    require_once 'required.php'; // should load the file in the same directory
    
    class SecondClass { public const VALUE = 1; }
    END
     cat <<'END' > required.php
    <?php echo basename(__DIR__), "\n";
    END
     cp required.php first/
     cp required.php second/
    
     php82 main.php 1  # first
     php82 main.php 2  # first
     php82 main.php 3  # second
     php82 main.php 4  # second
    
     php81 main.php 1  # second
     php81 main.php 2  # second
     php81 main.php 3  # second
     php81 main.php 4  # second

    main.php explicitly loads FirstClass.php, which loads SecondClass.php by autoloading, and then SecondClass.php loads required.php.
    require_once 'required.php' in second/SecondClass.php should load second/required.php, but first/required.php is loaded on some cases with PHP 8.2.

  5. ranvis commented on Jan 28, 2023

    @ranvis
    Contributor

    Since EG(filename_override) set on constant resolution always takes precedence,
    require_once call on second/SecondClass.php is still resolved based on first/FirstClass.php's path.

    ZEND_API zend_string *zend_get_executed_filename_ex(void) /* {{{ */
    {
    zend_string *filename_override = EG(filename_override);
    if (filename_override != NULL) {
    return filename_override;
    }

    I guess error reporting and path resolution should be differentiated here.

  6. FuZuK commented on Jan 26, 2024

    @FuZuK

    Hi,
    Yesterday faced with the same issue #13245
    The behavior really changed, prior to 8.2 it works. We have a lot of code that uses it.
    In doc it says that will firstly take a look in the directory of calling script and cwd (of course after include_path). But I think a lot of people are using that in this way like we did.

  7. added a commit that references this issue on Apr 22, 2024
    f8b9030
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions