# 做一个SaaS独立站(2)- 安装配置
参考: https://tenancyforlaravel.com/docs/v3/quickstart/ 一步一步来:
整个流程大概如下:
> 0,配置好租户的事件和数据建表,生成租户,触发各种bootstrap初始化。
> 1,租户域名--》识别租户--》切换租户数据库--》切换各种资源--》运行应用--》运行相应任务命令
> 2,主域名--》识别管理中心--》切换主数据库--》运行管理后台--》管理租户
### 第一:先安装laravel-shop
源码:https://github.com/summerblue/laravel-shop/tree/L05_8.x
安装后,简单运行一下是否正常,然后我们接下来把它改造成SaaS.
### 第二:安装 archtechx/tenancy
源码: https://github.com/archtechx/tenancy
```
composer require stancl/tenancy
```
```
php artisan tenancy:install
```
安装后生成:migrations, config file, route file and a service provider
然后 ,执行数据库迁移,生成 tenants 租户表,domains 域名表:
```
php artisan migrate
```
![](https://img.kancloud.cn/b1/4f/b14fcbe2c4054738dde202ec58da303a_393x59.png)
然后,注册 tenant包的 服务提供者,service provider in`config/app.php`.
![](https://img.kancloud.cn/b1/ee/b1ee99fe715472d47c4e7e59b0e715bb_673x323.png)
一般来说,会继承原来的Tenant model, 进一步修改,同时也要在`config/tenancy.php` 配置好 Model:
![](https://img.kancloud.cn/63/46/6346b0f9a9f3f4816c2133ea7dd27acc_709x448.png)
```
'tenant\_model' => \App\Models\Tenant::class,
```
这样!就算安装好了!下一步,我们要配置好 SaaS多租户的功能。
*****
*****
## 第三,配置租户生成事件(Events):
当新建租户的时候,会触发事件任务,例如执行 生成数据库CreateDatabase,迁移数据migration,填充数据seeder等等,在文件`TenancyServiceProvider`这里是配置 任务:
```
public function events()
{
return [
// Tenant events
Events\CreatingTenant::class => [],
Events\TenantCreated::class => [
JobPipeline::make([
Jobs\CreateDatabase::class,
Jobs\MigrateDatabase::class,
//Jobs\SeedDatabase::class,
CreateFrameworkDirectoriesForTenant::class, //建立租户文件夹
UpdateAdminMenuForTenant::class //更新租户数据表
// Your own jobs to prepare the tenant.
// Provision API keys, create S3 buckets, anything you want!
])->send(function (Events\TenantCreated $event) {
return $event->tenant;
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
],
```
## 第四,配置管理中心路由(Central routes):
在`app/Providers/RouteServiceProvider.php` 修改路由,这样就可以进入管理中心的路由,而不是进入租户的路由:
~~~php
public function boot()
{
$this->configureRateLimiting();
$this->mapWebRoutes();
$this->mapApiRoutes();
}
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');
}
~~~
## 第五,配置租户路由(Central routes):
在` routes/tenant.php` 配置租户的路由, `PreventAccessFromCentralDomains`的Middleware中间件是过滤掉不准主域名进入。`InitializeTenancyByDomain`是识别租户。
~~~php
Route::middleware([
'web',
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
])->group(function () {
Route::get('/', function () {
return 'This is your multi-tenant application.
The id of the current tenant is ' . tenant('id');
});
});
~~~
## 第六,配置数据迁移Migrations
手动把` database/migrations` 相关需要迁移的文件 复制到 `database/migrations/tenant` 里面。当执行租户数据迁移时候就会自动执行,生成租户需要的数据表:
```
php artisan tenants:migrate
```
## 最后,生成租户测试:
Tenant 是生成租户,而Domain是绑定租户Tenant,访问域名进行识别租户:
~~~php
$ php artisan tinker
>>> $tenant1 = App\Models\Tenant::create(['id' => 'foo']);
>>> $tenant1->domains()->create(['domain' => 'foo.localhost']);
>>>
>>> $tenant2 = App\Models\Tenant::create(['id' => 'bar']);
>>> $tenant2->domains()->create(['domain' => 'bar.localhost']);
~~~
这时候,你可以浏览器访问 'foo.localhost' ,就能够进入 租户的应用前端了。
(注:要在hosts绑定域名和本地ip)
租户的应用前端:
![](https://img.kancloud.cn/ab/cd/abcd13b04f14402ba34d1ddc6d10f5ff_755x721.png)
租户后台:
![](https://img.kancloud.cn/18/12/18123256ff62e54ae6901c8ff4abdacd_1169x438.png)
管理中心后台:
![](https://img.kancloud.cn/af/7f/af7fa1de994dd701244eae76a9f4aac1_1309x481.png)
*****
同时,可以在代码里面对租户进行如下操作:
~~~php
App\Models\Tenant::all()->runForEach(function () {
App\Models\User::factory()->create(); // 切换租户,执行操作
});
~~~
完成!以上就是多租户SaaS的基本安装和配置。下面具体说说配置的知识点。
*****
*****
## 注:配置的知识点:
#### 1,Config/tenancy.php 配置
```
'tenant_model' => \App\Models\Tenant::class, //配置好 Tenant和Domain的class
```
```
'central_domains' => [
str_replace(['https//', 'http//'], '', env('APP_URL')),
], // 配置好 管理中心的URL
```
```
//租户识别后,启动资源隔离:
'bootstrappers' => [
Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
],
```
*****
#### 2, TenancyServiceProvider.php 配置:
** 2.1 Events\TenantCreated (租户生成时)配置:**
租户生成时,具体执行的生成任务配置,这里举例几个:
```
····
public function events()
{
return [
// Tenant events
Events\CreatingTenant::class => [],
Events\TenantCreated::class => [
JobPipeline::make([
Jobs\CreateDatabase::class, //生成数据库
Jobs\MigrateDatabase::class, //迁移数据表
//Jobs\SeedDatabase::class, //填充数据
CreateFrameworkDirectoriesForTenant::class, //生成租户文件夹
UpdateAdminMenuForTenant::class //更新租户数据内容
// Your own jobs to prepare the tenant.
// Provision API keys, create S3 buckets, anything you want!
])->send(function (Events\TenantCreated $event) {
return $event->tenant;
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
],
Events\SavingTenant::class => [],
·····
```
例如,更新数据表内容如下:
![](https://img.kancloud.cn/99/07/99078b27c674005bd8595a8053ac1cc9_717x582.png)
例如,生成租户文件夹如下:
![](https://img.kancloud.cn/21/c6/21c60d389f703d93e120a257779e2e10_598x425.png)
*****
*****
** 2.2 boot() 启动租户任务配置:**
```
public function boot()
{
$this->bootEvents(); //启动事件监听
$this->mapRoutes(); //启动路由监听
$this->makeTenancyMiddlewareHighestPriority();
//以下是我们添加的 自定义配置
InitializeTenancyByDomain::$onFail = function () {
return redirect(env('APP_URL')); //租户访问失败,跳转主访问
};
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomainOrSubdomain::class; // 静态资源相关.
// 租户自定义配置.
// @see https://tenancyforlaravel.com/docs/v3/features/tenant-config
TenantConfig::$storageToConfigMap = [
// Do whatever you want.
];
DomainTenantResolver::$shouldCache = true; //租户路由缓存配置
}
protected function mapRoutes()
{
if (file_exists(base_path('routes/tenant.php'))) {
Route::namespace(static::$controllerNamespace)
->group(base_path('routes/tenant.php'));
}
}
```
*****
*****
#### 3,migration 和 seeder 初始化数据 配置
在`config/tenancy.php` 可以配置相关参数,我的习惯是 不要seeder,直接把有需要的seeder做成 一个migration,直接执行migration。
```
/**
* Parameters used by the tenants:migrate command.
*/
'migration_parameters' => [
'--force' => true, // This needs to be true to run migrations in production.
'--path' => [database_path('migrations/tenant')],
'--realpath' => true,
],
/**
* Parameters used by the tenants:seed command.
*/
'seeder_parameters' => [
'--class' => 'DatabaseSeeder', //'TenantDatabaseSeeder', // root seeder class
//'--force' => true,
],
```
#### 4,路由配置:
这里需要的路由配置有:中心应用路由,管理中心路由,租户应用路由,租户管理后台路由。
4.1 管理中心的路由配置 `app/Admin/routes.php`例子如下 :
```
/**
* 超级管理员可以通过此路由进入租户后台.
*/
Route::group([
'prefix' => config('admin.route.prefix'),
'namespace' => config('admin.route.namespace'),
'middleware' => config('admin.route.middleware'),
'domain' => config('tenancy.central_domains')[0], //限定管理中心域名才能进入
], function (Router $router) {
// 租户管理
$router->resource('/tenant', 'TenantController');
// 域名管理
$router->resource('/domain', 'DomainController')->only(['index', 'destroy', 'show']);
$router->get('/', 'HomeController@index');
$router->get('users', 'UsersController@index');
$router->get('products', 'ProductsController@index');
});
```
4.2 租户管理中心的路由配置 `app/Admin/routes.php`例子如下 :
```
/**
* 租户管理员可以通过此路由进入租户后台.
*/
Route::middleware([
'web','admin', // 要经过管理员登录验证
CheckTenantForMaintenanceMode::class, //检查是否维护状态
ScopeSessions::class,
InitializeTenancyByDomain::class, //识别租户,执行切换资源
PreventAccessFromCentralDomains::class, //防止管理中心访问的混入
])
->prefix(config('admin.route.prefix'))
->namespace(config('admin.route.namespace'))
->group(function (Router $router) {
$router->get('/', 'HomeController@index');
$router->get('users', 'UsersController@index');
$router->get('products', 'ProductsController@index');
$router->get('products/create', 'ProductsController@create');
$router->post('products', 'ProductsController@store');
$router->get('products/{id}/edit', 'ProductsController@edit');
$router->put('products/{id}', 'ProductsController@update');
// 开启上帝模式,管理中心是可以直接访问租户后台
$router->get('/god/{token}', function ($token) {
return UserImpersonation::makeResponse($token);
});
});
```
4.3 中心应用路由的路由配置 `routes/web.php`例子如下 :
```
//就是普通平时的路由,不需要解释
Route::get('/', 'PagesController@root')->name('root');
Auth::routes();
// 在之前的路由里加上一个 verify 参数
Auth::routes(['verify' => true]);
Route::get('products/favorites', 'ProductsController@favorites')->name('products.favorites');
// auth 中间件代表需要登录,verified中间件代表需要经过邮箱验证
Route::group(['middleware' => ['auth', 'verified']], function() {
Route::get('user_addresses', 'UserAddressesController@index')->name('user_addresses.index');
```
4.4 租户应用的路由配置 `routes/tenant.php`例子如下 :
```
Route::middleware([
'web',
InitializeTenancyByDomain::class, //识别租户,切换资源
PreventAccessFromCentralDomains::class, //防止中心应用的访问混入
])->group(function () {
Route::get('/', 'PagesController@root')->name('root');
Auth::routes(); // 按正常的用户验证就可以
// 在之前的路由里加上一个 verify 参数
Auth::routes(['verify' => true]);
Route::get('products/favorites', 'ProductsController@favorites')->name('products.favorites');
// auth 中间件代表需要登录,verified中间件代表需要经过邮箱验证
Route::group(['middleware' => ['auth', 'verified']], function() {
Route::get('user_addresses', 'UserAddressesController@index')->name('user_addresses.index');
·····
····
```
*****
### 路由知识点:
中心central 和 租户tenants 路由 的 相互限制方式:
```
'domain' => config('tenancy.central_domains')[0], //限定管理中心域名才能进入
```
```
PreventAccessFromCentralDomains::class, //防止中心应用的访问混入
```
*****
## 租户基本命令:
1,租户命令:
```
php artisan tenants:migrate
php artisan tenants:migrate --seed
php artisan tenants:migrate-fresh --seed
php artisan tenants:seed --tenants=XXXX
php artisan tenants:run larabbs:calculate-active-user
php artisan tenants:run email:send --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23 --option="queue=1" --option="subject=New Feature" --argument="body=We have launched a new feature. ..."
```
2,cron Kernel 配置租户执行命令方式:
```
$schedule->command('tenants:run larabbs:calculate-active-user')->everyMinute()->withoutOverlapping();
```
*****
## 多租户的图片资源使用方式:
默认是不对的地址:
```
http://foo9.larashop.test/storage/images/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87\_20211116190644.jpg
```
正确的图片地址应该是这样,有 `tenancy/assets` :
```
http://foo9.larashop.test/tenancy/assets/images/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87\_20211116190644.jpg
```
所以要图片资源的函数方法:
```
public function tenancyUrl($path) {
if (URL::isValidUrl($path)) {
return $path;
}
if (tenant()) {
return tenant_asset($path); // 这里是关键,会转换租户地址
}
return $this->getStorage()->url($path);
}
```
*****
## 其他资源的隔离注意:
队列,redis, redis缓存 , 多租户, 文件独立 等都需要注意隔离的配置。
#### 例如:文件缓存的报错, 有些资源是需要用tenant-aware的,如配置利用redis。
> This cache store does not support tagging
> Hi. If you want your cache to be tenant-aware, you need to use a driver that supports tagging, e.g. Redis.
> If you don't need tenant-aware cache, comment out the CacheTenancyBootstrapper in your tenancy.php config file.
*****
### 代码的github地址:
为了方便参考,这里提供我的github地址,有相关代码参考:https://github.com/liangdabiao/laravel-shop-saas
所有账号密码都是 admin admin
后台 /admin
*****
同时也可以参考我的论坛SaaS代码,另一种方式:https://github.com/liangdabiao/bbs-saas-skeleton