在 Laravel 中构建基于驱动程序的组件
组件化是一个构建可扩展且可靠的软件系统的好方法。 组件化使我们能够构建由解耦,独立且可重用的组件组成的大型系统。 组件化为我们提供了一种即插即用的方法来构建软件系统。
Laravel 作为一个框架,由可重用的组件组成(其中一些是第三方 Symfony 组件),这些组件都定义明确并组合在一起组成了这个系统。
组件
大多数现代软件系统都是通过组装小型,独立且可重用的实体来构建的,这些实体为系统提供特定的服务和功能。 软件组件本质上是一个小单元,这个小单元通常具有清晰明确的接口,这些接口构成了较大系统的组成基础。 组件将一组相关功能(或数据)封装到可重用的单元中。
基于驱动程序的组件
组件通常是软件系统中强制分离关注点的实体。它们是模块化的,用以负责向应用程序提供特定的服务,例如, Session 组件用来处理 web 应用程序中的会话状态。有趣的是,在某种意义上来说你可以允许它们以不同的交付服务方式来构建组件,然而却提供与它承诺的相同的契约。这就是基于驱动程序的方法去设计组件。
关键的是,在设计组件时你要考虑可扩展性,以某种方式将其默认行为替换为实现组件契约的对象。
在软件系统中,驱动程序是组件契约的特定实现。它提供一个底层基础架构的接口,在此基础之上组件服务可以被创建。
驱动程序和基于驱动程序的组件的这种思想被内置于 laravel 中,并且得到框架开箱即用的支持。 这就是我们想要在框架中探究的地方,以便看看如何在我们的应用程序中使用这个模式来构建基于驱动程序的组件。
Enter Managers!
管理器
当我们构建我们的基于驱动程序的组件时,我们需要一个途径去管理它们。我们希望能够创建多个预定义的驱动程序,甚至在应用程序生命周期的后期创建它们。我们希望能够请求特定驱动程序的实例,并且还具有备用驱动程序,可以在不指定驱动程序的情况下将调用代理进去。这就是管理器(Manager)要做的工作。
管理器是一个用来管理基于驱动程序的组件的创建工作的实例。 它负责根据应用程序的配置创建特定的驱动程序实现。
管理器的设计基于组件可以具有多个驱动程序(以不同的方式实现组件的实例)。使用管理器,组件可以定义创建其支持的驱动程序所需的逻辑。管理器充当创建和自定义组件驱动程序的中心,它是组件的门户。
如上所述,Laravel附带了对管理器的支持,我们希望用它来创建基于驱动程序的组件。让我们详细了解管理器的工作方式。
管理器类
Laravel在Support命名空间(Illuminate\Support\Manager
)中提供了一个抽象的Manager类。这个类定义了有用的方法来帮助我们管理驱动程序。首先, 逆你需要 extend
这个管理器类并在子类(组件的管理器类)中定义驱动程序的创建方法。
use Illuminate\Support\Manager;
class FooManager extends Manager
{
//
}
创建驱动程序
当然,创建基于驱动程序的组件需要我们能够创建驱动程序。管理器类定义了一个createDriver($driver)
方法,该方法完全按照之前的说明, 创建一个新的驱动程序实例;该方法接受一个参数要创建的驱动程序的名称。它假设扩展类已经定义了创建方法去创建驱动程序。这些创建方法应具有以下签名:
create[Drivername]Driver()
其中Drivername
是驱动程序封装后的名称。
在管理器类中定义的驱动程序创建方法应返回该驱动程序的实例。
获取驱动程序
就像订购Uber一样,获取管理器的实例,然后在其上调用driver($ driver = null)
方法。 基本管理器提供此方法,并接收一个可选参数。 从而获取该驱动程序实例。 然后,管理器通过调用您在管理器类中定义的驱动程序创建方法来为您制作驱动程序。 如果您没有要获取的驱动程序名称传递给driver($ driver = null)
方法,它将返回默认驱动程序的实例名称。
后备驱动程序
管理器是一个抽象类,它声明了一个必须由扩展管理器类定义的抽象getDefaultDriver()
方法。此方法应返回组件在未指定驱动程序时应默认使用的驱动程序的名称。这个回退驱动程序应该充当主驱动程序。
扩展组件
通过调用管理器上的extend()
方法,根据驱动程序的组件的预先设定,生成一个自定义的驱动程序。此方法提供了一种使用闭包注册自定义驱动程序创建的方法。当您从管理器请求一个驱动程序时,它检查该驱动程序是否存在自定义驱动程序创建方法,并调用该自定义创建方法。注册为自定义创建方法的闭包在调用时会收到\Illuminate\Foundation\Application
的实例。
protected function callCustomCreator($driver)
{
return $this->customCreators[$driver]($this->app);
}
如果尚未创建这些自定义驱动程序,则可以在管理器中重写具有相同名称的预定义驱动程序。
如果您想查看基本管理器类的完整实现,请在Github上查看它。(截至本文发表时,Laravel 5.7)
](https://github.com/laravel/framework/blob/...----------------------)
好吧,可以了,让我们看看管理器是如何通过在Laravel中构建一个简单的基于驱动程序的SMS组件来行动的。
SMS组件
我们想要构建一个具有多个驱动程序的简单SMS组件。 即将用到的组件将支持三个驱动程序:Nexmo驱动程序,Twilio驱动程序和Null驱动程序。 正如我们将在本文后面看到的那样,我们还可以扩展组件并为其创建自定义驱动程序。
我们的组件存在于以 App\Components\Sms
为命名空间的应用程序中。 首先,让我们创建组件的服务提供程序(ServiceProvider):
<?php
namespace App\Providers;
use App\Components\Sms\SmsManager;
use Illuminate\Support\ServiceProvider;
class SmsServiceProvider extends ServiceProvider
{
/**
* 是否延迟加载提供程序。
*
* @var bool
*/
protected $defer = true;
/**
* 注册任何应用程序服务。
*
* @return void
*/
public function register()
{
$this->app->singleton('sms', function ($app) {
return new SmsManager($app);
});
}
/**
* 获取服务商提供的服务。
*
* @return array
*/
public function provides()
{
return ['sms'];
}
}
这会将我们的sms组件注册为服务容器中的一个单例,并返回该组件的管理器的实例(App\Components\Sms\SmsManager)
。让我们在config/app.php
中快速向Laravel注册我们的服务提供商:
'providers' => [
// 其他服务提供商...
App\Providers\SmsServiceProvider::class,
],
接下来,我们继续定义 Manager:
<?php
namespace App\Components\Sms;
use Illuminate\Support\Manager;
use Nexmo\Client as NexmoClient;
use Twilio\Rest\Client as TwilioClient;
use App\Components\Sms\Drivers\NullDriver;
use App\Components\Sms\Drivers\NexmoDriver;
use App\Components\Sms\Drivers\TwilioDriver;
use Nexmo\Client\Credentials\Basic as NexmoBasicCredentials;
class SmsManager extends Manager
{
/**
* 获取驱动程序实例
*
* @param string|null $name
* @return mixed
*/
public function channel($name = null)
{
return $this->driver($name);
}
/**
* 创建 Nexmo SMS 驱动程序实例
*
* @return \App\Components\Sms\Drivers\NexmoDriver
*/
public function createNexmoDriver()
{
return new NexmoDriver(
$this->createNexmoClient(),
$this->app['config']['sms.nexmo.from']
);
}
/**
* 创建 Twilio SMS 驱动程序实例
*
* @return \App\Components\Sms\Drivers\TwilioDriver
*/
public function createTwilioDriver()
{
return new TwilioDriver(
$this->createTwilioClient(),
$this->app['config']['sms.twilio.from']
);
}
/**
* 创建 Nexmo 客户端
*
* @return \Nexmo\Client
*/
protected function createNexmoClient()
{
return new NexmoClient(
new NexmoBasicCredentials(
$this->app['config']['sms.nexmo.key'],
$this->app['config']['sms.nexmo.secret']
)
);
}
/**
* 创建 Twilio 客户端
*
* @return \Twilio\Rest\Client
*/
protected function createTwilioClient()
{
return new TwilioClient(
$this->app['config']['sms.twilio.key'],
$this->app['config']['sms.twilio.secret']
);
}
/**
* 创建空的 SMS 驱动程序实例
*
* @return \App\Components\Sms\Drivers\NullDriver
*/
public function createNullDriver()
{
return new NullDriver;
}
/**
* 获取默认的 SMS 驱动程序实例
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['sms.default'] ?? 'null';
}
}
首先要注意的是,我们的 Manager 类是如何扩展 Laravel 的 Manager 类 (Illuminate\Support\Manager
) 的。这是在 Laravel 中创建基于驱动程序组件的第一步。Manager 基类定义了帮助创建和管理驱动程序的逻辑。因为它是一个抽象类,并且声明了必须实现的 getDefaultDriver()
方法,所以,我们在 Manager 类中定义了该防范,并且返回了默认的驱动程序:
/**
*
* 获取默认的短信驱动名.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['sms.default'] ?? 'null';
}
你将会注意到配置来自配置文件(config/sms.php
),我们定义这个配置文件来存储我们组件的信息.这是一个非常简单的文件,其中包含每个驱动程序的凭据以及要使用的默认驱动程序. 如果默认的驱动没有设置,我们将回退到NullDriver
.
为了方便的选择使用哪一个驱动, 管理器类定义了一个driver($name)
方法 t,这个方法返回一个指定驱动的实例. 我们已经为我们的组件创建了一个方便的名为 channel($name)
的方法,这个方法调用基础的 driver($name)
方法并且传递我们我们想要获取的驱动的名称.
/**
* 获取一个驱动实例.
*
* @param string|null $name
* @return mixed
*/
public function channel($name = null)
{
return $this->driver($name);
}
我们的驱动定义在 App\Components\Sms\Drivers
命名空间中. 为了创建我们的驱动, 我们定义了3种创建方法,这些方法创建了组件开箱即用支持的每个驱动程序. 所有的驱动 extends
一个基础的 Driver
类 ,该类实现了我们组件的合同(App\Components\Sms\Contracts\SMS
). 该合同声明了一个 send
方法,组件的所有驱动必须实现该方法. 这是组件对系统的服务合同,它有望能够发送SMS.
<?php
namespace App\Components\Sms\Contracts;
interface SMS
{
/**
* 发送消息到指定的接受者.
*
* @return mixed
*/
public function send();
}
让我们看一看NexmoDriver,看看我们的组件在内部如何工作:
<?php
namespace App\Components\Sms\Drivers;
use Nexmo\Client as NexmoClient;
class NexmoDriver extends Driver
{
/**
* Nexmo客户端.
*
* @var \Nexmo\Client
*/
protected $client;
/**
* 短信应该从哪个手机号发送.
*
* @var string
*/
protected $from;
/**
* 创建一个新的Nexmo驱动实例.
*
* @param \Nexmo\Client $nexmo
* @param string $from
* @return void
*/
public function __construct(NexmoClient $nexmo, $from)
{
$this->client = $nexmo;
$this->from = $from;
}
/**
* {@inheritdoc}
*/
public function send()
{
return $this->client->message()->send([
'type' => 'text',
'from' => $this->from,
'to' => $this->recipient,
'text' => trim($this->message)
]);
}
}
这个驱动实现了 send
方法并且使用Nexmo PHP客户端传递了一条消息
一旦我们把组件都设置好了, 也就是说, 为我们的组件注册一个名为SMS
的Facade, 创建一个配置文件, 并且安装我们的依赖, 通过获取SmsManager
的实例我们可以快速使用这个组件 并且调用它的send
方法.
SMS::to($phoneNumber)
->content('Building driver-based components in Laravel')
->send();
to($phoneNumber)
和content($message)
方法在基础的 Driver
类中被定义 ,组件的所有驱动都继承自该类.
这里, 我们没有指定使用哪一个驱动 ,因此默认的 Nexmo
驱动会被使用,因为这是我们组建的默认驱动. 为了指定驱动,我们可以调用channel($name)
方法或者是基础的driver($name)
方法.
SMS::channel('twilio')
->to($phoneNumber)
->content('Using twilio driver to send SMS')
->send();
至此,我们已经在laravel中成功创建了一个基于驱动程序的组件. 如果你对完整的实现有兴趣的话我已经把源代码添加到了github, GitHub
现在我们已经创建了组件, 我们可以通过扩展组件添加更多的驱动并且添加自定义的驱动创建器. 为此,我们获取SmsManager
的实例并在其上调用extend
方法. 但是首先,我们将创建一个实现组件合同的驱动程序:
<?php
use App\Components\Sms\Contracts\SMS;
class FooSmsDriver implements SmsContract
{
protected $someDependency;
public function __construct($dependency)
{
$this->someDependency = $dependency;
}
public function send()
{
// Define send logic
}
}
然后, 我们将调用扩展方法并且提供自定义的驱动创建逻辑.
SMS::extend('foo', function ($app) {
return new FooSmsDriver($dependency);
});
一旦我们注册了自定义的驱动驱动创建器, 我们现在可以像使用该组件支持的其他现成驱动程序一样使用它.
SMS::channel('foo')
->send();
使用一个测试, 我们可以验证组件有能力使用包括自定义驱动在内的多个驱动.
<?php
namespace Tests\Unit;
use SMS;
use Tests\TestCase;
use App\Components\Sms\Drivers\NexmoDriver;
use App\Components\Sms\Drivers\TwilioDriver;
use App\Components\Sms\Drivers\NullDriver;
use App\Components\Sms\Contracts\SMS as SmsContract;
class SmsTest extends TestCase
{
/** @test */
public function component_can_be_extended_to_create_custom_drivers()
{
SMS::extend('foo', function ($app) {
return new FooSmsDriver('dependency');
});
$fooDriver = SMS::channel('foo');
$this->assertInstanceOf(FooSmsDriver::class, $fooDriver);
$this->assertEquals('Sent message from custom driver', $fooDriver->send());
}
/** @test */
public function component_can_use_multiple_drivers()
{
$nexmoDriver = SMS::channel('nexmo');
$this->assertInstanceOf(NexmoDriver::class, $nexmoDriver);
$twilioDriver = SMS::channel('twilio');
$this->assertInstanceOf(TwilioDriver::class, $twilioDriver);
$nullDriver = SMS::channel('null');
$this->assertInstanceOf(NullDriver::class, $nullDriver);
}
}
class FooSmsDriver implements SmsContract
{
protected $someDependency;
public function __construct($dependency)
{
$this->someDependency = $dependency;
}
public function send()
{
return 'Sent message from custom driver';
}
}
Conclusion
Laravel使用Manager类轻松创建基于驱动程序的组件. 我应该很快指出,构建基于驱动程序的组件时始终使用此Manager类并不是一成不变的, 您可以构建自己的Manager来处理组件的驱动程序创建和管理.
我希望这篇文章可以帮到你, 谢谢阅读.
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
学习了,laravel notification挺强大的,但是SMS只支持nexmo,可以自定义了感觉发sms就可以优雅很多了。
然后去github浏览了下官方sms channel driver,发现官方的还是🐂🍺(现在版本是7了,可能才新支持的自定义方式)。
既然目的是扩展自定义channel driver 直接调用Notification::resolved() 闭包调用mannger的extend即可。
不过整体可把控性应该是不如文中的方法~~