Rumah  >  Artikel  >  pembangunan bahagian belakang  >  Membina aplikasi berbilang penyewa dengan batu madu/konteks

Membina aplikasi berbilang penyewa dengan batu madu/konteks

PHPz
PHPzasal
2024-08-10 22:30:42924semak imbas

Tidak perlu dikelirukan dengan perpustakaan konteks baharu Laravel, pakej ini boleh digunakan untuk membina aplikasi berbilang penyewa berbilang konteks. Kebanyakan perpustakaan berbilang penyewa pada asasnya mempunyai satu konteks 'penyewa', jadi jika anda memerlukan berbilang konteks, perkara boleh menjadi agak rumit. Pakej baharu ini menyelesaikan masalah itu.

Mari kita lihat contoh?

Contoh projek

Untuk aplikasi contoh kami, kami akan mempunyai pangkalan pengguna global iaitu disusun ke dalam pasukan dan setiap pasukan akan mempunyai pelbagai projek. Ini adalah struktur yang agak biasa dalam kebanyakan aplikasi Perisian sebagai Perkhidmatan.

Adalah perkara biasa bagi aplikasi berbilang penyewa untuk mempunyai setiap pangkalan pengguna dalam konteks penyewa, tetapi untuk aplikasi contoh kami, kami mahu pengguna dapat menyertai berbilang pasukan, jadi asas pengguna global.
Rajah asas pengguna vs penyewa global

Building a multi-tenant application with honeystone/context

Sebagai SaaS, kemungkinan besar pasukan akan menjadi entiti yang boleh dibilkan (iaitu tempat duduk) dan ahli pasukan tertentu akan diberikan kebenaran untuk menguruskan pasukan. Saya tidak akan menyelami butiran pelaksanaan ini dalam contoh ini, tetapi semoga ia menyediakan beberapa konteks tambahan.

Pemasangan

Untuk memastikan siaran ini ringkas, saya tidak akan menerangkan cara memulakan Laravel anda projek. Terdapat banyak sumber yang lebih baik yang tersedia untuk itu, tidak terkecuali dokumentasi rasmi. anggap saja anda sudah mempunyai projek Laravel, dengan model Pengguna, Pasukan dan Projek, dan anda sudah bersedia untuk mula melaksanakan pakej konteks kami.

Pemasangan ialah pujian komposer ringkas:

composer install honeystone/context

Pustaka ini mempunyai fungsi kemudahan, context(), yang pada Laravel 11 bertembung dengan fungsi konteks Laravel sendiri. Ini bukan masalah sebenarnya. Anda boleh sama ada mengimport fungsi kami:

use function Honestone\Context\context;

Atau hanya gunakan bekas suntikan kebergantungan Laravel. Sepanjang siaran ini, saya akan menganggap anda telah mengimport fungsi tersebut dan menggunakannya dengan sewajarnya.

Modelnya

Mari mulakan dengan mengkonfigurasi model Pasukan kami:

<?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);
    }
}

Pasukan mempunyai nama, ahli dan projek. Dalam aplikasi kami, hanya ahli pasukan akan dapat mengakses pasukan atau projeknya.

Baiklah, jadi mari lihat Projek kami:

<?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);
    }
}

Sebuah projek mempunyai nama dan tergolong dalam pasukan.

Menentukan konteks

Apabila seseorang mengakses aplikasi kami, kami perlu menentukan pasukan dan projek yang mereka bekerjasama. Untuk memastikan perkara ini mudah, mari kita kendalikan perkara ini dengan parameter laluan. Kami juga akan menganggap bahawa hanya pengguna yang disahkan boleh mengakses aplikasi.

Konteks pasukan mahupun projek: app.mysaas.dev
Konteks pasukan sahaja: app.mysaas.dev/my-team
Konteks pasukan dan projek: app.mysaas.dev/my-team/my-project

Laluan kami akan kelihatan seperti ini:

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);
    });
});

Ini adalah pendekatan yang sangat tidak fleksibel, memandangkan potensi untuk pertembungan ruang nama, tetapi ia mengekalkan contoh yang ringkas. Dalam aplikasi dunia sebenar, anda ingin mengendalikan perkara ini sedikit berbeza, mungkin anothersaas.dev/teams/my-team/projects/my-project atau my-team.anothersas.dev/projects/my-project.

Kita harus melihat AppContextMiddleware kami dahulu. Perisian tengah ini memulakan konteks pasukan dan, jika ditetapkan, konteks projek:

<?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));
    }
}

Untuk bermula, kami ambil id pasukan daripada laluan dan kemudian lupakan parameter laluan. Kami tidak memerlukan parameter mencapai pengawal kami sebaik sahaja ia berada dalam konteks. Jika id projek ditetapkan, kami juga menariknya. Kami kemudiannya memulakan konteks menggunakan AppResolver kami yang menghantar id pasukan kami dan id projek kami (atau nol):

<?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']);
    }
}

Sedikit lagi berlaku di sini.

The define( ) kaedah bertanggungjawab untuk menentukan konteks yang sedang diselesaikan. Pasukan itu diperlukan dan mestilah model Pasukan dan projek itu diterima (iaitu pilihan) dan mestilah model Projek (atau batal).

resolveTeam() akan dipanggil secara dalaman pada permulaan. Ia mengembalikan Pasukan atau batal. Sekiranya tindak balas nol, CouldNotResolveRequiredContextException akan dilemparkan oleh ContextInitializer.

resolveProject() juga akan dipanggil secara dalaman semasa pengamulaan. Ia mengembalikan Projek atau null. Dalam kes ini, respons nol tidak akan menghasilkan pengecualian kerana projek tidak diperlukan oleh definisi.

Selepas menyelesaikan pasukan dan projek, ContextInitializer akan memanggil kaedah checkTeam() dan checkProject() pilihan. Kaedah ini menjalankan pemeriksaan integriti. Untuk checkTeam() kami memastikan bahawa pengguna yang disahkan ialah ahli pasukan dan untuk checkProject() kami menyemak sama ada projek itu milik pasukan.

Akhir sekali, setiap penyelesai memerlukan kaedah penyahserialisasian(). Kaedah ini digunakan untuk mengembalikan konteks bersiri. Paling ketara ini berlaku apabila konteks digunakan dalam kerja beratur.

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.

Atas ialah kandungan terperinci Membina aplikasi berbilang penyewa dengan batu madu/konteks. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn