onPHP5.com

PHP5: Articles, News, Tutorials, Interviews, Software and more
  
Featured Article:
Learning PHP Data Objects
 
 
Mon, 12 May 2008
 Home   About   Contribute   Contact Us   Polls 
Top Tags
article book conference mysql mysqli news onphp5 oop pdo php5 poll prado security solar symfony unicode zend zend core zend framework zend platform
More tags »

Not logged in
Login | Register

Exceptions in __autoload()

« Happy 2008! Advocating Namespaces »

By dennisp on Wednesday, 19 December 2007, 20:34
Published under: article   oop   php5
Views: 2691, comments: 4

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

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

Comments

#1  By nhm tanveer hossain khan (hasa on Monday, 21 January 2008, 17:39
seems like, php become more like dynamic language. this sounds good.


#2  By Anonymous on Tuesday, 22 January 2008, 19:18
You still have to make sure that $className isn't something like "Class1{}; mysql_query('DELETE FROM users WHERE 1'); class Class2".


#3  By Anonymous on Friday, 01 February 2008, 17:47
#2: When the hell would it ever be that? If you're calling a class name from user input your dumb as hell.


#4  By Giel on Monday, 03 March 2008, 11:20
#3: It could be a gap in the security from a GET.

Indeed, doing -ANYTHING- with user input other that stripping and checking it is plain wrong. Not to mention evaluating or even executing...

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.


© 2008 onPHP5.com