bylexus/php-injector

该软件包最新版本(3.0.1)的许可证信息不可用。

一个函数/方法参数注入助手。使您的依赖注入工作更顺畅。

3.0.1 2024-01-22 07:21 UTC

This package is auto-updated.

Last update: 2024-09-22 08:52:53 UTC


README

Tests

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);

现在这绝对是个糟糕的想法!不要直接将请求参数作为函数参数使用!这里可能会发生各种糟糕的事情(注入、远程代码调用……)。

这时注入器就派上用场了:它允许你

  1. 定义你想为函数接受的参数
  2. 用参数数组调用函数,参数顺序无关紧要(因此你不必实现复杂的变量排序算法)
  3. 定义类型转换,例如“我接受一个$age变量,但它被转换为int。”
  4. 甚至可以定义条件(例如'年龄必须大于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注释语法

语法类似于PhpDocumentor的@param定义

@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