Автозагрузка классов в PHP

Когда в проекте много 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 классов).

Вы можете скачать готовые примеры.

Оставьте комментарий!

Комментарий будет опубликован после проверки. Вы соглашаетесь с правилами сайта.

(обязательно)