cfxmarkets / php-persistence
一个库,提供了基于JSON API关系型资源概念的多种持久层抽象实现。
Requires
- php: >=5.5
- cfxmarkets/php-jsonapi-objects: ^2.0.0
- guzzlehttp/guzzle: ^6.3.0
- kael-shipman/php-std-traits: ^7.0.0
- moontoast/math: ^1.1.0
- psr/log: ^1.0.0
- ramsey/uuid: ^3.8.0
Requires (Dev)
- phpunit/phpunit: >=4.8.0
This package is auto-updated.
Last update: 2024-09-09 06:16:13 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)。其余的工作主要是由数据源对象完成的。
在这种情况下,有两个:UsersDatasource
和 OrdersDatasource
。在第一次操作中,我们使用标准的 users
属性获取器从 brokerage 数据上下文中获取 UsersDatasource
的实例。通常,您可以通过使用 JSON API 资源类型规范的驼峰表示法来从上下文中访问数据源实例。然后我们调用带有查询字符串的 UsersDatasource::get
方法以获取正确的用户。
虽然 get
方法未定义在 AbstractDatasource
中,但它通常遵循以下算法
首先,它使用其 parseDSL
方法解析查询字符串。如果没有重写此方法,那么这只会将字符串传递给 GenericDSLQuery::parse
方法,该方法返回一个 GenericDSLQuery
对象。这一步骤的作用是验证和清理查询字符串,因此如果传递了无效的字符,解析该字符串的 DSLQuery 类应抛出 BadQueryException
。
接下来,它使用 mapAttribute
和 mapRelationship
与其定义的 fieldMap
属性一起创建要获取的数据库列列表。它将此与对 getAddress
的调用结合起来,创建一个 SELECT 字符串,并将其作为 query
字段传递给 newSqlQuery
(这是一个将参数传递给 new \CFX\Persistence\Sql\Query
的工厂方法)。
它使用 DSL 查询的 getWhere
和 getParams
方法来完成 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文档。