Не путать с новой библиотекой контекстов Laravel: этот пакет можно использовать для создания мультиконтекстных мультитенантных приложений. Большинство мультитенантных библиотек по сути имеют один «арендаторский» контекст, поэтому, если вам нужно несколько контекстов, все может стать немного сложнее. Новый пакет решает эту проблему.
Давайте посмотрим на пример, ладно?
Для нашего примера приложения у нас будет глобальная база пользователей, которая организована в команды, и у каждой команды будет несколько проектов. Это довольно распространенная структура во многих приложениях «Программное обеспечение как услуга».
Нередко в мультитенантных приложениях каждая база пользователей существует в контексте клиента, но в нашем примере приложения мы хотим, чтобы пользователи могли присоединяться к нескольким командам, поэтому это глобальная база пользователей.
]
Диаграмма глобальной базы пользователей и базы пользователей арендаторов
В случае SaaS, вполне вероятно, что команда будет платной организацией (т. е. местом), и определенным членам команды будет предоставлено разрешение на управление командой. Я не буду углубляться в детали реализации в этом примере, но, надеюсь, он предоставит некоторый дополнительный контекст.
Чтобы этот пост был кратким, я не буду объяснять, как запустить проект Laravel. Для этого уже существует множество лучших ресурсов, не в последнюю очередь официальная документация. давайте просто предположим, что у вас уже есть проект Laravel с моделями «Пользователь», «Команда» и «Проект», и вы готовы приступить к реализации нашего контекстного пакета.
Установка представляет собой простую композицию композитора:
composer install honeystone/context
В этой библиотеке есть удобная функция context(), которая, начиная с версии Laravel 11, конфликтует с собственной контекстной функцией Laravel. На самом деле это не проблема. Вы можете импортировать нашу функцию:
use function Honestone\Context\context;
Или просто используйте контейнер внедрения зависимостей Laravel. В этом посте я буду предполагать, что вы импортировали функцию и используете ее соответствующим образом.
Давайте начнем с настройки модели нашей команды:
belongsToMany(User::class); } public function projects(): HasMany { return $this->hasMany(Project::class); } }
У команды есть название, участники и проекты. В нашем приложении только члены команды смогут получить доступ к команде или ее проектам.
Хорошо, давайте посмотрим на наш проект:
belongsTo(Team::class); } }
Проект имеет имя и принадлежит команде.
Когда кто-то получает доступ к нашему приложению, нам необходимо определить, в какой команде и над каким проектом он работает. Для простоты давайте решим это с помощью параметров маршрута. Мы также предполагаем, что только авторизованные пользователи могут получить доступ к приложению.
Ни команда, ни контекст проекта: app.mysaas.dev
Только контекст команды: app.mysaas.dev/my-team
Контекст команды и проекта: app.mysaas.dev/my-team/my-project
Наши маршруты будут выглядеть примерно так:
Route::middleware('auth')->group(function () { Route::get('/', DashboardController::class); Route::middleware(AppContextMiddleware::Class)->group(function () { Route::get('/{team}', TeamController::class); Route::get('/{team}/{project}', ProjectController::class); }); });
Это очень негибкий подход, учитывая возможность конфликтов пространств имен, но он позволяет сделать пример кратким. В реальном приложении вы захотите поступить с этим немного по-другому, например,othersaas.dev/teams/my-team/projects/my-project или my-team.anothersas.dev/projects/my-project.
Сначала нам следует взглянуть на наше AppContextMiddleware. Это промежуточное программное обеспечение инициализирует контекст команды и, если он установлен, контекст проекта:
route('team'); $request->route()->forgetParameter('team'); $projectId = null; //if there's a project, pull that too if ($request->route()->hasParamater('project')) { $projectId = $request->route('project'); $request->route()->forgetParameter('project'); } //initialise the context context()->initialize(new AppResolver($teamId, $projectId)); } }
Для начала мы берем идентификатор команды из маршрута, а затем забываем параметр маршрута. Нам не нужно, чтобы параметр достигал наших контроллеров, когда он находится в контексте. Если идентификатор проекта установлен, мы также извлекаем его. Затем мы инициализируем контекст, используя наш AppResolver, передавая идентификатор нашей команды и идентификатор нашего проекта (или ноль):
require('team', Team::class) ->accept('project', Project::class); } public function resolveTeam(): ?Team { return Team::with('members')->find($this->teamId); } public function resolveProject(): ?Project { return $this->projectId ?: Project::with('team')->find($this->projectId); } public function checkTeam(DefinesContext $definition, Team $team): bool { return $team->members->find(context()->auth()->getUser()) !== null; } public function checkProject(DefinesContext $definition, ?Project $project): bool { return $project === null || $project->team->id === $this->teamId; } public function deserialize(array $data): self { return new static($data['team'], $data['project']); } }
Здесь происходит еще кое-что.
Метод define() отвечает за определение разрешаемого контекста. Команда является обязательной и должна иметь модель Team, а проект принят (т. е. необязателен) и должен иметь модель Project (или нулевую).
resolveTeam() будет вызываться внутри при инициализации. Он возвращает команду или ноль. В случае нулевого ответа ContextInitializer выдаст исключение CouldNotResolveRequiredContextException.
resolveProject() также будет вызываться внутри при инициализации. Он возвращает проект или ноль. В этом случае нулевой ответ не приведет к исключению, поскольку проект не требуется по определению.
После разрешения команды и проекта ContextInitializer вызовет дополнительные методы checkTeam() и checkProject(). Эти методы выполняют проверки целостности. Для checkTeam() мы проверяем, что аутентифицированный пользователь является членом команды, а для checkProject() мы проверяем, что проект принадлежит команде.
Наконец, каждому преобразователю необходим метод десериализации(). Этот метод используется для восстановления сериализованного контекста. В частности, это происходит, когда контекст используется в задании, поставленном в очередь.
Теперь, когда контекст нашего приложения установлен, мы должны его использовать.
Как обычно, мы сделаем это просто, хотя и немного надуманно. При просмотре команды мы хотим видеть список проектов. Мы могли бы создать наш TeamController для удовлетворения этих требований следующим образом:
projects; return view('team', compact('projects')); } }
Достаточно просто. На наш взгляд передаются проекты, принадлежащие текущему контексту команды. Представьте, что теперь нам нужно запросить проекты для получения более специализированного представления. Мы могли бы сделать это:
id) ->where('name', 'like', "%$query%") ->get(); return view('queried-projects', compact('projects')); } }
Сейчас это становится немного затруднительно, и слишком легко случайно забыть «охватить» запрос командой. Мы можем решить эту проблему, используя признак BelongsToContext в нашей модели Project:
belongsTo(Team::class); } }
Все запросы проекта теперь будут обрабатываться контекстом команды, а текущая модель команды будет автоматически внедряться в новые модели проекта.
Давайте упростим этот контроллер:
get(); return view('queried-projects', compact('projects')); } }
С этого момента вы просто создаете свое приложение. Контекст легко доступен, ваши запросы ограничены, а задания в очереди автоматически получают доступ к тому же контексту, из которого они были отправлены.
Однако не все проблемы, связанные с контекстом, решены. Вероятно, вы захотите создать несколько макросов проверки, чтобы придать вашим правилам проверки небольшой контекст, и не забывайте, что к ручным запросам контекст не применяется автоматически.
Если вы планируете использовать этот пакет в своем следующем проекте, мы будем рады услышать ваше мнение. Обратная связь и вклад всегда приветствуются.
Дополнительную документацию можно найти в репозитории GitHub. Если наш пакет окажется для вас полезным, поставьте звездочку.
До новых встреч..
Эта статья изначально была опубликована в блоге Honeystone. Если вам нравятся наши статьи, рассмотрите возможность проверки нашего контента здесь.
Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.
Copyright© 2022 湘ICP备2022001581号-3