improvframework / service-provisioning
一个PSR-11兼容的包,旨在简化容器服务的组织和加载。
Requires
- php: >=7.0
- psr/container: ^1.0
Requires (Dev)
- fiunchinho/phpunit-randomizer: ^2.0
- pdepend/pdepend: ^2.2
- phing/phing: ^2.14
- phpdocumentor/phpdocumentor: ^2.9
- phploc/phploc: ^3.0
- phpmd/phpmd: ^2.4
- phpmetrics/phpmetrics: ^1.10
- phpunit/phpunit: ^5.4
- satooshi/php-coveralls: ^1.0
- sebastian/phpcpd: ^2.0
- squizlabs/php_codesniffer: ^2.6
This package is not auto-updated.
Last update: 2024-09-14 18:53:11 UTC
README
Improv Framework - 服务提供
一个PSR-11兼容的包,旨在简化容器服务的组织和加载。
动机
依赖注入容器主要用于检索在应用程序中共享的完全配置的服务。为了获取这些服务,它们必须安装或注册到容器中。本包旨在简化注册过程。
问题
许多框架和容器的文档会展示一些简单的“Hello World”示例。这些示例通常包括一个container.php、一个config.php或一个bootstrap.php等文件,其中示例应用程序的所有服务都被安装到共享容器中。当这个文件必须增长以支持稍微更多的功能时,事情会迅速变得复杂。例如:
<?php // File: container.php // Implements \PSR\Container\ContainerInterface // as well as \ArrayAccess (e.g. \Pimple\Container) $container = new \Some\Container(); $container['routing.table'] = function (ContainerInterface $container) { return new RoutingTable($container->get('config')); } $container['routing.router'] = function (ContainerInterface $container) { return new Router($container->get('routing.table')); }; $container['config'] = function () { return new Configuration(new ConfigLoader('/config.yml')); }; $container['application'] = function (ContainerInterface $container) { return new Application($container->get('routing.router')); }; $container['db'] = function (ContainerInterface $container) { return new DatabaseFactory::create(Database::TYPE_PDO, $container->get('config')); }; $container['repository.blog'] = function (ContainerInterface $container) { return new BlogRepository($container->get('db')); }; $container['repository.user'] = function (ContainerInterface $container) { return new UserRepository($container->get('db')); }; $container['service.blog'] = function (ContainerInterface $container) { return new BlogService($container->get('repository.blog')) }; $container['service.user'] = function (ContainerInterface $container) { return new UserService($container->get('repository.user')) }; $container['controller.blog'] = function (ContainerInterface $container) { return new BlogController($container->get('service.blog')); }; $container['controller.user'] = function (ContainerInterface $container) { return new UserController($container->get('service.user')); }; // etc. return $container;
这有助于保持应用程序入口点简洁,如HTTP前端控制器、CLI脚本、cron等。
<?php // File public/index.php $container = require_once('../container.php'); $application = $container->get('application'); $application->run();
上面的示例非常简单且有些天真,但问题很明显。我们只配置了少量主要服务,只有几个领域实体(博客和用户)。想象一个更大的应用程序,需要管理更多的实体。此外,每个服务在被实例化时没有任何其他操作,而许多对象在返回之前通常需要进行调整或配置。此外,这个容器没有任何形式的工厂、验证器、事件调度器、记录器、分析器、格式化器、身份验证服务、读写数据库、缓存存储等。不难看出,“内联”容器管理几乎立即变得难以管理,在现实世界的所有小应用中几乎不可能实现。
上面的例子中发生了两件事,这是初始化脚本运行的容器所必需的。首先,服务在container.php中被定义和配置。这本身对正在运行的PHP进程没有任何作用,除非这个文件通过包含在index.php中来调用,从而在那时加载服务。†
† 技术上,将服务加载到内存中的操作是延迟的,但从概念上讲,它在这里发生,因为包含文件是任何加载发生所必需的。这些细节与问题域不相关。
解决方案
Improv服务提供库旨在解决上述问题。
服务加载器
可以通过引入一个只负责“加载”应用程序中所有服务的层来抽象掉尴尬的$configuration = include('../container.php');行。从概念上讲,这可以简单地将include行替换为调用加载器类的方法,如loadServices。
<?php // File public/index.php $container = new ContainerInterface(); (new Custom\App\ServiceLoader())->loadServices($container); $container->get('application')->run();
我们可以将这个类或层视为一个服务加载器,其工作是将服务“加载”到正在运行的应用程序中。
这种方法的优点是服务的加载操作变得可测试,并封装了关于如何加载的详细信息。无论是实际只是读取同一个巨大的container.php文件,还是利用底层下的其他几个文件,它都被隐藏起来,因此加载变得更加可重用。除了读取文件外,它甚至可以调用另一层的类来定义服务(见下文)。此外,可以交换或混合加载策略,而不会影响消费应用程序。
服务提供者
无论是否使用上述描述的服务加载器,使用单个文件或一组文件仍然可能导致一个复杂的大杂烩。
我们可以通过将相关的服务形成逻辑分组成为它们自己的类来避免这种场景。这样的类可能被称为 ServiceProvider,在某种程度上,这个独立的类 提供 了一系列相关的 服务 给应用程序。
这种做法的积极影响与上面类似。提供者封装了它们特定的实现逻辑,并且也成为了可测试的单元。这使得代码更加易读、紧凑,并且更容易推理。提供者甚至可以与其服务一起提取到包中,并在应用程序之间重用。
包安装
使用Composer(推荐)
composer require improvframework/service-provisioning
手动
每个版本都可以从Github上的发布页面下载。或者,您也可以分叉、克隆,并构建包。然后,安装到您选择的目录。
此包符合PSR-4自动加载标准。
用法
ServiceLoaderInterace
接口 \Improv\ServiceProvisioning\ServiceLoaderInterface 可以用来封装将服务附加到 \Psr\Container\ContainerInterface 容器所需的任何策略。这可以通过实现接口的 loadServices(ContainerInterface $container) 方法来完成。
以下是一个例子,我们可以看看这个包中包含的一个这样的策略。
ClassNameServiceLoader
\Improv\ServiceProvisioning\Loaders\ClassNameServiceLoader 类是 ServiceLoaderInterface 的具体实现。它接受一个字符串类名数组,实例化每一个,并通过传入的回调函数将它们附加到容器。
// Build a map of classes to search for and instantiate $map = [ SomeServiceProvider::class, AnotherServiceProvider::class, // etc ]; // Create the loader, providing the map and a callable which // will operate on each of the above classes in some way. $service_loader = new ClassNameServiceLoader($map, function ($subject, ContainerInterface $container) { $subject->registerServicesInto($container); } ); // Invoke the loading action. This will iterate the classes // from $map and apply the callback to each. After this call, // services are available to be drawn via $container->get(...) $service_loader->loadServices($container);
可以对 $subject 进行操作,调用其方法,或者执行任何必要的操作。
假设 $map 中的所有类都是同一类型,它们在回调签名中可能具有类型提示。同样,任何具有 __invoke 方法的类都可以作为可调用对象传入,例如。
class CustomServiceInvoker { public function __invoke(CustomServiceInterface $subject, ContainerInterface $container) { $subject->registerServicesInto($container); } }
使用回调“调用”服务附加到容器意味着此 ServiceLoaderInterface 的实现可以与任何其他容器库(例如 Pimple 或 \League\Container,自定义等)一起使用,在专有代码和 Container 之间架起桥梁。
以下提供了一个基于类式的调用者的示例,Improv框架还提供了一个专门用于与流行的 Pimple\Container 项目集成的版本。
此库还提供了自己的服务提供者,将在下面介绍。
ServiceProviderInterface
\Improv\ServiceProvisioning\ServiceProviderInterface 定义了一个 register(ContainerInterface $container) 签名。
如上所述,将服务(需要注册到容器的类)聚集成相关功能的逻辑分组通常很有用。例如,添加“用户”功能可能需要在容器上设置几个服务;一个控制器,一些服务,一个存储库等。这些项目可以组合成一个“ServiceProvider”,使得提供者可以封装与“模块”相关的所有服务的注册细节。
在ServiceProvider上调用 register 应该会将模块的所有服务安装到容器中。在这种情况下,实现为应用程序 提供 一套 服务。
上述“问题”示例的实现可能如下所示
class UserModuleServiceProvider implements ServiceProviderInterface { public function register(ContainerInterface $container) { $container['repository.user'] = function (Container $container) { return new UserRepository($container->get('db')); }; $container['service.user'] = function (Container $container) { return new UserService($container->get('repository.user')) }; $container['controller.user'] = function (Container $container) { return new UserController($container->get('service.user')); }; } } // After this call, the User-related services are now // available to be retrieved from the container. (new UserModuleServiceProvider())->register($container);
整合起来
应该注意的是,此包中包含的 ServiceProviderInterface 和 ServiceLoaderInterface(及其具体实现)之间没有任何依赖关系。使用一个不需要(也不排除)使用另一个。
然而,这些概念是相辅相成的。对于从头开始的项目或迁移到其中之一的项目,利用两者可能是有意义的。因此,由于每个项目的占地面积都很小,这两个项目都包含在这个同一软件包中。这在未来可能会改变。
ServiceProviderInvoker
如果两个接口在同一个项目中使用,并且更多,使用“可调用”方法的扩展被用作定位器,那么这个包中还提供了一个便利的类来弥合两者之间的差距。
如前所述,注入到,例如,ClassNameServiceLoader的callable $invoker可能以类的形式出现,而不仅仅是lambda。因为将Improv ServiceProviderInterface实现安装到容器中只需要在提供者上调用register,所以创建一个在作为可调用时为我们执行此操作的类是非常简单的。\Improv\ServiceProvisioning\Invokers\ServiceProviderInvoker类正是如此。
因此,一个更新的例子可能看起来像
// Build a map of service providers, each of which implement this // package's \Improv\ServiceProvisioning\ServiceProviderInterface $map = [ CoreServiceProvider::class, PersistenceServiceProvider::class, UserModuleServiceProvider::class, BlogModuleServiceProvider::class, // etc ]; // Instantiate the loader with the map and this package's invoker $service_loader = new ClassNameServiceLoader($map, new ServiceProviderInvoker() ); $service_loader->loadServices($container);
因为$map中的每个类都实现了这个包中的ServiceProviderInterface,所以包含的ServiceProviderInvoker知道如何将它们附加到容器中。
结论
到此为止,container.php文件已被完全删除,取而代之的是更小、更独立的提供者,它们可以被自动加载器逐个读取。有一个可测试的ServiceLoader,它可以配置正确的策略来加载服务和提供者,它本身可以在消费应用程序最合适的任何地方传递或实例化。
待办事项
- 提供更多“加载器”实现
- 提供贡献说明
- 考虑向加载和注册实现添加事件处理
备注和问题
请注意,这是一个新的包,目前处于beta测试阶段。欢迎提出想法、错误报告或贡献问题。
其他文档
您可以通过运行API Doc构建目标来生成并浏览此包的API文档。
运行构建/测试套件
此包广泛使用Phing构建工具。
以下是一些值得注意的构建目标列表,但请随时查看build.xml文件以获取更多信息。
默认目标
./vendor/bin/phing将执行build目标(与执行./vendor/bin/phing build相同)。这包括代码审查、语法检查、运行所有静态分析工具、测试套件和生成API文档。
“完整”打包目标
执行./vendor/bin/phing package将运行所有上述检查,如果通过,则将源打包到只包含相关源代码的可分发文件中。
选定的单个目标
- 运行测试
./vendor/bin/phing test./vendor/bin/phpunit
- 执行静态分析
./vendor/bin/phing static-analysis- 生成的报告在
./build/output/reports
- 生成API文档
./vendor/bin/phing documentapi- 生成的文档在
./build/docs/api
- 从源构建包
./vendor/bin/phing package- 工件在
./build/output/artifacts