Ниже опубликован перевод статьи Matthew Weier O’Phinney Developing a Comprehensive Autoloader.

В этой статье я буду рассуждать о развитии и нововведениях Zend_Loader_Autoloader и смежного функционала. Тем не менее, основная цель статьи — показать различные проектировочные решения, которые ведут к созданию всеобъемлющего автозагрузчика для ваших PHP-приложений. Автозагрузка, внешне тривиальная задача, имеет много тонкостей, которые часто упускают.

История

Начиная с релиза Zend Framework 1.0, мы имеем Zend_Loader::autoload() и Zend_Loader::registerAutoload() (который использовал функцию PHP spl_autoload_register()). В течение этого времени мы сталкивались с проблемами в их использовании.
Во-первых, мы пытались загрузить класс используя прямой вызов include. Если это не срабатывало и вы получали notice, указывающий, что include потерпел неудачу — и это раздражало некоторых пуристов, которые считают, что автозагрузчик не должен выдавать никакие ошибки, так как другие автозагрузчики в цепочке могут загрузить этот класс.
Мы пытались использовать оператор подавления (‘@’). Это избавило от вывода ошибок (но они по-прежнему записывались в логи) — но также имеет очень неприятный побочный эффект: если подключаемый класс имел ошибки уровней парсинга или компиляции, ничего не выводилось, и вы видели пустой белый экран без какой-либо информации.
Тогда мы попытались сначала проверять файл на возможность чтения. Тем не менее, это является очень дорогостоящим процессом, подключение множества файлов сильно нагружало сервер; проблем с производительностью было достаточно, чтобы вернуться к прежнему варианту.
Тогда мы пришли к нынешнему варианту: он был рядом, но он не подходил для решения всех проблем используя упрощённый подход. Таким образом родился Zend_Loader_Autoloader.

Zend_Loader_Autoloader: Задачи и Дизайн

Этот компонент предназначен для решения следующих задач:

  • Обеспечение соответствия имён. Если namespace-префикса класса нет в списке зарегистрированных namespace’ов, будет просто возвращён false. Это устраняет множество проблем с самого начала.
  • Разрешение автозагрузки как запасного варианта автозагрузки. В случае, если команда широко распространена, или используется неопределённый набор namespace-перфиксов, автозагрузчик следует настраивать таким образом, чтобы он искал любые префиксы.
  • Разрешение оператора подавления ошибок. Мы понимаем — и PHP-сообщество тоже — подавление ошибок плохая мысль. Это является маской для реальных проблем приложения. Таким образом, по умолчанию мы отключили его. В то же время, если разработчик настаивает на включении, мы позволяем сделать это.
  • Можно определить свой собственный callback для автозагрузки. Некоторые разработчики не хотят использовать Zend_Loader::loadClass() для автозагрузки, но хотят сделать это через механизмы ZF. Мы позволяем использовать альтернативный callback для автозагрузки.
  • Можно управлять цепочкой вызовов spl_autoload. Назначение этого в возможности использования дополнительных автолоадеров, например, загрузчиков ресурсов для классов, которые могут быть зарегистрированы до или после основного ZF-автозагрузчика.

Последний пункт оказался довольно интересным. Как часть стратегии автозагрузки ZF, мы разработали Zend_Loader_Autoloader_Resource и его брата Zend_Application_Module_Autoloader; они позволяют вам сопоставить namespace-префиксы и компоненты с каталогами и подкаталогами — которые позволяют вам использовать деревья наподобие следующего:

1
2
3
4
5
6
7
8
.
|-- forms
| `-- Guestbook.php // Foo_Form_Guestbook
|-- models
| |-- DbTable
| | `-- Guestbook.php // Foo_Model_DbTable_Guestbook
| |-- Guestbook.php // Foo_Model_Guestbook
| `-- GuestbookMapper.php // Foo_Model_GuestbookMapper

Экземпляры этих автозагрузчиков имеют метод autoload(), который мы пытались зарегистрировать с spl_autoload(). Проблема заключается в том, что, если мы хотите добавить автозагрузчик в начало стека, или вставить между своим и автозагрузчиком по умолчанию, он не будет работать. По какой-то причине, возвращаемое значение spl_autoload_functions() не содержит некоторые callback’и, переданные туда. Единственным, что может быть повторно зарегистрировано, были callback’и с вызовом статичных методов — которые мы не хотим использовать, чтобы мы могли иметь несколько автозагрузчиков ресурсов для разных областей наших проектов.
Чтобы решить эту проблему, мы разработали реестр с Zend_Loader_Autoloader для поддержки этих callback’ов автозагрузчика. Как выяснилось, это было отличное решение, так как это позволило нам сделать кое-что ещё: автолоадеры ориентированы на некоторый namspace-префикс, что позволяет нам запускать только те автолоадеры, которые используют нужный нам префикс.

Zend_Loader_Autoloader: Миграция

В релизом Zend Framework 1.8.0 мы пометили Zend_Loader::autoload и Zend_Loader::registerAutoload() как deprecated. Простейший путь заменить их на новый подход:

1
2
3
4
5
6
7
// Если вы имели:
require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

// в большинстве случаев, вам подойдёт код:
require_once 'Zend/Loader/Autoloader.php';
Zend_Loader_Autoloader::getInstance();

Тем не менее, помните: Zend_Loader_Autoloader основан на пространствах имён. По умолчанию, зарегистрированы только Zend_ и ZendX_. Если вы имеете дополнительные пространства имён, вы должны сделать одно из нижеперечисленного:

  • Предпочтительно: Регистрируйте каждый namespace в автозагрузчике. Вы можете сделать это используя метод registerNamespace(), который принимает один префикс namespace или массив из них. Префикс namespace должен включать замыкающий символ подчёркивания, чтобы уменьшить возможность коллизий
    1
    2
    3
    $autoloader = Zend_Loader_Autoloader::getInstance();
          $autoloader->registerNamespace('Foo_');
          $autoloader->registerNamespace(array('Foo_', 'Bar_'));
  • Альтернатива: если вы не знаете, какой namespace используется, или вы используете библиотеки компонентов, например, PEAR, который не имеет общего префикса пространства имён, вы можете перевести автозагрузчик в fallback-режим:
    1
    2
    $autoloader = Zend_Loader_Autoloader::getInstance();
          $autoloader->setFallbackAutoloader(true);

Если вы продвинутый пользователь, можете также начать использовать Zend_Application. Zend_Application принимает опцию конфигурации “autoloaderNamespaces”, которая может быть массивом используемых пространств имён.

Загрузка ресурсов

Я уже упоминал автозагрузку ресурсов ранее. Zend_Loader_Autoloader_Resource определяет API, через который вы указываете namespace-префикс классов и базовый путь к файлам с этим префиксом. Затем вы можете зарегистрировать “ресурсы”, которые будут привязаны к определённому префиксу и располагаться в подкаталоге базового пути префикса. Это особенно полезно для группировки кода приложения в случае, если вы не хотите жёсткой иерархии, а вместо этого хотите сосредоточиться на группировке по обязанностям.
Как они связаны с Zend_Loader_Autoloader? Довольно просто: конструктор Zend_Loader_Autoloader_Resource регистрирует экземпляр Zend_Loader_Autoloader, использующий предоставляемый namespace-перфикс. Это позволяет основному экземпляру автозагрузчика перехватывать классы с этим namespace-префиксом и передать их в заргузчик ресурсов для изменения.
Как пользоваться автозагрузчиками ресурсов на практике? Простейший путь к пониманию, как они работают, взглянуть изнутри на Zend_Application_Module_Autoloader, который расширяет автозагрузчик ресурсов и добавляет рекоммендуемые префиксы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Zend_Application_Module_Autoloader extends Zend_Loader_Autoloader_Resource
{
 public function __construct($options)
 {
 parent::__construct($options);
 $this->initDefaultResourceTypes();
 }

 public function initDefaultResourceTypes()
 {
 $basePath = $this->getBasePath();
 $this->addResourceTypes(array(
 'dbtable' => array(
 'namespace' => 'Model_DbTable',
 'path' => 'models/DbTable',
 ),
 'form' => array(
 'namespace' => 'Form',
 'path' => 'forms',
 ),
 'model' => array(
 'namespace' => 'Model',
 'path' => 'models',
 ),
 'plugin' => array(
 'namespace' => 'Plugin',
 'path' => 'plugins',
 ),
 'service' => array(
 'namespace' => 'Service',
 'path' => 'services',
 ),
 'viewhelper' => array(
 'namespace' => 'View_Helper',
 'path' => 'views/helpers',
 ),
 'viewfilter' => array(
 'namespace' => 'View_Filter',
 'path' => 'views/filters',
 ),
 ));
 $this->setDefaultResourceType('model');
 }
}

Ресурс — это имя, соответствующее пространству имён компонента (которое добавлено в пространство имён автозагрузчика) и путь (относительно базового пути автозагрузчика). На практике это выглядит примерно так:

1
2
3
4
$loader = new Zend_Application_Module_Autoloader(array(
 'namespace' => 'Blog',
 'basePath' => APPLICATION_PATH . '/modules/blog',
));

Теперь можно просто инстанцировать соответствующие классы:

1
2
// класс расположен в файле APPLICATION_PATH . '/modules/blog/models/Comments.php':
$comments = new Blog_Model_Comments();

Замечание: если вы используете ресурс для бутстраппинга модулей, и определяете бутстраппинг в ваших модулях, которые используют Zend_Application_Module_Bootstrap, модуль автозагрузки ресурсов будет настроен автоматически.
Это устраняет необходимость во множестве выражений require_once, или настройке include_path, что также поможет вам отвязать ваш код от файловой системы и структура проекта будет более гибкой.

Зачем загружать всё автоматически?

Итак, зачем нам загружать всё автоматически?
В прошлом, когда существовал ряд проблем с автозагрузкой, и она работала медленнее, чем простое использование require_once. Тем не менее, начиная с серии релизов PHP 5.2, эта ситуация резко изменилась с добавлением realpath-кэша. Кроме того, в крупных фреймворках как Zend_Framework, зачастую есть ряд классов, которые являются зависимыми — и многочисленные require_once в начале каждого класса могут привести к значительным замедлениям, особенно в системах, где I/O является дорогостоящим.
Короче говоря, использовать автозагрузчик надо потому, что он откладывает загрузку файлов до последнего момента и гарантирует, что он будет загружен только один раз, это может быть серьёзным повышением производительности — особенно, если вы потратите время на чистку кода от вызовов require_once, прежде чем перейдёте к развёртыванию.
Другая причина, о которой я намекнул в последнем разделе: используя автозагрузку, у вас нет необходимости беспокоиться о том, где класс существует в вашем проекте. Во многих основанных на ZF проектах, с которыми я работал или ознакомлялся, имели много комплексных, сложных систем, используемых в попытке сопоставить классы с файлами, таких как изменение include_path (и помните, чем больше будет каталогов в include_path, тем сильнее это ухудшит производительность), использование Zend_Controller_Front для поиска каталогов модулей с целью создать относительный путь поиска, или подстановок в стиле dirname(__FILE__). Используя автозагрузчик ресурсов, вы можете выбрать, как вы хотите организовать подмножество вашего кода, написать правила и зарегистрировать их — и эти классы будут доступны отовсюду в вашем приложении.

Выводы

Автозагрузка кажется, и часто таки есть, простой, прямолинейной задачей. Тем не менее, создание всеобъемлющего автозагрузчика может быть сложной задачей. Вам необходимо беспокоиться обо всём, от приятных игр с другими автозагрузчиками до уверенности, что вы получаете актуальные сообщения об ошибках;от возможности конфигурирования автозагрузчиков в стеке до понимания ограничений и принуждений автозагрузчика SPL; от сопоставления стилей классов PEAR и ZF до маппинга классов приложения.
Надеюсь, теперь у вас сложилось лучше понимание того, как работают автозагрузчики, и некоторые проектировочные решения, которые следует учитывать при создании своего автозагрузчика для PHP.тем не менее, вы можете использовать решение, предложенное в этой заметке: Zend_Loader_Autoloader.

8 Comments

  1. Александр Махомет says:

    Сергей, авторазгрузчик это сильно :)
    Спасибо за перевод.

  2. Александр Махомет says:

    Allow the autoloader to act as a fallback autoloader. – Разрешение автозагрузки как запасного варианта авторазгрузки.

    Плохо получилось.

  3. Сергей Митрошин says:

    > авторазгрузчик это сильно
    Пальцы заплетаются :)
    Спасибо за замечание

    > Плохо получилось.
    Буду благодарен за поправки.

  4. Юрий Истомин says:

    Так я не понял – они предлагают всем-всем моим классам подобавлять префиксы? Моделям Model_Foo, плагинам Plugin_Foo, если это все лежит в модулях, то Modulename_Model_Foo и т.д.?

  5. Сергей Митрошин says:

    В статье подразумевается, что разработчик уже придерживается этого стиля именования.
    На самом деле, это довольно удобно.

  6. Юрий Истомин says:

    А если у меня у классов префиксов нет (ну или есть, но суффиксы – FooModel), то мне Zend_Loader_Autoloader не поможет?

  7. Сергей Митрошин says:

    Думаю, можно его доработать под такой функционал. Но сам я пока не вникал.

  8. Олег says:

    Большое спасибо! Очень помогла статья!

Leave a Reply