cfxmarkets / php-jsonapi-objects
一个提供PHP实现JSON API对象的类。注意,这并不是协议实现,它只是按照[json-api规范](https://jsonapi.fullstack.org.cn/format/)描述的类实现。
Requires
- php: >=5.4
- kael-shipman/php-std-traits: ^7.0.0
Requires (Dev)
- phpunit/phpunit: >=4.8.0
- dev-master
- v2.x-dev
- v2.1.0
- v2.0.0
- v1.5.x-dev
- v1.5.0
- v1.4.x-dev
- v1.4.11
- v1.4.10
- v1.4.9
- v1.4.8
- v1.4.7
- v1.4.6
- v1.4.5
- v1.4.4
- v1.4.3
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.x-dev
- v1.3.1
- v1.3.0
- v1.2.x-dev
- v1.2.3
- v1.2.2
- v1.2.1
- v1.2.0
- v1.1.x-dev
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.x-dev
- v1.0.2
- v1.0.1
- v1.0
- v0.8.1
- v0.8.0
- v0.7.5
- v0.7.4
- v0.7.3
- v0.7.2
- v0.7.1
- v0.7.0
- v0.6.3
- v0.6.2
- v0.6.1
- v0.6.0
- v0.5.0
- v0.4.6.1
- v0.4.6
- v0.4.5.2
- v0.4.5.1
- v0.4.5
- v0.4.4
- v0.4.3.1
- v0.4.3
- v0.4.2
- v0.4.1
- v0.4.0
- v0.3.0
- v0.2.1
- v0.2
- v0.1
- dev-future
This package is auto-updated.
Last update: 2024-09-18 07:55:40 UTC
README
这是一个包含各种类的库,这些类实现了PHP中的JSON-API规范对象。它旨在简化创建一个数据系统,其资源对象可以序列化为JSON-API。
通过这种方式,它可以用来解析接收到的JSON-API文档,以便进行验证和持久化,并且可以轻松地在使用JSON-API的服务之间传输数据,如REST API。
注意:在撰写本文时,几乎所有开发时间都投入到了
AbstractResource类。其余的类只是到满足JSON-API规范的程度,但它们并没有做得很好或得到很好的使用。
安装
可以使用标准的composer过程安装此库
composer require cfxmarkets/php-jsonapi-objects
使用
该库旨在成为您的JSON-API感知数据模型的基础(请参阅下面的设计哲学)。因此,您最可能使用它来实现资源对象。这些对象应由实现DatasourceInterface的数据源对象管理。
由于此库不包含任何特定资源对象的实现(除了GenericResource类,它不够具体,无法提供有用的示例),我将使用虚构的示例来展示用法。此外,由于此库不包含持久性逻辑,我将省略示例中的持久性讨论。
以下是如何使用从AbstractResource扩展的User类的一个示例
// Use user info for conditional logic, say on an admin panel if (!$user->isAtLeast('manager')) { throw new UnauthorizedAccessException("You must be at least a manager to view this page"); }
// User user info to prefill forms $form = '<form method="POST"> <input type="email" name="email" value="'.$user->getEmail().'"> <input type="phone" name="phone" value="'.$user->getPhone().'"> <input type="text" name="name" value="'.$user->getFullName().'">'; if ($user->likes('muffins')) { $form .= ' <h2>Your Muffins</h2>'; foreach ($user->getMuffins(['orderBy' => 'rating:desc']) as $muffin) { $form .= ' <p> <input type="number" name="muffins['.$muffin->getId().'][rating]" value="'.$muffin->getRating().'"> '.$muffin->getType().' <button data-muffin="'.$muffin->getId().'">Remove</button> </p>'; } $form .= ' </form>'; echo $form;
// Update user data in response to form input $user ->setName($_POST['name']) ->setEmail($_POST['email']) ->setPhone($_POST['phone']) ; $muffinData = $_POST['muffins']; $muffins = $myBlog->muffins->newCollection(); forach($muffinData as $id => $info) { $muffins[] = $myBlog->muffins->get("id=$id") ->setRating($info['rating']) ->save() ; } $user ->setMuffins($muffins) ->save() ;
// Update user data from jsonapi input $user ->updateFromData($jsonapiData) ->save();
上述示例展示了在库上构建的对象的典型使用方式。然而,这些用例与不在库上构建的对象没有太大区别。这是一个特性,也说明了库上的对象应该相对容易迁移。
回到示例。在许多情况下,数据是通过正常的getter和setter访问的。然而,在某些情况下,使用了更复杂的方法(如User::isAtLeast和User::likes,或者具有orderBy选项的修改后的getter getMuffins)。您决定抽象级别是您自己的选择,而这个库没有意图强加某种特定的哲学。您在这里的选择应该由您想要将您的编写逻辑与数据绑定得多紧密来指导。换句话说,您是否经常希望在应用程序的许多部分中知道用户喜欢什么或用户是否具有最低角色?如果是这样,那么您应该直接将这些方法构建到您的资源类中。如果不是这样,例如,如果您正在检查用户在其muffins集合中是否有特定类型的muffin,那么您最好使用更通用的getter来组合您想要的逻辑。
但是,您应该以对模型基于的基本数据结构的基本尊重来构建所有方法。
例如,User::isAtLeast 方法可能会使用一个存储在对象中并持久化为整数的 roles 按位掩码。该实现可能看起来像这样
class User extends \CFX\JsonApi\AbstractResource { protected $attributes = [ // default to "end-user" role 'roles' => 1, //.... ]; protected static function getValidRoles() { return [ 1 => 'end-user', 2 => 'advanced-user', 4 => 'manager', 8 => 'site-admin', 16 => 'sys-admin', ]; } public function getRoleInteger() { return $this->_getAttributeValue('roles'); } public function getRoles() { $roles = []; $userRoles = $this->getRoleInteger(); foreach (static::getValidRoles() as $roleInt => $role) { if ($userRoles & $roleInt) { $roles[] = $role; } } return $roles; } public function setRoles($val) { $roles = 0; $validRoles = static::getValidRoles(); $invalidRoles = []; if (is_array($val)) { foreach ($val as $role) { $roleInt = array_search($role, $validRoles); if ($roleInt === false) { $invalidRoles[] = $role; } else { $roles += $roleInt; } } } else { // Implement logic for validating integer role here.... } if (count($invalidRoles) > 0) { $this->setError('roles', 'invalid', [ "title" => 'Invalid Roles Provided', "detail" => "The following roles are invalid: `".implode("`, `", $invalidRoles)."`", ]); } else { $this->clearError('roles', 'invalid'); } return $this->_setAttribute('roles', $roles); } public function isAtLeast($role) { $roleLevel = array_search($role, static::getValidRoles(), true); if ($roleLevel === false) { throw new \RuntimeException("Unrecognized role `$role`. Valid roles are `".implode("`, `", static::getValidRoles())."`."); } return $this->getRoleInteger() >= $roleLevel); } }
使用这段代码,您可以在数据库中将角色持久化为整数,但在程序空间中以字符串的形式处理它们。这种方法结合了命名常量的可用性和效率以及字符串的简洁性。
这只是一个如何使用此类创建模型的简单示例。如您想象的那样,您可以使用这些对象几乎做任何事情。祝您玩得开心!
设计理念
以下是关于孕育此库的设计理念的更详细讨论。它并不那么涉及代码本身,而是试图解释可用的方法和在创建它们时试图解决的问题。
解决的问题
此库主要解决的问题是
- 如何定义一个持久性启用的资源类,而不强制执行持久性策略。
- 如何在允许具体类仅与基础松散耦合的同时,为 JSON API 数据序列化提供一个基础。
- 如何在程序空间中更轻松地与 JSON API 规范交互。
这些问题通过定义一些相对轻量级的接口来解决,这些接口为开发者用户提供了从开发角度构建和使用模型的简单方法。具体来说,ResourceInterface 定义了一个用于处理持久性启用且符合 JSON API 规范的资源对象的接口;而 DatasourceInterface 定义了一个接口,用于资源对象以促进持久性。
继续阅读,了解更多关于每个接口的信息。
资源
此库以 AbstractResource 为中心,作为持久性启用、JSON API 数据系统的基本。正如其名称所暗示的,此类资源旨在扩展到每个系统数据模型的各个类。例如,一个博客平台可能有以下模型类,所有这些类都扩展自 AbstractResource
用户帖子评论
尽管与数据源交互和序列化的低级细节包含在 AbstractResource 本身中,但这些具体的资源类将包含它们自己的验证规则和业务逻辑。事实上,预计所有派生资源类中包含的 96% 都是业务逻辑。只有偶尔会涉及到实现逻辑,以促进诸如复杂数据的序列化或建立复杂数据的“初始状态”等问题。这种分离允许相对“纯粹”的资源类(即“模型层”),可以轻松地适应与其他系统的集成。
持久性
通过定义的 DatasourceInterface 建立了与持久性的交互。然而,没有包含 DatasourceInterface 的实现。这允许我们保持此基础包相当轻量级,同时仍然与许多不同类型的持久性保持兼容。
此库对持久性的方法是在定义我们希望如何与持久性机制交互的方式,而不定义该持久性机制实际是什么。此外,我们假设数据源是 区分 的——也就是说,每个资源将有自己的伴随的资源特定数据源实例。考虑到这一点,包含的 DatasourceInterface 定义了以下公共函数
创建创建新集合获取保存删除转换获取相关膨胀相关初始化资源获取当前数据
其中一些方法相当明显,而其他方法需要一些解释。
首先从显而易见的功能开始,create 和 newCollection 是实例化器:它们返回数据源处理的数据类型(或其集合)的新实例。 get 根据传递给它的 DSL 字符串返回集合或特定资源(例如,"id=12345" 或 "name like '%tom%' and (role & 4)")。 save 根据传递的资源是否有 id 来保存新资源或更新现有资源。 delete 删除指定的资源。
接下来是更复杂的方法。
转换
这个库中的数据系统是围绕这样一个想法设计的:每个资源可能有几个“级别”,它们表示相同的资源类型。例如,后端系统需要一种方式来设置用户的“passwordHash”字段。我们不希望 API 用户能够设置此字段,也不希望将我们的哈希逻辑和密钥包含在公开分发的代码库中。
为了解决这个问题,我们可以创建一个“公开”版本的 User 类,它将 password 存储为只写明文字段,然后扩展它以创建一个 Private 版本,该版本实现了更复杂的哈希逻辑,从而生成成功的密码哈希。
由于实例化是在数据源级别完成的,因此数据源处理这些相关资源类型之间的转换是有意义的。因此,convert 接收一个资源和目标“级别”,并尝试将资源转换为目标级别的资源,同时保留资源的状态。在 User 示例中,我们可能会请求 $usersDatasource->convert($user, 'private') 来获取给定用户资源的一个“私有”版本。
getRelated 和 inflateRelated
由于 DatasourceInterface 是为了处理单个资源类型而设计的,我们还需要一种方法将不同资源类型的手柄委托给其他数据源。getRelated 和 inflateRelated 通过允许我们根据请求的关联名称调用不同的数据源来实现这一点。例如,如果 User 对象有一个 posts 关联,我们将在 getRelated 和 inflateRelated 中实现逻辑,将这些请求路由到 PostsDatasource 对象。
这当然意味着 UsersDatasource 知道 PostsDatasource 的存在。这个问题可以通过多种方式解决。例如,您可以使用 DataContext 将多个数据源分组到一个类似于相互关联的“目录”中(这是在 cfxmarkets/php-persistence 中引入的概念,我们在 CFX 中使用它)。或者,您可以在每个数据源本身中构建对更通用数据上下文的意识,使用一个包含路由和实例化逻辑的单一通用抽象数据源,该逻辑用于处理应用程序必须处理的各种类型的兄弟数据源。
无论如何,getRelated 和 inflateRelated 是两个允许您将相关资源请求委托给其他数据源的方法。
初始化资源
通常,您最终会得到一个未初始化的资源。例如,当您获取一个只有 ID 的关系时,这种情况就会发生。在这种情况下,您可以通过调用数据源来初始化资源,这基本上就是从持久化中获取资源,然后使用返回的数据以及资源的 restoreFromData 方法来膨胀对象。
这使我们来到了我们的最后一个方法,getCurrentData
获取当前数据
getCurrentData 方法是一种特别设计的方法,用于将公共 Datasource 对象和公共 Resource 对象与私有握手相结合。它是为了与 AbstractResource 类的 restoreFromData 类似工作而构思的
Datasource从其持久化源获取数据,并确保其以 JSON API 格式。数据源将数据放置在其受保护的currentData属性中。数据源要么调用Resource的restoreFromData方法,要么实例化一个新的资源(这会隐式调用此方法)。Resource调用数据源的getCurrentData方法,该方法应返回准备好的数据,然后重新将currentData设置为 null。Resource使用来自数据源的“可信”数据内部更新其字段。