إطار Laravel
التعددية المستأجرة في Laravel
التعددية المستأجرة في Laravel
التعددية المستأجرة هي بنية معمارية حيث يخدم مثيل تطبيق واحد عدة عملاء (مستأجرين)، مع عزل بيانات كل مستأجر عن الآخرين. توفر Laravel أدوات ممتازة لبناء تطبيقات متعددة المستأجرين.
فهم بنية التعددية المستأجرة
هناك ثلاثة أساليب رئيسية للتعددية المستأجرة:
أساليب التعددية المستأجرة:
- قاعدة بيانات واحدة، مخطط مشترك: جميع المستأجرين يشاركون نفس قاعدة البيانات والجداول، مع عمود tenant_id لفصل البيانات.
- قاعدة بيانات واحدة، مخططات منفصلة: كل مستأجر لديه مخطط خاص به ضمن نفس قاعدة البيانات.
- قواعد بيانات متعددة: كل مستأجر لديه قاعدة بيانات منفصلة تماماً.
نهج قاعدة البيانات الواحدة
النهج الأبسط يستخدم عمود معرف المستأجر في جداولك:
// Migration: إضافة tenant_id إلى الجداول
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('content');
$table->timestamps();
});
// Model مع نطاق المستأجر
class Post extends Model
{
protected static function booted()
{
static::addGlobalScope('tenant', function (Builder $query) {
if (auth()->check() && auth()->user()->tenant_id) {
$query->where('tenant_id', auth()->user()->tenant_id);
}
});
static::creating(function ($model) {
if (auth()->check() && auth()->user()->tenant_id) {
$model->tenant_id = auth()->user()->tenant_id;
}
});
}
}
تحديد هوية المستأجر
يمكنك تحديد المستأجرين من خلال طرق مختلفة:
// app/Models/Tenant.php
class Tenant extends Model
{
protected $fillable = ['name', 'domain', 'database', 'is_active'];
public function users()
{
return $this->hasMany(User::class);
}
}
// Middleware تحديد المستأجر
// app/Http/Middleware/IdentifyTenant.php
class IdentifyTenant
{
public function handle(Request $request, Closure $next)
{
// الطريقة 1: بناءً على النطاق الفرعي
$host = $request->getHost();
$subdomain = explode('.', $host)[0];
if ($subdomain !== 'www' && $subdomain !== config('app.domain')) {
$tenant = Tenant::where('domain', $subdomain)->firstOrFail();
app()->instance('tenant', $tenant);
}
// الطريقة 2: بناءً على المسار
if ($request->segment(1)) {
$tenant = Tenant::where('slug', $request->segment(1))->first();
if ($tenant) {
app()->instance('tenant', $tenant);
}
}
// الطريقة 3: بناءً على الرأس (للـ APIs)
if ($request->header('X-Tenant-ID')) {
$tenant = Tenant::find($request->header('X-Tenant-ID'));
if ($tenant) {
app()->instance('tenant', $tenant);
}
}
return $next($request);
}
}
أفضل ممارسات تحديد المستأجر:
- استخدم التحديد بناءً على النطاق الفرعي لعلامة تجارية أفضل
- نفذ التخزين المؤقت لتقليل استعلامات قاعدة البيانات
- تحقق دائماً من وجود المستأجر قبل المتابعة
- ضع في اعتبارك الآثار الأمنية لتبديل المستأجرين
نهج قواعد البيانات المتعددة
للعزل الكامل للبيانات، استخدم قواعد بيانات منفصلة لكل مستأجر:
// config/database.php - إضافة اتصال المستأجر
'tenant' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => null, // سيتم تعيينه ديناميكياً
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
],
// Middleware للتبديل بين قواعد البيانات
class SwitchTenantDatabase
{
public function handle(Request $request, Closure $next)
{
$tenant = app('tenant');
if ($tenant) {
// تكوين اتصال قاعدة بيانات المستأجر
Config::set('database.connections.tenant.database', $tenant->database);
// مسح وإعادة الاتصال
DB::purge('tenant');
DB::reconnect('tenant');
// تعيين كاتصال افتراضي
DB::setDefaultConnection('tenant');
}
return $next($request);
}
}
// الاستخدام في النماذج
class TenantPost extends Model
{
protected $connection = 'tenant';
protected $table = 'posts';
}
النطاقات العامة الواعية بالمستأجر
أنشئ نطاقات مستأجر قابلة لإعادة الاستخدام لنماذجك:
// app/Models/Scopes/TenantScope.php
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
if ($tenantId = $this->getTenantId()) {
$builder->where($model->getTable() . '.tenant_id', $tenantId);
}
}
protected function getTenantId()
{
if (app()->has('tenant')) {
return app('tenant')->id;
}
if (auth()->check()) {
return auth()->user()->tenant_id;
}
return null;
}
public function extend(Builder $builder)
{
$builder->macro('withoutTenant', function (Builder $builder) {
return $builder->withoutGlobalScope($this);
});
}
}
// Trait للاستخدام السهل
trait BelongsToTenant
{
protected static function bootBelongsToTenant()
{
static::addGlobalScope(new TenantScope);
static::creating(function ($model) {
if (app()->has('tenant')) {
$model->tenant_id = app('tenant')->id;
}
});
}
}
// الاستخدام في النماذج
class Product extends Model
{
use BelongsToTenant;
}
اعتبارات الأمان:
- تحقق دائماً من وصول المستأجر قبل العمليات
- امنع تسرب البيانات بين المستأجرين في العلاقات
- كن حذراً مع التحميل المسبق عبر المستأجرين
- نفذ المصادقة المناسبة لكل مستأجر
- استخدم مفاتيح تخزين مؤقت منفصلة لكل مستأجر
العلاقات الواعية بالمستأجر
تأكد من أن العلاقات تحترم حدود المستأجر:
// نموذج أساسي مع علاقات المستأجر
class TenantModel extends Model
{
use BelongsToTenant;
// تجاوز hasMany لإضافة نطاق المستأجر
public function hasMany($related, $foreignKey = null, $localKey = null)
{
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return $this->newHasMany(
$instance->newQuery()->where('tenant_id', $this->tenant_id),
$this,
$instance->getTable() . '.' . $foreignKey,
$localKey
);
}
// تجاوز belongsToMany لجداول الربط
public function belongsToMany($related, $table = null, $foreignPivotKey = null,
$relatedPivotKey = null, $parentKey = null,
$relatedKey = null, $relation = null)
{
$instance = $this->newRelatedInstance($related);
return parent::belongsToMany(
$related,
$table,
$foreignPivotKey,
$relatedPivotKey,
$parentKey,
$relatedKey,
$relation
)->wherePivot('tenant_id', $this->tenant_id);
}
}
// مثال على الاستخدام
class User extends TenantModel
{
public function posts()
{
return $this->hasMany(Post::class);
}
public function roles()
{
return $this->belongsToMany(Role::class)
->withPivot('tenant_id');
}
}
استخدام حزمة Spatie Multi-tenancy
لتطبيقات الإنتاج، فكر في استخدام حزمة مخصصة:
// تثبيت الحزمة
composer require spatie/laravel-multitenancy
// نشر الإعدادات
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider"
// تكوين نموذج المستأجر
// config/multitenancy.php
return [
'tenant_model' => App\Models\Tenant::class,
'tenant_finder' => Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class,
'switch_tenant_tasks' => [
Spatie\Multitenancy\Tasks\SwitchTenantDatabase::class,
// Spatie\Multitenancy\Tasks\SwitchRouteCacheTask::class,
// Spatie\Multitenancy\Tasks\SwitchConfigTask::class,
],
'queues_are_tenant_aware_by_default' => true,
];
// تنفيذ نموذج المستأجر
use Spatie\Multitenancy\Models\Tenant as SpatieTenant;
class Tenant extends SpatieTenant
{
public function makeCurrent(): static
{
// منطق مخصص قبل التبديل
return parent::makeCurrent();
}
public function forget(): static
{
// منطق تنظيف مخصص
return parent::forget();
}
}
// الاستخدام في وحدات التحكم
class DashboardController extends Controller
{
public function index()
{
// المستأجر الحالي متاح تلقائياً
$tenant = Tenancy::getTenant();
// أو تحقق مما إذا كنا في سياق المستأجر
if (Tenancy::checkCurrent()) {
// منطق خاص بالمستأجر
}
return view('dashboard', [
'tenant' => $tenant,
'stats' => $this->getTenantStats(),
]);
}
}
تمرين 1: أنشئ نظام مدونة متعدد المستأجرين باستخدام نهج قاعدة البيانات الواحدة:
- أنشئ نموذج Tenant مع حقول الاسم والنطاق
- أضف tenant_id إلى جداول المستخدمين والمنشورات والتعليقات
- نفذ middleware لتحديد المستأجر باستخدام النطاقات الفرعية
- أنشئ trait BelongsToTenant مع نطاق عام
- طبق الـ trait على نماذج Post و Comment
- اختبر عزل البيانات بين المستأجرين
تمرين 2: ابن نظام مستأجر متعدد قواعد البيانات:
- أنشئ قاعدة بيانات مركزية مع جدول tenants (الاسم، النطاق، اسم قاعدة البيانات)
- نفذ middleware للتبديل بين اتصالات قواعد البيانات
- أنشئ أمراً لتوفير قواعد بيانات المستأجرين الجدد
- قم بتشغيل migrations لكل قاعدة بيانات مستأجر
- أنشئ بيانات seeder خاصة بالمستأجر
- اختبر التبديل بين قواعد بيانات المستأجرين
تمرين 3: نفذ التخزين المؤقت والطوابير الواعية بالمستأجر:
- أنشئ TenantCacheManager يضيف بادئة لمفاتيح التخزين المؤقت مع معرف المستأجر
- نفذ إرسال الوظائف الواعي بالمستأجر
- أنشئ middleware لحقن سياق المستأجر في الوظائف المصفوفة
- اختبر أن وظائف الخلفية تعمل على بيانات المستأجر الصحيحة
- نفذ تحديد المعدل الخاص بالمستأجر