illuminatech / array-factory
允许从数组定义创建DI感知的对象
Requires
- illuminate/support: ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0
Requires (Dev)
- illuminate/container: *
- phpunit/phpunit: ^7.5 || ^8.0 || ^9.3 || ^10.5
README
Laravel Array Factory
此扩展允许从数组定义创建DI感知的对象。
有关许可证信息,请检查LICENSE文件。
安装
安装此扩展的首选方式是通过composer。
运行以下命令:
php composer.phar require --prefer-dist illuminatech/array-factory
或将以下内容添加到您的composer.json文件的要求部分。
"illuminatech/array-factory": "*"
用法
此扩展允许从数组定义创建DI感知的对象。创建是通过定义的工厂通过\Illuminatech\ArrayFactory\FactoryContract
合同进行的。可以使用\Illuminatech\ArrayFactory\Factory
进行特定实现。此类工厂允许从其数组定义创建任何对象。定义数组中的键按以下规则处理:
- '__class': string, 要实例化的类的完整限定名。
- '__construct()': array, 在构造函数调用期间要绑定的参数。
- 'methodName()': array, 要传递到通过键定义的对象方法的参数列表。
- 'fieldOrProperty': mixed, 要分配给公共字段或传递给setter方法的值。
- '()': callable, 一旦对象被实例化并对其应用所有其他配置,则调用的PHP回调。
假设我们在我们的项目中定义了以下类
<?php class Car { public $condition; public $registrationNumber; private $type = 'unknown'; private $color = 'unknown'; private $engineRunning = false; public function __construct(string $condition) { $this->condition = $condition; } public function setType(string $type) { $this->type = $type; } public function getType(): string { return $this->type; } public function color(string $color): self { $this->color = $color; return $this; } public function startEngine(): self { $this->engineRunning = true; return $this; } }
可以使用以下方式使用数组工厂实例化此类:
<?php /* @var $factory \Illuminatech\ArrayFactory\FactoryContract */ $car = $factory->make([ '__class' => Car::class, // class name '__construct()' => ['condition' => 'good'], // constructor arguments 'registrationNumber' => 'AB1234', // set public field `Car::$registrationNumber` 'type' => 'sedan', // pass value to the setter `Car::setType()` 'color()' => ['red'], // pass arguments to the method `Car::color()` '()' => function (Car $car) { // final adjustments to be made after object creation and other config application: $car->startEngine(); }, ]);
数组对象定义的主要优点是延迟加载:您可以将整个对象配置定义为简单的数组,甚至不需要加载类源文件,然后在需要时才实例化实际的对象。
定义的数组配置可以进行调整,应用默认值。例如
<?php /* @var $factory \Illuminatech\ArrayFactory\FactoryContract */ $config = [ 'registrationNumber' => 'AB1234', 'type' => 'sedan', 'color()' => ['red'], ]; // ... $defaultCarConfig = [ '__class' => Car::class, 'type' => 'sedan', 'condition' => 'good', ]; $car = $factory->make(array_merge($defaultCarConfig, $config));
您可以使用\Illuminatech\ArrayFactory\Facades\Factory
外观快速访问工厂功能。例如
<?php use Illuminatech\ArrayFactory\Facades\Factory; $car = Factory::make([ '__class' => Car::class, 'registrationNumber' => 'AB1234', 'type' => 'sedan', ]);
服务配置
数组工厂最常见的用例是创建特定应用程序服务的通用配置。假设我们创建了一个通过IP地址检测提供地理定位的库。由于有许多外部服务和解决方案可以解决这个问题,我们创建了一些高级合同,如下所示
<?php namespace MyVendor\GeoLocation; use Illuminate\Http\Request; interface DetectorContract { public function detect(Request $request): LocationInfo; }
此合同可能有多个不同的实现:每个不同的方法和服务。每个特定的实现都提供自己的一组配置参数,这些参数无法统一。使用数组工厂,我们可以以下方式定义此类库的服务提供者
<?php namespace MyVendor\GeoLocation; use Illuminatech\ArrayFactory\Factory; use Illuminate\Support\ServiceProvider; class DetectorServiceProvider extends ServiceProvider { public function register() { $this->app->singleton(DetectorContract::class, function ($app) { $factory = new Factory($app); $factory->make(array_merge( ['__class' => DefaultDetector::class], // default config $app->config->get('geoip', []) // developer defined config )); }); } }
这允许开发者指定任何特定的检测器类及其配置。实际的配置文件 'config/geoip.php' 可能如下所示
<?php /* file 'config/geoip.php' */ return [ '__class' => \MyVendor\GeoLocation\SomeExternalApiDetector::class, 'apiEndpoint' => 'https://some.external.service/api', 'apiKey' => env('SOME_EXTERNAL_API_KEY'), ];
它也可以如下所示
<?php /* file 'config/geoip.php' */ return [ '__class' => \MyVendor\GeoLocation\LocalFileDetector::class, 'geoipDatabaseFile' => __DIR__.'/geoip/local.db', ];
两种配置都可以与我们的服务提供者一起正常工作,并且对其他无数可能的地理定位检测器配置也是如此,这些检测器可能甚至还不存在。
注意!请记住,在创建应用程序配置时避免使用\Closure
,否则您将面临配置缓存的错误。
与DI容器的交互
\Illuminatech\ArrayFactory\Factory
是依赖注入(DI)感知的:它通过 \Illuminate\Contracts\Container\Container::make()
来执行对象实例化。因此,容器中设置的绑定将影响对象创建。例如
<?php use Illuminate\Container\Container; use Illuminatech\ArrayFactory\Factory; $container = Container::getInstance(); $factory = new Factory($container); $container->bind(Car::class, function() { $car = new Car(); $car->setType('by-di-container'); return $car; }); /* @var $car Car */ $car = $factory->make([ '__class' => Car::class, 'registrationNumber' => 'AB1234', ]); var_dump($car->getType()); // outputs: 'by-di-container'
注意:显然,如果在实例化的类中有DI容器的绑定,数组配置中的 '__construct()' 键将被忽略。
DI容器也用于配置方法调用期间,允许自动参数注入。例如
<?php use Illuminate\Container\Container; use Illuminatech\ArrayFactory\Factory; class Person { public $carRents = []; public function rentCar(Car $car, $price) { $this->carRents[] = ['car' => $car, 'price' => $price]; } } $container = Container::getInstance(); $factory = new Factory($container); $container->bind(Car::class, function() { $car = new Car(); $car->setType('by-di-container'); return $car; }); /* @var $person Person */ $person = $factory->make([ '__class' => Person::class, 'rentCar()' => ['price' => 12], ]); var_dump($person->carRents[0]['car']->getType()); // outputs: 'by-di-container' var_dump($person->carRents[0]['price']); // outputs: '12'
注意,最终处理程序回调('()' 配置键)不是DI感知的,不为其参数提供绑定。然而,工厂实例始终作为其第二个参数传递,允许您在需要时访问其DI容器。以下代码将产生与上一个示例相同的结果
<?php use Illuminate\Container\Container; use Illuminatech\ArrayFactory\Factory; $container = Container::getInstance(); $factory = new Factory($container); /* @var $person Person */ $person = $factory->make([ '__class' => Person::class, '()' => function (Person $person, Factory $factory) { $factory->getContainer()->call([$person, 'rentCar'], ['price' => 12]); }, ]);
独立配置
您可以使用数组工厂配置或重新配置已存在的对象。例如
<?php use Illuminatech\ArrayFactory\Factory; $factory = new Factory(); $car = new Car(); $car->setType('sedan'); $car->color('red'); /* @var $car Car */ $car = $factory->configure($car, [ 'type' => 'hatchback', 'color()' => ['green'], ]); var_dump($car->getType()); // outputs: 'hatchback' var_dump($car->getColor()); // outputs: 'green'
类型确保
您可以使用 ensure()
方法添加额外的检查,以确定创建的对象是否与特定的基类或接口匹配。例如
<?php use Illuminate\Support\Carbon; use Illuminate\Cache\RedisStore; use Illuminate\Contracts\Cache\Store; use Illuminatech\ArrayFactory\Factory; $factory = new Factory(); // successful creation: $cache = $factory->ensure( [ '__class' => RedisStore::class, ], Store::class ); // throws an exception: $cache = $factory->ensure( [ '__class' => Carbon::class, ], Store::class );
不可变方法处理
\Illuminatech\ArrayFactory\Factory
在对象配置期间处理不可变方法,从它们的调用返回新对象。例如:如果我们有以下类
<?php class CarImmutable extends Car { public function setType(string $type) { $new = clone $this; // immutability $new->type = $type; return $new; } public function color(string $color): self { $new = clone $this; // immutability $new->color = $color; return $new; } }
以下配置将被正确应用
<?php use Illuminatech\ArrayFactory\Factory; $factory = new Factory(); /* @var $car Car */ $car = $factory->make([ '__class' => CarImmutable::class, 'type' => 'sedan', 'color()' => ['green'], ]); var_dump($car->getType()); // outputs: 'sedan' var_dump($car->getColor()); // outputs: 'green'
注意:由于配置期间可能会调用不可变方法,您应始终使用
\Illuminatech\ArrayFactory\FactoryContract::configure()
方法的返回结果,而不是其参数。
递归创建
对于复杂的对象,如果它将其他对象作为其内部属性存储,可能需要使用数组定义来配置主对象和居民对象,并通过数组工厂解析它们。在这种情况下,可以创建如下定义
<?php $config = [ '__class' => Car::class, // ... 'engine' => [ '__class' => InternalCombustionEngine::class, // ... ], ];
然而,嵌套定义不是由数组工厂自动解析的。以下示例不会实例化引擎实例
<?php use Illuminatech\ArrayFactory\Factory; $factory = new Factory(); $config = [ '__class' => Car::class, // ... 'engine' => [ '__class' => InternalCombustionEngine::class, // ... ], ]; $car = $factory->make($config); var_dump($car->engine); // outputs array
这是为了允许将从属内部配置设置为创建的对象,以便可以根据其自身的内部逻辑以懒加载的方式解决。
然而,您可以将嵌套定义包装在 \Illuminatech\ArrayFactory\Definition
实例中强制解析。例如
<?php use Illuminatech\ArrayFactory\Factory; use Illuminatech\ArrayFactory\Definition; $factory = new Factory(); $config = [ '__class' => Car::class, // ... 'engine' => new Definition([ '__class' => InternalCombustionEngine::class, // ... ]), ]; $car = $factory->make($config); var_dump($car->engine); // outputs object