skqr / hateoas
GOintegro HATEOAS 库
Requires
- php: >=5.4.0
- doctrine/doctrine-bundle: *
- doctrine/orm: *
- predis/predis: *
- rhumsaa/uuid: *
- skqr/json: *
- skqr/raml: *
- symfony/doctrine-bridge: *
- symfony/filesystem: *
- symfony/http-foundation: *
- symfony/http-kernel: *
- symfony/routing: *
- symfony/security: *
- symfony/security-core: *
- symfony/translation: *
- symfony/validator: *
- symfony/yaml: *
Requires (Dev)
- codeception/codeception: 2.2.*
- codeclimate/php-test-reporter: dev-master
- phpdocumentor/phpdocumentor: 2.*
- phpunit/phpunit: *
Suggests
This package is not auto-updated.
Last update: 2024-09-28 20:03:50 UTC
README
这是一个使用 Doctrine 2 实体映射和 RAML API 定义来构建 HATEOAS API 的库,遵循 JSON-API 规范。
你不会得到脚手架,你会得到一个可工作的 API。
你将得到一个功能比一只猫的自尊心还要甜的 API。
特性
这就是我的意思。
你需要这些。
- 一个 Doctrine 2 实体映射;
- A RAML API 定义;
- 至少一个 Symfony 2 安全投票者。
试一试
查看 示例应用程序项目,这样你可以在不费吹灰之力的情况下感受到指尖上的魔力。
安装
查看 Symfony 2 扩展包,以实现全栈框架实现。
使用
在 RAML 语言 中设计你的 API,并遵循 JSON-API 规范。
例如,假设你有一个具有简短名称 User
的实体类。
#%RAML 0.8 title: HATEOAS Inc. Example API version: v1 baseUri: https://:8000/api/{version} mediaType: application/vnd.api+json /users: get: description: Fetches all users. responses: 200: post: description: Creates one or more users. responses: 201: /{user-ids}: get: description: Fetches users by Id. responses: 200: put: description: Updates one or more users by Id. responses: 200: delete: description: Deletes one or more users by Id. /links: /{relationship}: get: description: Fetches the related resources. responses: 200: post: description: Relates one or more resources. responses: 201: put: description: Updates the relationship. responses: 204: delete: description: Removes the relationship. /{relationship-ids}: delete: description: Removes to-many relationships by Id.
让实体实现资源接口。
不考虑命名空间,仅使用短名称来匹配上述定义的资源。
<?php namespace HateoasInc\Entity; use GoIntegro\Hateoas\JsonApi\ResourceEntityInterface class User implements ResourceEntityInterface {} ?>
哇 - 你会免费获得以下内容。
GET /users
GET /users/1
GET /users/1,2,3
GET /users/1/name
GET /users/1/linked/posts
GET /posts/1/linked/owner
GET /posts?owner=1
GET /posts?owner=1,2,3
GET /users?sort=name,-birth-date
GET /users?include=posts,posts.comments
GET /users?fields=name,email
GET /users?include=posts,posts.comments&fields[users]=name&fields[posts]=content
GET /users?page=1
GET /users?page=1&size=10
任何组合。
你还会得到这些。
POST /users
PUT /users/1
PUT /users/1,2,3
DELETE /users/1
DELETE /users/1,2,3
你可以这样链接或取消链接资源。
POST /users/1/links/user-groups
PUT /users/1/links/user-groups
DELETE /users/1/links/user-groups
DELETE /users/1/links/user-groups/1
DELETE /users/1/links/user-groups/1,2,3
甜蜜,对吧?
对于现在来说,
resource_type
必须 与计算出的类型匹配。例如,UserGroup
、user-groups
。
资源
但是你需要有一些控制权来决定你暴露的内容,对吧?我们为你解决了这个问题。
你可以为实体定义一个类似这样的类,并且可以定义你将在其中看到的任何属性和方法。
<?php namespace GoIntegro\Bundle\ExampleBundle\Rest2\Resource; // Symfony 2. use Symfony\Component\DependencyInjection\ContainerAwareInterface, Symfony\Component\DependencyInjection\ContainerAwareTrait; // HATEOAS. use GoIntegro\Hateoas\JsonApi\EntityResource; class UserResource extends EntityResource implements ContainerAwareInterface { use ContainerAwareTrait; /** * @var array */ public static $fieldWhitelist = ['name', 'surname', 'email']; /** * You wouldn't ever use both a blacklist and a whitelist. * @var array */ public static $fieldBlacklist = ['password']; /** * @var array */ public static $relationshipBlacklist = ['groups']; /** * These appear as top-level links but not in the resource object. * @var array */ public static $linkOnlyRelationships = ['followers']; /** * By injecting a field we can have both the JSON-API reserved key "type" and our own "user-type" attribute in the resource object. * @return string * @see https://jsonapi.fullstack.org.cn/format/#document-structure-resource-object-attributes */ public function injectUserType() { return $this->entity->getType(); } /** * We can use services if we implement the ContainerAwareInterface. * @return string */ public function injectSomethingExtraordinary() { return $this->container->get('mystery_machine')->amaze(); } } ?>
查看单元测试以获取更多详细信息。
JSON Schema
创建或更新资源的请求,其主体的内容将根据为该资源和方法定义的RAML架构进行验证。
由于JSON-API的请求体在获取、创建或更新时看起来很相似,因此您可以使用在RAML文档根目录中定义的、以资源类型为键的默认架构。
例如,这可能是/users
资源的RAML定义。
#%RAML 0.8 title: HATEOAS Inc. Example API version: v1 baseUri: http://api.hateoas-example.net/{version} mediaType: application/vnd.api+json schemas: - users: !include users.schema.json /users:
实体
这个包非常以实体为中心。您的实体外观以及它们在Doctrine 2中的映射关系对于本包在确定API外观时使用的智能至关重要。
安全
访问控制由Symfony的安全组件处理,因此必须配置security voters
或ACL
。
如果您根本不需要安全功能,只需配置一个接受任何实现GoIntegro\Hateoas\JsonApi\ResourceEntityInterface
的单个投票者即可。但这并不是最好的建议。
关于分页呢?我非常确定不会对集合中的每个实体调用isGranted
- 对吗?
绝对。
为了解决这个问题,我们提出了一个非常简单的解决方案。我们将安全投票者和自定义过滤器接口结合起来。
让您的投票者/过滤器实现GoIntegro\Hateoas\Security\VoterFilterInterface
,并使用security.voter
和hateoas.repo_helper.filter
标签进行标记。
# src/Example/Bundle/AppBundle/Resources/config/services.yml security.access.user_voter: class: HateoasInc\Bundle\ExampleBundle\Security\Authorization\Voter\UserVoter public: false tags: - { name: security.voter } - { name: hateoas.repo_helper.filter }
您应将查看实体的访问控制逻辑表达为方法vote
中的安全投票者,以及方法filter
中的获取请求过滤器。
好了。
验证
默认验证由Symfony的验证组件处理,因此您可以直接在实体上配置基本验证。
此外,您还可以通过编写自己的约束来扩展验证器。
由于约束可以是服务,这意味着您可能不需要创建自定义构建器或修改器。
违反唯一实体约束最终会导致409 HTTP响应状态,即冲突。 🔥
JSON-API要求仅在尝试创建已存在的关系时使用409。我们正在扩展其应用范围,包括任何由于与其他资源冲突而验证失败的实例。
如果您想创建一个自定义约束,其中违反意味着资源之间的冲突,只需使其实现GoIntegro\Hateoas\Entity\Validation\ConflictConstraintInterface
即可。
事务
JSON-API支持在单个请求中创建、更新或删除多个资源 - 但不允许部分更新。
我们在处理创建、更新和删除资源的控制器上使用显式事务划分,以便强制执行此规则。
创建、更新和删除
如前所述,默认提供创建、更新和删除资源的服务。
但您的业务逻辑呢?这不会走得太远。
您可以通过标记注册处理每种操作的特定资源的服务,称为构建器、修改器和删除器。
# src/Example/Bundle/AppBundle/Resources/config/services.yml example.your_resource.builder: class: Example\Bundle\AppBundle\Entity\YourEntityBuilder public: false arguments: - @doctrine.orm.entity_manager - @validator - @security.context tags: - { name: hateoas.entity.builder, resource_type: your_resource }
这个构建器类应该实现一个特定的接口。以下是可用的标签和接口。
更新关系和关联所有权
你已设置好一切,发送了一个应该关联两个资源的HTTP请求,获得了200/204的状态响应,但是你的关系并没有创建。
怎么回事?这里可能的原因。
记住,我们只会对您正在修改的资源关联的实体进行操作。即使在关联资源时,您也是在向其中之一发送请求。
例如:
POST /users/1/links/user-groups
正在作用于/users
。
如果您正在操作的实体在 Doctrine 看来不是关联的所有者,您的更改将不会被持久化。
您需要做两件事
- 在关联的逆向侧启用级联;
- 在逆向侧的设置器上,也要修改它得到的实体。
以下是一个示例。
<?php class Team { /** * @var ArrayCollection * @OneToMany( * targetEntity="User", * mappedBy="team", * cascade={"persist", "remove"} * ) */ private $members; /** * @param User $member * @return self */ public function addMember(User $member) { $this->members->add($member); $member->setTeam($this); // This is what you need. return $this; } } ?>
这些建议适合FAQ或故障排除部分。
可翻译内容
框架通过@stof的Bundle提供了对@l3pp4rd的Doctrine 扩展(也称为Gedmo)的可翻译实体功能的支持。
当获取或更新可翻译资源时,框架将根据由GoIntegro\Hateoas\JsonApi\Request\DefaultLocaleNegotiator
协商的locale进行操作。
您可以通过实现GoIntegro\Hateoas\JsonApi\Request\LocaleNegotiatorInterface
并使用标签hateoas.request_parser.locale
将其公开为服务来覆盖默认的locale协商者。
您可以通过传递查询字符串参数meta=i18n
来获取一个或多个资源的所有翻译。您也可以通过使用相同的body发送一个PUT
请求来更新它们。
这是示例应用中的一个示例。
GET /articles/1?meta=i18n
Accept-Language: en_GB
{ "links": { "articles.owner": { "href": "/api/v1/users/{articles.owner}", "type": "users" } }, "articles": { "id": "1", "type": "articles", "title": "This is my standing on stuff", "content": "Here's me, standing on stuff. E.g. a carrot.", "links": { "owner": "1" } }, "meta": { "articles": { "translations": { "content": [ { "locale": "fr", "value": "Ici est moi, debout sur des trucs. Par exemple une carotte." }, { "locale": "it", "value": "Qui sono io, in piedi su roba. E.g. una carota." } ], "title": [ { "locale": "fr", "value": "Ce est ma position sur la substance" }, { "locale": "it", "value": "Questa è la mia posizione su roba" } ] } } } }
你会说JSON-API吗?是的,是的。
扩展
麻瓜控制器
如果您想出于任何原因覆盖魔法控制器,只需创建一个旧的Symfony 2控制器。
您可以使用实现ResourceEntityInterface
的实体,并独立使用HATEOAS包提供的服务。
以下是一个相当基础的示例。
<?php use GoIntegro\Hateoas\Controller\Controller, Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; class UsersController extends Controller { /** * @Route("/users/{user}", name="api_get_user", methods="GET") * @return \GoIntegro\Hateoas\Http\JsonResponse */ public function getUserAction(User $user) { $resourceManager = $serviceContainer->get('hateoas.resource_manager'); $resource = $resourceManager->createResourceFactory() ->setEntity($user) ->create(); $json = $resourceManager->createSerializerFactory() ->setDocumentResources($resource) ->create() ->serialize(); return $this->createETagResponse($json); } ?>
查看包的services.yml
文件,看看我们那里保留的HATEOAS装备。
幽灵 👻
我知道你在想什么——如果我的资源没有实体怎么办?我在黑暗中孤军奋战吗?
不是这样。幽灵就在你身边,在黑暗中。帮助你。
幽灵是从持久化实体而不是从幽灵资源创建的。它们在自定义HATEOAS控制器中创建,并可以代替实体提供给资源工厂。它们定义自己的关系,而不是ORM事先知道它们。
您使用什么作为ID,以及您使用它们的程度,完全取决于您。
<?php namespace GoIntegro\Bundle\SomeBundle\Rest2\Ghost; // Entidades. use GoIntegro\Bundle\SomeBundle\Entity\Star; // JSON-API. use GoIntegro\Hateoas\JsonApi\GhostResourceEntity, GoIntegro\Hateoas\Metadata\Resource\ResourceRelationship, GoIntegro\Hateoas\Metadata\Resource\ResourceRelationships; // Colecciones. use Doctrine\Common\Collections\ArrayCollection; class StarCluster implements GhostResourceEntity { /** * @var string */ private $id; /** * @var Star */ private $brightestStar; /** * @var ArrayCollection */ private $stars; /** * @param string $id */ public function __construct($id) { $this->id = $id; $this->stars = new ArrayCollection; } /** * @return string */ public function getId() { return $this->id; } /** * @param Star $star * @return self */ public function setBrightestStar(Star $star) { $this->brightestStar = $star; return $this; } /** * @return Star */ public function getBrightestStar() { return $this->brightestStar; } /** * @return ArrayCollection */ public function getStars() { return $this->stars; } /** * @param \GoIntegro\Bundle\SomeBundle\Entity\Star $star * @return self */ public function addStar(Star $star) { $this->stars[] = $star; return $this; } /** * @return ResourceRelationships */ public static function getRelationships() { $relationships = new ResourceRelationships; $relationships->toMany['stars'] = new ResourceRelationship( 'GoIntegro\Bundle\SomeBundle\Entity\Star', 'stars', // resource type 'stars', // resource sub-type 'toMany', // relationship kind 'stars', // relationship name 'stars' // mapping field ); $relationships->toOne['brightest-star'] = new ResourceRelationship( 'GoIntegro\Bundle\SomeBundle\Entity\Star', 'stars', 'stars', 'toOne', 'brightest-star', 'brightestStar' ); return $relationships; } } ?>
查询过滤器
与包捆绑的标准获取请求过滤器。
这是提供这些过滤器服务的服务。
# GoIntegro/HateoasBundle/Resources/config/services.yml hateoas.repo_helper.default_filter: class: GoIntegro\Hateoas\JsonApi\Request\DefaultFilter public: false tags: - name: hateoas.repo_helper.filter
如果您对标记服务有所了解,您可能已经猜到您可以添加自己的。
只需让您的过滤器类实现GoIntegro\Hateoas\JsonApi\Request\FilterInterface
,并在将其声明为服务时添加hateoas.repo_helper.filter
标记。
您的过滤器应使用其获得的实体和过滤器参数来决定是否采取行动。请确保单个类不要承担过多的过滤责任。
测试
该软件包附带了一个舒适的PHPUnit测试用例,用于进行HATEOAS API功能测试。
简单的HTTP客户端发起请求,断言使用JSON模式进行。
<?php namespace GoIntegro\Entity\Suite; // Testing. use GoIntegro\Test\PHPUnit\ApiTestCase; // Fixtures. use GoIntegro\DataFixtures\ORM\Standard\SomeDataFixture; class SomeResourceTest extends ApiTestCase { const RESOURCE_PATH = '/api/v2/some-resource', RESOURCE_JSON_SCHEMA = '/schemas/some-resource.json'; /** * Doctrine 2 data fixtures to load *before the test case*. * @return array <FixtureInterface> */ protected static function getFixtures() { return array(new SomeDataFixture); } public function testGettingMany200() { /* Given... (Fixture) */ $url = $this->getRootUrl() . self::RESOURCE_PATH; $client = $this->createHttpClient($url); /* When... (Action) */ $transfer = $client->exec(); /* Then... (Assertions) */ $this->assertResponseOK($client); $this->assertJsonApiSchema($transfer); $schema = __DIR__ . self::RESOURCE_JSON_SCHEMA; $this->assertJsonSchema($schema, $transfer); } public function testGettingSortedBySomeCustomField400() { /* Given... (Fixture) */ $url = $this->getRootUrl() . self::RESOURCE_PATH . '?sort=some-custom-field'; $client = $this->createHttpClient($url); /* When... (Action) */ $transfer = $client->exec(); /* Then... (Assertions) */ $this->assertResponseBadRequest($client); } } ?>
错误处理
JSON-API还涵盖了如何通知错误。
我们的实现并不完全完整,但您可以让Twig使用我们的ExceptionController代替其自身的,以便正确序列化错误。
# app/config/config.yml twig: exception_controller: 'GoIntegro\Hateoas\Controller\ExceptionController::showAction'
获取多个URL
这里有一些有用但不遵循RESTful的东西。
您可以使用/multi操作来获取多个JSON-API URL,而且这甚至不会导致额外的HTTP请求。
/multi?url[]=%2Fapi%2Fv1%2Fusers&url[]=%2Fapi%2Fv1%2Fposts
只需对URL进行编码即可,但您可以使用支持的全部JSON-API功能。
一个blender服务将确保在提供的URL无法合并时通知您。
缓存
是的,这些过程并不便宜。
您可能希望保存您挖掘的元数据或序列化的资源一段时间。
资源元数据
资源元数据描述了资源类型。它描述了其名称、字段、与其他资源的关联以及其他类似内容。
它与Doctrine 2实体映射类似,即Doctrine\ORM\Mapping\ClassMetadata
。
我们通过检查实体的映射及其类,使用ORM和反射来获取此类。
您可以将资源类型的元数据缓存,直到这两个内容之一发生变化。
以下是方法。添加此参数。
# app/config/parameters.yml parameters: hateoas.resource_cache.class: GoIntegro\Hateoas\JsonApi\ArrayResourceCache
您可以使用以下选项中的任何一个来自定义Redis或Memcached配置。以下为默认值。
# app/config/config.yml go_integro_hateoas: cache: ~ # resource: # redis: # parameters: # scheme: tcp # host: 127.0.0.1 # port: 6379 # options: [] # memcached: ~ # persistent_id: null # servers: # - host: 127.0.0.1 # port: 11211
HTTP响应
获取响应都带有ETag。
ETag是从响应的完整主体创建的,因此它准确地代表了您正在获取的JSON-API文档,包括其包含的内容、稀疏字段、元数据等。
请求中的ETag使用Symfony进行检查。
反馈
如果您有任何有价值的(或无价值的)反馈,请随时提交问题。希望听到您的反馈(无论是哪种情况)。
如果您敢于在代码上做出如此大的改动,以至于要发起拉取请求,请使用master
分支进行修复,使用develop
分支进行提议的功能。
我们正在使用Git Flow分支模型。以下是一个很好的速查表,可以为您提供一般性的了解。
新代码不应超过传奇的80字符边界,并且需要完全文档化。
顺便说一下:我们仍然需要将问题从软件包仓库迁移过来。
路线图
任何被标记为增强的问题都会进行。
PATCH支持即将到来。
(大多数增强问题仍然在bundle仓库中。)
免责声明
你可能已经注意到了PHP代码片段中的一些奇怪之处。
关闭标签.