Мои мысли и проекты
Создаем сайт на Lumen (Laravel) - Мультисайтинг

Немного теории

Сайт - это просто скрипт, который обрабатывает поступающие от пользователя запросы и возвращает ответы. Каждый запрос включает в себя запрашиваемую ссылку(маршрут), заголовки запроса и, в случае если это 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 и он должен учитывать при обработке не только запрашиваемый адрес, но и домен на который пришел запрос. При этом нужно учитывать домен в следующих случаях:

  1. Проверка наличий файлов в папке public
  2. Настройки
  3. Маршруты
  4. Запросы в БД

Введем понятие Site ID (Идентификатор сайта) SID. Хотя в качестве идентификатора можно использовать имя домена, но так как я использую разные домены для разработки и продуктива, то мне нужна возможность ссылаться на разных доменах на одни и теже файлы. Именно поэтому я ввел SID.

1. Проверка наличий файлов в папке public

Как видно из схемы работы 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.

2. Настройки

Все остальные доработки мы будем выполнять в файле 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')

3. Маршруты

Маршруты определяются в файле 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

4. Запросы в БД

Есть несколько вариантов как решить разделение запросов по разным SID-ам

  1. Для каждого SID своя База Данных. Вариант простой в реализации, достаточно в настройках указывать свою БД для каждого SID. Однако мне не подходит потому что на хостинге ограничение на количество используемых БД. Поэтому хотелось бы использовать одну БД для всех сайтов. Тут есть проблема - если сайты имеют большую посещяемость, то могут быть ошибки из-за максимального количество соединений с БД. Однако у меня такой посещяемости нет + я буду кешировать запросы в БД. Если же возникнут проблемы, то всегда можно вынести такой сайт на отдельную БД.
  2. Одна БД, но разные таблицы для каждого SID. К примеру есть таблица пользователей users. Для каждого SID она будет иметь имя SID-users. Возможно вариант не самый плохой, но не очень я представляю как это реализовать в Lumen.
  3. Одна БД, одна таблица, но в каждой таблице есть поле 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

  1. trait для вставки в модель
<?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();
			}
        });		
    }
}
  1. Класс глобальной заготовки
<?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());
		}
    }
}
0

Комментарии

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