Создание модульной структуры с применением инверсии управления

в 16:19, , рубрики: ioc, php, модульная архитектура, метки: ,

В этой статье я расскажу о том как создать легко расширяемую, модульную структуру. Подобная организация используется в Symfony. Так же мы будем использовать Composer. Что это такое и как его использовать можно почитать тут.

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

Начнем с создания библеотеки управления модулями. Я назвал её Modular.

Сначала опишем composer.json:

{
    "name":"elfet/modular",
    "type":"library",
    "autoload": {
        "psr-0": {
            "Modular": "src/"
        }
    },
    "require":{
        "php":">=5.3.0",
        "elfet/ioc":"dev-master"
    }
}

Теперь там где мы будем использовать «modular» у нас будет подключаться IoC.

Предполагаемая структура нашей модульной системы будет такой:

index.php - Наш фронт контроллер
app/ 
     app.ini - список модулей
     ModuleOne/
              module.ini - описание модуля
     ModuleTwo/

Опишем класс фронт контроллера App:

namespace Modular;

use IoCContainer;
use ComposerAutoloadClassLoader; // Используем загрузчик из /vendor/autoload.php

class App
{
    protected $rootDir; // Путь до папки app/
    protected $ioc; // Наш ioc контейрен
    protected $loader; // Загрузчик модулей.

    public function __construct($rootDir, ClassLoader $classLoader)
    {
        $this->rootDir = $rootDir;
        $this->ioc = Container::getInstance();
        $this->loader = new Loader($this->ioc, $classLoader);
    }

    public function load()
    {
        $appConfig = parse_ini_file($this->rootDir . '/app.ini', true);
        // Загружаем список модулей из app.ini 
        // Каждой записи позволяем определить расположение модуля 
        // и класс модуля.

        foreach ($appConfig as $module => $config) {
            // По умолчанию используем для модуля класс ModularModule
            $config = array_merge(array(
                'class' => 'ModularModule',
                'path' => $this->rootDir . '/module/' . $module,
            ), $config);

            // Загружаем модули
            $this->loader->load(
                $module,
                $config['class'],
                $this->rootDir . '/' . $config['path']
            );
        }
    }

    public function run()
    {
        $this->load();
    }
}

Посмотрим как работает загрузка модулей:

    public function load($moduleName, $moduleClass, $moduleDir)
    {
        // Добавляем файлы модуля в автозагрузку
        // Имя директории должно соответствовать пространству имен модуля (Используется PSR-0)
        $this->classLoader->add($moduleName, dirname($moduleDir));

        // Создаём класс модуля
        $module = new $moduleClass;
        $module->setModuleDir($moduleDir);
        
        // И загружаем его интерфейсы/классы в IoC.
        // Модуль может переопределить метод load 
        // или описать используемые классы в module.ini
        $module->load($this->ioc);
    }

Создадим класс Module, который будет описывать наш модуль.

namespace Modular;

use IoCContainer;
use IoCAssocService;

class Module
{
    private $moduleDir; // Директория нашего модуля.

    public function load(Container $container)
    {
        $this->loadFromFile($container, $this->getModuleDir() . '/module.ini');
    }

    protected function loadFromFile(Container $container, $file)
    {
        $module = parse_ini_file($file, true);
        foreach ($module as $class => $params) {
            // В описании класса может быть указано несколько интерфейсов
            // если они не указаны IoC сам определит их через Reflection (соответственно классы будут загруженны).
            $interfaces = isset($params['interface']) ? (array)$params['interface'] : array();

            // Остальные параметры мы будем использовать для создания класса.
            unset($params['interface']);
 
            // Создаём ассоциацию-сервис с оставшимися параметрами.
            // Класс $class создаётся только при необходимости и всего один раз.
            // Конструктор этого класса может принимать параметры.
            $serviceAssoc = new Service($class, $params);
            $container->assoc($serviceAssoc, $interfaces);
        }
    }

    ...

}

Теперь попробуем создать и затем расширить модуль. Для простоты попробуем создать записную книжку. Весь код её можно найти тут.

Создадим composer.json:

{
    "require":{
        "php":">=5.3.0",
        "elfet/modular":"dev-master"
    }
}

и выполним composer install. Теперь у нас есть папка vendor/ со всем необходимым.

Создадим папку app/Notepad/ и начнем с создания интерфейса хранилища StorageInterface:

namespace Notepad;

interface StorageInterface
{
    public function set($key, $value);
    public function get($key);
    public function save();
    public function load();
}

и так же простую реализацию FileStorage.

Код

namespace Notepad;

use NotepadStorageInterface;

class FileStorage implements StorageInterface
{
    protected $store = array();
    protected $file;

    public function __construct($file = 'store.json')
    {
        $this->file = realpath(__DIR__ . '/../cache/' . $file);
    }

    public function set($key, $value)
    {
        $this->store[$key] = $value;
    }

    public function get($key)
    {
        return isset($this->store[$key]) ? $this->store[$key] : null;
    }

    public function save()
    {
        file_put_contents($this->file, json_encode($this->store));
    }

    public function load()
    {
        $content = file_get_contents($this->file);
        $this->store = (array)json_decode($content);
    }
}

Опишим этот класс в module.ini:

[NotepadFileStorage]
interface = NotepadStorageInterface
file = store.json

Теперь любой класс в конструкторе (например NotepadController) которого содержится StorageInterface получит FileStorage:

public function __construct(StorageInterface $storage)

Весь код модуля Notepad доступен тут.

Попробуем создать модуль MyNotepad который будет расширять модуль Notepad. Например, мы теперь хотим использовать DbStorage. Создадим app/MyNotepad/DbStorage.php и опишим его в app/MyNotepad/module.ini:

[MyNotepadDbStorage]
database = mystore.db

и добавим наш модуль в app.ini

[Notepad]
path = Notepad/

[MyNotepad]
path = MyNotepad/

Теперь класс NotepadController получит при создании экземпляр класса MyNotepadDbStorage. Вот так вот просто, без изменения модуля Notepad, бы расширили его функциональность. На github можно посмотреть как переопределить другие части Notepad.

Сылки

Автор: Elfet

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js