Модульность в php-проекте

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

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

Хорошим примером модульности может служить CodeIgniter (1-3 версий). В нём модуль представляет собой «обертку» над классами или функциям. То есть вместо того, чтобы писать сложный код подключения, инициализации и т.п., используется универсальный загрузчик, например так:

$this->load->library('encrypt');

«Loader» берёт на себя задачу по подключению файла класса и его инициализацию (и даже поддержку как singleton). Дальнейшее использование сводится к обращению к методам класса encrypt в достаточно простом варианте:

$encrypted_string = $this->encrypt->encode('My secret message');

В данном случае encrypt можно рассматривать именно как модуль CodeIgniter. Если бы речь шла о простом классе, то нам нужно было бы самостоятельно вызвать new, предварительно прописав подключение файла и т.д.

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

В таком случае подключение модуля становится не такой простой задачей, ведь среди всех файлов нужно найти его «точку входа». В разных php-фреймоворках задача решается различными способами. Самый простой — это размещение в каталоге модуля предопределенного файла, например bootstrap.php. То есть когда нужно выполнить подключение модуля, то загрузчик ищет этот «начальный» файл. Такой файл необходим, поскольку модуль может быть достаточно сложным и нужно обеспечить его корректную инициализацию. Также в этом файле обычно размещают и автозагрузчик классов.

Структура каталогов модулей этого варианта может быть такой:

modules/
    module1/
        ... каталоги и файлы модуля...
        bootstrap.php
        
    module2/
        ... каталоги и файлы модуля...
        bootstrap.php

Такой подход используется например в Fuel PHP Framework при подключени модулей packages.

Главный момент здесь в том, что файл bootstrap.php выполняет роль «буфера», через который происходит «адаптация» классов модуля к php-проекту.

Приведу небольшой абстрактный пример. Например нам нужно вывести слайдер на какой-то странице сайта. Сам слайдер требует предопределенную html-разметку. Также он содержит базовые css-стили и jQuery-плагин. Модуль должен уметь не просто сгенерировать готовый html-код, но и быть управляемым, например поддерживать все параметры jQuery-плагина через php-переменные, а также дополнительные опции, вроде своих css-классов.

Понятно, что такой модуль должен состоять из каких-то php-классов или функций, но также в модуле следует разместить и все «сопутствующие» файлы: css и js, например:

modules/
    MySlider/
        js/
            ... js-файлы ...
            
        css/
            ... css-файлы ...
        
        functions/
            ... файлы модуля ...
            
        MySlider.php — например основной класс
        
        bootstrap.php — «точка входа»

Файл bootstrap.php может содержать только загрузчик классов модуля. Поэтому работа с модулем может выглядеть так:

load_module('MySlider'); // здесь и подключается bootstrap.php
 
$slider = new MySlider();
 
... дальше работа с объектом $slider

Данный вариант хорош тем, что используется единый подход к работе с модулем. Но при этом возникает проблема для модулей, которые не имеют единой «точки входа». Например модуль представляет собой просто набор не связанных классов. Примером может послужить база данных. В зависимости от типа базы (MySQL, sqlite, pdo), будут использованы различные классы.

Обычно такие модули выделяют в специализированные каталоги, вроде «core», «database», «classes» и т.п. То есть формально это теже самые модули, но которые работают по другим «правилам». Назвать это проблемой нельзя, хотя такой подход ломает «стройность» проекта.

Но на самом деле, проблема не только в этом. На сегодняшний день существует огромное количество готовых php-проектов, решающих практически все задачи. То есть вместо того, чтобы «изобретать велосипед», можно воспользоваться уже готовой разработкой.

Чтобы оценить объем возможностей приведу как пример The PHP Package Repository, где размещены описания на почти 220 тысяч (!!!) проектов. Этот репозиторий используется менеджером Composer, который довольно часто используется в php-фреймворках.

Таким образом, если мы хотим воспользоваться чужой разработкой, то возникает довольно серьезная проблема встраивания чужого проекта в свой модуль. Дело в том, что php-разработчики используют самые разные подходы из-за чего с каждым проектом приходится разбираться индивидуально.

Например, кто-то использует классы, кто-то только функции. Кто-то использует namespace, кто-то нет. Кто-то придерживается PSR-4, кто-то нет.

В итоге структура модуля получит примерно такой вид:

modules/
    MySlider/
        distr/
            ... файлы сторонних дистрибутивов ...
            
        MySlider.php — например основной класс
        
        bootstrap.php — «точка входа»

Каталог distr содержит дистрибутивы сторонних разработок, которые используются в данном модуле.

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

Очевидным решением будет вынос всех сторонних дистрибутивов в отдельный каталог. Здесь самое время ещё раз упомянуть добрым словом Composer, который и решает данную задачу. С помощью специального файла конфигурации Composer не просто скачивает все необходимые дистрибутивы, но и создаёт для них специальный автозагрузчик классов. Дистрибутивы Composer сохраняет в каталог vendor, где будет готовый файл autoload.php.

При такой схеме модуль может уже не иметь bootstrap.php (если используется Composer и подключен его autoload.php). При этом уже не нужны никакие load_module().

Что получается в итоге?

Модули в php-проекте должны располагаться в отдельном каталоге и соответствовать стандарту PSR-4, что позволит упростить их использование (через автозагрузчик).

При этом каталог vendor нужно оставить для Composer. Поскольку он автоматически формирует autoload.php, то в своем проекте достаточно будет лишь проверить наличие этого файла и подключить его.

Все дистрибутивы, которые могут использоваться повторно, но не относящиеся к Composer, нужно выделить отдельно, например в каталог distr. Здесь придётся как-то решить вопрос с автозагрузкой, но индивидуально для каждого дистрибутива, поскольку они слишком уж «разношерстные».

vendor/
    ... каталог Composer'а
    autoload.php
    
distr/
    ... каталоги дистрибутивов свои и чужие, вне Composer'а
    autoload.php — для тех, которые не соответствуют PSR-4
    
modules/ — соответствие PSR-4
    module1/
        ... каталоги и файлы модуля
        module1.php
    
    module2/
        ... каталоги и файлы модуля
        module2.php
    
index.php — основной файл php-проекта
composer.json — конфигурация Composer'а

В заключении немного кода для index.php, чтобы стало понятно как это может работать в реальности.

<?php 

// set dirs
define('MODULES_DIR',  __DIR__ . '/modules/');
define('DISTR_DIR',    __DIR__ . '/distr/');
define('COMPOSER_DIR', __DIR__ . '/vendor/');
 
// autoload composer
if (file_exists(COMPOSER_DIR . 'autoload.php')) require COMPOSER_DIR . 'autoload.php'; 
 
// autoload distr (non psr-4)
if (file_exists(DISTR_DIR . 'autoload.php')) require DISTR_DIR . 'autoload.php'; 
 
// register autoload classes PSR-4
spl_autoload_register(function($class) {
    $a = explode('\\', $class);
    $fn = $class . '/' . array_pop($a) . '.php';
    $fn = str_replace('\\', '/', $fn);
    
    if (file_exists(MODULES_DIR . $fn)) require MODULES_DIR . $fn;
    else if (file_exists(DISTR_DIR . $fn)) require DISTR_DIR . $fn;    
});
 
// MY APPLICATION SAMPLE
 
// modules
$o1 = new Module1('Module1');
 
// submodule
$o2 = new Module1\SubModule1('SubModule1');
 
// distr/Project1
$o3 = new Project1('distr/Project1');
 
// module use distr/Project1
$o4 = new Module1\MProject1('MProject1 use Project1');
 
// composer Parsedown
$Parsedown = new Parsedown();
echo $Parsedown->text('Hello _Parsedown_!');

// module use composer Parsedown
$o5 = new Module1\MParsedown('This is _MParsedown_!');
 
# end of file
  • Вначале задаются константы для каталогов.
  • После определяются автозагрузчики классов.
  • Дальше показаны примеры использования в разных вариантах.

Вы можете скачать полный набор файлов. Классы простые для демонстрации, а для Composer'а я «прикрутил» парсер Parsedown. Вначале он вызывается сам по себе, а после из класса модуля.

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

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

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