Pull to refresh

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

Reading time4 min
Views5.3K
В этой статье я расскажу о том как создать легко расширяемую, модульную структуру. Подобная организация используется в 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 IoC\Container;
use Composer\Autoload\ClassLoader; // Используем загрузчик из /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) {
            // По умолчанию используем для модуля класс Modular\Module
            $config = array_merge(array(
                'class' => 'Modular\Module',
                '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 IoC\Container;
use IoC\Assoc\Service;

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 Notepad\StorageInterface;

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:
[Notepad\FileStorage]
interface = Notepad\StorageInterface
file = store.json


Теперь любой класс в конструкторе (например Notepad\Controller) которого содержится StorageInterface получит FileStorage:
public function __construct(StorageInterface $storage)


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

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


и добавим наш модуль в app.ini
[Notepad]
path = Notepad/

[MyNotepad]
path = MyNotepad/


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

Сылки


Tags:
Hubs:
Total votes 18: ↑16 and ↓2+14
Comments8

Articles