[Laravel8.x] stancl/tenancyでマルチテナントを構築する手順

$ composer create-project –prefer-dist laravel/laravel stancl
$ cd stancl
$ composer require laravel/jetstream
$ php artisan jetstream:install livewire
$ composer require stancl/tenancy
$ php artisan tenancy:install

$ php artisan migrate
mysql> show tables;
+————————+
| Tables_in_stancl |
+————————+
| domains |
| failed_jobs |
| migrations |
| password_resets |
| personal_access_tokens |
| sessions |
| tenants |
| users |
+————————+
8 rows in set (0.01 sec)

config/app.php

App\Providers\TenancyServiceProvider::class,

app/Models/Tenant.php

use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;

class Tenant extends BaseTenant implements TenantWithDatabase {
	use HasDatabase, HasDomains;
}

config/tenancy.php

    'tenant_model' => \App\Models\Tenant::class,

### Central routes
app/Providers/RouteServiceProvider.php

    protected function mapWebRoutes(){
        foreach($this->centralDomains() as $domain){
            Route::middleware('web')
                ->domain($domain)
                ->namespace($this->namespace)
                ->group(base_path('routes/web.php'));
        }
    }

    protected function mapApiRoutes(){
        foreach($this->centralDomains() as $domain){
            Route::prefix('api')
                ->domain($domain)
                ->middleware('api')
                ->namespace($this->namespace)
                ->group(base_path('routes/api.php'));
        }
    }

    protected function centralDomains(): array {
        return config('tenancy.central_domains');
    }

    public function boot()
    {
        $this->configureRateLimiting();

        // $this->routes(function () {
        //     Route::prefix('api')
        //         ->middleware('api')
        //         ->namespace($this->namespace)
        //         ->group(base_path('routes/api.php'));

        //     Route::middleware('web')
        //         ->namespace($this->namespace)
        //         ->group(base_path('routes/web.php'));
        // });
        $this->mapWebRoutes();
        $this->mapApiRoutes();
    }

config/tenancy.php

    'central_domains' => [
        // '127.0.0.1',
        '192.168.33.10',
        // 'localhost',

    ],

routes/tenant.php

    Route::get('/', function () {
    	dd(\App\Models\User::all());
        return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
    });

move the users table migration (the file database/migrations/2014_10_12_000000_create_users_table.php or similar) to database/migrations/tenant.

php artisan tinker
>>> $tenant1->domains()->create([‘domain’ => ‘hoge.192.168.33.10’]);
=> Stancl\Tenancy\Database\Models\Domain {#4734
domain: “hoge.192.168.33.10”,
tenant_id: “hoge”,
updated_at: “2021-08-08 11:59:44”,
created_at: “2021-08-08 11:59:44”,
id: 1,
tenant: App\Models\Tenant {#4792
id: “hoge”,
created_at: “2021-08-08 11:59:35”,
updated_at: “2021-08-08 11:59:35”,
data: null,
tenancy_db_name: “tenanthoge”,
},
}
>>> $tenant2 = App\Models\Tenant::create([‘id’ => ‘bar’]);
=> App\Models\Tenant {#4794
id: “bar”,
data: null,
updated_at: “2021-08-08 11:59:50”,
tenancy_db_name: “tenantbar”,
}
>>> $tenant2->domains()->create([‘domain’ => ‘bar.192.168.33.10’]);
=> Stancl\Tenancy\Database\Models\Domain {#4786
domain: “bar.192.168.33.10”,
tenant_id: “bar”,
updated_at: “2021-08-08 11:59:55”,
created_at: “2021-08-08 11:59:55”,
id: 2,
tenant: App\Models\Tenant {#3788
id: “bar”,
created_at: “2021-08-08 11:59:50”,
updated_at: “2021-08-08 11:59:50”,
data: null,
tenancy_db_name: “tenantbar”,
},
}

$ php artisan make:seeder TenantTableSeeder

    public function run()
    {
        App\Tenant::all()->runForEach(function () {
		    factory(App\User::class)->create();
		});
    }

mysql> select * from tenants;
+——+———————+———————+———————————–+
| id | created_at | updated_at | data |
+——+———————+———————+———————————–+
| bar | 2021-08-08 11:59:50 | 2021-08-08 11:59:50 | {“tenancy_db_name”: “tenantbar”} |
| foo | 2021-08-08 11:53:06 | 2021-08-08 11:53:06 | {“tenancy_db_name”: “tenantfoo”} |
| hoge | 2021-08-08 11:59:35 | 2021-08-08 11:59:35 | {“tenancy_db_name”: “tenanthoge”} |
+——+———————+———————+———————————–+

流れはわかったが、名前解決が出来ないな。。。
これも、ドメインでやるのかな。

1. お名前.comでドメインを取得してVPSにデプロイします。
2. お名前.com側ではDNS側でAレコードをワイルドカードで設定、VPS側ではCNAMEを設定して再度試します。

できたーーーーーーーーーーーーーーーーーーああああああああああああ
ウヒョーーーーーーーーー

database/tenant/* の中に、テナント用のmigration fileを作るわけね。
マルチテナントの開発の場合は、localhostや192.168.33.10などは名前解決できないので、ドメインを取得してテストする必要がある。

うむ、なかなか大変だわこれ。

[laravel8.x] hyn/multi-tenant その2

fqdnとは”Fully Qualified Domain Name”の略
トップレベルドメイン(TLD)までのすべてのラベルを含むドメイン名

routes/tenants.php

use Illuminate\Support\Facades\Route;

Route::get('/', function(){
	return view('tenants.home');
});

resources/views/tenants/home.php

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>
<body>
	<h1>Welcom!</h1>
	<p>This will be your <b>dashboard</b> for every tenant in your system.</p>
</body>
</html>

config/tenancy.php
L make sure that path would be right.

    'routes' => [
        'path' => base_path('routes/tenants.php'),
        'replace-global' => false,
    ],

### creating first tenant
$ php artisan make:command tenant/create
app/Console/Commands/tenant/create.php

namespace App\Console\Commands\tenant;

use Illuminate\Console\Command;

use Illuminate\Support\Str;

use Hyn\Tenancy\Models\Hostname;
use Hyn\Tenancy\Models\Website;
use Hyn\Tenancy\Repositories\HostnameRepository;
use Hyn\Tenancy\Repositories\WebsiteRepository;

class create extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    // protected $signature = 'command:name';
    protected $signature = 'tenant:create {fqdn}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Given a unique tenant name, creates a new tenant in the sytem';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {

        $fqdn = sprintf('$s.$s', $this->argument('fqdn'), env('APP_DOMAIN'));

        $website = new Website;
        $website->uuid = env('TENANT_WEBSITE_PREFIX') . Str::random(6);
        app(WebsiteRepository::class)->create($website);

        $hostname = new Hostname;
        $hostname->fqdn = $fqdn;
        $hostname = app(HostnameRepository::class)->create($hostname);
        app(HostnameRepository::class)->attach($hostname, $website);
        // return 0;
    }
}

.env

TENANT_WEBSITE_PREFIX=tenancy_demo_

php artisan tenant:create demo

mysql> show tables;
+————————+
| Tables_in_tenancy_demo |
+————————+
| failed_jobs |
| hostnames |
| migrations |
| password_resets |
| users |
| websites |
+————————+
6 rows in set (0.31 sec)

mysql> select * from websites;
+—-+———————+———————+———————+————+——————————–+
| id | uuid | created_at | updated_at | deleted_at | managed_by_database_connection |
+—-+———————+———————+———————+————+——————————–+
| 1 | tenancy_demo_NcmmUL | 2021-08-08 04:39:43 | 2021-08-08 04:39:43 | NULL | NULL |
| 2 | tenancy_demo_IbSPrJ | 2021-08-08 04:47:13 | 2021-08-08 04:47:13 | NULL | NULL |
+—-+———————+———————+———————+————+——————————–+
2 rows in set (0.37 sec)

conf.dの名前解決がうまくいっていないような印象。
あれ? マルチテナントの場合、サブドメインの設定ってするんだっけ?