Posted on Leave a comment

SaaS with laravel (3)-邮件通知租户

利用laravel重置密码功能:  Password Resets

php artisan make:notification TenantCreated

<?php

namespace App\Notifications;

use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Password;

class TenantCreated extends Notification
{
    private $hostname;

    public function __construct($hostname)
    {
        $this->hostname = $hostname;
    }

    public function via()
    {
        return ['mail'];
    }

    public function toMail($notifiable)
    {
        $token = Password::broker()->createToken($notifiable);
        $resetUrl = "https://{$this->hostname->fqdn}/password/reset/{$token}";

        $app = config('app.name');

        return (new MailMessage())
            ->subject("{$app} Invitation")
            ->greeting("Hello {$notifiable->name},")
            ->line("You have been invited to use {$app}!")
            ->line('To get started you need to set a password.')
            ->action('Set password', $resetUrl);
    }
}

然后修改 CreateTenant.php#handle():

...

$password = str_random();
$this->addAdmin($name, $email, $password)   
     ->notify(new TenantCreated($hostname));   // 触发通知

$this->info("Tenant '{$name}' is created and is now accessible at {$hostname->fqdn}");
$this->info("Admin {$email} has been invited!");
...

运行测试一下:

“Route [password.reset] not defined.”

还没有建立用户验证模块,执行:

php artisan make:auth

填写提交:

QueryException about missing password_resets table !!!

找不到 相关 数据表。

这是因为 系统 配置 routes/web.php 是查找系统 数据库,而不是租户的数据库。所以要修改 转向到 租户自己的数据库:

方法:利用 Laravel middleware

php artisan make:middleware EnforceTenancy
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Config;

class EnforceTenancy
{
    public function handle($request, Closure $next)
    {
        Config::set('database.default', 'tenant');

        return $next($request);
    }
}

应用中间件 middleware, 配置app/Http/Kernel.php

...
protected $routeMiddleware = [
    ...
    'tenancy.enforce' => \App\Http\Middleware\EnforceTenancy::class
];...

配置路由中间件:routes/web.php

Route::group(['middleware' => 'tenancy.enforce'], function () {
    Auth::routes();
});

现在再测试一下:

成功!

现在再回顾,重构一下:

新建文件 app/Tenant.php

<?php

namespace App;

use Hyn\Tenancy\Environment;
use Hyn\Tenancy\Models\Customer;
use Hyn\Tenancy\Models\Hostname;
use Hyn\Tenancy\Models\Website;
use Illuminate\Support\Facades\Hash;
use Hyn\Tenancy\Contracts\Repositories\CustomerRepository;
use Hyn\Tenancy\Contracts\Repositories\HostnameRepository;
use Hyn\Tenancy\Contracts\Repositories\WebsiteRepository;

/**
 * @property Customer customer
 * @property Website website
 * @property Hostname hostname
 * @property User admin
 */
class Tenant
{
    public function __construct(Customer $customer, Website $website = null, Hostname $hostname = null, User $admin = null)
    {
        $this->customer = $customer;
        $this->website = $website ?? $customer->websites->first();
        $this->hostname = $hostname ?? $customer->hostnames->first();
        $this->admin = $admin;
    }

    public function delete()
    {
        app(HostnameRepository::class)->delete($this->hostname, true);
        app(WebsiteRepository::class)->delete($this->website, true);
        app(CustomerRepository::class)->delete($this->customer, true);
    }

    public static function createFrom($name, $email): Tenant
    {
        // create a customer
        $customer = new Customer;
        $customer->name = $name;
        $customer->email = $email;

        app(CustomerRepository::class)->create($customer);

        // associate the customer with a website
        $website = new Website;
        $website->customer()->associate($customer);
        app(WebsiteRepository::class)->create($website);

        // associate the website with a hostname
        $hostname = new Hostname;
        $baseUrl = config('app.url_base');
        $hostname->fqdn = "{$name}.{$baseUrl}";
        $hostname->customer()->associate($customer);
        app(HostnameRepository::class)->attach($hostname, $website);
        // make hostname current
        app(Environment::class)->hostname($hostname);

        $admin = static::makeAdmin($name, $email, str_random());

        return new Tenant($customer, $website, $hostname, $admin);
    }

    private static function makeAdmin($name, $email, $password): User
    {
        $admin = User::create(['name' => $name, 'email' => $email, 'password' => Hash::make($password)]);
        $admin->guard_name = 'web';
        $admin->assignRole('admin');

        return $admin;
    }

    public static function retrieveBy($name): ?Tenant
    {
        if ($customer = Customer::where('name', $name)->with(['websites', 'hostnames'])->first()) {
            return new Tenant($customer);
        }

        return null;
    }
}
<?php

namespace App\Console\Commands;

use App\Notifications\TenantCreated;
use App\Tenant;
use Hyn\Tenancy\Models\Customer;
use Illuminate\Console\Command;

class CreateTenant extends Command
{
    protected $signature = 'tenant:create {name} {email}';

    protected $description = 'Creates a tenant with the provided name and email address e.g. php artisan tenant:create boise boise@example.com';

    public function handle()
    {
        $name = $this->argument('name');
        $email = $this->argument('email');

        if ($this->tenantExists($name, $email)) {
            $this->error("A tenant with name '{$name}' and/or '{$email}' already exists.");

            return;
        }

        $tenant = Tenant::createFrom($name, $email);
        $this->info("Tenant '{$name}' is created and is now accessible at {$tenant->hostname->fqdn}");

        // invite admin
        $tenant->admin->notify(new TenantCreated($tenant->hostname));
        $this->info("Admin {$email} has been invited!");
    }

    private function tenantExists($name, $email): bool
    {
        return Customer::where('name', $name)->orWhere('email', $email)->exists();
    }
}
<?php

namespace App\Console\Commands;

use App\Tenant;
use Illuminate\Console\Command;

class DeleteTenant extends Command
{
    protected $signature = 'tenant:delete {name}';
    protected $description = 'Deletes a tenant of the provided name. Only available on the local environment e.g. php artisan tenant:delete boise';

    public function handle()
    {
        // because this is a destructive command, we'll only allow to run this command
        // if the environment is local or testing
        if (!(app()->isLocal() || app()->runningUnitTests())) {
            $this->error('This command is only avilable on the local environment.');

            return;
        }

        $name = $this->argument('name');
        if ($tenant = Tenant::retrieveBy($name)) {
            $tenant->delete();
            $this->info("Tenant {$name} successfully deleted.");
        } else {
            $this->error("Couldn't find tenant {$name}");
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *