Автозагрузка классов в 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 классов).

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

Комментариев: 6 RSS

1Ян19-12-2020 14:16

Подскажи, в чём может быт причина. Пишу в index.php по примеру из статьи:

spl_autoload_register( function( $class ) {
 echo '<b>autoload: ' . $class . '</b>';
} );

И... ничего, такое ощущение, что spl_autoload_register просто не срабатывает. Папки с классами есть, лежат в той же директории, что и index.php

2MAX19-12-2020 14:31

Функция работает только, когда класс создаётся.

3Владимир21-01-2021 12:38

Спасибо за идею, дорогой! сделал себе автозагрузку.

Одного не понял, зачем нужна рекурсия директорий - зачем вкладывать файл класса в каталог с именем этого же класса.

Разве не достаточно иметь один верхний каталог Namespace и в нем файлы class1.php, class2.php ...

Слышал что то про вложенные классы, но не понимаю что это. Не намекнете или что почитать?

4MAX21-01-2021 13:22

Здесь нет никакой рекурсии. Имя каталога совпадает с его namespace. В проекте могут быть разные «верхние» каталоги, а значит и расположение классов.

5Аноним24-01-2021 20:26

Здесь нет никакой рекурсии. Имя каталога совпадает с его namespace. В проекте могут быть разные «верхние» каталоги, а значит и расположение классов

Проблема в том, что Ваша функция на аргумент класса \Namespace1\class1 возвращает путь "/Namespase1/class1/class1.php"

А моя модифицированная возвращает корректный путь: "/Namespace1/class1.php"

function 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;
}

6MAX25-01-2021 08:49

Namespace это и есть каталог класса. Вы можете организовать любую другую схему в рамках своей системы.

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

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

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