آموزش پیاده سازی مدل data-segregation در معماری multi-tenant با لاراول

آموزش پیاده سازی مدل data-segregation در معماری multi-tenant با لاراول

مقدمه

در مقاله ی "Multi-tenancy معرفی ، مزایا و معایب" معماری multi-tenant به همراه مزایا و معایب آن شرح داده شد و همچنین دو مدل پیاده سازی معروف آن که تفکیک داده ها (data-segregation) و همانند سازی (instance replication) می باشد مطرح شد. در این مطلب قصد داریم مدل تفکیک داده ها را در قالب یک پروژه توسط فریم ورک لاراول پیاده سازی کنیم.
بزرگ ترین تفاوت یک برنامه multi-tenant و عادی این است که برنامه های multi-tenant برای اجرا شدن به دو سری اطلاعات نیاز دارند. در یک برنامه عادی شما تنها نیاز دارید که بدانید چه کاربری در حال حاضر به برنامه وارد شده است. اما در یک برنامه multi-tenant علاوه بر کاربر وارد شده، نیاز داریم بدانیم که کاربر در کدام یک از tenant های برنامه ما است. به عنوان مثال سایت سکان آکادمی را در نظر بگیرید. شما به حساب کاربری خود وارد می شوید و همین برای استفاده کامل شما از امکانات سایت کافی است. اما تصور کنید سکان آکادمی بجز آموزش برنامه نویسی، آموزش تعمیرات کامپیوتر، بازاریابی و... نیز داشت. برای اینکه بتوانیم چنین برنامه ای داشته باشیم باید علاوه بر کاربر وارد شده، tenant مورد استفاده او را هم بدانیم. زیرا هر کدام از tenant ها (مثل بخش آموزش برنامه نویسی یا تعمیرات کامپیوتر) کاربران خاص خودشان را دارند. برای انجام این جدا سازی دو روش وجود دارد. اولین روش این است که ستونی با نام tenant_id را به همه جدول های برنامه خود اضافه کنیم و همیشه اطمینان حاصل کنیم که tenant_id صحیح را در کوئری های خود در نظر می گیریم. دومین روش این است که برای هر tenant یک دیتابیس جدا ایجاد کنیم و قبل از اجرای برنامه به دیتابیس tenant مورد نظر وصل شویم. روش اول رایج ترین روشی است که برای مدل تفکیک داده ها در multi-tenancy استفاده می شود. اما اینکه تصمیم بگیرید از کدام روش استفاده کنید کاملا به شما و پروژه تان بستگی دارد. از آنجایی که پیاده سازی روش اول پیچیدگی چندانی ندارد در این مطلب روش دوم که به ازای هر tenant یک دیتابیس داریم را توسط فریم ورک لاراول پیاده سازی خواهیم کرد.
در لاراول برای استفاده از چندین دیتابیس به صورت همزمان پکیج هایی وجود دارند که در انتها آنها را معرفی می کنیم اما در این مطلب بدون استفاده از آنها برنامه خود را multi-tenant خواهیم کرد. پیش از رفتن به بخش بعدی، یک نسخه تازه از لاراول را در سیستم خود نصب کنید.

پیکربندی دیتابیس

ابتدا با فایل پیکربندی دیتابیس ها که با نام database.php در پوشه config قرار دارد، آغاز می کنیم. همه connection های موجود در این فایل، به جز mysql که با آن کار داریم را پاک می کنیم. سپس connection باقی مانده را به tenant تغییر نام داده و آن را به عنوان مقدار پیش فرض connection دیتابیس خود قرار می دهیم. همچنین یک connection دیگر می سازیم و نام آن را landlord قرار می دهیم. این connection برای ادمین سیستم خواهد بود و ما در آن اطلاعات همه tenant ها به همراه منابع مشترک بین آنها را نگه داری می کنیم. در این connection همه متغیر های environment با پیشوند landlord آغاز می شوند. در آخر مقدار کلید database از کانکشن tenant را null قرار می دهیم زیرا قرار است آن را بر اساس tenant ای که به برنامه ما درخواست می دهد، تعیین کنیم. اکنون در فایل database.php کد های زیر قرار دارند.

<?php

use Illuminate\Support\Str;

return [

    /*
    |--------------------------------------------------------------------------
    | Default Database Connection Name
    |--------------------------------------------------------------------------
    |
    | Here you may specify which of the database connections below you wish
    | to use as your default connection for all database work. Of course
    | you may use many connections at once using the Database library.
    |
    */

    'default' => env('DB_CONNECTION', 'tenant'),

    /*
    |--------------------------------------------------------------------------
    | Database Connections
    |--------------------------------------------------------------------------
    |
    | Here are each of the database connections setup for your application.
    | Of course, examples of configuring each database platform that is
    | supported by Laravel is shown below to make development simple.
    |
    |
    | All database work in Laravel is done through the PHP PDO facilities
    | so make sure you have the driver for your particular database of
    | choice installed on your machine before you begin development.
    |
    */

    'connections' => [
        'tenant' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => null,
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],
        'landlord' => [
            'driver' => 'mysql',
            'url' => env('LANDLORD_DATABASE_URL'),
            'host' => env('LANDLORD_DB_HOST', '127.0.0.1'),
            'port' => env('LANDLORD_DB_PORT', '3306'),
            'database' => env('LANDLORD_DB_DATABASE', 'forge'),
            'username' => env('LANDLORD_DB_USERNAME', 'forge'),
            'password' => env('LANDLORD_DB_PASSWORD', ''),
            'unix_socket' => env('LANDLORD_DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Migration Repository Table
    |--------------------------------------------------------------------------
    |
    | This table keeps track of all the migrations that have already run for
    | your application. Using this information, we can determine which of
    | the migrations on disk haven't actually been run in the database.
    |
    */

    'migrations' => 'migrations',

    /*
    |--------------------------------------------------------------------------
    | Redis Databases
    |--------------------------------------------------------------------------
    |
    | Redis is an open source, fast, and advanced key-value store that also
    | provides a richer body of commands than a typical key-value system
    | such as APC or Memcached. Laravel makes it easy to dig right in.
    |
    */

    'redis' => [

        'client' => env('REDIS_CLIENT', 'phpredis'),

        'options' => [
            'cluster' => env('REDIS_CLUSTER', 'redis'),
            'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
        ],

        'default' => [
            'url' => env('REDIS_URL'),
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', '6379'),
            'database' => env('REDIS_DB', '0'),
        ],

        'cache' => [
            'url' => env('REDIS_URL'),
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', '6379'),
            'database' => env('REDIS_CACHE_DB', '1'),
        ],

    ],

];

اکنون در فایل env. پروژه متغیر های جدید که با landlord آغاز می شدند را اضافه می کنیم.

APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:O/u82tRuBnmMoYz3VSOlXISMA9u0ilJozOuwAs1sFUE=
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack
LOG_LEVEL=debug

DB_CONNECTION=tenant
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

#land lord database variables
LANDLORD_DB_HOST=127.0.0.1
LANDLORD_DB_PORT=3306
LANDLORD_DB_DATABASE=landlord
LANDLORD_DB_USERNAME=root
LANDLORD_DB_PASSWORD=

BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=database
SESSION_LIFETIME=120

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

در این فایل نام دیتابیس مربوط به ادمین را landlord گذاشتیم و با کمک phpMyAdmin دیتابیسی با همین نام ایجاد کردیم.

ایجاد و اجرای مایگریشن ها

اکنون به سراغ مایگریشن ها می رویم. در پوشه ای که همه مایگریشن های پروژه وجود دارند یک پوشه با نام landlord می سازیم و داخل آن یک مایگریشن برای ایجاد جدول tenants به شکل زیر می نویسیم.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTenantsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tenants', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('domain')->unique();
            $table->string('database')->unique();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('tenants');
    }
}

با توجه به این جدول هر tenant در برنامه ما دارای یک نام، دامین و یک دیتابیس است.
در گام بعدی مدل مربوط به جدول tenants را می سازیم و درون آن کانکشن را مشخص می کنیم.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    use HasFactory;

    /**
     * The connection name for the model.
     *
     * @var string|null
     */
    protected $connection = 'landlord';

    /**
     * The attributes that aren't mass assignable.
     *
     * @var string[]|bool
     */
    protected $guarded = [];
}

در این مدل مقدار کانکشن را landlord قرار می دهیم اما سایر مدل های برنامه که به بخش landlord مربوط نمی شوند از کانکشن پیش فرض استفاده می کنند که همان کانکشن tenant ای است که از طریق درخواست آمده به برنامه، آن را تشخیص می دهیم.
اکنون یک seeder برای کانکشن landlord می سازیم.
در این seeder دو tenant با نام های Laravel وParavel می سازیم.

<?php

namespace Database\Seeders;

use App\Models\Tenant;
use Illuminate\Database\Seeder;

class LandlordSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Tenant::create([
            'name' => 'Laravel',
            'domain' => 'laravel.com',
            'database' => 'laravel'
        ]);

        Tenant::create([
            'name' => 'Paravel',
            'domain' => 'paravel.com',
            'database' => 'paravel'
        ]);
    }
}

اکنون می توانیم با زدن دستور artisan زیر، مایگریشن های مربوط به landlord را به همراه seeder آن اجرا کنیم.

 php artisan migrate:fresh --database=landlord --path=database/migrations/landlord --seed

پس از اجرای این دستور جدول tenants به همراه دو رکورد در دیتابیس landlord ایجاد می شود.

ساخت دستور artisan برای اجرای مایگریشن ها

اکنون در صورتی که بخواهیم مایگریشن ها را برای همه tenant های موجود اجرا کنیم، باید چندین بار دستور php artisan migrate را با ورودی های متفاوت اجرا کنیم. برای راحت تر شدن اینکار و جلوگیری از خطا های احتمالی دستور artisan مخصوص این کار می نویسیم.
به کمک دستور زیر کلاس command خود را در مسیر app/Console/Commands می سازیم.

php artisan make:command TenantsMigrateCommand

سپس کدهای زیر را درون آن قرار می دهیم.

<?php

namespace App\Console\Commands;

use App\Models\Tenant;
use Illuminate\Console\Command;

class TenantsMigrateCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tenants:migrate {tenant?} {--fresh} {--seed}';

/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->argument('tenant')) {
$this->migrate(Tenant::find($this->argument('tenant')));

return;
}

Tenant::all()->each(
fn($tenant) => $this->migrate($tenant)
);
}

/**
* @param Tenant $tenant
* @return void
*/
public function migrate(Tenant $tenant)
{
$tenant->configure()->use();

$this->line('');
$this->line("-----------------------------------------");
$this->info("Migrating Tenant #{$tenant->id} ({$tenant->name})");
$this->line("-----------------------------------------");

$options = ['--force' => true];

if ($this->option('seed')) {
$options['--seed'] = true;
}

$this->call(
$this->option('fresh') ? 'migrate:fresh' : 'migrate',
$options
);
}
}

با توجه به کد بالا میتوانیم هنگام استفاده از دستور php artisan tenants:migrate یک tenant خاص را مشخص کنیم. در غیر این صورت مایگریشن ها برای همه tenant های برنامه اجرا می شوند. همچنین آپشن های seed-- و fresh-- هم برای اجرای seeder ها و اجرای migrate:fresh به جای migrate وجود دارند.

در متد handle بررسی می شود در صورتی که یک tenant خاص مشخص شده باشد تنها برای آن، مایگریشن ها اجرا می شوند و در غیر این صورت برای همه tenant های برنامه مایگریشن ها اجرا خواهند شد.
متد migrate این کلاس یک مدل tenant را به عنوان ورودی دریافت می کند و درنهایت دستور php artisan migrate:fresh را برای آن اجرا می کند. اما پیش از آن که بتوانیم مایگریشن های یک tenant را اجرا کنیم باید کانکشن دیتابیس آن را پیکربندی کرده و مشخص کنیم که از آن tenant استفاده می کنیم. به همین منظور دو متد زیر را به مدل Tenant اضافه می کنیم.

/**
* @return $this
*/
public function configure()
{
config([
'database.connections.tenant.database' => $this->database,
]);

DB::purge('tenant');

DB::reconnect('tenant');

Schema::connection('tenant')->getConnection()->reconnect();

return $this;
}

/**
* @return $this
*/
public function use()
{
app()->forgetInstance('tenant');

app()->instance('tenant', $this);

return $this;
}

در متد configure ابتدا مقدار کانکشن دیتابیس که در مراحل قبل در فایل پیکربندی دیتابیس null گذاشته بودیم را از جدول tenants می خوانیم و درconfig قرار می دهیم. سپس توسط متد purge در صورتی که کانکشنی با دیتابیس وجود داشته باشد، آن را قطع می کنیم و با متد reconnect، با کانکشن جدید مجددا به دیتابیس وصل می شویم. همچنین برای کانکشن شمای دیتابیس هم همین کار را انجام می دهیم تا مطمئن شویم در صورتی که از قبل، کانکشنی با دیتابیس وجود داشته است، آن را قطع کنیم.
در متد use، مدل tenant ای که با آن کار می کنیم را با کلید tenant درلاراول ثبت می کنیم تا هر وقت آن را فراخواندیم tenant ای که در آن هستیم را برگرداند. البته قبل از انجام اینکار با صدا زدن متد forgetInstance مطمئن می شویم که چنین کلیدی در container مقدار نداشته باشد. 
پیش از اجرای دستور artisan ای که ساختیم باید به گونه ای از اجرا شدن کلاس seeder ای که برای Landlord نوشتیم، جلوگیری کنیم تا داده های تکراری در جدول tenants ذخیره نشوند. برای این کار یک شرط ساده هنگام صدا زدن اضافه می کنیم.

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
if ($_SERVER['argv'] !== 'tenants:migrate') {
$this->call([LandlordSeeder::class]);
}
}
}

اکنون دستور artisan خود را اجرا می کنیم.

php artisan tenants:migrate --fresh --seed

پیاده سازی فرآیند تشخیص tenant از درخواست ها

حال می توانیم برنامه خود را به نحوی پیکر بندی کنیم که همیشه از tenant صحیح استفاده کند.
ابتدا یک service provider با نام TenancyServiceProvider می سازیم.

php artisan make:provider TenancyServiceProvider

سپس آن را درون آرایه providers که در فایل app.php پوشه config قرار دارد ثبت می کنیم.

/*
* Package Service Providers...
*/
App\Providers\TenancyServiceProvider::class,

درون این service provider کد های زیر را می نویسیم.

<?php

namespace App\Providers;

use App\Models\Tenant;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Support\ServiceProvider;

class TenancyServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        $this->configureRequests();

        $this->configureQueue();
    }

    /**
     *
     */
    public function configureRequests()
    {
        if (!$this->app->runningInConsole()) {
            $host = $this->app['request']->getHost();

            Tenant::whereDomain($host)->firstOrFail()->configure()->use();
        }
    }

    /**
     *
     */
    public function configureQueue()
    {
        $this->app['queue']->createPayloadUsing(function () {
            return $this->app['tenant'] ? ['tenant_id' => $this->app['tenant']->id] : [];
        });

        $this->app['events']->listen(JobProcessing::class, function ($event) {
            if (isset($event->job->payload()['tenant_id'])) {
                Tenant::find($event->job->payload()['tenant_id'])->configure()->use();
            }
        });
    }
}

در متد boot این service provider دو متد configureRequests و configureQueue را صدا می زنیم.
در متد configureRequests درصورتی که برنامه در console اجرا نمی شود ابتدا domain ای که از آن به برنامه ما درخواست ارسال شده را دریافت می کنیم و با استفاده از آن tenant صحیح را پیدا کرده و روی مدل آن متد های configure و use را که بالاتر نوشتیم، صدا می زنیم. بنابراین اگر از آدرس Paravel.com به ما درخواست بدهند ما آن را در دیتابیس خود پیدا می کنیم و کانکشن صحیح دیتابیس را برای آن پیکربندی می کنیم.
در متد configureQueue باید کاری انجام دهیم تا هنگامی که job های برنامه ما بعدا در صف اجرا می شوند از tenant ای که باید در آن کار انجام دهند اطلاع داشته باشند. برای انجام این کار از متد createPayloadUsing در کلاس Queue استفاده می کنیم تا شناسه tenant را به داده هایی که job ها ذخیره می کنند اضافه کنیم. سپس به event ای با نام JobProcessing، listen می کنیم تا هنگامی که یک job در حال handle شدن است، tenant صحیح آن را از دیتابیس پیدا کنیم و پیکربندی آن را توسط متد های configure و use در مدل Tenant انجام دهیم.
اکنون برنامه ما هنگام پردازش درخواست ها و job هایی که در صف قرار دارند از tenant صحیح استفاده می کند.

بر طرف کردن نقص امنیتی در session ها

در حال حاضر در برنامه ای که ما پیاده سازی کرده ایم یک نقص امنیتی بزرگ وجود دارد. تصور کنید کاربری با شناسه 5 در tenant ای با نام A و دامین tenant-a.com وارد برنامه شده باشد. این کاربر می تواند خودش را به جای کاربر 5 در tenant های دیگر برنامه جا بزند. برای انجام این کار کافی است در مرورگر خود دامنه session ذخیره شده را از tenant-a.com به دامنه آن tenant ای که می خواهد، تغییر دهد. به عبارتی دیگر session های برنامه ما از tenant ای که برای آن اطلاعات ذخیره می کنند آگاه نیستند. در لاراول درایورهایی که می توان برای ذخیره session قرار داد عبارت اند از: file, cookie, database, apc, memcached, redis, dynamodb, array
در صورتی که از درایور database استفاده می کنید این نقص امنیتی وجود نخواهد داشت، زیرا هر tenant دیتابیس مخصوص به خودش را دارد. همچنین توجه داشته باشید که از درایور cookie نمی توان استفاده کرد زیرا به راحتی توسط کاربران قابل دستکاری است.
برای رفع این مشکل می توان از یک middleware استفاده کرد. ابتدا با دستور زیر یک middleware می سازیم.

php artisan make:middleware TenantSessions

سپس به فایل kernel.php که در مسیر app/Http قرار دارد می رویم و middleware خود را در گروه web ثبت می کنیم.

/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Laravel\Jetstream\Http\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\TenantSessions::class,
],

'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

اکنون کد های زیر را درون middleware خود می نویسیم.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class TenantSessions
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if (!$request->session()->has('tenant_id')) {
$request->session()->put('tenant_id',app('tenant')->id);

return $next($request);
}

abort_if(
$request->session()->get('tenant_id') !== app('tenant')->id,
401
);

return $next($request);
}
}

در متد handle ابتدا در صورتی که session ای با کلی tenant_id وجود نداشته باشد، آن را می سازیم و شناسه tenant ای که از آن به برنامه ما درخواست ارسال شده است را به عنوان مقدار در آن ذخیره می کنیم (این شناسه را در انتهای بخش 4 در container لاراول ذخیره کردیم). اکنون در هر درخواستی که به سمت برنامه ما می آید، ابتدا بررسی می شود که شناسه tenant ای که از آن درخواست ارسال شده با شناسه tenant ای که در session قرار دارد مطابقت داشته باشد. در غیر این صورت از اجرای ادامه برنامه جلوگیری خواهد شد.

پکیج هایی برای پیاده سازی معماری multi-tenant

در صورتی که شما استفاده از پکیج را به پیاده سازی معماری multi-tenant توسط خودتان ترجیح می دهید، پکیج های زیر می توانند به شما کمک کنند.

https://github.com/romegasoftware/Multitenancy

 https://spatie.be/docs/laravel-multitenancy/v1/introduction

 https://tenancy.dev/docs/hyn/5.4

 https://tenancyforlaravel.com

 

جمع بندی

در این مطلب مدل data-segregation از معماری multi-tenant را بدون استفاده از پکیج در فریم ورک لاراول پیاده سازی کردیم. در برنامه ای که نوشتیم دیتابیس، صف و session ها از tenant خود آگاه اند. در صورتی که شما قصد استفاده از سایر امکانات فریم ورک مثل cache را در برنامه خود دارید، باید آن ها را نسبت به tenant ای که از آن به برنامه درخواست ارسال می شود، آگاه کنید تا از هرگونه تداخل بین tenant های مختلف برنامه جلوگیری شود. در صورتی که بخشی از این مطلب نیاز به توضیح بیشتری دارد لطفا در بخش نظرات با ما در میان بگذارید.

از بهترین نوشته‌های کاربران سکان آکادمی در سکان پلاس


online-support-icon