illuminatech/array-factory

允许从数组定义创建DI感知的对象

1.2.5 2024-03-25 10:23 UTC

This package is auto-updated.

Last update: 2024-08-25 11:20:21 UTC


README

Laravel Array Factory


此扩展允许从数组定义创建DI感知的对象。

有关许可证信息,请检查LICENSE文件。

Latest Stable Version Total Downloads Build Status

安装

安装此扩展的首选方式是通过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