miniature/component

PHP-classes组件的模板,用于组件架构。依赖注入的黑盒方法。

0.1.2 2021-06-27 06:53 UTC

This package is auto-updated.

Last update: 2024-09-24 21:59:59 UTC


README

组件

警告!

这仍然处于实验阶段。我们不知道目前是否已经把所有东西都整合好了。还需要进行广泛的测试。

目的

这是一个由[**依赖注入容器**](#the-di-mapping)提供的PHP-classes组件模板。容器本身由[**组件实例**](#the-instance-of-the-component)(黑盒方法)隐藏,从外部充当[门面](#coupling-detection-and-protection)。

作为门面,该组件提供了访问限制机制。这种访问限制反过来是连接组件耦合和所需瓶颈,以便容器中的类与外部世界进行通信。还有一个违规检测程序确保黑盒保持关闭。

可以通过某些注入和覆盖轻松更改行为。DI-Container是其自己的独立包。

在此上下文中“组件”是什么意思?

我们正在寻找一组工具来构建组合组件架构。我们正在尝试构建一个微小的框架,使我们能够相对容易地设置此类内容。

component architecture

如果您对此不熟悉,并且《内聚性与耦合性问题》不在您的议程上,那么您可能使用的是不同的术语,并且这个包绝对不适合您。

您可能首先想访问扩展图,以获得更好的理解。

设置组件的基本步骤

  • Miniature\Component\Component单例继承,并给它一个独特的自我描述性名称
  • 设置一个配置文件夹,并将目录路径信息注入到组件中
    • 提供配置文件(PHP数组或YAML)...
      • 包含依赖注入连接
      • 以及组件耦合连接
  • 然后您就可以出发了

在继续之前有一件事需要注意

此包提供某些依赖连接和黑/灰/白盒机制,这可能对发布组件架构很有用。这主要关于结构、访问控制和依赖管理。这并不意味着这些对于组件架构的任务来说是足够的。其他方法可能更加可持续。首先考虑捆绑、独立部署和版本控制。

 
 
 
 
 
 
 

安装

使用Composer

composer require miniature/component

下载包

您还需要DI-Container包

将两个包解压缩到名为Miniature的目录中。将以下内容添加到您的autoload中

<?php

function miniature_autoload($class)
{
    $fileName = str_replace('\\', '/', realpath(__DIR__) . '/' . $class ) . '.php';
    if (preg_match('/^(.*\/Miniature)\/(\w+)\/((\w+\/)*)(\w+)\.php/', $fileName)) {
        $newFileName = preg_replace(
            '/^(.*\/Miniature)\/(\w+)\/((\w+\/)*)(\w+)\.php/',
            '$1/$2/src/$3$5.php',
            $fileName
        );
        if (is_file($newFileName)) {
            require $newFileName;
        }
    }
}
spl_autoload_register('miniature_autoload');

可能您需要调整filePath的文件路径拼接,通过在filepath()语句中设置相对路径。

 
 
 
 
 
 
 

组件的实例

基本实例化

基本类是一个抽象的单例。这样做的目的是

  • 您的组件在PHP环境中被认为是唯一的
  • 将会有多个组件
  • 您的组件在任何时候都应该是全局可访问的

话虽如此,如果您已经想好了自己的名字,您可能只需要几行代码就能完成

<?php

use Miniature\Component\Component;

class SelfSpeakingComponent extends Component {    
    protected static ?Component $instance = null;
}

$selfSpeakingComponentInstance = SelfSpeakingComponent::getInstance();

这里最重要的是protected static $instance属性。它确保始终使用您创建的继承者的相同实例。否则,当您处理多个组件实例时,您将面临意外的行为,尽管在使用唯一实例时一切似乎都很正常。

这将没有错误,但它什么也不会做。

参数注入:配置目录的路径

组件实例需要知道在哪里读取配置。并且很可能还有更多信息和数据需要提供给组件。因此,存在一个参数类Miniature\Component\InitParameters,用于注入组件构造函数。在这个第一个例子中,配置目录的路径可能就足够了。

$paramObject = (new Miniature\Component\InitParameters())
    ->setConfigDirectory( __DIR__ . '/../config');
$selfSpeakingComponentInstance = SelfSpeakingComponent::getInstance($paramObject);

这可能对您来说看起来不太好,因为您不知道组件是在请求处理的哪个阶段第一次被调用的。因此,存在一个可重写的静态自动注入方法。

<?php

use Miniature\Component\Component;
use Miniature\Component\InitParametersInterface;
use Miniature\Component\InitParameters;

class SelfSpeakingComponent extends Component 
{
    protected static ?Component $instance = null;
    
    protected static function autoInject() : ?InitParametersInterface
    {
        return (new InitParameters())
            ->setConfigDirectory( __DIR__ . '/../config');
    }
}

这个操作使您能够“随时随地”访问组件实例。
调用始终看起来一样。即使当您创建它时。您永远不会知道您在创建它。

$instantlyNeededInstance = SelfSpeakingComponent::getInstance();

有关配置目录的内容和依赖关系的详细信息,请在此处了解。

您几乎完成组件

假设您想要为在DI-Container中生存的选定实例提供类型安全的或契约安全的访问,您的完成组件可能看起来像这样

<?php

namespace YourOwnExamleApp;

use Miniature\Component\Component;
use Miniature\Component\InitParametersInterface;
use Miniature\Component\InitParameters;

use YourOwnExamleApp\The2ndComponent;
use YourOwnExamleApp\ProductAccessInterface;
use YourOwnExamleApp\PersonAccessInterface;
use YourOwnExamleApp\AddressAccessInterface;

class SelfSpeakingComponent extends Component 
{
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
     *                 INIT
     * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    protected static ?Component $instance = null;
    
    protected static function autoInject() : ?InitParametersInterface
    {
        return (new InitParameters())
            ->setConfigDirectory( __DIR__ . '/../config');
    }
    
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
     *                 PROVIDE
     * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    public function providePerson() : PersonAccessInterface
    {
        return $this->container->get('person_access');
    }
    
    public function provideAddress() : AddressAccessInterface
    {
        return $this->container->get('address_access');
    }
         
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
     *                 CONSUME
     * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    public function consumeProduct() : ProductAccessInterface
    {
        return The2ndComponent::getInstance()->provideProduct();
    }
}

有两个公共方法,通过从DI-Container检索的对象提供某种访问。有一个方法从另一个组件检索另一个访问对象,最有可能将其返回到组件内部的一个类,从而消耗契约。

但您还没有真正完成。尽管您通过在公共方法中返回接口实现提供了契约,但您无法控制“谁”在使用它。没有控制,很容易就会在组件之间产生多个交叉连接,从而产生非常不希望的结构。这是一个架构任务的失败。

还有关于DI-Container内部哪些类有意义的限制。

因此,Miniature\Component提供了一种简单的连接机制

耦合检测和保护

Miniature\Component\Component提供了一种保护方法,确保只有通过连接耦合配置获得许可的类和方法才能访问。不需要返回值。insureCoupling()简单地将异常抛出。

   $this->insureCoupling();

在下面的示例中,提供调用,找到提供者和消费者方法。没有连接,它们将不会做任何事情,只会抛出异常。

<?php

namespace YourOwnExamleApp;

use Miniature\Component\Component;
use Miniature\Component\InitParametersInterface;
use Miniature\Component\InitParameters;

use YourOwnExamleApp\The2ndComponent;
use YourOwnExamleApp\ProductAccessInterface;
use YourOwnExamleApp\PersonAccessInterface;
use YourOwnExamleApp\AddressAccessInterface;

class SelfSpeakingComponent extends Component 
{
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
     *                 INIT
     * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    protected static ?Component $instance = null;
    
    protected static function autoInject() : ?InitParametersInterface
    {
        return (new InitParameters())
            ->setConfigDirectory( __DIR__ . '/../config');
    }
    
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
     *                 PROVIDE
     * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    public function providePerson() : PersonAccessInterface
    {
        $this->insureCoupling();
        return $this->container->get('person_access');
    }
    
    public function provideAddress() : AddressAccessInterface
    {
        $this->insureCoupling();
        return $this->container->get('address_access');
    }
        
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
     *                 CONSUME
     * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    public function consumeProduct() : ProductAccessInterface
    {
        $this->insureCoupling();
        return The2ndComponent::getInstance()->provideProduct();
    }
}
 
 
 
 
 
 
 

连接耦合

您可能首先想观察概述图,以便更好地理解。

这些布线示例大致基于上面显示的组件类示例所示。我们只假设存在一个The2ndComponent和一些来自容器内部的类。示例是简化的。当然,类名将在这里以完全限定形式出现。

请注意,YAML支持默认不可用

示例中解释的第一个布线

组件耦合部分开始

coupling:

方法SelfSpeakingComponent->providePerson() ...

   SelfSpeakingComponent::getInstance()->providePerson();
    SelfSpeakingComponent:
        providePerson:

... 允许通过The2ndComponent->consumePerson()调用。

   The2ndComponent::getInstance()->consumePerson();
            The2ndComponent:
                consumePerson: true

可以通过将值更改为false立即关闭此功能。

通用布线功能

通常,可以有多个类和多个方法的赋值。通过观察YAML结构应该会变得清楚。

除非证明这些赋值总体上实现了一些“一个接口”,否则应考虑组件之间的大量赋值是不受欢迎的。对于从DI-容器内部访问组件的类,可能会有所不同。

通配符表示法

还有授予一个类一般权限的选项。可能是为了测试或开发,或者它是一个位于容器内、专门用于与组件通信的类。

            OnlyInTestingEnvironment: true
 
 
 
 
 
 
 

DI映射

仅硬布线

抱歉,到目前为止,将不会有自动布线。除了缺乏开发时间和使用反射类可能引起的问题外,我们还认为,在接口到实现映射方面,您已经有所有手段了。

死锁保护

Miniature\DiContainer通过在实例化过程中构建的Miniature\DiContainer\DiNode-树提供死锁保护。未来可能需要一个整个布线冒泡的测试模式。

示例

让我们从PHP和YAML的简单示例开始。请注意,YAML支持默认不可用。另一方面,PHP可能具有产生动态结构的优势(或不确定性)。

如果您习惯于在流行的MVC框架中使用服务容器配置,这可能会让您感到熟悉。的确,我们曾考虑过从Symfony调整语法,但最终得出结论,使用键service在术语上会引起很多混淆。所以映射分支被命名为它所代表的内容:di_mapping。如果您想更改语法,可以通过覆盖来完成。

键的解释

类键

          - '@person'

args子分支中查找重复的二级键,如personaddressperson_managermysql_wrapperperson_access,前面加上一个@前缀。这就是如何将类映射为构造函数注入的参数(args)。命名完全由您选择。考虑名称空间。如上所述,相等的键将导致覆盖。这是为了实现基于环境的覆盖

对于每个类映射,必须使用键class,这很可能是指一个完全限定的类名。

参数数据的键

          - '%mysql_person_connection'

查找带有标记为%mysql_person_connection的参数的mysql_wrapper。您将在params分支中找到这个键(mysql_person_connection)。这将不经过任何更改传递给构造函数。同样,关于键access_token(分别对应%access_token)也是如此。

DI容器的键

          - '@miniature.di_container'

映射 person_access 展示了如何通过参数列表注入 DI 容器本身:@miniature.di_container

请注意,构造函数注入在单元测试中更容易处理。但有些情况下,你可能甚至需要它,因为这是直接访问容器的唯一方式。

键:'static'

        static: getInstance

另一个来自 person_access 的例子:键 static

这应该包含一个字符串,该字符串是用于代替普通构造函数调用的静态生成/访问方法的名字(例如 new ClassName($param))。在我们的例子中,这是用于单例模式的标准调用,它将导致

    $instance = AppDemo\PersonAccess::getInstance($container);

键:'singleton'

        singleton: true

singleton 键的使用与同名的设计模式完全独立。通过将其赋值为 true,你可以强制类只实例化一次。这不仅适用于你可能会使用设计模式的场景,而且在你可以确保不会在成员变量中存储结果的所有场景中都有用。

这是访问实例最快且节省资源的方式。内部键 instance 总是会被检查。如果有内容,它将被返回,并跳过所有其他进程。要真正地说,这不是真正的单例模式的替代品。(你可能会更喜欢称之为缓存。)你可以使用不同的键在两个不同的地方实例化相同的类,也许使用不同的参数。这可能就是你的意图?

键 'public'

        public: true

将键 public 设置为 true 使实例可以通过组件从外部访问。默认情况下,此功能仅适用于 'dev' 和 'test' 环境。你可以通过设置允许 public 的环境来控制此行为。

$paramObject = (new Miniature\Component\InitParameters())
    // other configurations
    ->setEnvAllowingPublicAccess('dev', 'test', 'prod');

在我们的情况下,可能是这样的(假设我们正在使用 自动注入

SelfSpeakingComponent::getInstance()->person_access->somePublicMethod('string_parameter');

如果你不喜欢魔法访问,你可能更喜欢像这里所示的 get($string)

SelfSpeakingComponent::getInstance()->get('person_access')->somePublicMethod('string_parameter');

如果你倾向于将所有内容都声明为 public 或者质疑这些繁琐的东西有什么用,我建议你了解凝聚性与耦合问题以及组件架构。

键 'skipViolationScan'

        skipViolationScan: true

此键不包括在上面的示例中。将此键设置为 true 将在违规扫描例程中禁用 违规检测

例如,当将供应商类包含到映射中时,这很有用。

动态覆盖参数

请注意:此功能与 singleton 功能不兼容。尝试组合这两个功能将导致异常。

这种覆盖参数的方式仅适用于已注入容器的类。从容器中检索实例是通过 get 来实现的。

$instanceFromMapping = $this->container->get('class_key_string');

动态覆盖是通过第二个参数实现的,这是一个数组,其字段与原始参数数组索引相同。你可以再次提供所有参数,或者你可能只想提供某些参数。在后一种情况下,字符串索引在参数中很有用。

考虑上述示例,假设 person_access-实例想要使用 @person@adress 的替换类检索一个 person_manager 实例,它可能看起来像这样

$instanceWithOverrides = 
    $this->container->get(
        'person_manager',
        [
            'address' => '@other_address_key',
            'person'  => '@other_person_key',
        ]
    );

由于 person_manager 保留一个关联数组作为参数,因此甚至不需要关心顺序。基本上,这种方法也可以与数字键一起使用,但你是否感到舒适呢?

 
 
 
 
 
 
 

读取配置目录

读取行为

配置目录及其子目录中所有文件都将被递归读取,前提是文件格式受支持。目前,这包括

  • PHP:PHP文件始终应返回一个数组
  • YAML:这取决于YAML PECL扩展是否加载。或者,可以注入一个装饰器,该装饰器包含您选择的基于PHP的YAML解释器。更多关于这方面的信息请点击此处

第1次递归级别:选择解释器

有三个主要键,用于三个主要目的:di_mappingparamscoupling

  • di_mapping包含实际的类映射、注入的参数和行为。
  • params包含数组数据,可以作为参数进行引用,但不会被解释。
  • coupling包含提供组件耦合的连接

(顺便说一下:如果您不喜欢命名方式,可以通过注入DI容器来更改它,但coupling键的命名除外。)

第2次递归级别:访问类和参数的键

下一递归级别上的键都是您选择的。这些是您的类和参数将被使用的名称。具有相同键的内容将被覆盖。这是使我们能够根据特定环境的需求进行覆盖的基本机制。

YAML支持

YAML不是PHP标准安装的一部分。另一方面,我们努力使Miniature免受外部依赖。尽管如此,为了所有配置工作,YAML非常受欢迎。

因此,目前有两种选择

  • 安装YAML的PECL扩展
  • 注入一个实现Miniature\Component\Reader\YamlParserDecoratorInterface的装饰器

配置读取器将检测YAML PECL扩展是否已安装,并将其作为首选。如果没有,它将检查是否注入了装饰器,并使用它。如果没有,它会在遇到没有可用解析器的YAML文件时抛出一个警告级错误。

我们假设,YamlParserDecoratorInterface本身说明了一切。InitParameters类提供了一个专门的设置器。

$paramObject = (new Miniature\Component\InitParameters())
    // other settings
    ->setYamlParserDecorator(new MyOwnAPP\YamlParserDecorator());

基于环境的覆盖

如上所述,配置读取器在覆盖第2次递归级别的键方面是无情的。它将继续这样做,通过递归地读取配置目录的每个子目录。您可以通过列出可用环境中的子目录来更改这一点。该列表的包含将防止以相同名称命名的目录被读取,除非当前环境碰巧与目录名称相同。

这就是基于环境的覆盖实现。没有更多。

 
 
 
 
 
 
 

配置环境

一般参数注入

InitParameters类

基本配置是通过一个名为InitParameters的参数对象完成的,该对象被传递给容器实例。(我们可能会在未来添加一些更多配置选项,以便使其更加方便。)

$paramObject = (new Miniature\Component\InitParameters())
    ->setConfigDirectory( __DIR__ . '/../config');
$selfSpeakingComponentInstance = SelfSpeakingComponent::getInstance($paramObject);

从长远来看,自动注入是更好的选择...

class SelfSpeakingComponent extends Component 
{
    protected static ?Component $instance = null;
    
    protected static function autoInject() : ?InitParametersInterface
    {
        return (new InitParameters())
            ->setConfigDirectory( __DIR__ . '/../config');
    }
}

...因为这样您可以随时从任何地方访问

$instantlyNeededInstance = SelfSpeakingComponent::getInstance();

InitParameters支持方法链

$paramObject = (new Miniature\Component\InitParameters())
    ->setAppRootPath(__DIR__ . '/..')
    ->setConfigDirectory('config')
    ->setDotEnvPath('');

路径配置

所有与路径相关的设置器都接受基于入口脚本脚本的相对路径。通常我们假设这是位于 public 目录中的 index.php。设置器方法会将它们转换为相对路径并检查其有效性。这也适用于绝对路径。

setAppRootPath()

这并非必需,但它可能会使事情更加方便。一旦设置,其他路径相关方法将连接到根路径字符串。

所以,而不是设置配置路径的相对路径

$paramObject = (new Miniature\Component\InitParameters())
    ->setAppRootPath(__DIR__ . '/..')
    ->setConfigDirectory(__DIR__ . '/../config');

可以直接这样做

$paramObject = (new Miniature\Component\InitParameters())
    ->setAppRootPath(__DIR__ . '/..')
    ->setConfigDirectory('config');

setConfigDirectory()

自解释,这是所有映射所在的目录。请注意 读取行为基于环境的覆盖

$paramObject->setConfigDirectory(__DIR__ . '/../config');

setDotEnvPath()

自解释,这是 .env 文件所在的目录路径。了解更多关于结果的 行为

$paramObject->setDotEnvPath(__DIR__ . '/..');

环境设置

setEnv()

直接使用字符串参数设置环境名称。在没有 .env 文件的情况下很有用。请注意,在不确定的情况下,这会导致覆盖从 .env 文件 中读取的值。这对于想要模拟生产行为的发展情况可能很有用。

setAvailableEnv()

这是一个已知于您系统的环境名称列表。这个列表尤其与 目录读取行为 相关。任何被命名为列表中名称之一的目录将不会自动递归扫描。

它可以接受环境名称的数组 ...

$paramObject->setAvailableEnv(
    ['dev', 'test', 'prod', 'another']
);

... 以及字符串参数列表。

$paramObject->setAvailableEnv('dev', 'test', 'prod', 'another');

setEnvAllowingPublicAccess()

正如在 setAvailableEnv() 中,这是一个环境名称列表。它可以作为数组传递,也可以传递可变数量的字符串参数。

$paramObject->setEnvAllowingPublicAccess('dev', 'test', 'another');

了解 这里 的用途。

行为注入

setYamlParserDecorator()

您可以通过实现 Miniature\Component\Reader\YamlParserDecoratorInterface 来提供基于 PHP 的 YAML 解析器。在有关 YAML 支持 的部分了解更多信息。

$paramObject->setYamlParserDecorator(new MyOwnAPP\YamlParserDecorator());

setDiSyntaxMapper()

覆盖语法

$paramObject->setDiSyntaxMapper(new Miniature\DiContainer\Syntax\MapperSymfonyStyle());

设置可用的环境

默认情况下,Component 类知道三个环境 devprodtest。您可以完全根据您的需求更改它。您可以有尽可能多的环境,并且可以按照您想要的命名它们。

$paramObject = (new Miniature\Component\InitParameters())
    ->setAvailableEnv('develop', 'testing', 'production', 'another');

读取 .env 文件

.env 已成为写入全局变量的配置值的标准。Miniature 不会写入全局变量,也不会从全局变量中读取。但它会在您想要的情况下读取 .env。目前,这的唯一目的是 APP_ENV 的值,它将被传递到您的组件类的 $env 标志,代表 当前环境

请注意,调用 setEnv() 将覆盖此值。

当前环境

当前环境是通过一个字符串命名的,该字符串命名了本地机器提供的环境。通常它类似于 developementproductiontest

此值可能来自 .env 文件,或者它可能是 直接设置 的。无论如何,它具有某些影响,尤其是在扫描 配置目录 期间的 读取行为