Мои мысли и проекты
Создаем сайт на Lumen (Laravel) - Структура проекта
Use the accounting equation to avoid errors and understand your company.

Проекты на Laravel и Lumen могут иметь произвольную файловую организацию. Самое время продумать как это будет организовано у меня.

Структура проекта

Я буду делать модульную систему (впрочем обычно так делают всегда). Т.е. в папке app создам папку Modules в которой и буду создавать модули. Модули должны подключаться автоматически и, по возможности, быстро. Для этого я буду генерировать файл автозагрузки init.php в папке ~SID. Для режима разработки буду его перегенерировать каждый раз, для продуктивного режима достаточно делать это один раз.

  • В файле info.json - параметры модуля.
{
	"active" : false, // Активация модуля. Для системных модулей указывать true. По умолчанию false
					  // Пользовательские модули будут включаться через настройку "app.modules" 
					  // где в строке через запятую необходимо будет указать список подключаемых модулей
	"require": ""	  // Список модулей (строка через запятую), от которых зависит текущий модуль. 
					  // При включении данного модуля все зависимости будут также включаться
}
  • В файле routes.php - маршруты модуля.
  • В файле config.php настройки модуля.
  • В папке views шаблоны с пространством имен по имени модуля
  • Список сервис-провайдеров определяем по наличию в предках класса \Illuminate\Support\ServiceProvider
  • Папку с миграциями БД будем определять по наличию в папке файлов классов у которых в предках есть класс \Illuminate\Database\Migrations\Migration
  • Список Artisan команд определяем по наличию в предках класса \Illuminate\Console\Command
  • Наличие singleton будем определять по наличию в предках интерфейса \App\ISingleton
<?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);
0

Комментарии

Чтобы оставлять комментарии войдите на сайт. Вы можете сделать это через социальную сеть