clancats / container
ClanCats IoC 容器。
Requires
- php: >=7.4
Requires (Dev)
- composer/composer: ^2.3
- phpstan/phpstan: ^1.5
- phpunit/phpunit: ^9.0
- vimeo/psalm: 5.x-dev
This package is auto-updated.
Last update: 2024-09-07 15:02:37 UTC
README
ClanCats Container
一个具有简单元语言、快速可编译的依赖注入的 PHP 服务容器。
需要 PHP >= 7.4
优点
- 开销最小化,因此非常快。
- 没有额外的依赖。
- 在生产环境中每天处理数百万个请求。
- 单例和工厂服务解析器。
- 元数据系统允许非常直观的服务查找。
- 容器构建器允许编译/序列化您的服务定义。
- 容器文件,一种简单的语言来定义您的服务和管理您的应用程序配置。
- Composer 集成,允许您从不同的包导入服务定义。
- 大型和动态类图的懒服务提供者。
可能不喜欢的功能
- 容器仅允许 命名服务。
- 目前 不支持 自动连接。
- 目前 不支持 对 容器文件 的 IDE 支持。
- 元语言可能不符合每个人的口味。
目录
性能
这个包对于服务容器来说可能看起来非常庞大,但在短暂的预热后,编译后的容器运行得飞快,几乎没有开销(3 个类/文件)。动态绑定和解析服务可能较慢,但仍然不会影响真实应用中的性能。
安装
容器遵循 PSR-4
自动加载,并可以使用 composer 安装
$ composer require clancats/container
语法高亮
我在这里创建了一个基本的 tmLanguage 定义: https://github.com/ClanCats/container-tmLanguage
文档 💡
完整文档可以在 clancats.io 上找到
快速开始 ⚡️
以下只是一个粗略示例,更详细和解释的指南可以在这里找到: 入门
设置
我们的目标目录结构将如下所示
app.php
app.ctn
composer.json
cache/ # make sure this is writable
src/
Human.php
SpaceShip.php
服务
为了演示如何使用此服务容器,我们需要创建两个类,一个 SpaceShip
和一个 Human
。
创建一个新的 php 文件 src/Human.php
class Human { public $name; public function setName(string $name) { $this->name = $name; } }
创建另一个 php 文件 src/SpaceShip.php
class SpaceShip { protected $captain; // every ship needs a captain! public function __construct(Human $captain) { $this->captain = $captain; } public function ayeAye() { return 'aye aye captain ' . $this->captain->name; } }
容器文件
容器文件允许您使用简单的元语言绑定您的服务和参数。
注意:如果您更喜欢在 PHP 中本身绑定服务,则此功能完全是可选的:请参阅 服务绑定
在您的应用程序根目录中创建一个名为 app.ctn
的新文件。
@malcolm: Human - setName('Reynolds') @firefly: SpaceShip(@malcolm)
容器工厂
现在我们需要解析容器文件并将其编译成一个新的类。为此,我们创建了一个app.php
文件。在那里,你需要引入composer自动加载器并引入你的源文件,或者配置composer来自动加载src/
目录中的类。
require "vendor/autoload.php"; // for the consistency of the example I leave this here // but I strongly recommend to autolaod your classes with composer. require "src/SpaceShip.php"; require "src/Human.php"; $factory = new \ClanCats\Container\ContainerFactory(__DIR__ . '/cache'); $container = $factory->create('AppContainer', function($builder) { // create a new container file namespace and parse our `app.ctn` file. $namespace = new \ClanCats\Container\ContainerNamespace(); $namespace->parse(__DIR__ . '/app.ctn'); // import the namespace data into the builder $builder->importNamespace($namespace); });
注意:确保
../cache
目录可写。
变量$container
现在包含一个名为AppContainer
的类实例。
echo $container->get('firefly')->ayeAye(); // "aye aye captain Reynolds"
使用示例
带有环境的 App 配置
容器参数无非是全局可用的值。我们用它们来存储大多数静态配置值,并处理不同的环境。
为此,我们通常创建两个文件。在这个例子中
config.ctn
主配置文件。config.ctn.env
环境特定的覆盖。
config.ctn
:
// default environment :env: 'stage' // debug mode :debug: false // Firewall whitelist :firewall.whitelisted_ips: { '127.0.0.1': 'Local', '1.2.3.4': 'Some Office', '4.3.2.1': 'Another Office', } // application name :app.name: 'My Awesome application' import config.env
config.ctn.env
:
override :env: 'dev' override :debug: true // Firewall whitelist override :firewall.whitelisted_ips: { '127.0.0.1': 'Local', '192.168.33.1': 'MyComputer', }
在PHP中,这些值作为参数可以访问。为了使这工作,你需要配置正确的导入路径在你的容器命名空间中。你可以在示例应用程序中找到一个例子。
echo $container->getParameter('app.name'); // 'My Awesome application' echo $container->getParameter('env'); // 'dev' echo $container->getParameter('debug'); // true
别名/服务定义/参数示例
# Parameters can be defined erverywhere :pipeline.prefix: 'myapp.' // you can define aliases to services @pipeline.queue: @queue.redis @pipeline.storage: @db.repo.pipeline.mysql // add function calls that will be run directly after construction of the service @pipeline: Pipeline\PipelineManager(@pipeline.queue, @pipeline.storage, @pipeline.executor) - setPrefix(:pipeline.prefix) - bind(@pipeline_handler.image.downloader) - bind(@pipeline_handler.image.process) @pipeline_handler.image.downloader: PipelineHandler\Images\DownloadHandler(@client.curl) @pipeline_handler.image.process: PipelineHandler\Images\ProcessHandler(@image.processor, { 'temp_dir': '/tmp/', 'backend': 'imagick' })
使用元数据进行 HTTP 路由
你可以使用容器元数据来直接通过服务定义定义路由
@controller.dashboard.home: App\Controller\Dashboard\HomepageAction = route: {'GET'}, '/dashboard/home' @controller.dashboard.sign_in: App\Controller\Dashboard\SignInAction = route: {'GET', 'POST'}, '/dashboard/signin' @controller.dashboard.sign_out: App\Controller\Dashboard\SignOutAction = route: {'GET'}, '/logout' @controller.dashboard.client: App\Controller\Dashboard\ClientDetailAction = route: {'GET'}, '/dashboard/clients/me' = route: {'GET'}, '/dashboard/clients/{clientId}'
显然,这取决于你的路由实现。你可以使用如下路由定义获取所有服务
使用FastRoute的示例
$dispatcher = \FastRoute\cachedDispatcher(function(RouteCollector $r) use($container) { foreach($container->serviceNamesWithMetaData('route') as $serviceName => $routeMetaData) { // an action can have multiple routes handle all of them foreach($routeMetaData as $routeData) { $r->addRoute($routeData[0], $routeData[1], $serviceName); } } }, [ 'cacheFile' => PATH_CACHE . '/RouterCache.php', 'cacheDisabled' => $container->getParameter('env') === 'dev', ]);
使用元数据进行事件监听
就像路由一样,你可以使用元数据系统来定义事件监听器
@signal.exception.http404: App\ExceptionHandler\NotFoundExceptionHandler = on: 'http.exception', call: 'onHTTPException' @signal.exception.http400: App\ExceptionHandler\BadRequestExceptionHandler = on: 'http.exception', call: 'onHTTPException' @signal.exception.http401: App\ExceptionHandler\UnauthorizedAccessExceptionHandler = on: 'http.exception', call: 'onHTTPException' @signal.bootstrap_handler: App\Bootstrap = on: 'bootstrap.pre', call: 'onBootstrapPre' = on: 'bootstrap.post', call: 'onBootstrapPost'
然后在你的事件调度器中注册所有具有匹配元数据的服务的服务。
以下示例展示了可能的实现方式。粘贴这段代码不会直接工作。
foreach($container->serviceNamesWithMetaData('on') as $serviceName => $signalHandlerMetaData) { // a action can have multiple routes handle all of them foreach($signalHandlerMetaData as $singalHandler) { if (!is_string($singalHandler[0] ?? false)) { throw new RegisterHandlerException('The signal handler event key must be a string.'); } if (!isset($singalHandler['call']) || !is_string($singalHandler['call'])) { throw new RegisterHandlerException('You must define the name of the function you would like to call.'); } $priority = $singalHandler['priority'] ?? 0; // register the signal handler $eventdispatcher->register($singalHandler[0], function(Signal $signal) use($container, $singalHandler, $serviceName) { $container->get($serviceName)->{$singalHandler['call']}($signal); }, $priority); } }
日志处理程序发现
或者你可能有一个带有monolog日志记录器的自定义框架,你想要使每个集成添加自定义日志处理器变得容易
/** * Log to Graylog */ :gelf.host: 'monitoring.example.com' :gelf.port: 12201 @gelf.transport: Gelf\Transport\UdpTransport(:gelf.host, :gelf.port) @gelf.publisher: Gelf\Publisher(@gelf.transport) @logger.error.gelf_handler: Monolog\Handler\GelfHandler(@gelf.publisher) = log_handler /** * Also send a slack notification */ @logger.,error.slack_handler: Example\MyCustom\SlackWebhookHandler('https://hooks.slack.com/services/...', '#logs') = log_handler
并且你的框架可以简单地查找暴露log_handler
元键的服务
// gather the log handlers $logHandlerServices = array_keys($container->serviceNamesWithMetaData('log_handler')); // bind the log hanlers foreach($logHandlerServices as $serviceName) { $logger->pushHandler($container->get($serviceName)); }
容器文件语法
容器文件是用一个非常简单的元语言编写的。
类型
该语言支持以下标量类型
- 字符串 单引号和双引号。
'hello'
和"world"
- 数字 浮点数/双精度数,整数。
3.14
,42
- 布尔值
true
和false
。 - 空值
null
- 数组 列表和关联。
{'A', 'B', 'C'}
,{'A': 10, 'B': 20}
数字
容器文件不区分不同的数字类型,因为这会是一个不必要的开销,我们直接将这项工作转发给PHP。
42 # Int 42.01 # Float -42.12345678912345 # Double
这意味着浮点数的精度也由PHP处理。所有值都是解释的,这意味着大双精度数可能会存储成舍入的形式。
字符串
字符串必须始终用单个'
或双引号"
括起来。这主要是为了在字符串中有许多引号时方便,不必全部转义。
特殊字符的转义方式与常规方式相同。
:say: 'Hello it\'s me!'`
喜爱的或讨厌的表情符号也能正常工作。
:snails: '🐌🐌🐌'
布尔值和 Null
关于它们就没有太多可说的了
:nothing: null
:positive: true :negative: false
数组
重要的是要注意,所有数组在内部都是关联的。当定义一个简单的列表时,关联键会自动生成,代表项目的索引。
这意味着数组{'A', 'B'}
等于{0: 'A', 1: 'B'}
。
数组可以定义多维。
{ 'title': 'Some catchy title with Star Wars', 'tags': {'top10', 'movies', 'space'}, 'body': 'Lorem ipsum ...', 'comments': { { 'text': 'Awesome!', 'by': 'Some Dude', } } }
参数
参数或配置值也可以在容器文件中定义。
参数总是以:
字符为前缀。
:database.hostname: "production.db.example.com" :database.port: 7878 :database.cache: true
服务定义
服务定义总是有名称,并且必须以@
字符为前缀。
## <service name>: <class name> @log.adapter: FileAdapter
类名可以包含完整的命名空间。
@log.adapter: Acme\Log\FileAdapter
构造函数
构造函数参数可以放在类名之后。
@dude: Person("Jeffery Lebowski")
引用参数
参数可以引用一个参数或服务。
:name: 'Jeffery Lebowski' @dude: Person(:name)
@mysql: MySQLAdapter('localhost', 'root', '') @repository.posts: Repositories/Post(@mysql)
方法调用
可以将方法调用分配给服务定义。
@jones: Person('Duncan Jones') @sam: Person('Sam Rockwell') @movie.moon: Movie('Moon') - setDirector(@jones) - addCast(@sam) - setTags({'Sci-fi', 'Space'})
服务元数据
可以将元数据分配给每个服务定义。
然后可以获取匹配元数据键的服务。
@controller.auth.sign_in: Controller\Auth\SignInController(@auth) = route: {'GET', 'POST'}, '/signin'
元数据键始终是一个向量/数组,因此您可以添加多个相同类型的元数据。
@controller.auth.sign_in: Controller\Auth\SignInController(@auth) = route: {'GET', 'POST'}, '/signin' = tag: 'users' = tag: 'auth'
元数据定义内部元素可以有命名键。
@app.bootstrap: Bootstrap() = on: 'app.start' call: 'onAppStart'
服务更新
可以更新已定义的服务,添加更多的构造调用和元数据。这对于组织大量具有动态查找的依赖项非常有用。
例如,您可以在一个文件中定义您的日志记录器。
@logger.main: Acme\Logger
然后在需要的地方使用构造调用添加观察者。
@logger.observers.email_devs: Acme\EmailLogObserver('dev@example.com') @logger.observers.email_support: Acme\EmailLogObserver('support@example.com') @logger.main - addObserver(@logger.observers.email_devs) - addObserver(@logger.observers.email_support)
对于元数据也是如此。
@controller.homepage: Controller\Homepage = on: '/homepage' // also show homepage on root @controller.homepage = on: '/'
导入
其他容器文件可以从中导入容器命名空间。
import config import app/dashboard import app/user import app/shop
覆盖
如果已定义,服务参数将被明确覆盖。
:ship: 'Star Destroyer' override :ship: 'X-Wing'
示例应用
这应该展示了使用CCContiner构建的应用程序的可能结构。这是我们私有服务框架使用的简化版本。
文件夹结构
# The main entry point for our container application
app.ctn
# A per environment defined config. This file
# is being generated by our deployment process
# individually for each node.
app.ctn.env
# We like to but all other container files in one directory
app/
# Most configuration parameters go here
config.ctn
# Command line commands are defined here
commands.ctn
# Application routes (HTTP), actions and controllers
routes.ctn
# General application services. Depending on the size of
# the project we split the services into more files to keep
# things organized.
services.ctn
# PHP Bootstrap
bootstrap.php
# Composer file
composer.json
# A writable directory for storing deployment
# depndent files.
var/
cache/
# PHP Source
src/
Controller/
ListBlogPostController.php
GetBlogPostController.php
Commands/
CreateUserCommand.php
Servies/
UserService.php
BlogService.php
引导(容器构建器)
此容器构建器执行以下操作
- 从使用composer安装的包中导入容器命名空间。
- 扫描
./app
目录中的ctn文件。 - 将环境容器文件添加到命名空间中。
<?php if (!defined('DS')) { define('DS', DIRECTORY_SEPARATOR); } define('PATH_ROOT', __DIR__); define('PATH_CACHE', PATH_ROOT . DS . 'var' . DS . 'cache'); define('PATH_APPCONFIG', PATH_ROOT . DS . 'app'); $factory = new \ClanCats\Container\ContainerFactory(PATH_CACHE); $container = $factory->create('AppContainer', function($builder) { $importPaths = [ 'app.env' => PATH_ROOT . '/app.ctn.env', ]; // find available container files $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(PATH_APPCONFIG)); foreach ($rii as $file) { // skip directories if ($file->isDir()) continue; // skip non ctn files if (substr($file->getPathname(), -4) !== '.ctn') continue; // get the import name $importName = 'app' . substr($file->getPathname(), strlen(PATH_APPCONFIG), -4); // add the file $importPaths[$importName] = $file->getPathname(); } // create a new container file namespace and parse our `app.ctn` file. $namespace = new \ClanCats\Container\ContainerNamespace($importPaths); $namespace->importFromVendor(PATH_ROOT . '/vendor'); // start with the app file $namespace->parse(__DIR__ . '/app.ctn'); // import the namespace data into the builder $builder->importNamespace($namespace); });
App 容器文件
第一个文件app.ctn
的主要任务很简单,就是包含其他文件,从而定义它们被读取的顺序。
app.ctn
:
/**
* Import the configuration
*/
import app/config
/**
* Import the services
*/
import app/services
/**
* Import the actions & routes
*/
import app/routes
/**
* Import the commands
*/
import app/commands
/**
* Load the environment config last so it is
* able to override most configs.
*/
import app.env
待办事项/功能愿望清单
- 容器文件
- 元数据支持
- 数组支持
- 别名支持
- 容器文件命名空间支持
- 通过“使用特质”进行自动装配
- 通过“实例为”进行自动装配
- 通过“有方法”进行自动装配
- 属性注入
- 参数连接
- 输入参数(用于环境检测)
- 后期服务覆盖(允许添加元数据或调用)
- 宏
- 容器
- 元数据支持
- 属性注入
鸣谢
许可
MIT许可(MIT)。有关更多信息,请参阅许可文件。