remotelyliving/php-query-bus

用于抽象查询、数据加载和图构建的 PHP 查询总线

1.0.0 2020-05-26 22:45 UTC

This package is auto-updated.

Last update: 2024-09-19 22:25:14 UTC


README

Build Status Total Downloads Coverage Status License Scrutinizer Code Quality

php-query-bus: 🚍 PHP 的查询总线实现 🚍

用例

如果您需要一个轻量级的 CQRS 命令总线的补充,这个库可能对您有所帮助。它与命令总线非常相似,但返回一个结果。

我以前使用过神奇的数据加载解决方案,但为特定用例创建一组特定的查询、结果和处理对象通常比基于魔法的实现更高效、更可预测、更明确。

安装

composer require remotelyliving/php-query-bus

使用方法

创建查询解析器

解析器可以手动添加处理程序或通过 PSR-11 服务容器定位它们。查询与处理程序一一对应,并按查询类名称作为查找键映射。

$resolver = Resolver::create($serviceContainer) // can locate in service container
    ->pushHandler(GetUserProfileQuery::class, new GetUserProfileHandler()) // can locate in a local map {query => handler}
    ->pushHandlerDeferred(GetUserQuery::class, $lazyCreateMethod); // can locate deferred to save un unnecessary object instantiation

创建查询总线

查询总线接收一个查询解析器,并在堆栈上推送您想要的任何中间件。

$queryBus = QueryBus::create($resolver)
    ->pushMiddleware($myMiddleware1);

$query = new GetUserProfile('id');
$result = $queryBus->handle($query);

中间件是任何返回结果的可调用函数。它包含一些基础中间件:src/Middleware

这就全部了!

查询

本库的查询有意留空未实现。它只是一个对象。我对查询对象的建议是,将它们保留为您需要通过数据源进行查询的数据传输对象(DTO)。

一个查询示例可能看起来像这样

class GetUserQuery
{
    private bool $shouldIncludeProfile = false;

    private string $userId;

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

    public function getUserId(): string
    {
        return $this->userId;
    }

    public function includeProfile(): self
    {
        $this->shouldIncludeProfile = true;
        return $this;
    }
}

如你所见,它只是几个获取器和选项构建器。

结果

结果同样留空未实现,除了提供的 AbstractResult。结果可以根据您的用例有自己的自定义获取器。上面的 GetUserQuery 的一个示例结果可能如下

class GetUserResult extends AbstractResult implements \JsonSerializable
{
    private User $user;

    private ?UserProfile $userProfile;

    public function __construct(User $user, ?UserProfile $userProfile)
    {
        $this->user = $user;
        $this->userProfileResult = $userProfile;
    }

    public function getUser(): User
    {
        return $this->user;
    }

    public function getUserProfile(): ?UserProfile
    {
        return $this->userProfile;
    }

    public function jsonSerialize(): array
    {
        return [
            'user' => $this->getUser(),
            'profile' => $this->getUserProfile(),
        ];
    }
}

如你所见,开始构建用于输出响应或馈送到应用程序其他部分的输出结果图并不难。

处理程序

处理程序是魔法发生的地方。注入您需要的任何存储库、API 客户端或 ORM 以加载数据。它会向查询请求查询参数,并返回一个结果。您还可以在处理程序内部从总线请求其他查询结果。以我们的 GetUserQuery 示例为例,一个处理程序可能如下所示

class GetUserHandler implements Interfaces\Handler
{
    public function handle(object $query, Interfaces\QueryBus $bus): Interfaces\Result
    {
        try {
            $user = $this->userRepository->getUserById($query->getUserId());
        } catch (ConnectectionError $e) {
            // can handle exceptions without blowing up and instead use messaging via
            // AbstractResult::getErrors(): \Throwable[] and AbstractResult::hasErrors(): bool
            return AbstractResult::withErrors($e);
        }

        
        if (!$user) {
            // can handle nullish cases by returning not found
            return AbstractResult::notFound();
        }
       
        if (!$query->shouldIncludeProfile()) {
            return new GetUserResult($user, null);
        }

        $profileResult = $bus->handle(new GetUserProfileQuery($query->getUserId()));

        return ($profileResult->isNotFound())
            ? new GetUserResult($user, null)
            : new GetUserResult($user, $profileResult->getUserProfile());
    }
}

中间件

本库附带了一些 中间件。默认执行顺序是后进先出(LIFO),签名非常简单。

中间件必须返回一个结果实例,并且是可调用的。就这么简单!

一个示例中间件可能像这样简单

$cachingMiddleware = function (object $query, callable $next) use ($queryCacher) : Interfaces\Result {
    if ($query instanceof Interfaces\CacheableQuery) {
        return $queryCacher->get($query, function () use ($next, $query) { return $next($query); });
    }
   
    return $next($query);
};

QueryCacher

此中间件通过利用 概率早期缓存过期 防止缓存踩踏,提供一些有趣的查询缓存。为了缓存,查询必须实现 CacheableQuery 接口。要重新计算缓存,只需发出一个返回值为 CacheableQuery::shouldRecomputeResult() 为 true 的查询即可。

QueryLogger

有助于调试,但最好留给开发环境和测试环境。在查询上查找 LoggableQuery 标记接口。

ResultErrorLogger

有助于调试和基于您的日志设置进行警报。

PerfBudgetLogger

允许您设置某些粗略的性能阈值,并记录已超过该阈值的任何内容。

未来开发

  • 结果过滤(应该在查询级别进行,但能够指定稀疏字段集将更好)