ZF-6810: Regression in Zend_Loader between 1.8.0 and 1.8.1, class_exists() causes warnings

Description

When updating from 1.8.0 to 1.8.1, I found some unit tests failing in my application. I looked around the issue list, found a lot of related problems, most of which were closed or marked as duplicate to other closed issues.

Here is the situation:

$className = "Prefix_To_Path_" . $token;

if( class_exists($className) ) { return new $className; }

It worked perfectly in 1.8.0. However, from 1.8.1, a file not found warning is issued.

The solution appears to be to enable error suppression. However, due to strict coding standards, that does not fit as a valid solution.

The code base is still in development, so performance is not a major issue at this time. I would rather have a file check with optional caching than receiving a warning.

If this issue cannot be resolved, I would like to know of a recommended alternative.

Comments

class_exists() can take an optional second argument. That argument can tell it to bypass the autoloader, if any. (See http://php.net/class_exists for more information.) In the example you have above, I'd recommend using:


if (class_exists($className, false)) {
    return new $className;
}

This will prevent the autoloader from triggering, which will also prevent the warnings from occurring.

Finally, it's best to register only the namespaces you use with the autoloader, and not use it as a fallback autoloader, as it will create warnings any time it is unable to resolve a given classname; if used only as a namespace autoloader, then warnings should typically never occur.

I will likely mark this as "won't fix" or "not reproducible" unless I hear back as to the class_exists() solution noted above.

I would expect the class to be autoloaded, hence disabling it does not appear to be a valid solution.

If the file does not exist, I only expect class_exists to return false, just as it did in 1.8.0. There is not much use in making a check if it fails while performing it.

I see no reason why this would fail:

$this->assertFalse( class_exists('Zend_NotAClass') );

Perhaps the check should be optional for performance issues, just like suppress errors is optional.

Well, it raises an E_WARNING, which PHPUnit then reports as an error (not a failure). My question is: why are you testing class_exists() in your test suite? and would it make sense to add the second "false" flag to the call (to suppress autoloading)?

You can also turn on the suppressNotFoundWarnings flag on the autoloader prior to the check if you want to prevent the warnings and make the test pass:


Zend_Loader_Autoloader::getInstance()->suppressNotFoundWarnings(true);

We made the flag false by default for 1.8.1 as it was an oversight that it was on by default in 1.8.0 (we'd never intended that, as one of the principle use cases for the component was to remove error suppression by default). You should only ever need the suppression if you are using the autoloader as a fallback autoloader.

The application is highly data driven and the goal is to be able to drop in code to extend it. In normal runs, nonexistent classes will never be loaded. However, while saving new configurations, multiple checks are made to avoid breakage later on. It gets tested by the test suite just because I verify that failures are handled correctly.

Suppressing the warnings work, however it does not seem like a good practice. As mentioned in other reports, it may hide problems.

Could this bug please be opened again?

Allow me to explain why I think class_exists() (and the file_exists() method that it calls) should not emit warnings. Let's say that you have a list of plugins that you want to try to load. You then write some code like this:

 
$plugins = array('plugin1', 'plugin2', 'plugin3'); // etc.

foreach($plugins as $plugin) {
   try {
      Zend_Loader::loadClass($plugin);
   } catch(Zend_Exception $e) {
      // Since this is a plugin, just ignore the error and continue if we failed to load the plugin.
      continue;
   }
   
   $instantiated_plugin = new $plugin();
   
   // If the plugin has the method we need, call the method.
   if(method_exists($instantiated_plugin, 'method')) {
      $instantiated_plugin->method();
   }
}

What we're trying to do here is load several "plugins" and then execute a method if the plugin has that method. We have a white list of plugins that are allowed to be loaded, and we try and load each one in turn. Under the current behaviour, a warning will be emitted every time a plugin fails to be loaded, but this is clearly not what is desired.

Is there any chance that the behaviour of this function could be changed so that it does not emit a warning when the class or file to be loaded does not exist?

@[~vestigal] {quote} Is there any chance that the behaviour of this function could be changed so that it does not emit a warning when the class or file to be loaded does not exist? {quote} {{Zend_Loader::loadClass()}} uses {{Zend_Loader::loadFile()}} and this uses:


if ($once) {
    include_once $filename;
} else {
    include $filename;
}

{quote}The include construct will emit a warning if it cannot find a file{quote} http://de3.php.net/manual/en/function.include.php

Frank, thank you for the confirmation. I realize that this is what is going on; what I am asking is that we reconsider how this function works and have it not emit a warning in the case that it fails to load a class because the class' file does not exist.

Vestigal -- this ship has sailed. If we do not emit the warning, developers don't know what failed. If we do emit it, some people complain about noisy logs and/or display_errors. We opted for making debugging easier.

Matthew, that makes sense.

Is there any chance that another parameter could be added to the {{loadFile()}} function that would allow for suppression of errors? The function would then look something like this:

public static function loadFile($filename, $dirs = null, $once = false, $show_errors = true) 
{
   ...
   
   if ($once) {
      if($show_errors) {
         include_once $filename;
      } else {
         @include_once $filename;
      }
   } else {
      if($show_errors) {
         include $filename;
      } else {
         @include $filename
      }
   }
   
   ...
}

While perhaps not the most elegant solution, I think this would work. {{loadClass()}} would then just need to tell {{loadFile()}} to not show any errors that it encountered.