В этой статье я расскажу о том как создать легко расширяемую, модульную структуру. Подобная организация используется в Symfony. Так же мы будем использовать Composer. Что это такое и как его использовать можно почитать тут.
Итак, наша модульная структура будет базироваться прежде всего на принципах инверсии управления. Мы будем использовать контейнеры IoC и мою же библиотеку.
Начнем с создания библеотеки управления модулями. Я назвал её Modular.
Сначала опишем composer.json:
Теперь там где мы будем использовать «modular» у нас будет подключаться IoC.
Предполагаемая структура нашей модульной системы будет такой:
Опишем класс фронт контроллера App:
Посмотрим как работает загрузка модулей:
Создадим класс Module, который будет описывать наш модуль.
Теперь попробуем создать и затем расширить модуль. Для простоты попробуем создать записную книжку. Весь код её можно найти тут.
Создадим composer.json:
и выполним composer install. Теперь у нас есть папка vendor/ со всем необходимым.
Создадим папку app/Notepad/ и начнем с создания интерфейса хранилища StorageInterface:
и так же простую реализацию FileStorage.
Опишим этот класс в module.ini:
Теперь любой класс в конструкторе (например Notepad\Controller) которого содержится StorageInterface получит FileStorage:
Весь код модуля Notepad доступен тут.
Попробуем создать модуль MyNotepad который будет расширять модуль Notepad. Например, мы теперь хотим использовать DbStorage. Создадим app/MyNotepad/DbStorage.php и опишим его в app/MyNotepad/module.ini:
и добавим наш модуль в app.ini
Теперь класс Notepad\Controller получит при создании экземпляр класса MyNotepad\DbStorage. Вот так вот просто, без изменения модуля Notepad, мы расширили его функциональность. На github можно посмотреть как переопределить другие части Notepad.
Итак, наша модульная структура будет базироваться прежде всего на принципах инверсии управления. Мы будем использовать контейнеры 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.