ホームページ  >  記事  >  バックエンド開発  >  ハニーストーン/コンテキストを使用したマルチテナント アプリケーションの構築

ハニーストーン/コンテキストを使用したマルチテナント アプリケーションの構築

PHPz
PHPzオリジナル
2024-08-10 22:30:421162ブラウズ

Laravel の新しいコンテキスト ライブラリと混同しないでください。このパッケージは、マルチコンテキスト、マルチテナント アプリケーションの構築に使用できます。ほとんどのマルチテナント ライブラリには基本的に単一の「テナント」コンテキストがあるため、複数のコンテキストが必要な場合は少し面倒になる可能性があります。この新しいパッケージはその問題を解決します。

例を見てみましょう?

サンプル プロジェクト

サンプル アプリケーションでは、次のようなグローバル ユーザー ベースがあります。チームに編成され、各チームには複数のプロジェクトがあります。これは、多くの Software as a Service アプリケーションでかなり一般的な構造です。

マルチテナント アプリケーションで各ユーザー ベースがテナント コンテキスト内に存在することは珍しいことではありませんが、このサンプル アプリケーションでは、ユーザーが複数のチームに参加できるため、グローバル ユーザー ベースになります。
グローバル ユーザー ベースとテナント ユーザー ベースの図

Building a multi-tenant application with honeystone/context

SaaS としては、おそらく次のようになります。チームが請求対象エンティティ (つまりシート) となり、特定のチーム メンバーにはチームを管理する権限が付与されます。ただし、この例ではこれらの実装の詳細には立ち入りませんが、追加のコンテキストが提供されることを願っています。

インストール

この投稿を簡潔にするために、Laravel の起動方法については説明しません。プロジェクト。これについては、公式ドキュメントをはじめ、より優れたリソースがすでにたくさんあります。ユーザー、チーム、プロジェクト モデルを含む Laravel プロジェクトがすでにあり、コンテキスト パッケージの実装を開始する準備ができていると仮定します。

インストールは簡単なコンポーザーです。

composer install honeystone/context

このライブラリには便利な関数 context() があり、Laravel 11 の時点では Laravel 独自の context 関数と衝突します。これは実際には問題ではありません。私たちの関数をインポートすることもできます:

use function Honestone\Context\context;

または、単に Laravel の依存関係注入コンテナーを使用することもできます。この投稿では、関数をインポートし、それに応じて使用していることを前提とします。

モデル

チーム モデルを構成することから始めましょう:

<?php declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Team extends Model
{
    protected $fillable = [&#39;name&#39;];

    public function members(): BelongsToMany
    {
        return $this->belongsToMany(User::class);
    }

    public function projects(): HasMany
    {
        return $this->hasMany(Project::class);
    }
}

チームには名前、メンバー、プロジェクトがあります。アプリケーション内では、チームのメンバーのみがチームまたはそのプロジェクトにアクセスできます。

それでは、プロジェクトを見てみましょう。

<?php declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Project extends Model
{
    protected $fillable = [&#39;name&#39;];

    public function team(): BelongsTo
    {
        return $this->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);
    });
});

これは、名前空間の衝突の可能性を考慮すると、非常に柔軟性の低いアプローチですが、例は簡潔に保たれています。実際のアプリケーションでは、これを少し異なる方法で処理する必要があるでしょう。おそらく、anothersaas.dev/teams/my-team/projects/my-project または my-team.anothersas.dev/projects/my-project.

のようになります。

最初に AppContextMiddleware を見てみましょう。このミドルウェアは、チーム コンテキストと、設定されている場合はプロジェクト コンテキストを初期化します。

<?php declare(strict_types=1);

namespace App\Http\Middleware;

use function Honestone\Context\context;

class TeamContextMiddleware
{
    public function handle(Request $request, Closure $next): mixed
    {
        //pull the team parameter from the route
        $teamId = $request->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));
    }
}

まず、ルートからチーム ID を取得し、その後、ルート パラメーターを忘れます。パラメーターがコンテキスト内に入ったら、コントローラーにパラメーターを渡す必要はありません。プロジェクト ID が設定されている場合は、それもプルします。次に、チーム ID とプロジェクト ID (または null) を渡す AppResolver を使用してコンテキストを初期化します。

<?php declare(strict_types=1);

namespace App\Context\Resolvers;

use App\Models\Team;
use Honeystone\Context\ContextResolver;
use Honeystone\Context\Contracts\DefinesContext;

use function Honestone\Context\context;

class AppResolver extends ContextResolver
{
    public function __construct(
        private readonly int $teamId,
        private readonly ?int $projectId = null,
    ) {}

    public function define(DefinesContext $definition): void 
    {
        $definition
            ->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']);
    }
}

ここではもう少し詳しく説明します。

The define( ) メソッドは、解決されるコンテキストを定義する役割を果たします。チームは必須であり、チーム モデルである必要があります。プロジェクトは受け入れられ (つまり、オプション)、プロジェクト モデル (または null) である必要があります。

resolveTeam() は初期化時に内部的に呼び出されます。チームまたは null を返します。 null 応答のイベントでは、ContextInitializer によって CouldNotResolveRequiredContextException がスローされます。

resolveProject() も初期化時に内部的に呼び出されます。プロジェクトまたは null を返します。この場合、プロジェクトは定義で必要とされていないため、null 応答は例外にはなりません。

チームとプロジェクトを解決した後、ContextInitializer はオプションの checkTeam() メソッドと checkProject() メソッドを呼び出します。これらのメソッドは整合性チェックを実行します。 checkTeam() では、認証されたユーザーがチームのメンバーであることを確認し、checkProject() では、プロジェクトがチームに属していることを確認します。

最後に、すべてのリゾルバーには deserialization() メソッドが必要です。このメソッドは、シリアル化されたコンテキストを復元するために使用されます。これは最も顕著なのは、コンテキストがキューに入れられたジョブで使用される場合に発生します。

Now that our application context is set, we should use it.

Accessing the context

As usual, we’ll keep it simple, if a little contrived. When viewing the team we want to see a list of projects. We could build our TeamController to handle this requirements like this:

<?php declare(strict_types=1);

namespace App\Http\Controllers;

use Illuminate\View\View;

use function compact;
use function Honestone\Context\context;
use function view;

class TeamController
{
    public function __invoke(Request $request): View
    {
        $projects = context(&#39;team&#39;)->projects;

        return view('team', compact('projects'));
    }
}

Easy enough. The projects belonging to the current team context are passed to our view. Imagine we now need to query projects for a more specialised view. We could do this:

<?php declare(strict_types=1);

namespace App\Http\Controllers;

use Illuminate\View\View;

use function compact;
use function Honestone\Context\context;
use function view;

class ProjectQueryController
{
    public function __invoke(Request $request, string $query): View
    {
        $projects = Project::where(&#39;team_id&#39;, context(&#39;team&#39;)->id)
            ->where('name', 'like', "%$query%")
            ->get();

        return view('queried-projects', compact('projects'));
    }
}

It’s getting a little fiddly now, and it’s far too easy to accidentally forget to ‘scope’ the query by team. We can solve this using the BelongsToContext trait on our Project model:

<?php declare(strict_types=1);

namespace App\Models;

use Honeystone\Context\Models\Concerns\BelongsToContext;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Project extends Model
{
    use BelongsToContext;

    protected static array $context = [&#39;team&#39;];

    protected $fillable = [&#39;name&#39;];

    public function team(): BelongsTo
    {
        return $this->belongsTo(Team::class);
    }
}

All project queries will now be scooped by the team context and the current Team model will be automatically injected into new Project models.

Let’s simplify that controller:

<?php declare(strict_types=1);

namespace App\Http\Controllers;

use Illuminate\View\View;

use function compact;
use function view;

class ProjectQueryController
{
    public function __invoke(Request $request, string $query): View
    {
        $projects = Project::where(&#39;name&#39;, &#39;like&#39;, "%$query%")->get();

        return view('queried-projects', compact('projects'));
    }
}

That’s all folks

From here onwards, you’re just building your application. The context is easily at hand, your queries are scoped and queued jobs will automagically have access to the same context from which they were dispatched.

Not all context related problems are solved though. You’ll probably want to create some validation macros to give your validation rules a little context, and don’t forget manual queries will not have the context automatically applied.

If you’re planning to use this package in your next project, we’d love to hear from you. Feedback and contribution is always welcome.

You can checkout the GitHub repository for additional documentation. If you find our package useful, please drop a star.

Until next time..


This article was originally posted to the Honeystone Blog. If you like our articles, consider checking our more of our content over there.

以上がハニーストーン/コンテキストを使用したマルチテナント アプリケーションの構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。