Сайт вебмастера

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

23-04-2019Время чтения ~ 5 мин.PHP 15806

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

Похожие записи
Комментарии (8) RSS
1 Ян 2020-12-19 14:16:45

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

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

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


2 Admin 2020-12-19 14:31:15 admin

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


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

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

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

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

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


4 Admin 2021-01-21 13:22:23 admin

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


5 Аноним 2021-01-24 20:26:58
Здесь нет никакой рекурсии. Имя каталога совпадает с его 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;
}

6 Admin 2021-01-25 08:49:41 admin

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


7 Вероломство 2021-04-22 14:40:13

для file_exists директория - это тоже файл, поэтому автозагрузчик будет пытаться подключить ПАПКА.php )))

надо проверять is_file потому что если это файл то ясен пень он и существует )))

и кто там после выброса ещё и die сделал выброс и так останавливает выполнение кода


8 Admin 2021-04-22 15:08:30 admin

Формально, касательно функции 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