Автозагрузка классов в PHP
23-04-2019Время чтения ~ 5 мин.PHP 16976
Когда в проекте много php-классов, то возникает проблема подключения их файлов. Например есть классы Class1 и Class2. Для того, чтобы их использовать, вначале нужно подключить их php-файлы. Само по себе подключение — это обычные require или include, поэтому задача сводится к тому, чтобы указать правильный путь к php-файлу. По мере, того как классов становится всё больше, возникает серьезная проблема с подключением каждого файла класса. В идеале процесс подключения должен быть автоматизирован и в PHP для этого используется механизм автозагрузчика.
Автозагрузчик будет срабатывать каждый раз, когда в коде встретится неизвестный класс. Сам по себе автозагрузчик представляет собой обычную функцию, которая и выполняет подключение файла с помощью require. Функция автозагрузчика должна быть зарегистрирована через специальную функцию spl_autoload_register. При этом функций можно зарегистрировать несколько — PHP последовательно их выполнит пока не подключит файл с нужным классом.
Давайте рассмотрим несколько примеров, чтобы лучше понять как это всё работает (исходные файлы можно скачать в конце статьи).
Создадим простой проект, где будет два класса. Но перед этим я хочу отдельно отметить, что существуют общепринятые стандарты кодирования, которые именуются как PSR. В отношении автозагрузки используется PSR-4 (и старый PSR-0). В этом стандарте предлагается несколько правил по которым принято работать с файлами классов. Они несложные, и если кратко, то вот основные:
- Каждый класс размещается в отдельном файле.
- Имя файла (+ расширение «.php») совпадает именем класса.
- Пространство имен (namespace) класса совпадает его расположением в каталогах.
- Может быть «базовый каталог», относительно которого указывается namespace.
Исходя из этого создадим проект на два класса. Разместим их в каталоге lib:
lib/ Class1/ Class1.php Class2/ Class2.php index.php
Файл index.php будет главным. Файлы классов пусть будут простыми:
// Class1.php
class Class1 {
function __construct()
{
echo 'construct Class1 <br>';
}
}
// Class2.php
class Class2 {
function __construct()
{
echo 'construct Class2 <br>';
}
}
В классах я пока специально не указываю namespace.
Теперь перейдём к index.php. Первым делом нам нужно определиться с автозагрузчиком. Поскольку мы придерживаемся PSR-4, то автозагрузчик может определить имя файла по имени класса.
spl_autoload_register(function($class) {
echo '<b>autoload: ' . $class . '</b><br>';
$fn = 'lib/' . $class . '/' . $class . '.php';
if (file_exists($fn)) require $fn;
});
$o1 = new Class1();
$o2 = new Class2();
Автозагрузчик мы указали как анонимную функцию к spl_autoload_register. В её параметре PHP будет передавать имя нового класса. То есть когда пойдёт выполнение строчки:
$o1 = new Class1();
PHP определит, что класс неизвестен и будет запущена функция автозагрузчика. В этой функции мы формируем имя файла и если он есть, то происходит его подключение. В результате в браузере будет выведено:
autoload: Class1 construct Class1 autoload: Class2 construct Class2
Если мы попробуем создать объекты уже подключенных классов, то автозагрузчик не сработает:
$o1 = new Class1(); $o2 = new Class2(); $o3 = new Class2(); $o4 = new Class2(); $o5 = new Class1(); autoload: Class1 construct Class1 autoload: Class2 construct Class2 construct Class2 construct Class2 construct Class1
Если мы попробуем использовать несуществующий класс, например
$o6 = new Class3();
то получим ожидаемую ошибку: Fatal error: Class 'Class3' not found.
Теперь рассмотрим вопрос использования namespace. В нашем автозагрузчике мы использовали каталог lib в качестве хранилища всех классов. Но, в реальном проекте каталоги могут быть совершенно другими, а значит каталог нужно определять исходя из вызываемого класса. Согласно стандарту это будет так:
$o1 = new lib\Class1(); $o2 = new lib\Class2();
Очевидно, что из автозагрузчика нужно убрать lib и добавим ещё вывод результирующего имени файла для контроля:
spl_autoload_register(function($class) {
$fn = $class . '/' . $class . '.php';
echo '<b>autoload: ' . $class . '</b> file: ' . $fn . '<br>';
if (file_exists($fn)) require $fn;
});
$o1 = new lib\Class1();
Результат:
autoload: lib\Class1 file: lib\Class1/lib\Class1.php Fatal error: Class 'lib\Class1' not found
То есть автозагрузчик получает полное имя класса, включая namespace из-за чего имя файла формируется неверно. По этой причине имя класса следует предварительно обработать, чтобы отдельно получить каталоги и имя файла. Реализация функции может быть различной, я покажу свой вариант:
spl_autoload_register(function($class) {
$a = explode('\\', $class);
$last = array_pop($a);
$fn = $class . '/' . $last . '.php';
$fn = str_replace('\\', '/', $fn);
echo '<b>autoload: ' . $class . '</b> file: ' . $fn . '<br>';
if (file_exists($fn)) require $fn;
});
Если с этой функцией выполнить:
$o1 = new lib\Class1(); $o2 = new lib\Class2();
то получим:
autoload: lib\Class1 file: lib/Class1/Class1.php Fatal error: Class 'lib\Class1' not found
То есть имя файла сформировано верно, но класс всё равно не найден. В чём же дело?
Всё из-за того, что наши классы объявлены в глобальном пространстве имён, а вызываем мы их из namespace lib. То есть в файлы классов следует добавить строчку:
namespace lib; ...
После чего всё замечательно работает:
autoload: lib\Class1 file: lib/Class1/Class1.php construct Class1 autoload: lib\Class2 file: lib/Class2/Class2.php construct Class2
По этой же схеме можно строить классы с любым уровнем вложенности. На этом можно было бы сказать, что задача по автозагрузчику выполнена, но не спешите. :-) Хотя у нас теперь поддерживается и вложенность и namespace, но может возникнуть задача, когда путь к файлу будет лежать в другом каталоге. Например в рамках проекта нужно переместить каталог lib в подкаталог modules. Очевидно, что такая смена потребует не только правки исходного кода создания объектов, но и самих классов, точнее их namespace.
Чтобы избежать таких проблем, автозагрузчик должен ориентироваться на базовый каталог и подключать файлы относительно него. В простом случае, базовый каталог задаётся с помощью константы. Например так:
define('LIB_DIR', __DIR__ . '/modules/');
spl_autoload_register(function($class) {
$a = explode('\\', $class);
$last = array_pop($a);
$fn = $class . '/'. $last . '.php';
$fn = LIB_DIR . str_replace('\\', '/', $fn);
echo '<b>autoload: ' . $class . '</b> file: ' . $fn . '<br>';
if (file_exists($fn)) require $fn;
});
Если классы подключаются из нескольких базовых каталогов, то в автозагрузчике последовательно проверяются файлы, пока не будет найден подходящий. Другой способ — это зарегистрировать ещё одну функцию автозагрузчика, которая будет проверять уже «свой каталог».
В некоторых php-фреймворках используются т.н. карты классов (classmap), которые представляют собой массив, где указываются соответствия класса и его php-файла. Конечно это не самая лучшая идея, поскольку требует подготовки этого массива. Когда классов много, то classmap оказывается очень большим (хороший антипример — Yii, где нужно указывать более 400 классов).
Подскажи, в чём может быт причина. Пишу в index.php по примеру из статьи:
spl_autoload_register( function( $class ) {<br> echo '<b>autoload: ' . $class . '</b>';<br>} );И... ничего, такое ощущение, что spl_autoload_register просто не срабатывает. Папки с классами есть, лежат в той же директории, что и index.php
Функция работает только, когда класс создаётся.
Спасибо за идею, дорогой! сделал себе автозагрузку.
Одного не понял, зачем нужна рекурсия директорий - зачем вкладывать файл класса в каталог с именем этого же класса.
Разве не достаточно иметь один верхний каталог Namespace и в нем файлы class1.php, class2.php ...
Слышал что то про вложенные классы, но не понимаю что это. Не намекнете или что почитать?
Здесь нет никакой рекурсии. Имя каталога совпадает с его namespace. В проекте могут быть разные «верхние» каталоги, а значит и расположение классов.
Проблема в том, что Ваша функция на аргумент класса
\Namespace1\class1возвращает путь/Namespase1/class1/class1.phpА моя модифицированная возвращает корректный путь:
/Namespace1/class1.phpfunction ClassAutoloader(string $class) { $array = explode('\\', $class); $className = array_pop($array); // имя класса $nameSpace = str_replace('\\', '/', implode('/', $array)); $filePath = LIB_DIR . '/' . $nameSpace . '/' . $className . '.class.php'; // echo 'autoload: ' . $class . ' file: ' . $filePath . PHP_EOL; if (!file_exists($filePath)) { throw new Exception("No such file for autoload: " . $filePath); die; } require_once $filePath; }Namespace это и есть каталог класса. Вы можете организовать любую другую схему в рамках своей системы.
для file_exists директория - это тоже файл, поэтому автозагрузчик будет пытаться подключить ПАПКА.php )))
надо проверять is_file потому что если это файл то ясен пень он и существует )))
и кто там после выброса ещё и die сделал выброс и так останавливает выполнение кода
Формально, касательно функции file_exists() замечание верное. Но по факту мы всегда подключаем файл КЛАСС.php и чтобы получилось КАТАЛОГ.php нужно использовать php-класс без namespace. А это уже никакой не PSR-4.
Другой случай, когда namespace целенаправлено указывается на каталог, например при использовании карты классов (class map). Тогда да, нужна проверка на is_file() и is_dir(), чтобы подключить корректный файл. У меня в Alpbireo как раз так и сделано. В CodeIgniter, кстати похожая схема.
https://github.com/maxsite/albireo/blob/main/albireo/lib/functions.php#L996