Creating a modular structure using inversion of control

In this article I will talk about how to create an easily extensible, modular structure. A similar organization is used in Symfony . We will also use Composer . What is it and how to use it can be read here .

So, our modular structure will be based primarily on the principles of control inversion . We will use IoC containers and my own library .

Let's start by creating a module management library. I called it Modular .

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

Now where we will use "modular" we will connect IoC.

The proposed structure of our modular system will be as follows:
index.php - Наш фронт контроллер
app/ 
     app.ini - список модулей
     ModuleOne/
              module.ini - описание модуля
     ModuleTwo/


We describe the class of the front of the App controller:
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();
    }
}


Let's see how the loading of modules works:
    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);
    }


Create a module class that will describe our 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);
        }
    }

    ...

}


Now let's try to create and then expand the module. For simplicity, try to create a notebook. All its code can be found here .

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


and run composer install. Now we have a vendor / folder with everything we need.

Create the app / Notepad / folder and start by creating the StorageInterface storage interface:
namespace Notepad;

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


and also a simple implementation of FileStorage .
Code
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);
    }
}



We describe this class in module.ini :
[Notepad\FileStorage]
interface = Notepad\StorageInterface
file = store.json


Now, any class in the constructor (for example Notepad \ Controller ) of which StorageInterface is contained will receive a FileStorage:
public function __construct(StorageInterface $storage)


All Notepad code is available here .

Let's try to create the MyNotepad module which will expand the Notepad module. For example, we now want to use DbStorage. Create app / MyNotepad / DbStorage.php and describe it in app / MyNotepad / module.ini:
[MyNotepad\DbStorage]
database = mystore.db


and add our module to app.ini
[Notepad]
path = Notepad/

[MyNotepad]
path = MyNotepad/


Now, the Notepad \ Controller class will receive an instance of the MyNotepad \ DbStorage class when creating. Just like that, without changing the Notepad module, we expanded its functionality. On github, you can see how to override other parts of Notepad.

Links