Март 05 2008

Нюансы автоматической загрузки классов в PHP с использованием __autoload()

Категория: PHPgugglegum @ 18:25

(данный пост был в последствии изменен, см. примечание в конце)

Как известно, в PHP, начиная с 5-ой версии, появилась замечательная возможность загружать классы автоматически по мере возникновения в них необходимости. Теперь, вместо того, чтобы писать в файле каждого класса список используемых файлов классов, достаточно где-нибудь в файле инициализации объявить функцию с именем __autoload(), которая получает в качестве параметра имя требуемого класса и пытается его загрузить. Эта функция — своего рода последний рубеж перед возникновением ошибки “Fatal Error”.

У данного метода есть два преимущества:

  1. Не нужно отслеживать где и какие классы мы используем. Ведь ошибиться здесь очень легко. Например, в одном классе мы используем другой класс, который уже был использован и загружен ранее, и который мы забыли явно объявить через require_once. Скрипт работает нормально, т.к. класс загружен. Но вот мы начали вызывать этот класс из другого места и там требуемый класс еще не был загружен — возникнет фатальная ошибка. Если необходимость в этом классе возникает не постоянно, а лишь в некоторых редких ситуациях (например, при обработке исключений), то отловить это будет крайне сложно.
  2. Нет избыточных вызовов 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 в одном коде”.

Один отзыв на “Нюансы автоматической загрузки классов в PHP с использованием __autoload()”

  1. Димка says:

    У меня вот так реализовано
    //Файл 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 );
    }

Напишите что Вы об этом думаете


*


*


rel=nofollow включен, спам не поднимет Page Rank Вашего сайта



Anti-Spam Image
Введите этот код, чтобы подтвердить, что вы человек
*

* — Обязательно для заполнения

Перейти на главную страницу