- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

Сначала опишем 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);
        }
    }

    ...

}

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

Создадим 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 [9].

Код

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 [10]:

[NotepadFileStorage]
interface = NotepadStorageInterface
file = store.json

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

public function __construct(StorageInterface $storage)

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

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

[MyNotepadDbStorage]
database = mystore.db

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

[Notepad]
path = Notepad/

[MyNotepad]
path = MyNotepad/

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

Сылки

Автор: Elfet


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/php-2/12989

Ссылки в тексте:

[1] Symfony: http://symfony.com/doc/current/components/dependency_injection/introduction.html

[2] Composer: http://getcomposer.org/

[3] тут: http://habrahabr.ru/post/145946/

[4] инверсии управления: http://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%B2%D0%B5%D1%80%D1%81%D0%B8%D1%8F_%D1%83%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F

[5] контейнеры IoC: http://habrahabr.ru/post/132084/

[6] библиотеку: https://github.com/Elfet/IoC

[7] Modular: https://github.com/Elfet/Modular

[8] тут: https://github.com/Elfet/modular-example

[9] FileStorage: https://github.com/Elfet/modular-example/blob/master/app/Notepad/FileStorage.php

[10] module.ini: https://github.com/Elfet/modular-example/blob/master/app/Notepad/module.ini

[11] Controller: https://github.com/Elfet/modular-example/blob/master/app/Notepad/Controller.php

[12] тут: https://github.com/Elfet/modular-example/tree/master/app/Notepad

[13] app/MyNotepad/DbStorage.php: https://github.com/Elfet/modular-example/blob/master/app/MyNotepad/DbStorage.php

[14] app.ini: https://github.com/Elfet/modular-example/blob/master/app/app.ini