Autoload

Автозагрузка классов в PHP приложении.

Содержание:

Предисловие

Современные web-приложения, как правило, состоят из множества классов, каждый из которых решает определённую задачу.

Итак, по умолчанию, если в некотором нашем приложении нам необходимо использовать некий класс, скажем, MyClass, нам нужно удостоверится, что файл, в котором описан данный класс, был ранее подключён с помощью операторов require или include:

require_once ("path_to/myclass.php");
…
$instance = new MyClass();
…

Т.е. каждый класс (а также интерфейс и пр.), который мы собираемся использовать в приложении мы должны «подключить» заранее или «по ходу дела». Одна из проблем состоит в том, что зачастую приложения обладают отнюдь не линейной структурой и далеко не всегда мы можем знать заранее какие из множества классов (которые потенциально могут быть использованы в ходе выполнения скриптов), будут действительно задействованы, а какие — нет. Подключение заранее всех потенциально используемых классов ведёт к нерациональному использованию ресурса памяти, а «ручное» подключение нужных классов «по ходу дела» скорее всего приведёт к использованию разл. ухищрений в виде проверок условий и операторов включения, и в последствии - усложнению кода, и ухудшению его удобочитаемости.

В рамках данной статьи речь пойдёт именно об автоматической загрузке классов в PHP приложении.

Средства SPL

В PHP (начиная примерно с версии 5) появилась замечательная возможность автоматической загрузки ранее не загруженных классов. Т.е. когда PHP интерпретатор встречает в коде упоминание некоего класса, он теоретически может самостоятельно попытаться выполнить загрузку указанного класса. Однако, чтобы это стало возможным, нам всё же придётся описать некую функцию (метод), которая определяет, где именно находится описание нужного класса. Обычно она называется функция-загрузчик.

Не будем рассматривать здесь использование ф-ции __autoload(); т. к. этот путь ныне считается несколько устаревшим и практически полностью вытеснен повсеместно средствами из набора SPL.

Описать и зарегистрировать функцию-загрузчик можно несколькими способами, например так:

...
spl_autoload_register( function($classname){
	require_once( "/my_classes_folder/" .  $classname . ".php");	
} );
...

Стандартная функция spl_autoload_registered(); позволяет зарегистрировать функцию-загрузчик в очередь __autoload(), т. е. зарегистрировать пользовательскую функцию-загрузчик, как АВТОзагрузчик. В теории мы можем зарегистрировать и несколько функций-загрузчиков, если это необходимо. В нашем же примере мы регистрируем некую анонимную функцию, которая будет выполнять загрузку класса по переданному ей имени класса.

Работает это примерно так:
Когда PHP интерпретатор встретит в коде имя незнакомого класса, он попытается его загрузить последовательно с помощью всех таких зарегистрированных автозагрузчиков. Т.е. каждой функции-загрузчику по очереди в качестве параметра (в нашем случае - $classname) будет передано полное* имя этого «незнакомого» класса, затем чтобы функция-загрузчик смогла попытаться найти соотв. файл с описанием этого класса и включить его. Если ни одна из зарегистрированных ф-ций-загрузчиков не смогла найти и подключить класс, в итоге будет выброшена ошибка о том, что такой класс не был найден.

Более подробную информацию о spl-функциях управления автозагрузкой с примерами можно найти по ссылке: http://php.net/manual/ru/ref.spl.php

В данном примере, мы приняли, что в нашем приложении файлы с описанием классов расположены в некой папке /my_classes_folder/, а ещё имя файла класса обязано совпадать с именем самого класса. Т.е. по нашему соглашению, чтобы найти и загрузить скажем, MyClass мы должны добавить к пути /my_classes_folder/ имя класса (MyClass), затем добавить к нему расширение .php и включить с его помощью require. Что, собственно, и описано в примере выше. Все проверки на существование соотв файлов и путей и прочее здесь опущены простоты ради.

Однако и здесь возникают ньюансы, например, а что если классы в приложении имеют некую сложную иерархическую структуру или не сосредоточены в одной подпапке или же вовсе не подчиняются подобным соглашениям. Как вариант можно рассматривать некий маппинг классов и путей к файлам с их описанием, но сей вариант имеет и свои недостатки и в отдельных случаях может иметь довольно сложную реализацию.

Пространства имён, PSR-0 и PSR-4

Начиная с версии 5.3 PHP стал ещё немного ближе к другим развитым языкам программирования, и вместе с тем в нём появилась поддержка пространств имён (namespaces). В контексте нашей задачи это прежде всего означает, что для всякого класса (интерфейса, примеси) мы приобрели также возможность указывать его место в общей иерархии классов.

Подробнее о пространствах имён в PHP можно узнать из официальной документации: http://php.net/manual/ru/language.namespaces.php

Предположим, что наш класс MyClass относится к некой группе утилитарных (вспомогательных) классов, т.е входит в подгруппу Utilities, которая в свою очередь входит в общую подгруппу MyClasses:

…
namespace MyClasses\Utilities;

class MyClass { … }
…

Как такие нововведения могут помочь решить некоторые из упомянутых ранее проблем?

Рассмотрим этот пример класса MyClass, в контексте с анонимной функцией-загрузчиком, описанным выше. Начнём с того, что полное имя класса в нашем теперешнем случае будет состоять из имени пространства + имени самого класса, т. е. - \MyClasses\Utilities\MyClass. И именно в таком виде оно будет передано в кач-ве фактического параметра в функцию-загрузчик. То есть используя прежнюю схему без изменения мы получим по факту такой путь к файлу с описанием класса: /my_classes_folder/MyClasses\Utilities\MyСlass.php
А если заменим в этой строке все обратные слэши на обычные? Получим на вид типичный путь, состоящий из иерархии папок и файл MyClass.php в конце.

/my_classes_folder/MyClasses/Utilities/MyСlass.php

Модифицируем соотв. образом код нашей ф-ции-загрузчика:

...
spl_autoload_register( function($classname){
	require_once(realpath("/my_classes_root_folder/" .  str_replace("\\", "/", $classname) . ".php"));	
} );
...

Вывод:
Мы можем размещать классы нашего приложения в соотв. папках и подпапках, точно повторяя структуру пространств имён нашего приложения, и таким образом с помощью простого преобразования полного имени класса мы сможем получать частичный или полный путь к файлу с описанием этого класса.

Именно этот принцип положен в основу соглашений об именовании Классов и пространств имён а также структуре папок в приложениях, которые на сегодняшний день используют многие современные PHP фреймворки, библиотеки. Эти соглашения получили название стандартов автозагрузки PSR-0 и PSR-4. По сути PSR-4 является улучшением, базирующимся на основе того же PSR-0. Подробнее об этих стандартах можно прочесть, например, здесь.

Таким образом, мы пришли к более современному подходу к автозагрузке классов, который базируется на стандартах PSR-0 / PSR-4. Рассмотрим пример простого класса, который реализует подобный механизм автозагрузки по стандарту PSR-0.

class Autoloader
{
    /**
     * @var         Autoloader instance
     */
    protected static $instance;

    /**
     * @var array   Namespace mapping
     */
    protected static $ns_map = [];

    /**
     * Autoloader constructor.
     */
    protected function __construct(){
        spl_autoload_register([$this, 'load']);
    }

    /**
     * Register namespace root path
     *
     * @param $namespace    Root namespace
     * @param $root_path    Namespace root path
     */
    public function addNamspacePath($namespace, $root_path){
        self::$ns_map[$namespace] = $root_path;
    }

    /**
     * Load class
     *
     * @param $classname    Loader method
     */
    public function load($classname){
        if($path = $this->getClassPath($classname) ){
            require_once $path;
        }
    }

    /**
     * Get realpath to the class definition file
     */
    protected function getClassPath($classname){
        $class_path = $classname . '.php';
        if( !empty(self::$ns_map) ){
            foreach(self::$ns_map as $ns => $path){
                $lookup_pattern = sprintf('/^%s/', $ns);
                if(preg_match($lookup_pattern, $classname)){
                    $class_path = preg_replace($lookup_pattern, $path, $class_path);
                    break;
                }
            }
        }

        return realpath(str_replace('\\', '/', $class_path));
    }

    /**
     * Get loader instance
     */
    public static function getInstance(){
        if(null === self::$instance){
            self::$instance = new self;
        }

        return self::$instance;
    }

    private function __clone(){}
    private function __wakeup(){}
}

return Autoloader::getInstance();

Ниже также приведём пример использования данного автозагрузчика в коде приложения:

...
$loader = require_once '../classes/Autoloader.php';
$loader->addNamspacePath('MyClasses', __DIR__ . '/../vendor/dimmask/myclasses/');
...
$my = new \MyClasses\Utilities\MyClass();
// Этот класс будет загружен автоматически
...

Ниже предлагается краткий обзор назначения методов вышеописанного класса Autoloader. Класс использует паттерн "одиночка", почему именно так? Каждый раз, когда создаётся новый экземпляр класса, его метод load() будет зарегистрирован как автозагрузчик. Нам нет необходимости регистрировать одну и ту же функцию как загрузчик несколько раз, потому мы таким образом закрываем эту возможность.

Свойство / Метод Описание
 __construct Конструктор класса. Именно в нём производится регистрация метода load из этого же класса, как функции-загрузчика.
 addNamspacePath

$namespace
$rootpath

Регистрирует в маппинге корневой путь $root_path для загрузки классов пространства $namespace.
 load

$classname

Непосредственно сам метод-загрузчик, который пытается подгрузить класс по переданному в $classname полному имени.
 getClassPath

$classname

Вычисляет и возвращает полный путь к файлу, в котором согласно соглашениям PSR-0, должно находится описание класса $classname.
 getInstance Возвращает новый экземпляр класса Autoloader или уже существующий, если он уже был ранее создан.
 __clone Метод "зарублен" чтобы исключить возможность клонирования экземпляров класса Autoloader.
 __wakeup Метод "зарублен" чтобы исключить возможность клонирования экземпляров класса Autoloader при десериализации.

Использование загрузчика Composer

В современной практике разработки web-приложений крайне редко весь функционал приложения разрабатывается "с нуля". Как правило приложение опирается на какой-л набор уже готовых решений (библиотек), которые поставляются в виде т.н. пакетов, которые устанавливаются в отдельную папку (обычно это папка /vendor ).

Composer - это менеджер таких пакетов. Он позволяет управлять пакетами а также их зависимостями. Кроме того, composer имеет собственные средства для автозагрузки классов, описанных в этих подключаемых библиотеках но не только. Ниже описан пример того, как можно спользовать загрузчик composer для загрузки "собственных" классов приложения.

Структура мини-проекта

Предположим, у нас есть следующая структура папок приложения.
папка /classes содержит непосредственно классы приложения, папка /public - публичные файлы (DOCUMENT_ROOT).

Для реализации нашего плана, прежде всего нужно установить сам composer. Это можно сделать несколькими способами. Не буду описывать здесь весь процесс установки composer, найти эту информацию можно здесь: https://getcomposer.org/doc/00-intro.md. Итак, предположим, что композер уже установлен. Переходим в консоли в папку с проектом, где находятся наши /public и /classes. Далее - в папке выполним команду инициализации проекта composer:

$ composer init

После чего в интерактивном режиме composer запросит у нас название пакета, зависимости и прочие параметры. Опять же, не буду описывать весь этот процесс, т.к. нам интересно другое. Завершив процесс ининциации проекта (можно согласится на все предложенные варианты по умолчанию), мы увидим в корне файл - composer.json

Далее необходимо запустить команду установки пакетов. Даже если на этапе инициализации проекта вы не указали ни одной зависимости, это нужно сделать. Т.к. должен быть установлен пакет composer, который важен для нашего эксперимернта. Выполним в консоли команду:

$ composer install
Структура проекта с composer

После чего структура приобретёт следующий вид. В проекте появилась папка /vendor, в ней - папка composer и файл autoload.php

Включением этого файла как раз и производится регистрация автозагрузчика, который должен подключать классы из пакетов, однако, как и в примере с нашим классом Autoload выше, сначала нам нужно подключить сам загрузчик в приложение.

В случае с composer, сделать это мы можем в нашем index.php, например так:

...
$loader = require_once "../vendor/autoload.php";
// Регистрируем путь к пространству имён
$loader->addPsr4('MyClasses\\', __DIR__ . '/../classes/');

$mc = new \MyClasses\Utilities\MyClass();
// И этот класс будет загружен загрузчиком composer.
...

Как видим, пример с composer очень похож в использовании на пример из предыдущего параграфа, с той разницей, что нам не пришлось писать сам класс загрузчика.

Немного другой вариант

Есть ещё один вариант, как заставить composer загружать классы из нашего приложения. Откроем файл composer.json в корне проекта и добавим в него секцию autoload. Вот как здесь к примеру:

{
    "name": "dimmask/myproject",
    "authors": [
        {
            "name": "dimmask",
            "email": "dimmask@gmail.com"
        }
    ],
    "autoload": {
        "psr-4": {
            "MyClasses\\": "classes/"
        }
    },
    "require": {}
}

Сохраняем и выполняем в консоли команду:

$ composer dump-autoload

После чего composer Добавит указанные нами пространства имён в свою карту загрузки и как результат, нам не нужно будет регистрировать их в коде вручную. Таким образом, код нашего файла index.php становится ещё немного "легче":

...
require_once "../vendor/autoload.php";

$mc = new \MyClasses\Utilities\MyClass();
// Класс будет загружен загрузчиком composer.
...

Вывод:
Как видим, для более-менее масштабных проектов, где есть резон использовать composer, довольно удобно пользоваться встроенным автозагрузчиком. Тем более, что это не требует написания значительного объёма кода.

Google Plus