Простая Мультитенантность в Laravel через Trait

Мультитенантность в Laravel

Мультитенантность( она же Мультиарендность, она же Multi-Tenancy) распространенное явление в веб-проектах — когда вы предоставляете доступ к записям только тем пользователям, кто их создал. Другими словами, каждый управляет своими собственными данными и не видит чужие. Эта статья покажет вам, как реализовать это в одной базе данных самым простым способом.

Внимание: есть несколько более сложных и гибких реализаций мультитенантности, включая использование различных пакетов, но в этой статье я расскажу о самом быстром способе.

Что будем создавать

Представьте себе сайт по управлению Книгами и каждый пользователь может видеть только свои загруженные книги. Каждая книга привязана к стране происхождения из модели Country и эта модель должна быть доступна каждому, без мультитенантности.

Create Book

Таким образом, мы будем имитировать более сложные примеры из реальной жизни, где часть моделей мультитенантны, а часть — публичны.

Шаг 1. Вход Пользователя, создавшего запись

Для всех моделей, которые должны быть мультитенантны, нам нужно создать дополнительное поле в их таблице базы данных.

php artisan make:migration add_created_by_user_id_to_books_table

и код миграции:

Schema::table('books', function (Blueprint $table) {
    $table->unsignedInteger('created_by_user_id');
    $table->foreign('created_by_user_id')->references('id')->on('users');
});

и добавляем в app/Book.php — смотри последнее значение:

protected $fillable = [
    'title',
    'country_id',
    'created_at',
    'updated_at',
    'created_by_user_id',
];

Как мы теперь можем заполнить это поле автоматически? Конечно можно это сделать через Наблюдателя (Observer), но, для этого примера, давайте создадим Trait, который будем использовать как для сохранения записи, так и для ее фильтрации.

Создаем новую папку app/Traits и файл app/Traits/Multitenantable.php и вставляем туда:

namespace App\Traits;

trait Multitenantable {

    protected static function bootMultitenantable()
    {
        if (auth()->check()) {
            static::creating(function ($model) {
                $model->created_by_user_id = auth()->id();
            });
        }
    }

}

Обратите внимание на имя метода bootMultitenantable() — соглашение об именах bootXYZ() означает, что Laravel автоматически запустит этот метод при использовании Trait’а. Можно назвать это «конструктором» Trait’а.

Итак, что мы здесь делаем? Если пользователь залогинен, мы добавляем его идентификатор в поле created_by_user_id, какая бы ни была модель.
Видите, мы нигде здесь не упоминаем Book или какую-либо другую модель. Таким образом, мы можем добавить этот Trait к любым моделям, которые захотим.

Но давайте начнем с Book. Добавить Trait очень просто — всего пара строк в app/Book.php:

use App\Traits\Multitenantable;

// ...

class Book extends Model
{
    use Multitenantable;
    // ...
}

И всё, поле create_by_user_id будет заполнено автоматически при вызове Book::create().

Шаг 2. Фильтрация данных по пользователю

Следующим шагом будет фильтрация данных, когда кто-либо получает доступ к списку книг или пытается получить книгу по ее идентификатору.

Нам нужно добавить глобальный Scope для всех запросов к этой модели.
И это тоже можно гибко реализовать в этом же самом app/Traits/Multitenantable.php. Вот его полный код:

namespace App\Traits;

use Illuminate\Database\Eloquent\Builder;

trait Multitenantable {

    protected static function bootMultitenantable()
    {
        if (auth()->check()) {
            static::creating(function ($model) {
                $model->created_by_user_id = auth()->id();
            });

            static::addGlobalScope('created_by_user_id', function (Builder $builder) {
                $builder->where('created_by_user_id', auth()->id());
            });
        }
    }

}

Мы подключили класс Eloquent/Builder, а затем использовали static::addGlobalScope() для фильтрации любого запроса с залогиненным пользователем.

Вот и все, теперь каждый пользователь увидит только свои книги.

В дополнение к этому, фильтр будет работать каждый раз, когда кто-либо получает список книг, где бы он ни находился в проекте. Например, если в будущем book_id будет полем для некоторых других таблиц, таких как chapters — они по прежнему смогут выбирать их только из своих книг.

 

Шаг 3. Добавление Trait к другим моделям

Помните, нашей задачей было оставить модель Countries доступной для всех пользователей? А решение этого в том, что мы просто не используем наш Trait в модели app/Country.php.

Короче говоря, правило простое: для моделей, которые должны быть «мультитенантными» используйте этот Trait. Вот и все.

Шаг 4. Как насчет администратора, который может просмотреть все записи?

Очевидно, что какой-то «суперпользователь» системы должен видеть все книги, верно?
Этот функционал теперь зависит от вашей реализации системы пользователь-роль-права, но, в общих чертах, я расскажу, как вы можете добавить эти права.

В фильтре Trait нужно добавить:

protected static function bootMultitenantable()
{
    if (auth()->check()) {
        static::creating(function ($model) {
            $model->created_by_user_id = auth()->id();
        });

        // если пользователь не администратор (role_id 1)
        if (auth()->user()->role_id != 1) {
            static::addGlobalScope('created_by_user_id', function (Builder $builder) {
                $builder->where('created_by_user_id', auth()->id());
            });
        }
    }
}

Да, всё просто — проверяйте через оператор if, кто из пользователей может обойти этот фильтр.

 

Автор: Povilas Korop
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.