dhii/module-interface

v0.3.0-alpha2 2021-08-23 08:23 UTC

README

Continuous Integration Latest Stable Version Latest Unstable Version

详细信息

此软件包包含描述模块及其属性和行为的接口。

需求

  • PHP: 7.1 及以上,至 8.0

接口

  • ModuleInterface - 模块的接口。模块是一个表示应用片段的对象。模块使用 setup() 进行准备,该方法返回一个 ServiceProviderInterface 实例,应用程序可以消费它,并使用 run() 调用它,消耗应用程序的 DI 容器。
  • ModuleAwareInterface - 可以检索模块的对象。
  • ModuleExceptionInterface - 模块抛出的异常。

使用方法

模块包

在你的模块包中,创建一个返回模块工厂的文件。此工厂必须从这个包返回一个 ModuleInterface 实例。按照惯例,此文件名为 module.php,位于根目录。以下是一个非常基础的示例。在实际生活中,服务提供者和模块通常会各自有自己的命名类,而工厂和扩展将分别位于 services.phpextensions.php,按照惯例。

// module.php
use Dhii\Modular\Module\ModuleInterface;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;

return function () {
    return new class () implements ModuleInterface {
    
        /**
         * Declares services of this module.
         * 
         * @return ServiceProviderInterface The service provider with the factories and extensions of this module.
         */
        public function setup() : ServiceProviderInterface
        {
            return new class () implements ServiceProviderInterface
            {
                /**
                 * Only the factory of the last module in load order is applied. 
                 * 
                 * @return array|callable[] A map of service names to service definitions.
                 */
                public function getFactories()
                {
                    return [
                        // A factory always gets one parameter: the container.
                        'my_module/my_service' => function (ContainerInterface $c) {
                            // Create and return your service instance
                            return new MyService(); 
                        },
                    ];
                }
                
                /**
                 * All extensions are always applied, in load order. 
                 * 
                 * @return array|callable[] A map of service names to extensions.
                 */            
                public function getExtensions()
                {
                    return [
                        // An extension gets an additional parameter:
                        // the value returned by the factory or the previously applied extensions.
                        'other_module/other_service' => function (
                            ContainerInterface $c,
                            OtherServiceInterface $previous
                        ): OtherServiceInterface {
                            // Perhaps decorate $previous and return the decorator
                            return new MyDecorator($previous);
                        },
                    ];
                }
            };    
        }

        /**
         * Consumes services of this and other modules.
         * 
         * @param ContainerInterface $c A container with the services of all modules.
         */
        public function run(ContainerInterface $c): void
        {
            $myService = $c->get('my_module/my_service');
            $myService->doSomething();
        }       
    };
};

在上面的示例中,模块声明了一个服务 my_module/my_service,以及一个针对 other_module/other_service 的扩展,这可能位于另一个模块中。请注意,按照惯例,服务名称包含模块名称前缀,由正斜杠 / 分隔。可以通过添加斜杠分隔的“级别”进一步“嵌套”服务。在未来,一些容器实现将为使用此约定的模块提供一些好处。

应用程序通常需要能够对它们所需的任意模块集执行某些操作。为了让一个应用程序能够将所有模块分组在一起,按照惯例,在您的 composer.json 中将包类型声明为 dhii-mod。遵循此约定将允许所有作者编写的所有模块都被统一处理。

{
    "name": "me/my_module",
    "type": "dhii-mod"
}

这里重要的是什么

  1. 模块的 setup() 方法不应产生副作用。

    setup 方法旨在让模块为行动做准备。模块不应在该方法中实际执行操作。在这个方法中,容器不可用,因此模块不能使用任何服务,无论是自己的还是其他模块的服务。不要尝试在这里让模块使用自己的服务。

  2. 实现正确的接口。

    模块必须实现 ModuleInterface。模块的 setup() 方法必须返回 ServiceProviderInterface。虽然 服务提供者 标准是实验性的,并且已经实验性很长时间了,但模块标准严重依赖于它。如果模块标准变得普遍,这可能会推动 FIG 推进服务提供者标准,希望将其纳入 PSR。

  3. 遵守约定。

    这里概述的约定非常重要。其中一些对于模块和/或消费应用的正常运行是必要的。其他一些现在可能没有影响,但将来可能带来好处。请遵守这些约定,以确保您和其他标准用户都能获得最佳体验。

消费者包

模块安装

消耗模块的包,通常是应用程序,需要要求模块。下面的示例使用oomphinc/composer-installers-extender库配置Composer,以便将所有dhii-mod包安装到应用程序根目录中的modules目录。因此,包me/my_moduleme/my_other_module将分别放入modules/me/my_modulemodules/me/my_other_module

{
  "name": "me/my_app",
  "require": {
    "me/my_module": "^0.1",
    "me/my_other_module": "^0.1",
    "oomphinc/composer-installers-extender": "^1.1"
  },
  
  "extra": {
    "installer-types": ["dhii-mod"],
    "installer-paths": {
      "modules/{$vendor}/{$name}": ["type:dhii-mod"]
    }
  }
}
模块加载

一旦需要了一个模块,它就必须被加载。模块文件必须由应用程序显式加载,因为应用程序决定了模块的加载顺序。加载顺序是允许模块以简单直观的方式扩展和覆盖彼此服务的根本原则。

  1. 后来加载的模块中的工厂将完全覆盖早期加载的模块的工厂。

    最终,对于每个服务,只使用一个工厂:最后一个声明的工厂。所以如果my_other_modulemy_module之后加载,并且它声明了服务my_module/my_service,那么它将覆盖my_module声明的my_module/my_service服务。简而言之:最后一个工厂胜出

  2. 后来加载的模块中的扩展将在早期加载的模块的扩展之后应用。

    最终,来自所有模块的扩展将应用于工厂返回的内容之上。所以如果my_other_module声明了一个扩展other_module/other_service,它将在my_module声明的扩展other_module/other_service之后应用。简而言之:后来的扩展扩展先前的扩展

从上面的例子继续,如果应用程序中某个请求my_other_module声明的服务other_module/other_service,这将发生以下情况

  1. my_other_module中的工厂被调用。
  2. my_module中的扩展被调用,并接收上述工厂的结果作为$previous
  3. my_other_module中的扩展被调用,并接收上述扩展的结果作为$previous
  4. get('other_module/other_service')的调用者接收上述扩展的结果。

因此,任何模块都可以覆盖和/或扩展来自任何其他模块的服务。以下是一个应用程序引导代码的示例。此示例使用来自dhii/containers的类。

// bootstrap.php

use Dhii\Modular\Module\ModuleInterface;
use Interop\Container\ServiceProviderInterface;
use Dhii\Container\CompositeCachingServiceProvider;
use Dhii\Container\DelegatingContainer;
use Dhii\Container\CachingContainer;

(function ($file) {
    $baseDir = dirname($file);
    $modulesDir = "$baseDir/modules";
    
    // Order is important!
    $moduleNames = [
        'me/my_module',
        'me/my_other_module',
    ];
    
    // Create and load all modules
    /* @var $modules ModuleInterface[] */
    $modules = [];
    foreach ($moduleNames as $moduleName) {
        $moduleFactory = require_once("$modulesDir/$moduleName/module.php");
        $module = $moduleFactory();
        $modules[$moduleName] = $module;
    }

    // Retrieve all modules' service providers
    /* @var $providers ServiceProviderInterface[] */
    $providers = [];
    foreach ($modules as $module) {
        $providers[] = $module->setup();
    }
    
    // Group all service providers into one
    $provider = new CompositeCachingServiceProvider();
    $container = new CachingContainer(new DelegatingContainer($provider, $parentContainer = null));

    // Run all modules
    foreach ($modules as $module) {
        $module->run($container);
    }
})(__FILE__);

上述代码将按顺序从modules目录加载、设置和运行模块me/my_moduleme/my_other_module,前提是这些模块遵循了约定。这里需要注意的重要事项

  1. 首先设置所有模块,然后运行所有模块。

    如果您在相同步骤中设置和运行模块,则不会起作用,因为引导过程没有机会使用所有模块的服务来配置应用程序的DI容器。

  2. CompositeCachingServiceProvider负责正确解析服务。

    这减轻了应用程序的负担,因为整个过程可能看起来很复杂,而且相当可重用。建议使用此类。

  3. DelegatingContainer可以可选地接受一个父容器。

    如果您的应用程序本身就是一个模块,并且需要成为包含自己DI容器的更大应用程序的一部分,请将其作为第二个参数提供。这将确保无论定义在哪里声明,服务始终从最顶层的容器中检索。

  4. CachingContainer 确保服务被缓存。

    实际上,这意味着所有服务都是单例的,即应用程序中每个服务只有一个实例。这通常是期望的行为。没有 CachingContainer,即只使用 DelegatingContainer,每次调用 get() 时,服务定义都会被调用,这通常是不希望的。

  5. 约定很重要。

    如果模块没有将 module.php 文件放置在其根目录中,引导程序将无法仅通过包名来加载每个模块。不遵循该约定的模块必须单独加载其 module.php 文件,这将使引导代码更复杂。