[Laravel 8.45.1] マルチテナントアーキテクチャ

Laravelでのマルチテナントは、tenancy/tenancyを使用する。
tenancy/tenancyと、ベースの機能のみで必要な分は後から追加するtenancy/frameworkがある。
tenancy/framework推奨

$ composer require tenancy/framework
$ php artisan make:model Organization -m

create_organizations_table.php

public function up()
    {
        Schema::create('organizations', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->timestamps();
        });
    }

$ php artisan migrate

Organization.php

use Illuminate\Http\Request;
use Tenancy\Identification\Contracts\Tenant;

class Organization extends Model implements Tenant
{
    use HasFactory;

    protected $fillable = [
    	'name',
    	'subdomain',
    ];

    protected $dispatchesEvents = [
    	'created' => \Tenancy\Tenant\Events\Created::class,
    	'updated' => \Tenancy\Tenant\Events\Updated::class,
    	'deleted' => \Tenancy\Tenant\Events\Deleted::class,
    ];

    public function getTenantKeyName(): string {
    	return 'name';
    }

    public function getTenantKey(){
    	return $this->name;
    }

    public function getTenantIdentifier(): string {
    	return get_class($this);
    }
}

Tenancy\Identification\Concerns\AllowsTenantIdentification を使うだけでもできる
-テナントが追加されたら、テナント用のデータベースを作成してマイグレーション
-テナントが更新されたらマイグレーション実行
-テナントが削除されたらデータベース削除
-テナントを識別するため、Resolverに登録

app/Providers/AppServiceProvider

use Tenancy\Identification\Contracts\ResolvesTenants;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        //
        $this->app->resolving(ResolvesTenants::class, function(ResolvesTenants $resolver){
            $resolver->addModel(Organization::class);
            return resolver;
        });
    }
}

MySQLのデータベースドライバをインストール
$ composer require tenancy/db-driver-mysql

affects connections : テナントのDBにテナントのDBにアクセスできるようにする
$ composer require tenancy/affects-connections

tenancyの拡張パッケージは Event/Listner で構成
$ php artisan make:listener ResolveTenantConnection
/listeners/ResolveTenantConnection.php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Tenancy\Identification\Contracts\Tenant;
use Tenancy\Affects\Connections\Contracts\ProvidesConfiguration;

class ResolveTenantConnection
{
    public function handle(Resolving $event)
    {
        //
        return $this;
    }

    public function configure(Tenant $tenant): array {
        $config = [];

        event(new Configuring($tenant, $config, $this));

        return $config;
    }
}

/app/Providers/EventServiceProvider.php

use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [

        \Tenancy\Affects\Connections\Events\Resolving::class => [
            App\Listeners\ResolveTenantConnection\ResolveTenantConnection::class,
        ],
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
    ];
}

$ composer require tenancy/hooks-migration
$ php artisan make:listener ConfigureTenantMigrations

use Tenancy\Hooks\Migration\Events\ConfigureMigrations;
use Tenancy\Tenant\Events\Deleted;

class ConfigureTenantMigrations
{
    public function handle(ConfigureMigrations $event)
    {
        //
        if($event->event->tenant){
            if($event->event instanceof Deleted){
                $event->disable();
            } else {
                $event->path(database_path('tenant/migrations'));
            }
        }
    }
}

EventServiceProvider.php

use Illuminate\Support\Facades\Event;
use App\Listeners\ConfigureTenantMigrations;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
        $this->app->resolving(ResolvesTenants::class, function(ResolvesTenants $resolver){
            $resolver->addModel(Organization::class);
            return resolver;
        });
    }

    protected $listen = [
        \Tenancy\Hooks\Migration\Events\ConfigureMigrations::class => [
            ConfigureTenantMigrations::class,
        ],
    ];
}

$ php artisan make:listener ConfigureTenantSeeds

use Database\Tenant\Seeders\DatabaseSeeder;
use Tenancy\Hooks\Migration\Events\ConfigureSeeds;
use Tenancy\Tenant\Events\Deleted;
use Tenancy\Tenant\Events\Updated;

class ConfigureTenantSeeds
{
    public function handle(ConfigureSeeds $event)
    {
        //
        if($event->event->tenant){
            if($event->event instanceof Deleted || $event->event instanceof Updated){
                $event->disable();
            } else {
                $event->seed(DatabaseSeeder::class);
            }
        }
    }
}

EventServiceProvider.php

    protected $listen = [

        \Tenancy\Affects\Connections\Events\Resolving::class => [
            App\Listeners\ResolveTenantConnection\ResolveTenantConnection::class,
        ],
        \Tenancy\Hooks\Migration\Events\ConfigureMigrations::class => [
            ConfigureTenantMigrations::class,
        ],
        \Tenancy\Hooks\Migration\Events\ConfigureSeeds::class => [
            ConfigureTenantSeed::class
        ],
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
    ];

.env.testing

TENANT_SLUG=test

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=test_central
DB_USERNAME=root
DB_PASSWORD=password

あれ? これでどうやって実装するのかようわからんな。。