Март 05 2008
Нюансы автоматической загрузки классов в PHP с использованием __autoload()
(данный пост был в последствии изменен, см. примечание в конце)
Как известно, в PHP, начиная с 5-ой версии, появилась замечательная возможность загружать классы автоматически по мере возникновения в них необходимости. Теперь, вместо того, чтобы писать в файле каждого класса список используемых файлов классов, достаточно где-нибудь в файле инициализации объявить функцию с именем __autoload(), которая получает в качестве параметра имя требуемого класса и пытается его загрузить. Эта функция — своего рода последний рубеж перед возникновением ошибки “Fatal Error”.
У данного метода есть два преимущества:
- Не нужно отслеживать где и какие классы мы используем. Ведь ошибиться здесь очень легко. Например, в одном классе мы используем другой класс, который уже был использован и загружен ранее, и который мы забыли явно объявить через require_once. Скрипт работает нормально, т.к. класс загружен. Но вот мы начали вызывать этот класс из другого места и там требуемый класс еще не был загружен — возникнет фатальная ошибка. Если необходимость в этом классе возникает не постоянно, а лишь в некоторых редких ситуациях (например, при обработке исключений), то отловить это будет крайне сложно.
- Нет избыточных вызовов require_once. Ведь если файл с классом загружен, то нет смысла вызывать каждый раз в каждом классе require_once одних и тех же файлов классов.
Однако, есть у данного метода и недостатки. Функцию автоматической загрузки класса можно объявить только одну. Если проект состоит из нескольких независимых частей, написанными разными разработчиками, у которых разные соглашения относительно месторасположения и именования файлов с классами, то возникает проблема. Решается она исключительно созданием гибрида из двух этих функций.
Но даже если сторонние скрипты не используют __autoload() (возможно, они вообще ориентированы на PHP4), как правило, они имеют встроенный механизм загрузки, который может выглядеть, например, так:
if (!class_exists($ClassName)) {
$PrefixArray = $Context->Configuration['LIBRARY_NAMESPACE_ARRAY'];
$PrefixArrayCount = count($PrefixArray);
$i = 0;
for ($i = 0; $i < $PrefixArrayCount; $i++) {
$File = $Context->Configuration['LIBRARY_PATH'].$PrefixArray[$i].'/'.$PrefixArray[$i].'.Class.'.$ClassName.'.php';
if (file_exists($File)) {
include($File);
break;
}
}
// If it failed to find the class, throw a fatal error
if (!class_exists($ClassName)) $Context->ErrorManager->AddError($Context, 'ObjectFactory', 'NewObject', 'The "'.$ClassName.'" class referenced by "'.$ClassLabel.'" does not appear to exist.');
}
(фрагмент кода форума Vanilla 1.1.4)
Проблема в том, что функция class_exists(), которая всего лишь проверяет загружен класс или нет, в случае отсутствия класса вызывает функцию __autoload() (кстати, в документации про это нет ни слова и лично мне такое поведение PHP кажется неправильным), и если вы используете пример из документации:
<?php
function __autoload($class_name) {
require_once $class_name . '.php';
}
$obj = new MyClass1();
$obj2 = new MyClass2();
?>
то это приведет к фатальной ошибке, т.к. ваш __autoload() не найдет класс, относящийся к чужому скрипту, да его может и вовсе не существовать — логика программы может быть построена так: если класс есть, то делаем одно, если класса нет, то делаем другое. А тут у нас жестко — require_once. Нет класса? Фатальная ошибка!
Таким образом, функция, которая предоставляет PHP-движку “a last chance to load the class before PHP fails with an error”, сама приводит к фатальной ошибке. Причем, сообщение PHP о фатальной ошибке будет указывать не на то место, где было обращение к несуществующему классу, а всегда на одно и то же место — место require_once в __autoload(), что при отладке совершенно бесполезно.
Решение состоит в том, чтобы заменить require_once на @include_once. Последний не вызывает фатальной ошибки, а “@” вообще отключает вывод варнингов. Мой init.php выглядит следующим образом:
<?php
define('STD_CLASSES_DIR', dirname(__FILE__).'/std_classes');
define('PEAR_DIR', dirname(__FILE__).'/PEAR');
ini_set('include_path', implode(PATH_SEPARATOR, array(
dirname(dirname(__FILE__)),
STD_CLASSES_DIR,
PEAR_DIR,
dirname(__FILE__).'/includes',
)));
require_once 'class_func.php';
function __autoload($class) {
@include_once(convert_class_to_filename($class));
}
if (ini_get('magic_quotes_gpc'))
include_once 'magic_quotes_off.php';
mb_internal_encoding('UTF-8');
?>
А class_func.php выглядит так:
<?php
function convert_class_to_filename($class) {
return str_replace('_', '/', $class).'.php';
}
function class_file_exists($class) {
$file = convert_class_to_filename($class);
$dirs = explode(PATH_SEPARATOR, ini_get('include_path'));
foreach ($dirs as $dir) {
if (is_file($dir.'/'.$file))
return true;
}
return false;
}
?>
В моем случае всегда есть однозначное соответствие между именами классов и именами соответствующих файлов на диске и это очень удобно.
Добавлено 29.09.2008:
Проблема единственности функции __autoload давно уже не проблема — читайте “Несколько функций __autoload в одном коде”.

Март 10th, 2010 at 14:00
У меня вот так реализовано
//Файл Loader.php
class Loader
{
function Loader()
{
}
static function Company()
{
static $Company;
if(!$Company)
{
$Company = new Company();
}
return $Company;
}
static function DB( $config = false )
{
static $DB;
if( !$DB )
{
$DB = new MySQL($config);
}
return $DB;
}
static function Data()
{
static $Data;
if(!$Data)
{
$Data = new Data();
}
return $Data;
}
static function Request()
{
static $Request;
if(!$Request)
{
$Request = new Request();
}
return $Request;
}
static function Session()
{
static $Session;
if(!$Session)
{
$Session = new Session();
}
return $Session;
}
/**
*
* @param string $className
*/
static function autoLoader( $className )
{
$sDocRoot = $_SERVER[’DOCUMENT_ROOT’];
$directories = array(
”,
$sDocRoot. ‘/incl/class/’,
‘./incl/class/’,
);
$fileNameFormats = array(
‘%s.php’,
‘%s.class.php’,
‘class.%s.php’,
‘%s.inc.php’
);
$className = str_ireplace( ‘_’, ‘/’, $className );
if( @include_once( $className.’.php’ ) )
{
return;
}
foreach( $directories as $directory )
{
foreach( $fileNameFormats as $fileNameFormat )
{
$path = $directory . sprintf( $fileNameFormat, $className );
if( file_exists( $path ) )
{
@include_once( $path );
return;
}
}
}
}
}
function __autoload( $class_name )
{
Loader::autoLoader( $class_name );
}