bbc/ipr-resolver

一个依赖解析库。

v0.2.1 2017-01-09 12:41 UTC

README

这是一个用于普通PHP对象的简单需求解析系统。

Build Status Latest Stable Version Total Downloads License

安装

这是一个标准的Composer库,因此适用常规安装规则。

$ composer require bbc/ipr-resolver

此库需要 PHP 5.5 或更高版本,因为它使用了 生成器

背景

对象通常相互依赖。这很麻烦。以一个独特的广播电台为例;我们有时在我们的页面上放置“品牌”对象(比如“Archers”)。然而,我们已经决定向观众展示该品牌的最新一集是最有用的。但这不是品牌元数据的一部分,所以我们必须单独调用它。我们最终得到这样一些东西

$brand = $this->fetchBrand('The Archers');
$brand->loadLatestEpisode();

$twig->renderBrand($brand);

如果你有一系列品牌并且需要循环遍历每个品牌,用它们的最新剧集来激活它们,这会变得更糟。现在工作很可能是在系列中进行的,并且肯定会使你的代码变得杂乱无章。

foreach ($brands as $brand) {
    // do this 50 times.
    $brand->loadLatestEpisode();
}

那么,如果最新剧集也必须进行数据调用以使其完整!这是一个噩梦。我该如何将依赖项注入这个链条呢?!

如果我们能够创建一个品牌对象,并说:“自己解决吧!”那会怎么样呢?是的,我们也这样想。

请允许我隆重推出:解析器。

使用方法

为了标记一个对象有依赖项,你需要实现 BBC\iPlayerRadio\Resolver\HasRequirements 接口,它有一个单独的方法: requires()

use BBC\iPlayerRadio\Resolver\HasRequirements;

class Brand implements HasRequirements
{
    ...
    public function requires(array $flags = [])
    {
        $episodes = (yield new LatestEpisodesForProgramme($this->getPID(), 3));
        $this->latestEpisodes = ($episodes)? $episodes : [];
    }
    ...
}

如果你以前从未遇到过 协程,这可能看起来有点奇怪!但你可以把它想象成一个懒加载的承诺,yield 会向解析器抛出一个“我需要这个来继续,处理它!”的信息。

这就是你如何获取一个完整的项目的方法

use BBC\iPlayerRadio\Resolver\Resolver;

$resolver = new Resolver();
$resolver->addBackend(new EpisodesBackend);

$brand1 = new Brand(['id' => 'p865ddf6']);
$brand2 = new Brand(['id' => 'pabcdefs']);

$resolver->resolve([$brand1, $brand2]);

// $brand1 and $brand2 are now fully hydrated with their latest episodes in the  $this->latestEpisodes variable.

等等,那个 new EpisodesBackend 是什么?这是解析器知道如何解决需求的方式。解析器后端接收需求并生成需求的结果。

因此,EpisodesBackend 可能看起来像这样

class EpisodesBackend implements BBC\iPlayerRadio\Resolver\ResolverBackend
{
    /**
     * Returns whether this backend can handle a given Requirement. Requirements
     * can be absolutely anything, so make sure to verify correctly against it.
     *
     * @param   mixed   $requirement
     * @return  bool
     */
    public function canResolve($requirement)
    {
        return $requirement instanceof LatestEpisodesForProgramme;
    }

    /**
     * Given a list of requirements, perform their resolutions. Requirements can
     * be absolutely anything from strings to full-bore objects.
     *
     * @param   array   $requirements
     * @return  array
     */
    public function doResolve(array $requirements)
    {
        $results = [];
        foreach ($requirements as $req) {
            if ($req instanceof LatestEpisodesForProgramme) {
                $results = $this->fetchEpisodesForProgramme($req->id, $req->limit);
            }
        }
        return $results;
    }
}

虽然使用解析器的语法稍微优雅一些,但这对性能有什么帮助呢?我们仍在循环和同步运行每个查询。

好吧,考虑到 fetchLatestEpisodes 正在执行 cURL 请求。我们现在可以将所有这些批量起来,作为一个单一的多 cURL 请求来执行。

public function doResolve(array $requirements)
{
    $results = [];
    $urls = [];
    foreach ($requirements as $req) {
        if ($req instanceof LatestEpisodesForProgramme) {
            $urls[] = $req->getDataURL();
        }
    }
    
    // Now kick off a multi-curl request for all those URLs:
    $ch = curl_multi_init();
    ...
    
    return $results;
}

使用解析器允许你忽略对象获取数据的具体细节,只需简单地定义它们需要什么,然后让解析器和解析器后端来完成工作!

提示提示想象一下将它与 WebserviceKit 库配合使用,使用解析器后端查找 QueryInterface 实例,然后执行 multiFetch()... ;)

你还可以向解析器传递标志来帮助 requires() 函数确定它们需要做什么

class Brand implements HasRequirements
{
    public function requires(array $flags = [])
    {
        if (in_array('WITH_ATTRIBUTION', $flags)) {
            $attribution = (yield new WithAttribution($this->parent));
        }
    }
}
$resolver->resolve($brand, ['WITH_ATTRIBUTION']);

注意:所有 requires() 函数都看到所有标志,所以请保持它们具体以避免问题!

如果一个需求没有得到任何后端的支持(没有一个 canResolve() 函数返回 true),则会抛出一个 BBC\iPlayerRadio\Resolver\UnresolvableRequirementException 异常,其中包含失败的需求,你可以使用 getFailedRequirement() 方法来访问它。

解析后端

解析后端有两个功能;首先是通过 canResolve 函数来声明它们是否理解一个需求,然后接受一个可以解析的需求列表并解析它们(通过 doResolve)!

编写自己的解析后端有一些规则

  • canResolve 中尽可能具体和安全。确保你确实可以解析它
  • 保持后端通用。有一个 "CURLResolverBackend",而不是单独的 "EpisodeResolver"、"BrandResolver" 等。解析后端应该基于 "解析策略" 而不是数据模型。越通用越好。
  • 传递给 doResolve 的需求可以是无序的,也可以来自多个对象。这样处理它们!
  • 始终 以与需求相同的顺序返回结果!如果不这样做,会发生奇怪的事情!

这是如何实际工作的

你可能想知道整个 yield 的工作方式。这是 PHP 中的一种功能,称为 生成器。通常人们想到生成器时,会想到“便宜的迭代器”这一面,但生成器还有一个叫做“协程”的方面。

这基本上涉及到利用 PHP 在达到 yield 时暂停函数执行的事实,只有在循环前进时才返回控制权。通过调用在循环外部 yield 的函数,你可以得到一个可以手动推进的对象,这样你就可以有效地将函数挂起,直到你有一个实际值为止。

感觉难以置信吗?这里有一些非常好的链接,帮助我理解它们

致谢

这个库是基于在 PHPUK 2015 大会上由 Bastian Hofmann 展示的极其相似的技术。感谢 Bastian!