Сайт - это просто скрипт, который обрабатывает поступающие от пользователя запросы и возвращает ответы. Каждый запрос включает в себя запрашиваемую ссылку(маршрут), заголовки запроса и, в случае если это POST запрос - данные.
В случае с Lumen таким обработчиком является public/index.php (Все пути я указываю относительно папки домена). Теоретически каждый запрос должен проходить через этот скрипт. На практике если это статический ресурс (т.е. его не требуется генерировать и его сожержимое сотоянно), то такой файл достаточно просто поместить в папку public. К примеру пользователь запрашивает ссылку /images/avatar.png если такой файл есть в папке public, то он будет отправлен пользователю. Если файл отсутствует, то будет вызван скрипт public/index.php и уже внутри него будет сгенерирован ответ. Соответствующие настройки делаются в файле public/.htaccess Lumen поддерживает маршрутизацию (роуминг), т.е. вы можете указать что вот для такого маршрута нужно вызвать такую функцию для генерации ответа сервера. Настройки маршрутов находятся в routes/web.php Если вы откроете его, то увидите следующий код
$router->get('/', function () use ($router) {
return $router->app->version();
});
Т.е. если запрашивается маршрут вида '/', то нужно вернуть строку номера версии установленного фреймфока. Именно её мы и увидим если запросим адрес вида "dev-lumen.shasoft.com/". Т.е. чтобы добавить новую страницу нашего сайта нам нужно просто добавить свой маршрут и указать его обработчик.
Маршруты можно задавать несколькими способами. Об этом можно почитать в официальной документации или на рускоязычных сайтах: laravel.ru, laravel.su. Общая схема работы в Lumen
В первой части я упоминал что у меня несколько сайтов. Можно для каждого сайта копировать код, а можно один код использовать на всех сайтах. Второй вариант для меня предпочтительнее, так как у меня все сайты у одного хостера и в случае доработок кода я хочу обновлять его всего один раз, а не для каждого сайта отдельно. Т.е. запросы с разных доменов должны приходить в один скрипт public/index.php и он должен учитывать при обработке не только запрашиваемый адрес, но и домен на который пришел запрос. При этом нужно учитывать домен в следующих случаях:
Введем понятие Site ID (Идентификатор сайта) SID. Хотя в качестве идентификатора можно использовать имя домена, но так как я использую разные домены для разработки и продуктива, то мне нужна возможность ссылаться на разных доменах на одни и теже файлы. Именно поэтому я ввел SID.
Как видно из схемы работы Lumen проверка на существование файла в папке public происходит до старта PHP скрипта в файле public/.htaccess поэтому инициализацию SID тоже необходимо делать в нем. Добавим в файл public/.htaccess код для определения SID по имени домена
#-- Определить идентификатор сайта
SetEnvIfNoCase Host cdn.shasoft.com SID=001
SetEnvIfNoCase Host dev-cdn.shasoft.com SID=001
SetEnvIfNoCase Host lumen.shasoft.com SID=002
SetEnvIfNoCase Host dev-lumen.shasoft.com SID=002
В php коде получить SID можно с помощью функции
getenv("SID")
Файлы папки public должны быть доступны всем сайтам, но при это должна быть папка файлами, которые должны быть доступны на конкретном сайте. Эту логику также разместим в public/.htaccess
#-- Проверим наличие в КЕШ-е по SID сайта
RewriteCond %{DOCUMENT_ROOT}/~%{ENV:SID}%{REQUEST_URI} -f
RewriteRule (.*)$ /~%{ENV:SID}%{REQUEST_URI} [L]
Теперь можно создать в папке public подпапку ~<SID> где будут храниться файлы для каждого конкретного SID.
Все остальные доработки мы будем выполнять в файле bootstrap/app.php и прежде всего нам нужно добавить глобальную функцию которая будет возвращать текущий SID. Точнее содадим файл App\SID.php с классом SID в котором укажем статический метод get для получения текущего значения SID.
<?php
namespace App;
use Illuminate\Database\Schema\Blueprint;
class SID
{
// Получить текущее значение
static public function get() {
return getenv("SID");
}
}
Теперь у нас есть функция \App\SID::get() вызвав которую мы получим текущий SID.
В Laravel Все настройки располагались в директории config и этих файлов было много. В Lumen такой директории нет и чтобы добавить свою настройку необходимо вызвать функцию config()
config([
'<имя настройки 1>'=>'<значение>',
'<имя настройки 2>'=>'<значение>',
...
]);
Настройки по умолчанию храняться в vendor/laravel/lumen-framework/config и подгружаются с помощью функции $app->configure('<имя файла>')
Нам нужно проверить наличие файла для текущего SID и если он есть, то добавить эти настройки в общие. Для этого добавим следующий код перед секцией "Register Container Bindings"
/*
|--------------------------------------------------------------------------
| Конфигурация для конкретного SID
|--------------------------------------------------------------------------
|
| Проверяем наличие конфигуряции для SID и если она есть, то добавляем их
|
*/
// Файл настроек для SID
$fileConfigSID = __DIR__ . '/../~'.\App\SID::get().'/config.php';
// Файл существует?
if( file_exists($fileConfigSID) ) {
// Добавить настройки
config( require $fileConfigSID );
}
Теперь мы можем создать файл ~<SID>/config.php
<?php
return [
'my.param'=>'myvalue'
];
и эти настройки будут добавлены. Мы сможем их получить с помощью config('my.param')
Маршруты определяются в файле routes/web.php и подключаются они в секции "Load The Application Routes" Доработаем этот код для загрузки маршрутов для текущего SID следующим образом
$app->router->group([
'namespace' => 'App\Http\Controllers',
], function ($router) {
require __DIR__.'/../routes/web.php';
// Маршруты для SID
$fileRoutesSID = __DIR__ . '/../~'.\App\SID::get().'/routes.php';
// Файл существует?
if( file_exists($fileRoutesSID) ) {
// Добавить маршруты
require $fileRoutesSID;
}
});
Теперь мы можем создать файл ~<SID>/routes.php и определить в нем марщруты аналогично routes/web.php
Есть несколько вариантов как решить разделение запросов по разным SID-ам
Чтобы не писать условие выбора по полю SID мы сделаем базовую модель с условием и от неё будем наследовать все остальные модели
<?php
namespace App;
class Model extends \Illuminate\Database\Eloquent\Model
{
// Успользовать SID при выборе данных
protected $_isSID = true;
// Выбирать по SID?
public function isSID() {
return $this->_isSID;
}
// Отключить выборку по SID
public function scopeNoSID($query) {
$this->_isSID = false;
return $query;
}
//
public static function boot()
{
parent::boot();
// Глобальная заготовка для выбора данных
static::addGlobalScope(new ModelScope);
// При создании инициализировать поле SID
self::creating(function (Model $model) {
// Инициализация поля если оно не инициализировано
if( is_null($model->sid) ) {
$model->sid = \App\SID::get();
}
});
}
}
и класс глобальной заготовки
<?php
namespace App;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class ModelScope implements Scope
{
// Применить условие
public function apply(Builder $builder, Model $model)
{
// Для модели включен выбор по SID?
if( $model->isSID() ) {
// Добавить условие выбора по SID
$builder->where(SID::fieldname(),'=',\App\SID::get());
}
}
}
В класс SID (App\SID.php) добавим ещё два статических метода: имя поля SID в таблице и метод для определения поля в таблице миграций. Это не обязательно, но для унификации весьма полезно.
<?php
namespace App;
use Illuminate\Database\Schema\Blueprint;
class SID
{
// Получить текущее значение
static public function get() {
return getenv("SID");
}
// Имя поля SID
static public function fieldname() {
return 'sid';
}
// Определить поле SID
static public function defField(Blueprint $table) {
$table->char(self::fieldname(),3);
}
// Получить список доменов и их SID
static protected $startSID = 'SetEnvIfNoCase Host';
static public function domains() {
$ret = [];
//
$data = file_get_contents( base_path('public/.htaccess') );
$lines = explode("\n",$data);
$lines = array_map(function($line){
return trim($line);
},$lines);
$lines = array_filter($lines,function($line){
return starts_with($line,self::$startSID);
});
$lines = array_map(function($line){
$line = trim(substr($line,strlen(self::$startSID)));
$b = strpos($line,' ');
$e = strrpos($line,'=')+1;
return [trim(substr($line,0,$b)),trim(substr($line,$e))];
},$lines);
foreach($lines as $line) {
$ret[$line[0]] = $line[1];
}
//
return $ret;
}
}
Первоначально я сделал именно этот вариант, в котором все делается через trait. Однако потом я подумал и понял что поле sid нужно во всех таблицах и лучше все сделать через базовую модель. Однако для общего развития этот пункт я оставлю. Мы сделаем реализацию как это реализовано для мягкого удаления в trait Illuminate\Database\Eloquent\SoftDeletes Для этого создадим trait App\SID который необходимо добавлить в модель. Используем глобальное условие для добавления условия выборки по sid для всех запросов. Также используем функцию локальных условий для добавления в модель условия noSID() которое позволит отключать наше глобальноее условие и выбирать все записи. Также используем событие модели creating для того чтобы инициализировать поле sid В итоге у нас будет два файла. Создадим их в директории app
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
trait SID
{
// Успользовать SID при выборе данных
protected $_isSID = true;
// Выбирать по SID?
public function isSID() {
return $this->_isSID;
}
// Отключить выборку по SID
public function scopeNoSID($query) {
$this->_isSID = false;
return $query;
}
//
public static function bootSID()
{
// Глобальная заготовка для выбора данных
static::addGlobalScope(new SIDScope);
// При создании инициализировать поле SID
self::creating(function (Model $model) {
// Инициализация поля если оно не инициализировано
if( is_null($model->sid) ) {
$model->sid = \App\SID::get();
}
});
}
}
<?php
namespace App;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class SIDScope implements Scope
{
// Применить условие
public function apply(Builder $builder, Model $model)
{
// Для модели включен выбор по SID?
if( $model->isSID() ) {
// Добавить условие выбора по SID
$builder->where('sid','=',\App\SID::get());
}
}
}