cfxmarkets/php-persistence

一个库,提供了基于JSON API关系型资源概念的多种持久层抽象实现。

v0.6.4 2020-04-08 20:36 UTC

README

此包为CFX模型框架提供基础持久层。

这是组成CFX数据系统的两个必要包中的第二个。第一个是 cfxmarkets/php-jsonapi-objects,如果您还没有阅读过这个包的内容,您现在应该去看看。此包基于那个包中引入的概念。

CFX数据系统的总体理念是,在计算机系统中模拟现实世界时,我们主要处理存储在数据源中的资源对象,并且所有资源都存在于一个数据上下文中。

虽然 php-jsonapi-objects 包提供了资源对象的实现逻辑和数据源对象的定义,但此包实际上实现了SQL和REST数据源对象的基础,并引入了 DataContext 的概念。

安装

此库可以通过标准composer过程安装

composer require cfxmarkets/php-persistence

用法

因为这是一个基础库,所以它只提供(除少数例外)抽象类。此外,因为它为多种类型的持久性(REST和SQL,在撰写本文时)提供基础,所以解释起来可能有点复杂。让我们从宏观角度开始。

30,000英尺

在这个包中,您将处理四个主要对象类别

  • DataContext
  • Datasource
  • DSLQuery
  • SQLQuery

关于查询的说明

CFX决定使用DSL来简化查询。这允许我们根据需要尽可能模糊或具体,而不会引入完整查询语言带来的安全漏洞。

这就是为什么在 \CFX\JsonApi\DatasourceInterface 中定义的 get 函数只需要一个查询字符串。根据实现方式,该查询字符串可以解释为一般或具体。在我们的情况下,我们选择将其解释为DSL查询字符串,并将其(默认情况下)解析为该包中的抽象类中的DSLQuery对象。

GenericDSLQuery 只能处理标准的id查询: id=abcde12345。如果格式有误,它将抛出 BadQueryException。然而,每个数据源 parseDSL 方法的返回值可以是一个更具体的实现,该实现返回一个 DSLQueryInterface。因此,对于需要额外查询功能的数据源,可以通过扩展 GenericDSLQuery 类和重写 AbstractDatasource::parseDSL 方法来实现。

关于SQL查询,我们在这里犯了点错误。实际上,SQL查询只是一个增加了使用PDO查询过程功能的常规DSL查询。然而,我们的实现并没有反映这一点,而是将DSL查询和SQL查询定义为完全不同的两件事。这是即将开发的项目之一,因为它肯定可以帮助简化包中查询元素的整体结构...

不过,目前,请知道这是一个待解决的问题,SQL查询和DSL查询在目前以尴尬的方式交叉。

10,000英尺

更接近一点,让我们谈谈您通常如何使用这些类。

首先,查询类是内部的。这些类仅在数据源内部使用,因为这是它们真正有意义的唯一上下文。

现在,在这个包对 DataContexts 的实现中,您可以通过调用 DataContext 的属性来获取数据源,而不是调用获取器方法。(这是一个关于可用性的决定,与我们通常不使用“魔法”获取器的惯例相矛盾)。在示例中,您将看到 DataContexts 的主要价值在于简单地协调各种资源对象及其数据源之间的通信。以下是一个非常简单的示例,看看它可能是什么样子

$brokerage = new \CFX\Brokerage\DataContext($pdos);

$user = $brokerage->users->get("id=$_SESSION[userId]");
$user
    ->updateFromData($_POST['userData'])
    ->save();

$orders = $user->getOrders();

$outstandingOrders = $cfx->orders->newCollection();
foreach($orders as $order) {
    if (!$order->isComplete()) {
        $outstandingOrders[] = $order;
    }
}

echo json_encode(['data' => $outstandingOrders ]);

请注意,这个示例可以是 REST 上下文或 SQL 上下文——它们的使用方式完全相同。不过,在这种情况下,让我们假设我们正在使用 SQL 上下文,因为这种情况更常见,将提供更好的功能概述。

2,000 英尺

在上面的示例中,我们启动了 Brokerage 数据上下文,获取了当前会话中登录的用户,从他们提交的表单中更新了该用户的信息,然后返回了该用户的未完成订单集合。(显然,这是一个完全虚构的示例,根本没有任何这样做的原因。)

您可以看到,我们能够通过一个简单的字符串查询来获取用户,该查询仅请求用户的 ID,然后我们将提交的 userData 数组直接放入 updateFromData 方法,并尝试保存。之后,我们获取了所有用户的订单,然后遍历它们并聚合了当前不完整的订单。

这里有很多细节。以下是相同的示例,但添加了一些注释来澄清正在发生的事情

$brokerage = new \CFX\Brokerage\DataContext($pdos);

// Remember, query strings are parsed by default by the `GenericDSLQuery` class, so you can make sure to properly sanitize values
// in that class and derivatives
$user = $brokerage->users->get("id=$_SESSION[userId]");

// AbstractDatasource first checks for input errors, then checks for uniqueness before proceeding to save, so if there are
// problems, this will throw exceptions, which we can catch at an application level
$user
    ->updateFromData($_POST['userData'])
    ->save();

// Now we're getting related data. The `getOrders` method will call up to the Datasource to get all related orders, and the
// Datasource will delegate this to its DataContext. This call is equivalent to saying, `$orders = $brokerage->orders->get("userId={$user->getId()}")`
$orders = $user->getOrders();

// We use the orders datasource to instantiate a new orders collection (usually just a generic \CFX\JsonApi\ResourceCollection, but overridable per datasource)
$outstandingOrders = $cfx->orders->newCollection();
foreach($orders as $order) {
    if (!$order->isComplete()) {
        $outstandingOrders[] = $order;
    }
}

// Finally, we output using json_encode, which automatically serializes each order object to JSON API format
echo json_encode(['data' => $outstandingOrders ]);

500 英尺

更详细地查看,让我们简要地看看这些对象实际上在做什么。实际上,大部分工作是由资源对象完成的,这些对象超出了本次讨论的范围(有关这些对象,请参阅 php-jsonapi-objects)。其余的工作主要是由数据源对象完成的。

在这种情况下,有两个:UsersDatasourceOrdersDatasource。在第一次操作中,我们使用标准的 users 属性获取器从 brokerage 数据上下文中获取 UsersDatasource 的实例。通常,您可以通过使用 JSON API 资源类型规范的驼峰表示法来从上下文中访问数据源实例。然后我们调用带有查询字符串的 UsersDatasource::get 方法以获取正确的用户。

虽然 get 方法未定义在 AbstractDatasource 中,但它通常遵循以下算法

首先,它使用其 parseDSL 方法解析查询字符串。如果没有重写此方法,那么这只会将字符串传递给 GenericDSLQuery::parse 方法,该方法返回一个 GenericDSLQuery 对象。这一步骤的作用是验证和清理查询字符串,因此如果传递了无效的字符,解析该字符串的 DSLQuery 类应抛出 BadQueryException

接下来,它使用 mapAttributemapRelationship 与其定义的 fieldMap 属性一起创建要获取的数据库列列表。它将此与对 getAddress 的调用结合起来,创建一个 SELECT 字符串,并将其作为 query 字段传递给 newSqlQuery(这是一个将参数传递给 new \CFX\Persistence\Sql\Query 的工厂方法)。

它使用 DSL 查询的 getWheregetParams 方法来完成 SQL 查询,以填写剩余所需的数据字段,然后执行该查询。

当返回时,它可能对原始结果进行一些预处理(取决于其一致性或不一致性),然后将结果发送到 convertToJsonApi,在那里它被转换为 JSON API 格式,然后到 inflateData,在那里它被转换为资源对象。(由于与查询逻辑封装相关的设计缺陷,当适用时,从 convertToJsonApi 方法抛出 ResourceNotFoundException。当解决查询的概念时,这可能会改变。)

笔记的下一个操作是save操作。虽然这个操作是在资源对象上调用的,但实际上它是委托给数据源对象的。数据源首先检查资源对象是否有错误(如果有必要,则抛出BadInputException)。然后,它会检查对象是否有ID。如果没有,它被视为新对象,该方法会检查它是否是重复的(如果有必要,则抛出DuplicateResourceException),然后将其委托给saveNew方法。如果它确实有一个id,那么它将委托给saveExisting方法。

由于使用了异常,实际上您不必检查保存是否成功。您可以假设如果没有抛出异常,则保存成功。

最后要强调的重要操作是getOrders方法。虽然这是一个特定于资源的操作,但它使用数据源来获取相关的订单。(如此问题所述,在撰写本文时,数据源完成此操作的机制相当笨拙。)

注意:目前系统对多对多关系处理得并不好。对于这个问题有一些相对简单的修复方案,但它们尚未列入高优先级列表。

关于这一点,重要的是要注意资源对象是如何委托给数据源对象,数据源对象又进一步委托给DataContext对象以获取相关订单的。这个操作实际上相当简单:资源对象告诉数据源,“我想获取与我相关的所有订单对象”。数据源将这个调用转换为使用资源类型和id编写的DSL查询字符串,然后使用类型从附加的数据上下文中获取OrdersDatasource(尝试)并对其执行查询。最终结果与调用$brokerage->orders->get("userId={$user->getId()}")相同,只是在幕后处理,使整个过程更具程序员友好性。

结论

就这些了!这不是一个庞大的库,因此您可以自由地深入研究源代码,以了解更多关于实现细节和可能性的信息。

已知问题和未来

以下是我们要解决的问题的一些已知问题

  • 前面提到的与SQL查询和DSL查询的问题
  • 实现更复杂和强大的DSL查询解析
  • 缩小DataContext的角色(目前,它们为数据源处理实际查询,这实际上并没有什么意义)

关于API文档的说明

我们希望很快推出一个网站(https://developers.cfxtrading.com),我们将能够提供更全面的API文档和其他资源供开发者使用。虽然这个网站还没有上线,但您仍然可以通过克隆库、安装Sami并运行sami.phar update sami.config.php来生成此库的良好API文档。