Автозагрузка классов в PHP
23-04-2019Время чтения ~ 5 мин.PHP 15914
Когда в проекте много 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 просто не срабатывает. Папки с классами есть, лежат в той же директории, что и index.php
Функция работает только, когда класс создаётся.
Спасибо за идею, дорогой! сделал себе автозагрузку.
Одного не понял, зачем нужна рекурсия директорий - зачем вкладывать файл класса в каталог с именем этого же класса.
Разве не достаточно иметь один верхний каталог Namespace и в нем файлы class1.php, class2.php ...
Слышал что то про вложенные классы, но не понимаю что это. Не намекнете или что почитать?
Здесь нет никакой рекурсии. Имя каталога совпадает с его namespace. В проекте могут быть разные «верхние» каталоги, а значит и расположение классов.
Проблема в том, что Ваша функция на аргумент класса
\Namespace1\class1
возвращает путь/Namespase1/class1/class1.php
А моя модифицированная возвращает корректный путь:
/Namespace1/class1.php
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