Laravel Eloquent: сложные многопользовательские полиморфные отношения с нарушением ограничений Pivot в заданиях очередиPhp

Кемеровские программисты php общаются здесь
Ответить
Anonymous
 Laravel Eloquent: сложные многопользовательские полиморфные отношения с нарушением ограничений Pivot в заданиях очереди

Сообщение Anonymous »

Laravel 11.35.1 | PHP 8.4.2 | PostgreSQL 16.5
Я реализую мультиарендный SaaS с полиморфными отношениями между 3 подключениями к базе данных (арендатор1, арендатор2, общий). Проблема возникает только в заданиях очереди, где ограничения поворота не работают из-за потери контекста клиента.
Схема базы данных
SQL

Код: Выделить всё

-- shared database
tenants
id (uuid), name, database_name

-- tenant1 database
users
id (uuid), tenant_id (uuid), name, email

workspaces
id (uuid), tenant_id (uuid), name

-- tenantX databases (dynamic)
workspace_members (pivot)
id (uuid), workspace_id (uuid), member_id (uuid), member_type
-- member_type: 'App\Models\User', 'App\Models\Team', 'App\Models\ExternalGuest'
Модели
PHP

Код: Выделить всё

// app/Models/Tenant.php (shared DB)
class Tenant extends Model
{
protected $connection = 'shared';

public function database()
{
return $this->belongsToMany(Workspace::class, 'workspace_members',
'tenant_id', 'workspace_id')->withPivot('member_id', 'member_type');
}
}

// app/Models/Workspace.php (tenant DB)
class Workspace extends Model
{
protected $connection = null; // Dynamic

public function members()
{
return $this->morphToMany(
get_class(), // Dynamic: User|Team|ExternalGuest
'member',
'workspace_members',
'workspace_id',
'member_id'
)->withPivot('tenant_id'); // Tenant isolation
}

public function membersInCurrentTenant()
{
return $this->members()->wherePivot('tenant_id', tenant()->id);
}
}

// Dynamic model resolution
class DynamicModelResolver
{
public static function resolve(string $type): Model
{
$tenantId = tenant()->id;

// Switch DB connection
config(['database.connections.tenant.database' => "tenant_{$tenantId}"]);
DB::purge('tenant');
DB::reconnect('tenant');

return match($type) {
'App\Models\User' => User::on('tenant'),
'App\Models\Team' => Team::on('tenant'),
'App\Models\ExternalGuest' => ExternalGuest::on('tenant')
};
}
}
Проблема
Работает в HTTP-запросах ✅
PHP

Код: Выделить всё

// In controller - WORKS
$workspace = Workspace::find('uuid');
$members = $workspace->membersInCurrentTenant()->get();
// Returns only members where pivot.tenant_id = current_tenant
Ошибка выполнения заданий в очереди ❌
PHP

Код: Выделить всё

// In queue job - BROKEN
class ProcessWorkspaceActivity extends Job
{
public function handle()
{
$workspace = Workspace::find('uuid');
$members = $workspace->membersInCurrentTenant()->get();
// Returns ALL members across ALL tenants!
// pivot.tenant_id constraint is ignored
}
}
Вывод отладки
HTTP-запрос:
текст

Код: Выделить всё

SQL: select * from workspace_members
where workspace_id = ?
and tenant_id = 'tenant-uuid-here'
Задание в очереди:
текст

Код: Выделить всё

SQL: select * from workspace_members
where workspace_id = ?
-- NO tenant_id constraint!
Что я пробовал
  • Промежуточное программное обеспечение арендатора в заданиях ❌
PHP

Код: Выделить всё

class TenantAwareJob extends Job
{
public $tenantId;

public function handle()
{
tenant($this->tenantId); // Sets global tenant
// Still broken
}
}
  • Глобальные области ❌
PHP

Код: Выделить всё

// WorkspaceMemberObserver
class WorkspaceMemberObserver
{
public function retrieved(WorkspaceMember $model)
{
if (!tenant()) return;
$model->where('tenant_id', tenant()->id);
}
}
  • Переключение соединения ❌
PHP

Код: Выделить всё

DB::connection('tenant')->table('workspace_members')
->where('workspace_id', $id)
->where('tenant_id', tenant()->id);  // Works but bypasses Eloquent
Минимально воспроизводимый пример
PHP

Код: Выделить всё

// DatabaseSeeder
DB::connection('tenant1')->table('workspace_members')->insert([
['workspace_id' => 'ws1', 'member_id' => 'user1', 'member_type' => 'App\Models\User', 'tenant_id' => 'tenant1'],
['workspace_id' => 'ws1', 'member_id' => 'user2', 'member_type' => 'App\Models\User', 'tenant_id' => 'tenant2'], // Cross-tenant!
]);

// Test job
php artisan queue:work --once
Ожидается: 1 участник (только клиент1) Фактическое: 2 участника (оба клиента)
Вопросы
  • Почему ограничение сводки исчезает в заданиях очереди?
  • Как сохранить контекст клиента в отношениях Eloquent во время обработки очереди?
  • Лучшая практика для многопользовательских полиморфных отношений с динамическими подключениями к БД?
Текущее решение (уродливо)
PHP

Код: Выделить всё

// Forces tenant constraint manually
$members = $workspace->members()
->whereHas('pivot', fn($q) => $q->where('tenant_id', tenant()->id))
->get();
Но это не работает:
  • с нетерпеливой загрузкой ('members')
  • members()->count()
  • Любая цепочка отношений
Есть кто-нибудь решал мультитенантные полиморфные отношения в очередях Laravel? 🤯

Подробнее здесь: https://stackoverflow.com/questions/798 ... t-constrai
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Php»