前言
公司的后臺(tái)是使用Laravel框架寫的,最近在把其中的訂單處理部分抽出來,準(zhǔn)備寫個(gè)單獨(dú)的Library。特地好好的研究了一下設(shè)計(jì)模式诸迟,Laravel學(xué)院上面有一個(gè)專題,便是談設(shè)計(jì)模式的愕乎,甚好!
為了降低耦合性壁公,在我的項(xiàng)目中使用了Laravel Container以支持IoC(控制反轉(zhuǎn))感论。但是就如何在Laravel之外使用
illuminate/container
這方面資料寥寥無幾,所以這篇文章記錄一下自己的學(xué)習(xí)心得紊册。
背景知識(shí)
- php/composer
- IoC
安裝
直接使用composer:
composer require illuminate/container
Laravel之外使用Illuminate/Container
還是以我的Order項(xiàng)目為例比肄,用其中的部分作為講解。先看一下部分架構(gòu)圖囊陡。
訂單生成的主要流程:
Cashier
通過OrderFactory
來生成訂單芳绩,OrderFactory
內(nèi)部存在一條Pipeline
,數(shù)據(jù)以流的形式在Pipeline
中流經(jīng)各個(gè)PipeWorker
撞反,通過不同的加工階段最終生成訂單妥色。
-
Container
首先需要在自己項(xiàng)目中定義一個(gè)容器對(duì)象,來“容納”所有的實(shí)例或者方法等遏片。這里我將Cashier
類作為統(tǒng)一的對(duì)外處理對(duì)象嘹害,它繼承自Illuminate\Container\Container
撮竿。
一般配置信息會(huì)作為容器的構(gòu)造函數(shù)的參數(shù)。在Laravel
中笔呀,由于配置內(nèi)容較多幢踏,這一形式表現(xiàn)為__construct
函數(shù)中的$basePath
參數(shù)。通過約定的目錄結(jié)構(gòu)結(jié)合$appPath
動(dòng)態(tài)讀取配置信息许师。本項(xiàng)目則選擇直接給予一個(gè)Config
類的形式房蝉,注入配置信息。Config
類實(shí)質(zhì)是一個(gè)鍵值對(duì)的數(shù)組微渠。
class Cashier extends Container implements \WilliamWei\FancyOrder\Contracts\Cashier
{
protected $providers = [
OrderServiceProvider::class,
PipelineServiceProvider::class,
];
public function __construct($config)
{
$this['config'] = function () use ($config) {
return new Config($config);
};
//register the provider
foreach ($this->providers as $provider)
{
$provider = new $provider();
$provider->register($this);
}
}
}
Illuminate\Container\Container
已經(jīng)實(shí)現(xiàn)了ArrayAccess
接口搭幻,可以直接以數(shù)組方式訪問容器中的對(duì)象。構(gòu)造時(shí)敛助,首先為config
對(duì)象定義實(shí)例化的閉包函數(shù)粗卜。然后依次將各個(gè)模塊對(duì)應(yīng)的ServiceProvider
進(jìn)行注冊(cè)。
-
ServiceProvider
將項(xiàng)目劃分為更小的子模塊有助于控制規(guī)模纳击,可以大大提高可維護(hù)性以及可測(cè)試性续扔。每個(gè)子模塊都有一個(gè)ServiceProvider
,用于暴露本模塊可提供的服務(wù)對(duì)象焕数。自定義的ServiceProvider
可以繼承Illuminate\Support\ServiceProvider
纱昧,這里我是選擇自己實(shí)現(xiàn)的ServiceProvider
。
接口:
interface ServiceProvider
{
public function register(Container $app);
}
以PipelineServiceProvider為例:
class PipelineServiceProvider implements ServiceProvider
{
public function register(Container $app)
{
$workers = [];
foreach ($app['config']['pipeline_workers'] as $name => $worker)
{
$app->bind($name,function($app) use ($worker) {
return new $worker();
});
array_push($workers,$app[$name]);
}
$app->bind('pipeline',function($app) use ($workers) {
return new Pipeline($workers);
});
}
}
PipelineServiceProvider
在register
方法中堡赔,將特定函數(shù)與實(shí)例的名字進(jìn)行綁定识脆,當(dāng)通過該名字訪問實(shí)例時(shí),若對(duì)象不存在善已,則容器會(huì)調(diào)用被綁定的函數(shù)來實(shí)例化一個(gè)對(duì)象灼捂,并將其置于容器,以供后續(xù)調(diào)用使用换团。對(duì)于Pipeline
悉稠,他并不需要關(guān)心是哪些Pipeworker
在工作,只需要知道他們存在艘包,并且可以正常工作就好的猛,從而以這種形式達(dá)到解耦目的。
當(dāng)然有可能有時(shí)候需要訪問容器內(nèi)其他對(duì)象想虎,則可以將容器本身作為構(gòu)造函數(shù)的參數(shù)傳入卦尊,如:
$app->bind('pipeline',function($app) use ($workers) {
return new Pipeline($app,$workers);
});
那么在Pipeline
內(nèi)部就可以通過
$this->app['XXX']
的形式訪問XXX
對(duì)象,同時(shí)也無需關(guān)心XXX
是如何構(gòu)造的舌厨。
代碼分析
這里的代碼分析部分只關(guān)注主體部分岂却,不會(huì)面面具到的分析到每個(gè)函數(shù)。
可以看到整個(gè)包一共就3個(gè)PHP文件,最核心的是Container.php
淌友,它定義了容器類煌恢,并實(shí)現(xiàn)了其中絕大多數(shù)功能。當(dāng)我們bind
一個(gè)實(shí)例到通過[]
下標(biāo)訪問時(shí)發(fā)生了什么震庭?
/**
* Register a binding with the container.
*
* @param string|array $abstract
* @param \Closure|string|null $concrete
* @param bool $shared
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false)
{
// If no concrete type was given, we will simply set the concrete type to the
// abstract type. After that, the concrete type to be registered as shared
// without being forced to state their classes in both of the parameters.
$this->dropStaleInstances($abstract);
if (is_null($concrete)) {
$concrete = $abstract;
}
// If the factory is not a Closure, it means it is just a class name which is
// bound into this container to the abstract type and we will just wrap it
// up inside its own Closure to give us more convenience when extending.
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
// If the abstract type was already resolved in this container we'll fire the
// rebound listener so that any objects which have already gotten resolved
// can have their copy of the object updated via the listener callbacks.
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
這是bind
函數(shù)瑰抵,當(dāng)我們執(zhí)行一個(gè)綁定操作時(shí),容器首先會(huì)把該名字之前綁定的實(shí)例與別名清除掉器联,即$this->dropStaleInstances($abstract);
如果該名字對(duì)應(yīng)實(shí)例是已經(jīng)解析過的二汛,則會(huì)觸發(fā)rebound
,執(zhí)行對(duì)應(yīng)回調(diào)。對(duì)于第一次綁定則不會(huì)出現(xiàn)這種情況拨拓。到此bind
就結(jié)束了肴颊。
當(dāng)通過下標(biāo)方式獲取實(shí)例時(shí)
public function offsetGet($key)
{
return $this->make($key);
}
可以看到調(diào)用了make
方法。
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @return mixed
*/
public function make($abstract)
{
$needsContextualBuild = ! is_null(
$this->getContextualConcrete($abstract = $this->getAlias($abstract))
);
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$concrete = $this->getConcrete($abstract);
// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
return $object;
}
略有些復(fù)雜渣磷,大致流程是先檢查實(shí)例數(shù)組中該實(shí)例是否存在婿着,存在的話則返回。對(duì)于不存在的情況醋界,由于我們是實(shí)用閉包的方式進(jìn)行bind
竟宋,所以會(huì)調(diào)用該閉包,即$object = $this->build($concrete);
得到$object形纺。后面會(huì)通知該實(shí)例對(duì)應(yīng)類的子類丘侠,告知他們?cè)搶?shí)例已被創(chuàng)建。注冊(cè)該實(shí)例到實(shí)例數(shù)組逐样,(如果存在解析完成回調(diào)函數(shù)則會(huì)去執(zhí)行)蜗字,返回實(shí)例。
這個(gè)便是容器內(nèi)部的一個(gè)流程脂新。
BoundMethod.php
主要以靜態(tài)的形式實(shí)現(xiàn)了直接調(diào)用某個(gè)類的某一方法的目標(biāo)挪捕。
ContextualBindingBuilder.php
則主要是用于將實(shí)例與一個(gè)上下文情景進(jìn)行綁定。這兩部分都是比較高級(jí)的內(nèi)容争便,這里不作展開了担神。
歡迎關(guān)注個(gè)人博客 :)