Notice: Undefined index: HTTPS in /home/onphp5/public_html/index.php5 on line 66
onPHP5.com - Exceptions in __autoload()
 

onPHP5.com

PHP5: Articles, News, Tutorials, Interviews, Software and more
  
Featured Article:
Learning PHP Data Objects
 
 
Sun, 23 Nov 2014
 Home   About   Contribute   Contact Us   Polls 
Top Tags
article codeigniter conference mysql namespace news onphp5 oop php5 poll prado security simplexml solar symfony unicode url zend core zend framework zend platform
More tags »

Not logged in
Login | Register

den_hotmail@fbzz

Exceptions in __autoload()

« Issues with Non-ASCII Chars in URLs Advocating Namespaces »

By dennisp on Wednesday, 19 December 2007, 20:32
Published under: article   oop   php5
Views: 17547, comments: 2

There's been many talks on exceptions cannot be thrown from the __autoload() function, as well as many times the workarounds have been discussed (all using the eval() trick to automatically define the missing class). Here I will talk about negative effects of such workarounds as well as suggest a somewhat "better" workaround (but still based on the eval() functionality and ready for namespaces).


Many folks (including me) have been complaining why the __autoload() function cannot throw an exception when it fails to locate the requested class. The main problem is that when a class is missing, the program execution stops and there's no way to prevent that. __autoload() will trigger a fatal error, and PHP does not allow to intercept fatal errors with a custom error handler. Exceptions could allow to resume the program's execution and completely hide the erroneous behavior from the user.

However, let's ask ourselves - do we really need an exception in such situation? Normally, while we are developing, it does not really matter if the autoloading fails with a fatal error or an exception. On production sites class autoloading must always succeed - I assume that any changes are thoroughly tested before being released.

There is one situation (which I can think of) when the autoloading may fail. When the application's architecture is based on plugins, where the class names are not hardcoded in the sources, then there's a possibility that the code may refer to a class that is not discoverable (ie, not uploaded to the server). However, in such case, the application should always rely on reflection or the class_exists() function to determine whether the class is discoverable. When the __autoload() function is called implicitly from the ReflectionClass::__construct() or class_exists() then it won't die with a fatal error. Instead, a ReflectionException or false will be returned, respectively.

Why eval() is not that good


All the existing workarounds that actually allow to throw exceptions from the __autoload() function use the eval() function to "define" a dummy class of the required name and to throw an exception from within its constructor. While this approach is effective, it has serious drawbacks:

  • since the class is actually defined, reflection will always succeed and ReflectionClass will reflect an instance of the dummy class. Similarly, class_exists() will always return true. This may lead to hard-to-track logical errors in the code

  • if the class is accessed statically, then this approach will not work at all (a fatal error will be produced)


Improving the eval() approach


It is still possible to use the eval() function to return dummy classes that throw an exception when their static methods are called. Also, I will show how to deal with namespaced declarations. Let's take the simplest eval() example:

<?php
function __autoload($className) {
  
// Assume that all class files are located in the same dir
  
if(is_readable($className '.php')) {
    include(
$className '.php');
    return;
  }
  
// If we are here, class could not be located
  
eval("class $className { 
          function __construct() { 
            throw new Exception('Class $className not found');
          }
        }"
);
}
?>


Let's test it with a small code snippet that tries to instantiate a non-existing class:

<?php
try {
  
$x = new Test();
} catch(
Exception $e) {
  echo 
$e->getMessage();
}
?>


It will print the string Class Test not found from within the catch {...} block. However, when we try to use reflection on the non-existing Test class, no ReflectionException will be thrown:

<?php
try {
  
$x = new Test();
} catch(
Exception $e) {
  echo 
$e->getMessage(), "\r\n";
}

try {
  
$rc = new ReflectionClass('Test');
  echo 
"Still alive\r\n";
} catch(
ReflectionException $re) {
  echo 
$re->getMessage(), "\r\n";
}
?>


will result with:


Class Test not found
Still alive


And, finally, to prove that static calls will still fail with a fatal error, consider the following snippet:

<?php
Test
::doSmth();
?>


which will result with a fatal error:

Fatal error: Call to undefined method Test::dosmth() ...

Intercepting this situation is pretty straightforward. We should just define the magic __callstatic() method in our dummy class which we create within the eval() function:

<?php
function __autoload($className) {
  
// Assume that all class files are located in the same dir
  
if(is_readable($className '.php')) {
    include(
$className '.php');
    return;
  }
  
// If we are here, class could not be located
  
eval("class $className { 
          function __construct() { 
            throw new Exception('Class $className not found');
          }
          
          static function __callstatic(\$m, \$args) {
            throw new Exception('Class $className not found');
          }
        }"
);
}
?>


Let's now test it with a static method call:

<?php
try {
  
Test::doSmth();
} catch(
Exception $e) {
  echo 
$e->getMessage(), "\r\n";
}
?>


As expected, this will print Class Test not found and the script won't die. However, we cannot do anything if a static property of a missing class is accessed - PHP will die with a fatal error (just like it will die when you access an undefined static property of an existing class).

And, to show you how to deal with missing classes with namespaces, consider the following autoloading code:

<?php
function __autoload($className) {
  
// Assume that all class files are located in the same dir and subdirs
  
$fname str_replace('::'DIRECTORY_SEPARATOR$className) . '.php';
  if(
is_file($fname)) {
    include_once(
$fname);
    return;
  } 
  
  
$namespace substr($className0strrpos($className'::'));
  
$localClassName substr($classNamestrrpos($className'::') + 2);
  if(
$namespace) {
    eval(
"namespace $namespace;
          class $localClassName {
            function __construct() {
              throw new Exception('Class $namespace::$localClassName not found');
            }
        
            static function __callstatic(\$m, \$args) {
              throw new Exception('Class $className not found');
            }
          }"
);
  } else {
    eval(
"class $className { 
            function __construct() { 
              throw new Exception('Class $className not found');
            }
          
            static function __callstatic(\$m, \$args) {
              throw new Exception('Class $className not found');
            }
          }"
);
  }
}
?>


Now, to test this, you can use the following code:

<?php
try {
  
$x = new Name::Space::Test();
} catch(
Exception $e) {
  echo 
"No such class\r\n";
}

try {
  
Name::Space::Test::doSmth();
} catch(
Exception $e) {
  echo 
"No such class\r\n";
}
?>


which will output the following:

No such class
No such class


Summary


As we have seen, using the eval() to automatically define classes that throw exceptions within the __autoload() function has several serious drawbacks. Normally, your application should always find all the classes during autoloading. When it depends on external modules like plugins, you can always use reflection to nicely handle missing class cases.

Related articles

Advocating Namespaces
Most Important Feature of PHP 5?
Learning PHP Data Objects
SimpleXML, DOM and Encodings
i18n with PHP5: Pitfalls
PHP Version 5.2.2 Released
PHP Version 5.2.4 Released
PHP Version 5.2.4 (RC1) Released for Testing
PHP Version 5.2.2 (RC1) Released for Testing
PHP Version 5.2.3 Released
Issues with Non-ASCII Chars in URLs
Clickable, Obfuscated Email Addresses
Some SEO Tips You Would Not Like to Miss
Sorting Non-English Strings with MySQL and PHP (Part 1)
PHP5 More Secure than PHP4
PHP Version 5.2.1 Released

Comments

#1  By Martin Poirier Théorêt on Saturday, 22 August 2009, 22:11
HI

I just add this before in the code so I can cover the class_exist case.

<?php
foreach(debug_backtrace() as $info)
    {
      if(isset(
$info['args'][0]) && $info['args'][0] == $class)
      {
        if(
$info['function'] == 'class_exists')
        {
          return 
false;
        }
      }
      else 
      {
        eval(
"class $class { 
          function __construct() { 
            throw new Exception('Class [$class] not found');
          }
          
          static function __callstatic(\$m, \$args) {
            throw new Exception('Class [$class] not found');
          } 
        }"
); 
        
        return 
true;
      }
    }
?>


I do have a flag to know if I'm in debug mode or not (I'm using the framework Symfony) so I check it before :

<?php
if(!SF_DEBUG)
    {
      return 
false;
    }
?>


#2  By Markus Malkusch on Tuesday, 18 May 2010, 15:00
I have written a general purpose Autoloader: http://php-autoloader.malkusch.de/en/

I liked your Exception Concept and wanted to adopt it. But this is not applicable for my autoloader, as it would still cause fatal errors when returning a class instead of an interface.

Post your comment

Your name:

Comment:

Protection code:
 

Note: Comments to this article are premoderated. They won't be immediately published.
Only comments that are related to this article will be published.


© 2014 onPHP5.com