Итак, наша модульная структура будет базироваться прежде всего на принципах инверсии управления. Мы будем использовать контейнеры 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.

