此包已被弃用,不再维护。未建议替代包。

WordPress 的一个微小的依赖注入容器和工厂类

1.1.0 2014-09-07 12:04 UTC

This package is auto-updated.

Last update: 2023-02-26 11:02:53 UTC


README

Faber

一个WordPress特定的依赖注入容器,在对象分解方面表现良好。

它主要受到 Pimple 的启发,但允许更轻松地创建对象实例。

它不是一个完整的插件,而是一个库,可以通过 Composer 集成到更大的项目中。

Faber(以及其他DI容器)管理两种不同的数据:服务属性

属性是存储“原样”的变量,在需要时可以检索。

服务是用于应用程序不同部分的对象,当它们需要反复使用时,返回相同的实例。服务通过 工厂闭包 注册:它们是返回对象实例的 匿名函数

Faber 还实现了工厂模式:可以使用已注册的工厂闭包始终获取对象的 新鲜、纯实例。

Travis Status

#工作原理

#要求、安装和许可

#创建容器实例

在能够做任何事情之前,需要一个容器实例。有两种方式:静态方式和动态方式。

##静态实例化:使用 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对象将调用链中的下一个方法。

这没问题:自定义错误对象将错误添加到其错误存储中,然后返回自身,因此在链的末尾获得一个错误对象,它记录了调用它的每个方法:调试快乐。

##要求

##安装

composer.json中按如下方式要求Faber:

{
    "require": {
        "php": ">=5.4",
        "giuseppe-mazzapica/faber": "dev-master"
    }
}

##许可

Faber在MIT许可下发布。