1. 前言

在 Laravel 框架中,Facade 使用的是抽象类,而 Contract 使用的就是接口。

接着上一篇 PHP 抽象类和接口的区别,我们看下 Laravel 中抽象类和接口的具体应用,并重新梳理一下服务容器(Service Container)、服务提供者(Service Provider)、Facade、Contract 之间的关系。

一个最直接的例子是为 Laravel 提供哈希处理的 Laravel Hashing 服务,实际上它只是对 BcryptArgon2 的封装。

2. 定义 Contract

首先,Laravel 定义了一个接口,接口中声明了哈希处理用到三个方法:

Illuminate/Contracts/Hashing/Hasher.php

namespace Illuminate\Contracts\Hashing;

interface Hasher
{
    /**
     * Hash the given value.
     *
     * @param  string  $value
     * @param  array   $options
     * @return string
     */
    public function make($value, array $options = []);

    /**
     * Check the given plain value against a hash.
     *
     * @param  string  $value
     * @param  string  $hashedValue
     * @param  array   $options
     * @return bool
     */
    public function check($value, $hashedValue, array $options = []);

    /**
     * Check if the given hash has been hashed using the given options.
     *
     * @param  string  $hashedValue
     * @param  array   $options
     * @return bool
     */
    public function needsRehash($hashedValue, array $options = []);
}

Contract 的主要目的是声明服务要实现的方法,规范接口。这样即使要修改或者替换方法内容,也不会对其它使用此功能的代码产生影响。

3. 实现 Contract

对于一个服务来说,可能会有多种不同的实现。哈希目前只有一个:

Illuminate/Hashing/BcryptHasher.php

namespace Illuminate\Hashing;

use RuntimeException;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;

class BcryptHasher implements HasherContract
{
    /**
     * Default crypt cost factor.
     *
     * @var int
     */
    protected $rounds = 10;

    /**
     * Hash the given value.
     *
     * @param  string  $value
     * @param  array   $options
     * @return string
     *
     * @throws \RuntimeException
     */
    public function make($value, array $options = [])
    {
        $hash = password_hash($value, PASSWORD_BCRYPT, [
            'cost' => $this->cost($options),
        ]);

        if ($hash === false) {
            throw new RuntimeException('Bcrypt hashing not supported.');
        }

        return $hash;
    }

    /**
     * Check the given plain value against a hash.
     *
     * @param  string  $value
     * @param  string  $hashedValue
     * @param  array   $options
     * @return bool
     */
    public function check($value, $hashedValue, array $options = [])
    {
        if (strlen($hashedValue) === 0) {
            return false;
        }

        return password_verify($value, $hashedValue);
    }

    /**
     * Check if the given hash has been hashed using the given options.
     *
     * @param  string  $hashedValue
     * @param  array   $options
     * @return bool
     */
    public function needsRehash($hashedValue, array $options = [])
    {
        return password_needs_rehash($hashedValue, PASSWORD_BCRYPT, [
            'cost' => $this->cost($options),
        ]);
    }

    /**
     * Set the default password work factor.
     *
     * @param  int  $rounds
     * @return $this
     */
    public function setRounds($rounds)
    {
        $this->rounds = (int) $rounds;

        return $this;
    }

    /**
     * Extract the cost value from the options array.
     *
     * @param  array  $options
     * @return int
     */
    protected function cost(array $options = [])
    {
        return $options['rounds'] ?? $this->rounds;
    }
}

除了实现 Contracts 中定义的三个方法外,还是实现了额外的 setRounds 方法。

4. 添加到服务提供者

下一步就是将 Contract 的实现添加到服务提供者:

Illuminate/Hashing/HashServiceProvider.php

namespace Illuminate\Hashing;

use Illuminate\Support\ServiceProvider;

class HashServiceProvider extends ServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = true;

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('hash', function () {
            return new BcryptHasher;
        });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return ['hash'];
    }
}

上述代码中,应用会在服务容器中将 hash 绑定到 BcryptHasher 的单例上。

如果要延迟加载服务提供者,可以将 defer 属性设置为 true,并定义 provides 方法。provides 方法返回注册到服务容器时提供的绑定。

5. 注册服务提供者

最后在应用启动时注册服务提供者。只需要将其添加到 config/app.php 文件的 providers 数组中即可。

现在,就可以从服务容器中解析并使用服务了:

$this->app->make('hash')->make('password');
$this->app['hash']->make('password');

如果无法访问 $app 变量,可以使用全局辅助函数 resolve

resolve('hash')->make('password');

或者在支持依赖注入的地方使用类型提示。

6. Facade

除了通过服务容器访问服务外,Laravel 还提供了另一种访问方式,就是使用 Facade。

哈希对应的 Facade 如下:

Illuminate/Support/Facades/Hash.php

namespace Illuminate\Support\Facades;

/**
 * @see \Illuminate\Hashing\BcryptHasher
 */
class Hash extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'hash';
    }
}

里面只有一个简单的方法 getFacadeAccessor,用于返回注册到服务容器时提供的绑定,对应我们这里,就是服务提供者中注册时使用的 hash

所有 Facade 都继承自 Illuminate\Support\Facades\Facade 基类,如之前所述,此类是一个抽象类。当我们使用 Facade 时,它会从服务容器中解析出对应的 Contract 的实现,然后调用相应的方法。就像这样:

\Illuminate\Support\Facades\Hash::make('password');

当然,使用时明显感觉到类名长了点,不太方便。我们可以利用 PHP 的 class_alias 函数为类取个短点的别名。当然,在 Laravel 中,只需要将其添加到 config/app.phpaliases 数组中就可以了:

return [
.
.
.
    'aliases' => [
        .
        .
        .
        'Hash' => Illuminate\Support\Facades\Hash::class,
        .
        .
        .
    ],
.
.
.
];

最后,我们可以更方便地调用:

\Hash::make('password');

7. 总结

服务容器是 Laravel 的核心,Contract 定义了服务所需方法的列表,服务提供者将服务与具体实现绑定到服务容器中,而 Facade 提供了访问服务的另一种简单方法。