bylexus / php-injector
一个函数/方法参数注入助手。使您的依赖注入工作更顺畅。
Requires
- php: >= 8.0
- ext-mbstring: *
- psr/container: ^2.0
Requires (Dev)
- phpunit/phpunit: ^9.6
- squizlabs/php_codesniffer: ^3.8
README
php-injector
一个函数/方法参数注入和类自动装配助手。使您的依赖注入工作更顺畅。
功能
- 依赖注入:用作依赖注入机制中的辅助库,以带所需参数调用您的函数/方法
- 允许使用与函数参数匹配的参数数组调用函数/方法
- 允许通过在实例化时注入依赖构造函数参数来自动装配类
- 从函数/方法/构造函数签名中提取参数(及其名称)
- 从函数/方法/构造函数签名中提取参数的默认值
- 允许用户在需要时以简单的DocBlock格式定义类型和条件
- 帮助您进行参数验证/转换。特别适用于在面向前端的面板控制器中使用。
- 检查输入变量是否符合条件(例如,字符串长度、特定范围内的数字、日期大于当前日期等...)
- 可以按名称或类类型注入参数
- 支持从PSR-11服务容器解析参数
安装
通过 Composer
composer require bylexus/php-injector
然后只需使用Composer的自动加载功能
require_once('vendor/autoload.php'); // Method invocation: $injector = new \PhpInjector\Injector('myfunction'); // Class autowiring: $aw = new \PhpInjector\AutoWire($diContainer); $serviceA = $aw->createInstance(ServiceA::class);
总结 - 方法/函数调用
PHP Injector可以帮助您使用参数注入调用函数/方法。此应用程序有两个使用案例
- 在依赖注入场景中调用函数/方法
- 使用请求参数调用函数,但具有类型检查机制
场景:依赖注入
在依赖注入场景中,开发用户希望从依赖注入容器中请求一个服务。最简单的方法是允许开发人员仅将服务类型作为函数参数声明
public function myFunction(Request $request) { $param = $request->input('param); }
在这种情况下,请求对象会神奇地由周围框架注入。这正是PHP Injector发挥作用的地方:它允许框架构建者注入所需的服务。
场景:Web请求参数注入
在某些情况下,可能需要或想要以某种方式动态地“收集”参数,然后再将其注入到某个函数中,而不是直接调用该函数。
为什么?
以为例,你可能想将Web请求参数作为控制器函数的参数传递,如下所示
$_REQUEST = array('name' => 'Alex','age' => '24', 'active' => 'true'); function storePersonInfo($name, $age, $active = false) { // ... do some complex things } $res = storePersonInfo($_REQUEST);
现在这绝对是个糟糕的想法!不要直接将请求参数作为函数参数使用!这里可能会发生各种糟糕的事情(注入、远程代码调用……)。
这时注入器就派上用场了:它允许你
- 定义你想为函数接受的参数
- 用参数数组调用函数,参数顺序无关紧要(因此你不必实现复杂的变量排序算法)
- 定义类型转换,例如“我接受一个$age变量,但它被转换为int。”
- 甚至可以定义条件(例如'年龄必须大于20')
使用注入器,这看起来如下所示
$_REQUEST = array('name' => 'Alex','age' => '24','active' => 'true'); /** * @param string $name A name * @param int[>20] $age The age * @param bool $active */ function storePersonInfo($name, $age, $active = false) { // here, $age ist casted to an int, and must be > 20, // while $active is a proper boolean (true) instead of a string. } // Invoke the function via injector: $injectStore = new Injector('storePersonInfo'); $res = $injectStore->invoke($_REQUEST);
注入器确保您的请求参数按正确的顺序传递给函数,并且进行类型转换,因此在这个例子中,$age是一个正确转换的Integer。
这也适用于对象方法
$_REQUEST = array('name' => 'Alex','age' => '24','active' => 'true'); class MyController { /** * @param string $name A name * @param int[>20] $age The age * @param bool $active */ public function storePersonInfo($name, $age, $active = false) { // here, $age ist casted to an int, and must be > 20, // while $active is a proper boolean (true) instead of a string. } } // Invoke the method via injector: $controller = new Controller(); $injectStore = new Injector(array($controller,'storePersonInfo')); $res = $injectStore->invoke($_REQUEST);
函数/方法定义
无类型转换/条件
如果你只想调用你的函数/方法,而不进行任何类型转换/条件检查,你只需像往常一样定义你的函数
// Normal function: function teaser($text, $maxlen = 80, $tail = '...') { if (mb_strlen($text) > $maxlen) { return mb_substr($text,0,$maxlen-mb_strlen($tail)) . $tail; } else { return $text; } } // Class method: class TextHandler { public function teaser($text, $maxlen = 80, $tail = '...') { if (mb_strlen($text) > $maxlen) { return mb_substr($text,0,$maxlen-mb_strlen($tail)) . $tail; } else { return $text; } } }
然后你可以使用注入器调用函数(注意,参数数组参数不必按函数参数顺序排列)
// Function injector: $injector = new \PhpInjector\Injector('teaser'); $ret = $injector->invoke(array( 'maxlen' => 10, 'text' => 'My fancy long text that gets cut' ); // Method injector: $th = new TextHandler(); $injector = new \PhpInjector\Injector(array($th,'teaser')); $ret = $injector->invoke(array( 'maxlen' => 10, 'text' => 'My fancy long text that gets cut' );
在这个例子中,将使用$text、$maxlen = 10和$tail = '...'参数调用teaser函数/方法。
通过类类型(对象注入)进行注入
如果你想按Class类型匹配参数(而不是按参数名),只需使用类型提示即可
function doSome(\Psr\Http\Message $message) { // do something with the $message object }
此方法可以按如下方式调用
$injector = new \PhpInjector\Injector('doSome'); $ret = $injector->invoke(array( 'Psr\Http\Message' => new HttpMessage() );
注意,你必须为参数数组中的匹配提供完整的命名空间类名。
强制类型转换/参数条件
PhpInjector的一个大优点是它可以将输入参数转换为特定的(基)类型,并检查它们是否满足某些条件。这对于前端输入参数验证非常有用。
由于PHP缺乏适当的类型提示(基类型不能用作函数签名中的提示),我们决定使用标准DocBlock注释来描述参数。这的好处是,我们还可以以标准且可读的语法定义特殊类型和条件,而不会干扰默认的PHP函数定义。
DocBlock注释语法
@param [Type] [name] [description]
示例
/** * A fancy function with some weird input params * * @param string $param_a The name of some monster * @param int $param_b The age of the monster * @param string $param_c The favourite color of the monster * @param bool $param_d Is the monster dangerous? function funcWithSomeParams($param_a, $param_b, $param_c = 'red', $param_d = false) {}
这与标准DocBlock注释(例如PhpDocumentor)完全兼容,但同时也为注入器提供了所需的信息来转换输入值。
支持类型
标准PHP类型
bool
,boolean
int
,integer
float
,double
(结果为float
)string
array
object
mixed
(不进行转换)
特殊类型
timestamp
:尝试通过strtotime
猜测输入,将参数转换为Unix时间戳(例如:输入:'22.01.2015 22:00',参数值:1421964000)json
:通过json_decode
转换输入
使用参数条件
(尚未实现)
特别是对于前端输入验证,检查输入值是否满足某些条件非常有用,例如一个数字是否在一个给定的范围内,一个字符串是否适合maxlength,一个日期是否在某个范围内等。PhpInjector附带一组条件定义。我们同样使用上面显示的DocBlock注释,并扩展下面的例子中的Type字段
/** * A fancy function with some weird input params * * @param string[<100] $param_a The name of some monster, max. 100 chars * @param int[>=0] $param_b The age of the monster * @param timestamp[>=01.01.2000] $param_c Date of viewing, min 1.1.2000 * @param bool $param_d Is the monster dangerous? function funcWithSomeParams($param_a, $param_b, $param_c = 'red', $param_d = false) {}
这定义了一些输入参数的条件。如果它们不匹配,在通过注入器调用方法时将抛出异常。
可用条件
<, <=, >, >=, <> [nr|timestamp]
- 数字:输入必须符合比较条件(例如 <=100:输入必须小于或等于100)
- 字符串:输入长度必须符合比较条件
- 时间戳:输入的日期/时间必须符合比较的日期/时间
[下限]..[上限]
:输入必须在范围内- 对于数字:例如
int[1..100]
:输入值必须在1到100之间(包括1和100) - 对于字符串:例如
string[5..20]
:输入必须至少有5个字符,但最长不超过20个字符 - 对于时间戳:例如
timestamp[1.1.2000..31.12.2010]
:输入的日期必须在给定的日期范围内
- 对于数字:例如
word1[[|word2]...]
:输入的字符串必须包含其中一个单词。例如:@param string[word1|word2|word3] $str
总结 - 类自动装配
自动绑定对于有依赖其他服务的服务类非常有用。例如
// ServiceB is independant class ServiceB {} // ServiceA depends on ServiceB: class ServiceA { public ServiceB $b; public function __construct(ServiceB $b) { $this->b = $b; } }
在这种情况下,ServiceA
只能通过获取构造函数参数ServiceB
来实例化。这是依赖注入场景中的典型用例。
AutoWire
类可以帮助您解决依赖关系并实例化依赖类
use PhpInjector\AutoWire; $aw = new AutoWire(); // In this case, ServiceB will also be instantiated and injected: $instA = $aw->createInstance(ClassNameA::class);
如果您使用依赖注入容器,可以在设置服务实例时使用自动绑定
use PhpInjector\AutoWire; // a PSR-11-compatible service container: $container = new Container(); // register the container with the AutoWire class: $aw = new AutoWire($container); // register ServiceB, which is independant. We could just use `new ServiceB()` here, // but we can also ask the AutoWire class to create an instance: // This makes the instantiation of ServiceB future-proof, e.g. // if ServiceB becomes a dependant of a ServiceC in the future: $container->set(ServiceB::class, $aw->createInstance(ServiceB::class)); // register ServiceA, while ServiceB is fetched from the containe: $container->set(ServiceA::class, $aw->createInstance(ServiceA::class));
使用手动参数进行类构造
有时一个类在构造函数中需要的不仅仅是其他服务。考虑以下类
class ServiceC { private $b; private $name; private $isActive; public function __construct(ServiceB $b, string $name, bool $isActive) { $this->b = $b; $this->name = $name; $this->isActive = $isActive; } }
在这里,$name
和$isActive
不能被确定为一个类型
或要注入的服务。在这种情况下,您可以在自动绑定类时使用额外的参数
use PhpInjector\AutoWire; $aw = new AutoWire($container); $instC = $aw->createInstance(ServiceC::class, [ 'isActive' => true, 'name' => 'Alex', ]);
如果名称匹配,这些额外的参数将被传递到类的构造函数中
示例 - 函数/方法注入
简单的函数参数注入
require_once('vendor/autoload.php'); // Function to use for injection: function fact($n) { if ($n > 1) { return $n*fact($n-1); } else return 1; } // Injector with no type casting / conditions: $injector = new \PhpInjector\Injector('fact'); $ret = $injector->invoke(array('n'=>4)); // 24
带类型转换的参数注入
require_once('vendor/autoload.php'); // Function to use for injection: DocBlock defines the casting type: /** * calculates the factorial of n. * * @param int $n The value to calculate the factorial from */ function fact($n) { // here, $n is casted to an int: if ($n > 1) { return $n*fact($n-1); } else return 1; } // Injector with type casting to int: $injector = new \PhpInjector\Injector('fact'); $ret = $injector->invoke(array('n'=>4.5)); // 24
带类型转换和条件的参数注入
require_once('vendor/autoload.php'); // Function to use for injection: DocBlock defines the casting type and condition: /** * calculates the factorial of n. * * @param int[>0] $n The value to calculate the factorial from */ function fact($n) { // here, $n is casted to an int. An exception is thrown when $n is < 1. if ($n > 1) { return $n*fact($n-1); } else return 1; } // Injector with type casting to int: $injector = new \PhpInjector\Injector('fact'); $ret = $injector->invoke(array('n'=>4.5)); // 24
使用原生PHP函数
您甚至可以使用原生PHP函数
$inj = new \PhpInjector\Injector('strtolower'); echo $inj->invoke(array('str'=>'HEY, WHY AM I SMALL?'));
注入类对象
您还可以通过类型名称而不是变量名称注入类对象。如果您想在调用中实现依赖注入框架,这非常有用
// Function that receives an object of type 'Psr\Http\Request': function processRequest(\Psr\Http\Request $req, $param1, $param2) { // process Request }
要调用此函数,您可以使用如下所示的Invoker
$inj = new \PhpInjector\Injector('processRequest'); echo $inj->invoke(['Psr\Http\Request' => new Request(), 'param1' => 'foo', 'param2' => 'bar']);
从PSR-11服务容器注入服务
如果方法签名请求来自此类服务容器的服务类,则可以提供PSR-11服务容器。这在依赖注入场景中特别有用
use MyServices\FooService; // Function that receives an object of type 'Psr\Http\Request': function processRequest(FooService $myService, $param1) { $myService->doSome($param1); }
要调用此函数,您可以使用如下所示的Invoker
// Service container with available service, created / intantiated elswhere: $container = get_the_PSR_11_Service_container(); $container->registerService(new FooService()); // Now just give the service container in the options array: $inj = new \PhpInjector\Injector('processRequest', ['service_container' => $container]); echo $inj->invoke(['param1' => 'foo']); // Or, optionally, set the service container later: $inj = new \PhpInjector\Injector('processRequest'); $inj->setServiceContainer($container); echo $inj->invoke(['param1' => 'foo']);
示例 - 类自动装配
简单的自动装配
在其最简单的形式中,您不需要DI容器,如果存在依赖类就足够了。在这种情况下,AutoWire只是实例化这些类
use PhpInjector\AutoWire; class ServiceA {} class ServiceB { public function __construct(ServiceA $a) {} } $aw = new AutoWire(); // here, ServiceA is just instantiated with "new ServiceA()", along with ServiceB: $b = $aw->createInstance(ServiceB::class);
带手动参数的自动装配
如果您没有服务容器或想手动提供额外的构造函数参数,只需提供一个参数数组即可
use PhpInjector\AutoWire; class ServiceA {} class ServiceB { public function __construct(ServiceA $a, string $name) {} } $aw = new AutoWire(); // here, ServiceA is just instantiated with "new ServiceA()", along with ServiceB: $b = $aw->createInstance(ServiceB::class, [ 'name' => 'foo', new ServiceA(), ]);
在这里,$name
通过名称在参数数组中找到,而$a
通过其类型声明找到:在参数数组中搜索匹配的类型,并将其分配。
带PSR-11服务容器的自动装配
如果您使用PSR-11兼容的依赖注入容器,则可以使用它为AutoWire查找其依赖项
use PhpInjector\AutoWire; // ServiceB is independant class ServiceB {} // ServiceA depends on ServiceB: class ServiceA { public ServiceB $b; public function __construct(ServiceB $b) { $this->b = $b; } } // a PSR-11-compatible service container: $container = new Container(); // register the container with the AutoWire class: $aw = new AutoWire($container); // ServiceB is indepenadnt $container->set(ServiceB::class, new ServiceB()); // register ServiceA, while ServiceB is fetched from the containe: $container->set(ServiceA::class, $aw->createInstance(ServiceA::class));
开发
设置开发环境
# Without docker: $ git clone git@github.com:bylexus/php-injector.git $ cd php-injector $ composer install $ composer test
# With docker: $ git clone git@github.com:bylexus/php-injector.git $ cd php-injector $ docker build -t php-injector docker/ $ docker run --name php-injector -ti -v "$(pwd):/src" php-injector bash docker> composer install docker> composer test
运行单元测试
$ composer test
或手动,使用PHPUnit
$ php vendor/bin/phpunit ./tests
兼容性
- V1.0.0:需要PHP >= 7.0
- V1.2.0:需要PHP >= 7.2
- V2.0.0:需要PHP >= 7.4
- V3.0.0:需要PHP >= 8.0
变更日志
V3.0.0
- [重大更改]不再可以调用不可访问的类方法(以前甚至允许调用私有方法)
- [功能]引入AutoWire:通过自动注入构造函数参数创建类实例
许可证
许可协议:MIT,版权所有 2015-2021 Alexander Schenkel