giuseppe-mazzapica / faber
WordPress 的一个微小的依赖注入容器和工厂类
Requires
- php: >=5.4
Requires (Dev)
- mockery/mockery: dev-master
- phpunit/phpunit: 3.7.*
This package is auto-updated.
Last update: 2023-02-26 11:02:53 UTC
README
一个WordPress特定的依赖注入容器,在对象分解方面表现良好。
它主要受到 Pimple 的启发,但允许更轻松地创建对象实例。
它不是一个完整的插件,而是一个库,可以通过 Composer 集成到更大的项目中。
Faber(以及其他DI容器)管理两种不同的数据:服务和属性。
属性是存储“原样”的变量,在需要时可以检索。
服务是用于应用程序不同部分的对象,当它们需要反复使用时,返回相同的实例。服务通过 工厂闭包 注册:它们是返回对象实例的 匿名函数。
Faber 还实现了工厂模式:可以使用已注册的工厂闭包始终获取对象的 新鲜、纯实例。
#工作原理
#要求、安装和许可
#创建容器实例
在能够做任何事情之前,需要一个容器实例。有两种方式:静态方式和动态方式。
##静态实例化:使用 instance()
方法
创建容器实例的第一种方法是使用静态 instance()
方法
$container = GM\Faber::instance( 'my_plugin' );
传递给实例方法的参数是实例的ID:可以通过传递不同的ID创建任意数量的实例,它们将完全相互隔离
$plugin_container = GM\Faber::instance( 'my_plugin' ); $theme_container = GM\Faber::instance( 'my_theme' );
这种方法的优点很简单:一旦有了容器,如果它不能从应用程序的任何部分访问,它就没有用了。使用这种方法,可以通过重复调用 instance()
方法来从应用程序的任何地方访问容器。
请注意,instance()
方法有一个别名:i()
$container = GM\Faber::i( 'my_app' ); // is identical to GM\Faber::instance( 'my_app' );
##动态实例化
还可以使用 规范 方法创建容器实例
$container = new GM\Faber();
以这种方式实例化后,应安排一种方法来检索容器实例,可能使用全局变量
global $myapp; $myapp = new GM\Faber();
##初始化钩子
无论使用什么方法实例化容器,当创建实例时,都会触发一个动作钩子:"faber_{$id}_init"
。其中$id
部分是实例ID。
当使用动态方法实例化容器时,传递ID是可选的,但如果构造函数没有传递ID,则会自动创建一个唯一ID,它不可预测,但可以使用getId()
方法获取。
$myapp = new GM\Faber(); $id = $myapp->getId(); // will be something like 'faber_5400d85c5a18c'
然而,当计划使用初始化钩子和动态实例化时,最好像这样向构造函数传递一个ID
$myapp = new GM\Faber( [], 'my_app' ); $id = $myapp->getId(); // will be 'my_app'
ID是第二个参数,因为第一个参数是在实例创建过程中要注册的属性/服务数组。
初始化钩子可用于向容器添加属性和服务,因为它将刚创建的容器实例作为参数传递给钩子回调
add_action( 'faber_' . $container->getId() . '_init', function( $container ) { // do something with container });
# 注册和获取服务
拥有容器实例后,可以在整个应用程序代码中注册服务。
服务是作为更大系统的一部分执行某些操作的对象。
服务是通过闭包(匿名函数)定义的,这些闭包返回一个对象的实例
// define some services $container['bar'] = function ( $cont, $args ) { return new My\App\Bar( $cont['foo'] ); }; $container['foo'] = function ( $cont, $args ) { return new My\App\Foo(); };
在之前的代码中需要注意的事项
- 使用数组访问接口添加服务(容器对象被视为数组)
- 工厂闭包可以访问容器实例,并可用于向要创建的对象中注入依赖项(在上面的示例中,将
Foo
类的实例注入到Bar
类中) - 工厂闭包可以访问
$args
变量:它是在检索实例时使用的参数数组(下面将有更好的解释) - 定义服务的顺序无关紧要:工厂在需要服务时执行,而不是在定义服务时执行。
要获取已注册的服务,可以使用数组访问
$bar = $container['bar']; // $bar is an instance of My\App\Bar
或者,作为替代,使用容器的get()
方法
$bar = $container->get( 'bar' ); // $bar is an instance of My\App\Bar
服务被缓存,这意味着在第一次请求相同的服务后,如果再次请求相同的服务,则返回相同的实例
$bar_1 = $container['bar']; $bar_2 = $container->get( 'bar' ); var_dump( $bar_1 === $bar_2 ); // TRUE $foo = $container['foo']; // here getFoo() returns the instance of Foo injected in Bar constructor $bar_foo = $bar_1->getFoo(); var_dump( $foo === $bar_foo ); // TRUE
## 带参数的对象
有时类需要一些参数才能实例化,而这些参数因实例而异。
一个示例
class Post { private $wp_post; private $options; function __construct( $id, Options $options ) { $this->wp_post = get_post( $id ); $this->options = $options; } }
实际上,这并不是一个服务,但它是一个需要变量参数($id
)和服务($options
)的对象。
虽然服务适合上面解释的工作流程(将Options
实例保存到容器中并传递给Post
),但无法对$id
执行相同操作。
这是许多依赖注入容器的限制,但幸运的是,Faber并不受此限制。
例如,以下定义
$container['post'] = function ( $cont, $args ) { return new Post( $args['id'], $cont['options'] ); }; $container['options'] = function ( $cont, $args ) { return new Options; };
可以构建一些类似这样的帖子对象
$post_1 = $container->get( 'post', [ 'id' => 1 ] ); $post_2 = $container->get( 'post', [ 'id' => 2 ] ); $post_1_again = $container->get( 'post', [ 'id' => 1 ] ); var_dump( $post_1 === $post_2 ); // FALSE: different args => different object var_dump( $post_1 === $post_1_again ); // TRUE: same args => same object
简而言之,get()
方法可以用于向工厂闭包传递一个参数数组,其中它用于生成对象。
当向get()
方法传递相同的ID和相同的参数时,将获得相同的实例;更改参数将返回不同的实例。
注意:在上面的代码中,传递给Post
对象的Options
实例始终是相同的。
## 如何强制创建新实例
通常,服务被缓存:如果向get()
方法传递相同的参数,则返回相同的实例。
如果需要具有ID 1的新实例,即使之前已经请求过,怎么办?
make()
就是为了这个目的而存在的
$post_1 = $container->get( 'post', [ 'id' => 1 ] ); $post_1_again = $container->get( 'post', [ 'id' => 1 ] ); $post_1_fresh = $container->make( 'post', [ 'id' => 1 ] ); var_dump( $post_1 === $post_1_again ); // TRUE var_dump( $post_1 === $post_1_fresh ); // FALSE: make() force fresh instances
## 使用“Demeter链”ID获取数据
假设一些类定义如下
class Foo { function getBar() { return new Bar; } } class Bar { function getBaz() { return new Baz; } } class Baz { function getResult() { return 'Result!'; } }
并且第一个类按照如下方式注册到容器中
$container['foo'] = function() { return new Foo; }
要调用getResult()
方法,可以对Baz
类做如下操作
$result = $container['foo']->getBar()->getBaz()->getResult(); // 'Result!'
好的。但是(从版本1.1开始)在Faber中也可以做
$result = $container['foo->getBar->getBaz->getResult'] // 'Result!'
可以通过使用对象赋值运算符(->
)将对象ID和要调用的方法“粘合”在一起,以链式的方式访问容器中的对象方法。
##注册和获取属性
属性通常是全局可访问的非对象变量。它们以与服务相同的方式注册和检索。
// define properties $container->add( 'version', '1.0.0' ); $container['timeout'] = HOUR_IN_SECONDS; $container['app_path'] = plugin_dir_path( __FILE__ ); $container['app_assets_url'] = plugins_url( 'assets/' , __FILE__ ); // get properties $version = $container['version']; $timeout = $container->get( 'timeout' );
对于属性,就像服务一样,可以使用add()
或数组访问来注册属性,以及使用get()
或数组访问来检索它们。
可以使用额外的prop()
方法获取属性,如果与服务ID一起使用,则返回错误。
$app_path = $container->prop( 'app_path' );
##将闭包作为属性保存
当闭包被添加到容器中时,默认情况下它被视为服务的工厂闭包。要“原样”存储闭包,将其作为属性处理,可以使用protect()
方法。
$container->protect( 'greeting', function() { return date('a') === 'am' ? 'Good Morning' : 'Good Evening'; });
##钩子
Faber类内部只触发三个钩子。
第一个是"faber_{$id}_init"
,在这里解释。
其他两个是过滤器,它们是
"faber_{$id}_get_{$which}"
"faber_{$id}_get_prop_{$which}"
每当从容器中检索服务或属性时,分别触发这些过滤器。
$id
变量部分是容器实例ID。
$which
变量部分是正在检索的服务/属性的ID。
它们传递给钩子回调的第一个参数是刚刚从容器中检索的值。第二个参数是容器的实例本身。
$container = GM\Faber::i( 'test' ); $container['foo'] => 'foo'; $container['bar'] => function() { new Bar; }; $foo = $container['foo']; // result passes through 'faber_test_get_prop_foo' filter $bar = $container['bar']; // result passes through 'faber_test_get_bar' filter
##批量注册
除了使用数组访问或add()
方法逐个注册服务外,还可以一次注册多个服务(和属性)。这可以通过三种方式完成
- 将定义数组传递给构造函数(仅在使用动态实例化方法时)
- 将定义数组传递给
load()
方法 - 将PHP文件的路径传递给
loadFile()
方法,该文件返回定义数组
##传递给构造函数的定义
$def = [ 'timeout' => HOUR_IN_SECONDS, 'version' => '1.0.0', 'foo' => function( $container, $args ) { return new Foo; }, 'bar' => function( $container, $args ) { return new Bar( $container['foo'] ); } ]; $container = new GM\Faber( $def );
##传递给load()
方法的定义
$container = new GM\Faber; $container->load( $def ); // $def is same of above
##传递给loadFile()
方法的定义
首先需要一个定义文件,类似于以下内容
<?php // file must return an array return [ 'timeout' => HOUR_IN_SECONDS, 'version' => '1.0.0', 'foo' => function( $container, $args ) { return new Foo; }, 'bar' => function( $container, $args ) { return new Bar( $container['foo'] ); } ];
然后
$container = new GM\Faber; $container->loadFile( 'full/path/to/definitions/file.php' );
##初始化时加载
load()
和loadFile()
方法的一个有趣用法是结合Faber初始化钩子。
add_action( 'faber_' . $container->getId() . '_init', function( $container ) { $container->loadFile( 'full/path/to/definitions.php' ); });
这是一种简单的方法,可以注册服务,同时保持代码干净和可读。
##更新、删除、冻结和解冻
##更新
一旦注册了服务或属性,就可以使用数组访问或update()
方法进行更新。
$container[ 'foo' ] = 'Foo'; $container[ 'bar' ] = 'Bar'; echo $container[ 'foo' ]; // Output "Foo" echo $container[ 'bar' ]; // Output "Bar" $container[ 'foo' ] = 'New Foo'; $container->update( 'bar', 'New Bar' ); echo $container[ 'foo' ]; // Output "New Foo" echo $container[ 'bar' ]; // Output "New Bar"
请注意,服务只能用另一个服务和一个受保护的闭包更新另一个闭包。
$container[ 'foo_service' ] = function() { return Foo; }; $container->protect( 'closure', function() { return 'Hello World!'; }); $container[ 'foo_service' ] = 'a string'; // will fail $container->update( 'closure', [ 'a', 'b' ] ); // will fail
##删除
一旦注册了服务或属性,就可以使用unset()
和数组访问或通过remove()
方法进行删除。
$container[ 'foo' ] = 'Foo'; $container[ 'bar' ] = 'Bar'; unset( $container[ 'foo' ] ); $container->remove( 'foo' );
##冻结和解冻
可以通过freeze()
方法来避免更新和删除定义,猜猜看,这就是freeze()
方法。
冻结后,属性或服务无法更新或删除,直到通过unfreeze()
方法解冻。
$container[ 'foo' ] = 'Foo!'; $container->freeze( 'foo' ); unset( $container[ 'foo' ] ); // will fail echo $container[ 'foo' ]; // still output "Foo!" $container->update( 'foo', 'New Foo!' ); // will fail echo $container[ 'foo' ]; // still output "Foo!" $container->unfreeze( 'foo' ); $container[ 'foo' ] = 'New Foo!'; // will success echo $container[ 'foo' ]; // output "New Foo!" $container->remove( 'foo' ); // will success echo $container[ 'foo' ]; // will output an error message
##流畅接口
设置容器上东西的方法返回容器本身的实例,允许流畅接口,即链式方法。
支持此接口的方法包括
load()
loadFile()
add()
protect()
freeze()
unfreeze()
update()
remove()
例如,以下代码是有效的
$foo = GM\Faber::instance('myapp') ->load( $defs ) ->loadFile('defs.php') ->add( 'foo', 'Foo!' ) ->add( 'bar', 'Bar!' ) ->protect( 'hello', function() { return 'Hello!'; } ) ->freeze( 'foo' ) ->freeze( 'hello' ) ->unfreeze( 'foo' ) ->update( 'foo', 'New Foo!' ) ->remove( 'bar' ) ->get('foo'); // return 'New Foo!'
##发行者和信息获取器
##条件方法,也称为Issers
为了更好地控制应用程序流程并避免错误,检查特定ID是否已在容器中注册,以及它是否与属性或工厂相关联,是否已冻结等,是合理的。
为了满足这些需求,有一组条件方法(“issers”)。它们是
$c->isProp( $id )
如果给定的ID与属性相关联(受保护的闭包也是属性),则返回true$c->isFactory( $id )
如果给定的ID与工厂闭包相关联,则返回true$c->isProtected( $id )
如果给定的ID与受保护的闭包相关联,则返回true$c->isFrozen( $id )
如果给定的ID与冻结项相关联(无论是否为属性或工厂),则返回true$c->isCachedObject( $key )
如果给定的key与缓存对象相关联,则返回true(《更多信息》:点击这里)
(其中$c
是容器的实例,当然)
##信息获取器
有时,也希望通过容器实例的当前状态获取信息。
有一些获取器可以提供有用的信息
$c->getID()
返回容器实例的ID$c->getHash()
返回与容器实例关联的哈希,在内部用于不同位置$c->getFactoryIds()
返回与工厂闭包相关联的所有ID的数组$c->getPropIds()
返回与属性(包括受保护的闭包)相关联的所有ID的数组$c->getFrozenIds()
返回所有冻结项(属性和工厂闭包)的ID的数组$c->getObjectsInfo()
返回一个包含有关所有缓存对象的数组(《更多信息》:点击这里)$c->getInfo()
返回一个普通对象(stdClass
),其属性是关于实例当前状态的信息,是所有先前获取器的汇总。请注意,Faber
实现了JsonSerializable
接口,当使用容器实例调用json_encode
时,编码的是此方法返回的对象
#缓存对象
由于工厂闭包可以接受参数,因此对于每个工厂闭包都可以存在不同的缓存对象。例如:
// define a service $container['foo'] = function( $c, $args ) { return new Foo( $args ); }; // create some objects $foo_1 = $container->get( 'foo', [ 'id' => 'foo_1' ] ); $foo_2 = $container->get( 'foo', [ 'id' => 'foo_2' ] ); $foo_3 = $container->get( 'foo', [ 'id' => 'foo_3' ] );
因此有三个缓存对象,都与ID为“foo”的工厂相关。
使用以下代码可以证明对象被缓存
$foo_3_bis = $container->get( 'foo', [ 'id' => 'foo_3' ] ); var_dump( $foo_3_bis === $foo_3 ); // TRUE: objects are cached
值得注意的是,每个缓存对象都在容器中以一个标识符(通常称为$key
)存储。
此标识符(也称为“key”)可通过$c->getObjectInfo()
或$c->getInfo()
方法检索的信息中可见。
它是一个长的哈希字符串,但如果是已知的工厂闭包ID和使用的参数数组,它是可预测的。事实上,使用这些信息,可以通过getObjectKey()
方法检索对象键
$foo_3_key = $container->getObjectKey( 'foo', [ 'id' => 'foo_3' ] ); echo $foo_3_key; // "foo_00000002264fbf000000005edc5c64_3cb01b53c9fbca307016d562fa42ce"
可以使用isCachedObject()
方法与对象键一起使用,以了解特定对象是否已被缓存
$key = $container->getObjectKey( 'foo', [ 'id' => 'foo_3' ] ); if ( $container->isCachedObject( $key ) ) { // Yes, objects is cached } else { // No, objects is not cached }
##冻结和解除冻结缓存对象
有关如何冻结和解除冻结工厂闭包的说明,请参阅本页上方。Faber还允许冻结和解除冻结特定的缓存对象:可以使用与对象键而不是工厂ID相同的方
注意:当删除或更新工厂闭包时,它生成的所有缓存对象都会被删除,除非特定的缓存对象被冻结。
示例
// define a service $container['foo'] = function( $c, $args ) { return new Foo( $args ); }; // create some objects $foo_1 = $container->get( 'foo', [ 'id' => 'foo_1' ] ); $foo_2 = $container->get( 'foo', [ 'id' => 'foo_2' ] ); // get object keys $key_1 = $container->getObjectKey( 'foo', [ 'id' => 'foo_1' ] ); $key_2 = $container->getObjectKey( 'foo', [ 'id' => 'foo_2' ] ); // test: are object cached? var_dump( $container->isCachedObject( $key_1 ) ); // TRUE var_dump( $container->isCachedObject( $key_2 ) ); // TRUE // freeze first object $container->freeze( $key_1 ); // delete factory closure unset( $container['foo'] ); // test: are objects related to deleted factory deleted as well? var_dump( $container->isCachedObject( $key_1 ) ); // TRUE: still there because frozen var_dump( $container->isCachedObject( $key_2 ) ); // FALSE: vanished // get cached object $cached_foo = $container->get( $key_1 );
Faber有一个辅助方法getAndFreeze()
,它获取一个对象并将其冻结
$foo = $container->getAndFreeze( 'foo', [ 'id' => 'foo_1' ] ); $key = $container->getObjectKey( 'foo', [ 'id' => 'foo_1' ] ); var_dump( $container->isFrozen( $key ) ); // TRUE
#关于序列化
PHP当前版本存在一个限制:不能序列化闭包或包含闭包的任何变量,例如包含闭包的数组或具有闭包属性的对象:尝试序列化它们会抛出可捕获的致命错误。
Faber设计为拥有大量作为属性的闭包,但实例序列化时不会发生错误,因为Faber实现了__sleep()
和__wakeup()
PHP方法来防止这种情况。
当Faber的实例被序列化和反序列化后,序列化/反序列化的对象只包含原始对象的ID和哈希值,由于它们是字符串,因此不会带来问题。
一个小技巧:在同一个请求期间序列化和反序列化Faber实例将创建原始对象的精确克隆,因为在唤醒时容器状态是从保存的实例克隆的。
$faber = GM\Faber::i( 'test' ); // or $faber = new GM\Faber() $faber['foo'] = function() { return new Foo; }; $sleep = serialize( $faber ); $wakeup = unserialize( $sleep ); $foo = $wakeup['foo']; var_dump( $foo instanceof Foo ); // TRUE
##错误处理
事情可能会出错。Faber的每个方法都可能因不同原因失败。
当这种情况发生时,Faber返回一个子类为WP_Error
的错误对象,因此可以使用is_wp_error()
函数进行检查。
$container[ 'foo' ] = 'Foo!'; $foo = $container[ 'foo' ]; if ( ! is_wp_error( $foo ) ) { echo $foo; // Output "Foo!"; } $meh = $container[ 'meh' ]; // 'meh' is an unregistered ID: $meh is a WP_Error object if ( ! is_wp_error( $meh ) ) { echo $meh; // not executed }
Faber使用的扩展WP_Error
类与流畅接口很好地配合工作。当使用流畅接口时,链中的每个函数都可能失败并返回一个WP_Error
对象,所以错误对象而不是Faber
对象将调用链中的下一个方法。
这没问题:自定义错误对象将错误添加到其错误存储中,然后返回自身,因此在链的末尾获得一个错误对象,它记录了调用它的每个方法:调试快乐。
##要求
- PHP 5.4+
- Composer
- WordPress 3.9+
##安装
在composer.json
中按如下方式要求Faber:
{
"require": {
"php": ">=5.4",
"giuseppe-mazzapica/faber": "dev-master"
}
}
##许可
Faber在MIT许可下发布。