Проекты на Laravel и Lumen могут иметь произвольную файловую организацию. Самое время продумать как это будет организовано у меня.
Я буду делать модульную систему (впрочем обычно так делают всегда). Т.е. в папке app создам папку Modules в которой и буду создавать модули. Модули должны подключаться автоматически и, по возможности, быстро. Для этого я буду генерировать файл автозагрузки init.php в папке ~SID. Для режима разработки буду его перегенерировать каждый раз, для продуктивного режима достаточно делать это один раз.
{
"active" : false, // Активация модуля. Для системных модулей указывать true. По умолчанию false
// Пользовательские модули будут включаться через настройку "app.modules"
// где в строке через запятую необходимо будет указать список подключаемых модулей
"require": "" // Список модулей (строка через запятую), от которых зависит текущий модуль.
// При включении данного модуля все зависимости будут также включаться
}
<?php
namespace App;
//
use File;
//
use Illuminate\Support\Arr;
//
use PhpParser\Error;
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;
use PhpParser\Node;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
//
class Init
{
// Перебрать список файлов и директорий
// $callback($file) - если для директории возвращается false, то директория исключается из обработки
static public function fetchFiles($path,$callback) {
if ($handle = @opendir($path)) {
$offset = strlen( base_path() ) + 1;
while (($itemName = readdir($handle)) !== false) {
if( $itemName != '.' && $itemName != '..') {
$filepath = $path.'/'.$itemName;
$file = new \Symfony\Component\Finder\SplFileInfo($filepath,substr($filepath,$offset),'');
$rc = $callback($file);
//dump($filepath,$rc);
if($file->isDir() && $rc!==false) {
// Обработать поддиреторию
self::fetchFiles($filepath,$callback);
}
}
}
closedir($handle);
}
}
// Получить список классов в директории
// для работы необходим пакет nikic/php-parser https://github.com/nikic/PHP-Parser
static public function getListClasses($path) {
$isLog = false;//(basename($path)=='Log');
$ret = [];
$files=[];
self::fetchFiles($path,function($file) use (&$files) {
if($file->isDir()) {
// Проверим: если в директории присутствует файл exclude.classes то внутрь этой директории не входить.
$filepath = $file->getRealPath().'/exclude.classes';
$ret = !file_exists($filepath);
//dump($filepath,$ret);
return $ret;
} else {
$files[] = $file;
if( count($files)>1000 ) {
dd('Слишком много файлов нашлось!!!');
}
}
});
//dd($files);
if($isLog) dump($path,$files);
//
$parser = false;
//
foreach($files as $file) {
if( $file->getExtension()=='php' ) {
// Если парсер не создан
if($parser===false) {
// то создать его
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
}
try {
// Получить AST дерево
$ast = $parser->parse( file_get_contents($file->getRealPath()) );
// Создать объект для обхода
$visitor = new class extends NodeVisitorAbstract {
public $namespace = '';
public $classes = [];
public function enterNode(Node $node) {
if ($node instanceof Namespace_) {
// Clean out the function body
$this->namespace = $node->name->toString();
}
else if ($node instanceof Class_) {
// Clean out the function body
$this->classes[] = $node->name->toString();
}
}
};
//
$traverser = new NodeTraverser();
$traverser->addVisitor($visitor);
$ast = $traverser->traverse($ast);
//
if( !empty($visitor->namespace) ) {
$visitor->namespace .= '\\';
}
$visitor->namespace = '\\'.$visitor->namespace;
if($isLog) dump($visitor->classes);
// Перебрать все найденные в файле классы
foreach($visitor->classes as $classname) {
$ret[ $visitor->namespace.$classname ] = $file->getRealPath();
}
} catch (Error $error) {
echo '<div style="padding:8px;border: 2px solid red">';
echo '<div>Ошибка в файле <b style="color:red">'.$file->getRealPath().'</b></div>';
dump($error);
echo "</div>";
}
}
}
return $ret;
}
// Если есть файл/директория, то вернуть её
static public function getIsFileFile($filepath,$name=null) {
if( file_exists( base_path($filepath) ) ) {
if( is_null($name) ) {
return [$filepath];
}
return [$filepath=>$name];
}
return [];
}
// Проверить наличие класса в родителях, но чтобы это был не сам этот класс
static public function is_subclass_of($classname,$subclass) {
return ($classname!=$subclass && is_subclass_of($classname,$subclass));
}
// Получить информацию о директории
static protected $siteName = 'Site';
static public function getPathInfo($path,$name=null,$active=false) {
//
$filemoduleinfo = base_path($path).'/info.json';
if( file_exists($filemoduleinfo) ) {
$ret = json_decode( file_get_contents($filemoduleinfo), true) ;
} else {
$ret = [];
}
if( !array_key_exists('active',$ret) ) {
$ret['active'] = $active;
}
// Имя
if( is_null($name) ) {
$name = basename($path);
}
$id = strtolower($name);
$ret['name'] = $name;
// Идентификатор
$ret['id'] = strtolower($name);
// Путь
$ret['path'] = $path;
// Настройки
$ret['configs'] = self::getIsFileFile($path.'/config.php',$name==self::$siteName ? '' : $id);
// Шаблоны
$ret['views'] = self::getIsFileFile($path.'/views',$name);
// Маршруты
$ret['routes'] = self::getIsFileFile($path.'/routes.php',$name==self::$siteName ? '' : '\\App\\Modules\\'.$name);
// Получить список классов директории
$classes = self::getListClasses( base_path($path) );
//
$ret['services'] = [];
$ret['commands'] = [];
$ret['singletons'] = [];
$migrations = [];
// Смещение
$offset = strlen( base_path() ) + 1;
// Перебрать все классы
foreach($classes as $classname=>$filepath) {
// Если класс не существует
if( !class_exists($classname) ) {
// то подключить файл
require $filepath;
}
// Может это сервис-провайдер?
if( self::is_subclass_of($classname,\Illuminate\Support\ServiceProvider::class) ) {
$ret['services'][] = $classname;
}
// Может это команда?
if( self::is_subclass_of($classname,\Illuminate\Console\Command::class) ) {
// Вытащить имя команды из сигнатуры
$oReflectionClass = new \ReflectionClass($classname);
$prop = $oReflectionClass->getProperty('signature');
$prop->setAccessible(true);
$cmd = new $classname;
$cmdName = $prop->getValue($cmd);
$pos = strpos($cmdName,' ');
if($pos!==false) {
$cmdName = substr($cmdName,0,$pos);
}
$cmdName = 'command.'.$cmdName;
$ret['commands'][$cmdName] = $classname;
}
// Может это Singleton?
if( self::is_subclass_of($classname,\App\ISingleton::class) ) {
$ret['singletons'][$classname] = call_user_func_array([$classname,'getAlias'],[]);
}
// Миграции БД
// Класс миграции не должен быть загружен в память так как он
if( self::is_subclass_of($classname,\Illuminate\Database\Migrations\Migration::class) ) {
$migrations[dirname( substr($filepath,$offset) )] = 1;
}
//dump( $classname.' : '.var_export(class_exists($classname),true) );
}
$ret['migrations'] = array_keys($migrations);
return $ret;
}
// Создать файл инициализациии вернуть его имя
static public function get() {
$fileInit = base_path('~'.SID::get().'/_init.php');
// Если это режим разработки
if( config('app.debug') ) {
// и файл инициализации существует
if( file_exists($fileInit) ) {
// то удалить его
unlink($fileInit);
}
}
// Если файла инициализации не сществует
if( !file_exists($fileInit) ) {
//
$offset = strlen( base_path() ) + 1;
// Список данных о папках
$modules=[];
self::fetchFiles(base_path('app/Modules'),function($file) use (&$modules) {
if($file->isDir()) {
// Информация о модуле
$modules[] = self::getPathInfo($file->getRelativePath());
// Внутрь директории не заходить
return false;
}
});
//dd($modules);
if( SID::get() ) {
$modules[] = self::getPathInfo('~'.SID::get(),self::$siteName,true);
}
// Определить список активных модулей
if( SID::get() ) {
// Текущий SID
$sids = [SID::get()];
} else {
// Получить список доменов
$domains = SID::domains();
// Получить список SID
$sids = array_unique(array_values($domains));
}
// Активные модули
$actives = [];
foreach($sids as $sid) {
// Файл настроек для SID
$fileConfigSID = base_path('~'.$sid.'/config.php');
// Файл существует?
if( file_exists($fileConfigSID) ) {
// Добавить настройки
$config = require $fileConfigSID;
$lines = explode(',',Arr::get($config,'app.modules',''));
foreach($lines as $line) {
$id = strtolower(trim($line));
if( !empty($id) ) {
$actives[$id] = 1;
}
}
//dd($config);
}
}
$modules = array_map(function($module) use (&$actives) {
if($module['active']==false) {
if( array_key_exists($module['id'],$actives) ) {
$module['active'] = true;
}
}
return $module;
},$modules);
// Включить родительские модули
$ids = [];
for($i=0;$i<count($modules);$i++) {
$ids[ $modules[$i]['id'] ] = $i;
}
// Включить все модули от которых зависит активный модуль
do {
$fLoop = false;
foreach($modules as $module) {
if( $module['active'] && !empty($module['require']) ) {
$lines = explode('=',$module['require']);
foreach($lines as $line) {
$line = strtolower(trim($line));
if( !empty($line) ) {
if( !array_key_exists($line,$ids) ) {
throw new \Exception('В модуле '.$module['name'].' require ссылка на неизвестный модуль '.$line.' в файле info.json');
} else {
$active = $modules[ $ids[$line] ]['active'];
if( !$active ) {
$modules[ $ids[$line] ]['active'] = true;
// Выполнить ещё один цикл включения
$fLoop = true;
}
}
}
}
}
}
} while($fLoop);
// Удалить из списка неактивные модули
$modules = array_filter($modules,function($module){
return $module['active'];
});
//dd($actives,$modules);
// Сформировать данные
$bootstrap = [];
foreach($modules as $module) {
foreach($module as $key=>$value) {
if( is_array($value) ) {
if( !array_key_exists($key,$bootstrap) ) {
$bootstrap[$key] = [];
}
$bootstrap[$key] = array_merge($bootstrap[$key],$value);
}
}
}
//dump($bootstrap);
// Создать директорию
@File::makeDirectory( dirname($fileInit) );
// Сохрнаить файл
file_put_contents($fileInit,"<?php\n".(string)view('init',$bootstrap));
}
//
return $fileInit;
}
}
Метод \App\Init::get() генерирует файл инициализации и возвращает его путь. Вставим его вызов в bootstrap\app.php после секции "Create The Application" Также добавим псевдоним File для работы с файлами
// Дополнительные псевдонимы https://laravel.com/api/5.3/Illuminate/Filesystem/Filesystem.html
class_alias(\Illuminate\Support\Facades\File::class,'File');
/*
|--------------------------------------------------------------------------
| Сгенерировать файл инициализации и подключить его
|--------------------------------------------------------------------------
*/
require_once \App\Init::get();
Секцию подключения маршрутов изменим на
$app->router->group([
'namespace' => 'App\Http\Controllers',
], function ($router) {
// Эту строку можно удалить в финальной версии и подключать маршруты конкретного домена в соответствующем ~SID/routes.php
require __DIR__ . '/../routes/web.php';
// Инициализация маршрутов модлей и SID
init_routes($router);
});
Пример сгенерированного файла инициализации для SID = 001
<?php
// Функция инициализации
function init($app) {
//-- Настройки -----------------
$config = config('modules.cache',[]);
$config = require base_path('app/Modules/Cache/config.php');
config(['modules.cache'=>$config]);
//-- Сервис-провайдеры -----------------
$app->register("\\App\\Modules\\Cache\\ServiceProvider");
//-- Singletons -----------------
$app->singleton('\\App\\Modules\\Cache\\Singleton', function ($app) {
return new \App\Modules\Cache\Singleton();
});
//-- Views -----------------
$app['view']->addNamespace('Cache', base_path('app/Modules/Cache/views') );
// Консоль
if( $app->runningInConsole() ) {
// Миграции
$app->afterResolving('migrator', function ($migrator) {
$migrator->path( base_path('app\\Modules\\Cache\\migrations') );
});
// Artisan команды
$app->bind('command.module:cache:clear', '\\App\\Modules\\Cache\\Commands\\Clear');
$app->bind('command.module:file:clear', '\\App\\Modules\\File\\Commands\\Clear');
}
}
// Фасады
if( !class_exists('\\SCache') ) {
class SCache extends \Illuminate\Support\Facades\Facade {
protected static function getFacadeAccessor()
{
return '\\App\\Modules\\Cache\\Singleton';
}
}
}
// Инициализация маршрутов
function init_routes($router) {
$router->group(['namespace' => '\\App\\Modules\\Cache'], function() use ($router) {
require base_path('app/Modules/Cache/routes.php');
});
}
// Запустить инициализацию
init($app);