rg/injektor

受google-guice启发的依赖注入容器

4.0.0 2024-01-08 16:22 UTC

README

rg\injektor 是一个受 Guice 启发的 PHP 依赖注入容器。与基于反射的其他容器不同,rg\injektor 包含一个工厂类生成器,您可以使用它来防止在生产环境中使用反射。

Test status

先决条件

此库需要 PHP 8.1+。

它已在 PHP 8.1 和 PHP 8.2 上进行过测试。

安装

您可以直接使用 composer 安装此库。只需在您的项目目录中运行此命令

$ composer require rg/injektor

用法

安装 rg\injektor 后,您可以使用它如下

$configuration = new \rg\injektor\Configuration($pathToConfigFile, $pathToFactoryDirectory);
$dic = new \rg\injektor\DependencyInjectionContainer($configuration);

$instance = $dic->getInstanceOfClass('ClassName');
$result = $dic->callMethodOnObject($instance, 'methodName');

有关 rg\injektor 的特定功能的更多详细信息,请参阅下面。

如果您使用某种 MVC 框架,建议将 rg\injektor 包含在您的入口控制器中,以创建控制器对象并在其上调用方法。

生成工厂

默认情况下,rg\injektor 严重依赖反射,这对于您的开发环境来说是可以的,但会不必要地减慢生产环境。因此,您应该使用内置的可能性使用生成的工厂类。为了做到这一点,您必须在部署项目之前生成这些工厂。

首先,您必须在您的代码中使用 \rg\injektor\FactoryDependencyInjectionContainer 类

$configuration = new \rg\injektor\Configuration($pathToConfigFile, $pathToFactoryDirectory);
$dic = new \rg\injektor\FactoryDependencyInjectionContainer($configuration);

如果没有工厂,\rg\injektor\FactoryDependencyInjectionContainer 将回退到反射。

要生成工厂,您必须编写一个小脚本,该脚本遍历您的 PHP 文件并为每个文件创建工厂。以下是基于 Symfony Console 组件的此类脚本的示例

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use rg\injektor\WritingFactoryGenerator;

class GenerateDependencyInjectionFactories extends \Symfony\Component\Console\Command\Command
{
    /**
     * @var \rg\injektor\DependencyInjectionContainer
     */
    private $dic;

    /**
     * @var \rg\injektor\WritingFactoryGenerator
     */
    private $factoryGenerator;

    /**
     * @var string
     */
    private $root;

    protected function configure()
    {
        $this->setDescription('generates factories for dependency injection container');
        $this->setHelp('generates factories for dependency injection container');
    }

    /**
     * @param InputInterface $input
     * @param OutputInterface $output
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln('Generating Factories');

        $this->root = '/path/to/your/project';

        $factoryPath = $this->root . '/folder/for/generated/factories';

        if (!file_exists($factoryPath)) {
            mkdir($factoryPath, 0777, true);
        }

        $pathToConfigFile = '/config/dic.php';

        $configuration = new \rg\injektor\Configuration($pathToConfigFile, $factoryPath);
        $this->dic = new \rg\injektor\FactoryDependencyInjectionContainer($configuration);

        $this->factoryGenerator = new WritingFactoryGenerator($this->dic->getConfig(), $factoryPath);

        $this->factoryGenerator->cleanUpGenerationDirectory($factoryPath);

        $this->processAllDirectories($output);
    }

    /**
     * @param OutputInterface $output
     */
    private function processAllDirectories(OutputInterface $output)
    {
        $this->processDirectory($this->root . DIRECTORY_SEPARATOR . 'folderWithPhpClasses', $output);
    }

    /**
     * @param $directory
     * @param OutputInterface $output
     */
    private function processDirectory($directory, OutputInterface $output)
    {
        $output->writeln('Directory: ' . $directory);
        $directoryIterator = new \RecursiveDirectoryIterator($directory);
        $iterator = new \RecursiveIteratorIterator($directoryIterator);
        $regexIterator = new \RegexIterator($iterator, '/^.+\.php$/i', \RecursiveRegexIterator::GET_MATCH);
        foreach ($regexIterator as $file) {
            $this->processFile($file[0], $output);
        }
    }

    /**
     * @param $fullpath
     * @param OutputInterface $output
     */
    private function processFile($fullpath, OutputInterface $output)
    {
        $output->writeln('Process file [' . $fullpath . ']');

        require_once $fullpath;

        $astLocator = (new \Roave\BetterReflection\BetterReflection())->astLocator();

        $reflector  = new \Roave\BetterReflection\Reflector\DefaultReflector(new Roave\BetterReflection\SourceLocator\Type\SingleFileSourceLocator($fileName, $astLocator));
        $classes = $reflector->reflectAllClasses();

        foreach ($classes as $class) {
            $generator->processClass($class->getName());
        }
    }

    /**
     * @param \Laminas\Code\Reflection\ClassReflection $class
     */
    private function processClass(\Laminas\Code\Reflection\ClassReflection $class)
    {
        if (!$class->isInstantiable()) {
            return;
        }
        $this->factoryGenerator->processFileForClass($class->name);
    }
}

功能

构造函数注入

class Foo
{
    /**
     * @inject
     * @param Bar $bar
     */
    public function __construct(Bar $bar)
    {

    }
}

class Bar
{

}

$dic->getInstanceOfClass('Foo');

Bar 的一个实例将被注入为构造函数参数 $bar。当然,Bar 也可以使用依赖注入。容器可以注入任何可注入的类,因为

  • 它们在构造函数中有 @inject 注解
  • 它们没有参数的构造函数
  • 它们没有构造函数
  • 参数是可选的
  • 参数是配置的(见下文)

构造函数可以是 __construct 方法或如果类配置为单例且 __construct 方法是私有或受保护的,则类的静态 getInstance 方法。

class Foo
{
    /**
     * @inject
     * @param Bar $bar
     */
    public function __construct(Bar $bar)
    {

    }
}

/**
 * @singleton
 */
class Bar
{
    private function __construct()
    {

    }

    public static function getInstance()
    {

    }
}

$dic->getInstanceOfClass('Foo');

属性注入

class Foo
{
    /**
     * @inject
     * @var Bar
     */
    protected $bar;
}

class Bar
{

}

$dic->getInstanceOfClass('Foo');

字段 $bar 将有一个 Bar 的实例。为了使其工作,该字段不能是私有的,而必须是受保护的或公共的。这也可以与构造函数注入结合使用。

注入具体实现

class Foo
{
    /**
     * @inject
     * @var Bar
     */
    protected $bar;
}

/**
 * @implementedBy BarImpl
 */
interface Bar
{

}

class BarImpl implements Bar
{

}

$dic->getInstanceOfClass('Foo');

而不是 Bar,BarImpl 被注入到 $bar 中。您也可以在依赖注入配置中配置此操作,而不是使用注解

'Bar' => array(
    'class' => 'BarImpl'
)

使用提供者类

class Foo
{
    /**
     * @inject
     * @var Bar
     */
    protected $bar;
}

/**
 * @providedBy BarProvider
 */
interface Bar
{

}

class BarImpl implements Bar
{

}

class BarProvider implements rg\injektor\Provider
{
    public function get()
    {
        return new BarImpl();
    }
}

$dic->getInstanceOfClass('Foo');

而不是 Bar,BarProvider 的 get 方法(BarImpl)的返回值被注入到 $bar 中。您也可以在依赖注入配置中配置此操作,而不是使用注解

'Bar' => array(
    'provider' => array(
        'class' => 'BarImpl'
    )
)

向提供者传递固定数据

class Foo
{
    /**
     * @inject
     * @var Bar
     */
    protected $bar;
}

/**
 * @providedBy BarProvider {'foo' : 'bar'}
 */
interface Bar
{

}

class BarImpl implements Bar
{

}

class BarProvider implements rg\injektor\Provider
{

    /**
     * @inject
     */
    public function __construct(SomeClass $someClass, $foo)
    {
    }

    public function get()
    {
        return new BarImpl();
    }
}

$dic->getInstanceOfClass('Foo');

在这里,提供者接收一个 SomeClass 的额外实例注入。变量 $foo 被设置为 'bar'。您也可以在配置中配置此操作

'Bar' => array(
    'provider' => array(
        'class' => 'BarImpl',
        'params' => array(
            'foo' => 'bar',
        )
    )
)

作为单例注入

class Foo
{
    /**
     * @inject
     * @var Bar
     */
    protected $bar;
}

/**
 * @singleton
 */
class Bar
{

}

$instanceOne = $dic->getInstanceOfClass('Foo');
$instanceTwo = $dic->getInstanceOfClass('Foo');

$instanceOne 和 $instanceTwo 都将具有相同的 Bar 注入实例。

您也可以在依赖注入配置中配置此操作,而不是使用注解

'Bar' => array(
    'singleton' => true
)

请注意,对于单例 injektor,它会分析注入类的给定参数以确定是否已创建了所需的实例。

这意味着在这个例子中

class Foo
{
    /**
     * @inject
     * @var Bar
     */
    protected $bar;
}

/**
 * @singleton
 */
class Bar
{
    public function __construct($arg)
    {
    }
}

$instanceOne = $dic->getInstanceOfClass('Foo', array('arg' => 1));
$instanceTwo = $dic->getInstanceOfClass('Foo', array('arg' => 2));

$instanceOne 和 $instanceTwo 将是不同的实例。虽然这个特性有速度上的优势,但如果您想要无论参数如何都能保持相同的实例,或者总是传递相同的或注入所有参数,请将其标记为服务(见下文)。

作为服务注入

class Foo
{
    /**
     * @inject
     * @var Bar
     */
    protected $bar;
}

/**
 * @service
 */
class Bar
{

}

$instanceOne = $dic->getInstanceOfClass('Foo');
$instanceTwo = $dic->getInstanceOfClass('Foo');

$instanceOne 和 $instanceTwo 都将具有相同的 Bar 注入实例。

您也可以在依赖注入配置中配置此操作,而不是使用注解

'Bar' => array(
    'service' => true
)

与单例模式相比,在这个服务示例中

class Foo
{
    /**
     * @inject
     * @var Bar
     */
    protected $bar;
}

/**
 * @service
 */
class Bar
{
    public function __construct($arg)
    {
    }
}

$instanceOne = $dic->getInstanceOfClass('Foo', array('arg' => 1));
$instanceTwo = $dic->getInstanceOfClass('Foo', array('arg' => 2));

将导致 $instanceOne 和 $instanceTwo 是相同的对象实例。

配置参数

您还可以在配置中配置容器应传递给 __construct 或 getInstance 方法的所有或某些参数的内容,而不是让容器从类型提示中猜测它们。

class Foo
{
    /**
     * @inject
     */
    public function __construct($bar)
    {

    }
}

/**
 * @singleton
 */
class Bar
{
    private function __construct()
    {

    }

    /**
     * @inject
     */
    public static function getInstance($foo, $buzz)
    {

    }
}

$dic->getInstanceOfClass('Foo');

配置

'Foo' => array(
    'params' => array(
        'bar' => array(
            'class' => 'Bar'
        )
    )
),
'Bar' = array(
    'params' => array(
        'foo' => array(
            'value' => 'fooBar'
        ),
        'buzz' => array(
            'value' => true
        )
    )
)

或者,您也可以使用注解来配置

class Foo
{
    /**
     * @inject
     * @var Bar {"foo":456,"buzz":"content"}
     */
    protected $propertyInjection;


    /**
     * @inject
     * @param Bar $bar {"foo":123,"buzz":"content"}
     */
    public function __construct(Bar $bar)
    {

    }
}

/**
 * @singleton
 */
class Bar
{
    private function __construct()
    {

    }

    /**
     * @inject
     */
    public static function getInstance($foo, $buzz)
    {

    }
}

$dic->getInstanceOfClass('Foo');

在运行时传递额外的参数

您还可以在运行时向新实例传递一些值。

class Foo
{
    /**
     * @inject
     */
    public function __construct($val, Bar $bar, Buzz $buzz)
    {

    }
}

class Bar
{
}

class Buzz
{
}

$dic->getInstanceOfClass('Foo', array(
    'val' => 123,
    'buzz' => new Buzz()
));

这也可以与配置参数结合使用。

命名注入

class Foo
{
    /**
     * @var Bar
     * @named barOne
     */
    protected $bar;

    /**
     * @inject
     * @param Bar $one
     * @param Bar $two
     * @param Bar $default
     * @named barOne $one
     * @named barTwo $two
     */
    public function __construct(Bar $one, Bar $two, Bar $default)
    {

    }
}

interface Bar
{

}

class BarImplDefault implements Bar
{

}

class BarImplOne implements Bar
{

}

class BarImplTwo implements Bar
{

}

$dic->getInstanceOfClass('Foo');

配置

'Bar' => array(
    'class' => 'BarImplDefault'
    'named' => array(
        'barOne' => 'BarImplOne',
        'barTwo' => 'BarImplTwo'
    )
)

您也可以使用注解直接配置此内容

/**
 * @implementedBy BarImplDefault
 * @implementedBy barOne BarImplOne
 * @implementedBy barTwo BarImplTwo
 */
interface Bar
{

}

class BarImplDefault implements Bar
{

}

class BarImplOne implements Bar
{

}

class BarImplTwo implements Bar
{

}

您还可以为默认实现命名,这样我们的配置看起来会更加简洁。结果是一样的

/**
 * @implementedBy default BarImplDefault
 * @implementedBy barOne  BarImplOne
 * @implementedBy barTwo  BarImplTwo
 */
interface Bar
{

}

命名提供者

class Foo
{
    /**
     * @var Bar
     * @named barOne
     */
    protected $bar;

    /**
     * @inject
     * @param Bar $one
     * @param Bar $two
     * @param Bar $default
     * @named barOne $one
     * @named barTwo $two
     */
    public function __construct(Bar $one, Bar $two, Bar $default)
    {
    }
}

interface Bar
{

}

$dic->getInstanceOfClass('Foo');

配置

'Bar' => array(
    'provider' => array(
        'class' => 'BarProvider'
    ),
    'namedProviders' => array(
        'barOne' => array(
            'class' => 'BarProvider',
            'parameters' => array('name' => 'barOne')
        ),
        'barTwo' => array(
            'class' => 'BarProvider',
            'parameters' => array('name' => 'barTwo')
        )
    )
)

您也可以使用注解直接配置此内容

/**
 * @providedBy BarProvider
 * @providedBy barOne BarProvider {"name" : "barOne"}
 * @providedBy barTwo BarProvider {"name" : "barOne"}
 */
interface Bar
{

}

class BarProvider implements rg\injektor\Provider
{
    private $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function get()
    {
        switch ($this->name) {
            case 'barOne':
                return new BarImplOne();
            case 'barTwo':
                return new BarImplTwo();
        }

        return new BarImplDefault();
    }
}

class BarImplDefault implements Bar
{

}

class BarImplOne implements Bar
{

}

class BarImplTwo implements Bar
{

}

您还可以为默认提供者命名,这样我们的配置看起来会更加简洁。结果是一样的

/**
 * @providedBy default BarProvider
 * @providedBy barOne BarProvider {"name" : "barOne"}
 * @providedBy barTwo BarProvider {"name" : "barOne"}
 */
interface Bar
{

}

在对象实例上调用方法

容器还可以在实例上调用方法并注入所有方法参数

class Foo
{
    /**
     * @inject
     */
    public function doSomething(Bar $bar)
    {
    }
}

class Bar
{

}

$foo = new Foo();
$dic->callMethodOnObject($foo, 'doSomething');

当然,您也可以使用命名注入。

您还可以在方法调用中添加额外的值,就像在对象创建时一样

class Foo
{
    /**
     * @inject
     */
    public function doSomething(Bar $bar, $foo)
    {
    }
}

class Bar
{

}

$foo = new Foo();
$dic->callMethodOnObject($foo, 'doSomething', array('foo' => 'value'));