undabot / json-api-symfony
允许 symfony 应用轻松处理与 JSON API 兼容的请求和响应
Requires
- php: ^8
- ext-json: *
- beberlei/assert: ^3.3
- doctrine/annotations: ^1.12
- ramsey/uuid: ^4.1
- sensio/framework-extra-bundle: ^6.1
- symfony/http-kernel: ^5.0 || ^6.0
- symfony/orm-pack: ^2.1
- symfony/property-access: ^5.0 || ^6.0
- symfony/serializer: ^5.0 || ^6.0
- symfony/validator: ^5.0 || ^6.0
- symfony/yaml: ^5.0 || ^6.0
- undabot/json-api-core: 2.1.7
Requires (Dev)
- friendsofphp/php-cs-fixer: ^2.18
- phpstan/extension-installer: ^1.1
- phpstan/phpstan: ^0.12
- phpstan/phpstan-beberlei-assert: ^0.12
- phpstan/phpstan-phpunit: ^0.12
- phpunit/phpunit: ^9.5
- roave/security-advisories: dev-latest
- thecodingmachine/phpstan-strict-rules: ^0.12
- dev-master
- v3.0.0.x-dev
- v2.4.1.x-dev
- v2.4.0.x-dev
- v2.3.7
- v2.3.6
- v2.3.5
- v2.3.4
- v2.3.3
- v2.3.2
- v2.3.1
- v2.3.0
- v2.2.4
- v2.2.3
- v2.2.2
- v2.2.1
- v2.2.0
- v2.1.5.x-dev
- v2.1.5
- v2.1.4.x-dev
- v2.1.4
- v2.1.3.x-dev
- v2.1.3
- v2.1.2
- v2.1.1
- v2.1.0
- v2.0.0
- v1.0.0
- dev-develop
- dev-fix/tests_correction
- dev-refactor/phpstan-psalm
- dev-github_actions
- dev-feat/query_builder
- dev-upgrade/php
- dev-feature/open-api-support
- dev-dev-v2
This package is auto-updated.
Last update: 2024-09-06 07:43:13 UTC
README
这个库的想法是使用 Symfony 作为应用程序基础设施提供商来返回 JSON:API 兼容的响应。该库为开发者提供在应用程序内部拥有对象并将它们“附加”到响应类的支持。最终结果是具有 JSON:API 数据结构的响应类。该库还提供处理请求查询参数和解析 JSON:API 兼容请求体为单独的 PHP 类的支持。
该库本身是围绕 Symfony 框架的包装,它使用 json-api-core 库在创建与 JSON:API 兼容的请求和响应时进行繁重的工作。
本文档涵盖了以下部分
使用方法
本节涵盖了使用示例和库元素的详细解释。在阅读完毕后,您将拥有使用该库的充足知识,无需进一步的帮助。
在继续之前,这里有一些注意事项
- 文章和评论的命名空间仅用于演示目的。您可以根据需要将其放在任何位置。
- 我们使用
readonly
来设置属性,因为我们不希望对象创建期间分配的值被更改,但您不需要这样做。如果您使用的是 PHP 8.1 之前的版本,您可以在类上添加@psalm-immutable
(有关 Psalm 的更多信息,请参阅 此处)注释以使属性为只读。 - final 类用于类,因为我们不希望扩展读取模型,但如果您需要扩展它,请移除 final 声明(尽管我们建议每个资源只有一个读取模型,该模型仅实现 ApiModel 并不扩展其他模型)。
- 给定的示例由于可读性在控制器中包含大量逻辑。在实际应用中,我们建议将逻辑拆分并移动到单独的类中。
- 给定的示例将查询参数传递到查询总线,该总线应返回结果数组。除了使用查询总线,您还可以注入存储库并将参数直接发送给它,甚至注入数据库连接并使用给定参数进行原始查询。使用您在构建应用程序时使用的任何方法。
如何返回 JSON:API 兼容的响应?
要返回 JSON:API 兼容的响应,您需要经过几个步骤 - 您需要读取或写入端和响应器。读取和写入端在逻辑上由多个类组成
- 控制器,请求的入口点
- 用于创建实体或返回请求信息的模型
- 存储数据的实体
响应器作为粘合剂,将实体映射到模型。
在深入探讨读取和写入模型、响应者和控制器之前,先描述一下我们如何在我们模型中区分属性和关系是个好主意。为了识别哪个属性是属性,哪个是关系,我们使用注解。每个模型都应该有一个“主要”注解,用于确定其类型,这是任何资源对象的顶级成员。这个注解放置在类声明上方,如下所示
/** @ResourceType(type="articles") */ final class ArticleWriteModel implements ApiModel
除了 @ResourceType
注解之外,还有三个注解 - @Attribute
、@ToOne
和 @ToMany
。
@Attribute
注解表示该属性被视为属性。
@ToOne
和 @ToMany
注解表示该属性是关系。关系必须在 ToOne
和 ToMany
注解中包含名称和类型值,例如:
/** * @var array<int,string> * @ToMany(name="article_comments", type="comments") */ public readonly array $commentIds,
名称 值是我们希望在响应中显示的内容。例如,在这个例子中,article_comments
是将在响应中返回的此关系的名称。如果没有在注解中定义名称,则关系将继承属性名称。
类型 值是我们所引用的关系的资源类型。在这里,我们指的是评论,这意味着与该模型相关的评论模型是代码库的一部分。请注意,库仅链接具有确切名称的类型,因此如果您的模型类型为 comment
,并且您犯了一个错误并写成复数形式,库将抛出错误。
关系可以是可空的,要将可空关系添加到模型中,您只需在注解中为 nullable
属性分配一个布尔值即可。以下是一个示例。在这种情况下,不要忘记对属性进行空安全类型提示,并记住 - 关系默认不可为空。
/** * @var array<int,string> * @ToOne(name="article_author", type="authors", nullable=true) */ public readonly ?string $authorId,
有了这些知识,让我们深入了解更具体的示例。
写入侧
请求和响应生命周期包括从客户端接收数据并将其返回给客户端 - 写入和读取侧。在接收数据时,它必须以符合 JSON:API 的 JSON 字符串形式通过请求体发送。这个库允许我们获取给定数据,验证它并将其转换为 PHP 类。
创建
写入模型由构建我们即将创建的资源的相关注解属性组成。因此,如果我们即将创建具有 id、标题和一些相关评论的文章,则创建(写入)模型将如下所示。
<?php declare(strict_types=1); namespace App; use Undabot\SymfonyJsonApi\Model\ApiModel; use Undabot\SymfonyJsonApi\Model\Resource\Annotation\Attribute; use Undabot\SymfonyJsonApi\Model\Resource\Annotation\ToMany; use Undabot\SymfonyJsonApi\Model\Resource\Annotation\ToOne; use Undabot\SymfonyJsonApi\Service\Resource\Validation\Constraint\ResourceType; /** @ResourceType(type="articles") */ final class ArticleWriteModel implements ApiModel { public function __construct( public readonly string $id, /** @Attribute */ public readonly string $title, /** @ToOne(name="author", type="authors") */ public readonly string $authorId, /** * @var array<int,string> * @ToMany(name="comments", type="comments") */ public readonly array $commentIds, ) { } }
此类完全由请求数据创建,而不是由另一个类创建,因此它只有一个构造函数。每个属性都有一个注解,表示它是一个关系或属性。每个关系在 ToOne
和 ToMany
注解中都必须有名称和类型值。
正如您稍后将看到的那样,我们通常在读取和写入模型中具有相同的属性。因此,如果您遇到这种情况,您可以将它们合并到同一个模型中,例如 ArticleModel
。此外,如果您的更新模型与写入模型相同,则可以将它们合并为一个,并有一个写入模型(用于创建和更新),以及一个读取模型。
现在,当您知道如何创建写入侧的模型时,让我们看看还需要什么。假设您已经有一个文章实体,这里缺少的是控制器。
要从请求中提取数据到模型中,我们需要注入整个请求。下面是一个示例,我们将使用 SimpleResourceHandler 和 CreateResourceRequestInterface(及其具体实现)来帮助。
<?php declare(strict_types=1); namespace App; use Undabot\JsonApi\Definition\Model\Request\CreateResourceRequestInterface; use Undabot\SymfonyJsonApi\Http\Model\Response\ResourceCreatedResponse; use Undabot\SymfonyJsonApi\Http\Service\SimpleResourceHandler; class Controller { public function create( CreateResourceRequestInterface $request, SimpleResourceHandler $resourceHandler, Responder $responder, ): ResourceCreatedResponse { /** @var ArticleWriteModel $articleWriteModel */ $articleWriteModel = $resourceHandler->getModelFromRequest( $request, ArticleWriteModel::class, ); // now you can use something like $articleWriteModel->title; return $responder->resourceCreated($article, $includes); }
更新
在更新资源时,客户端可能会向您发送一些属于读取模型的字段,而不是整个模型。例如,如果我们正在更新包含内容的文章,则无需发送标题或其他属性。然而,对于所提到的案例,我们需要一些模型,并需要有机会从当前状态创建它。因此,我们可以使用上面示例中的写入模型,但我们必须添加fromSomething
方法,然后在控制器中像以下示例那样使用它。
<?php declare(strict_types=1); namespace App; use Undabot\JsonApi\Definition\Model\Request\UpdateResourceRequestInterface; use Undabot\SymfonyJsonApi\Http\Model\Response\ResourceUpdatedResponse; use Undabot\SymfonyJsonApi\Http\Service\SimpleResourceHandler; use Undabot\SymfonyJsonApi\Service\Resource\Factory\ResourceFactory; class Controller { public function update( ArticleId $id, UpdateResourceRequestInterface $request, SimpleResourceHandler $resourceHandler, Responder $responder, ResourceFactory $resourceFactory, ): ResourceUpdatedResponse { $article = // fetch article by id $baseModel = ArticleWriteModel::fromEntity($article); $baseResource = $resourceFactory->make($baseModel); $updateResource = new CombinedResource($baseResource, $request->getResource()); /** @var ArticleWriteModel $articleUpdateModel */ $articleUpdateModel = $resourceHandler->getModelFromResource( $updateResource, ArticleWriteModel::class, ); // now you can use something like $articleUpdateModel->title; return $responder->resourceUpdated($article, $includes); }
如果您打算使用相同的模型来创建和更新资源,则该模型需要具有fromSomething
方法。如上所述,您可以将其称为ArticleModel
,它看起来像这样
<?php declare(strict_types=1); namespace App; use Undabot\SymfonyJsonApi\Model\ApiModel; use Undabot\SymfonyJsonApi\Model\Resource\Annotation\Attribute; use Undabot\SymfonyJsonApi\Model\Resource\Annotation\ToMany; use Undabot\SymfonyJsonApi\Model\Resource\Annotation\ToOne; use Undabot\SymfonyJsonApi\Service\Resource\Validation\Constraint\ResourceType; /** @ResourceType(type="articles") */ final class ArticleModel implements ApiModel { public function __construct( public readonly string $id, /** @Attribute */ public readonly string $title, /** @ToOne(name="author", type="authors") */ public readonly string $authorId, /** * @var array<int,string> * @ToMany(name="comments", type="comments") */ public readonly array $commentIds, ) { } public static function fromSomething(Article $article): self { return new self( (string) $article->id(), (string) $article->title(), (string) $article->author()->id(), $article->comments()->map(static function (Comment $comment): string { return (string) $comment->id(); })->toArray(), ); } }
在实践中,fromSomething
方法被称为fromEntity
,因为我们通常从实体创建更新/读取模型。例如,如果您使用视图模型,则可以写成这样
public static function fromEntity(Article $article): self { $viewModel = $article->viewModel(); return new self( $viewModel->id, $viewModel->title, $viewModel->authorId, $viewModel->commentIds, ); }
读取侧
与写入模型一样,读取模型是一个具有注解属性的类,您希望将其返回给客户端。例如,如果您需要返回此JSON:API响应
{ "links": { "self": "http://example.com/articles", "next": "http://example.com/articles?page[offset]=2", "last": "http://example.com/articles?page[offset]=10" }, "data": [{ "type": "articles", "id": "01FTZBPQ1EY5P5N3QW4ZHHFCM3", "attributes": { "title": "JSON:API Symfony rocks!" }, "relationships": { "author": { "links": { "self": "http://example.com/articles/01FTZBPQ1EY5P5N3QW4ZHHFCM3/relationships/author", "related": "http://example.com/articles/01FTZBPQ1EY5P5N3QW4ZHHFCM3/author" }, "data": { "type": "authors", "id": "01FTZBN5HZ590S48WM0VEYFMBY" } }, "comments": { "links": { "self": "http://example.com/articles/01FTZBPQ1EY5P5N3QW4ZHHFCM3/relationships/comments", "related": "http://example.com/articles/01FTZBPQ1EY5P5N3QW4ZHHFCM3/comments" }, "data": [{ "type": "comments", "id": "01FTZBQRGVWX1AZG19NN0FMQZR" }, { "type": "comments", "id": "01FTZBNTAB25AM278R6G3YRCKT" } ] } }, "links": { "self": "http://example.com/articles/01FTZBPQ1EY5P5N3QW4ZHHFCM3" } }] }
则您将创建此模型
<?php declare(strict_types=1); namespace App; use App\Article; use App\Comment; use Undabot\SymfonyJsonApi\Model\ApiModel; use Undabot\SymfonyJsonApi\Model\Resource\Annotation\Attribute; use Undabot\SymfonyJsonApi\Model\Resource\Annotation\ToMany; use Undabot\SymfonyJsonApi\Model\Resource\Annotation\ToOne; use Undabot\SymfonyJsonApi\Service\Resource\Validation\Constraint\ResourceType; /** @ResourceType(type="articles") */ final class ArticleReadModel implements ApiModel { public function __construct( public readonly string $id, /** @Attribute */ public readonly string $title, /** @ToOne(name="author", type="authors") */ public readonly string $authorId, /** * @var array<int,string> * @ToMany(name="comments", type="comments") */ public readonly array $commentIds, ) { } public static function fromSomething(Article $article): self { return new self( (string) $article->id(), (string) $article->title(), (string) $article->author()->id(), $article->comments()->map(static function (Comment $comment): string { return (string) $comment->id(); })->toArray(), ); } }
与创建和/或更新模型相同,读取模型由具有注解的属性组成。每个属性都有一个注解,表示它是否为关系或属性。
此类的另一个部分是在创建读取模型时使用的静态方法fromSomething
。如前所述,我们通常使用fromEntity
命名。此外,我们经常使用其他命名来表示此方法,例如fromValueObject
或fromAggregate
。我们通常使用的数据流与此类似
- 我们从数据库中以实体的形式获取数据,并通过我们的应用程序处理该实体。
- 我们使用实体,直到我们需要返回响应的那一刻。
- 在那个时刻,我们将实体传递给这个库以创建适当的读取模型。
在前面写下的所有内容中,我们已经涵盖了基本模型。如果模型不是那么基础,并且需要包含、排序、过滤等,怎么办?
包含
我们经常需要返回与给定资源相关的对象。也许我们的客户端需要的不仅仅是关系的id和类型,并且我们需要在我们的示例中包含所有评论和作者的详细信息。例如,我们的客户端需要类似以下响应。
{ "links": { "self": "http://example.com/articles", "next": "http://example.com/articles?page[offset]=2", "last": "http://example.com/articles?page[offset]=10" }, "data": [{ "type": "articles", "id": "01FTZBPQ1EY5P5N3QW4ZHHFCM3", "attributes": { "title": "JSON:API Symfony rocks!" }, "relationships": { "author": { "links": { "self": "http://example.com/articles/01FTZBPQ1EY5P5N3QW4ZHHFCM3/relationships/author", "related": "http://example.com/articles/01FTZBPQ1EY5P5N3QW4ZHHFCM3/author" }, "data": { "type": "authors", "id": "01FTZBN5HZ590S48WM0VEYFMBY" } }, "comments": { "links": { "self": "http://example.com/articles/01FTZBPQ1EY5P5N3QW4ZHHFCM3/relationships/comments", "related": "http://example.com/articles/01FTZBPQ1EY5P5N3QW4ZHHFCM3/comments" }, "data": [{ "type": "comments", "id": "01FTZBQRGVWX1AZG19NN0FMQZR" }, { "type": "comments", "id": "01FTZBNTAB25AM278R6G3YRCKT" } ] } }, "links": { "self": "http://example.com/articles/01FTZBPQ1EY5P5N3QW4ZHHFCM3" } }], "included": [{ "type": "authors", "id": "01FTZBN5HZ590S48WM0VEYFMBY", "attributes": { "firstName": "John", "lastName": "Doe", "twitter": "jhde" }, "links": { "self": "http://example.com/authors/01FTZBN5HZ590S48WM0VEYFMBY" } }, { "type": "comments", "id": "01FTZBQRGVWX1AZG19NN0FMQZR", "attributes": { "body": "First!" }, "relationships": { "author": { "data": { "type": "authors", "id": "01FTZBT0Q2G6G8ZGBBQ0DTSW1P" } } }, "links": { "self": "http://example.com/comments/01FTZBQRGVWX1AZG19NN0FMQZR" } }, { "type": "comments", "id": "01FTZBNTAB25AM278R6G3YRCKT", "attributes": { "body": "I like XML better" }, "relationships": { "author": { "data": { "type": "authors", "id": "01FTZBN5HZ590S48WM0VEYFMBY" } } }, "links": { "self": "http://example.com/comments/01FTZBNTAB25AM278R6G3YRCKT" } }] }
为了向客户端返回不仅仅是资源指针,我们需要传递一个对象数组,作为调用方法的第二个参数。例如,如果我们需要包括与文章相关的所有评论的详细信息,则将作为从响应者调用的方法的第二个参数传递一个评论实体数组。例如
<?php declare(strict_types=1); namespace App; class Controller { public function get( ArticleId $id, Responder $responder, ): ResourceCollectionResponse { $article = # fetch article by id // use toArray() method since comments are a collection return $responder->resource($article, $article->comments->toArray()); }
无论客户端是否明确请求包含项,还是响应需要包含所有可能的包含项,响应者都需要对响应中返回的每个对象的读取模型进行映射。
当在对象的列表中返回包含项时(例如,从/articles
端点返回文章列表),不同资源可能会有相同的关系。在这种情况下,我们只想包含关系一次。例如,如果我们有一个文章列表,其中一些文章有相同的作者,我们只想包含该作者一次。我们可以使用\Undabot\SymfonyJsonApi\Model\Collection\UniqueCollection来实现。
以下是一些示例
<?php declare(strict_types=1); namespace App; use Undabot\SymfonyJsonApi\Model\Collection\UniqueCollection; class Controller { public function list( Responder $responder, ): ResourceCollectionResponse { $articles = # fetch array of articles $includes = new UniqueCollection(); foreach ($articles as $article) { $includes->addObject($article->author()); // only one $includes->addObjects($article->comments()->toArray()); // multiple objects } // if there are many articles with same author, only 1 will be added return $responder->resource($article, $includes); }
<?php declare(strict_types=1); namespace App; use Undabot\JsonApi\Definition\Model\Request\GetResourceCollectionRequestInterface; use Undabot\SymfonyJsonApi\Model\Collection\UniqueCollection; class Controller { public function list( GetResourceCollectionRequestInterface $request, Responder $responder, ): ResourceCollectionResponse { $request->allowIncluded(['author', 'comments']); $articles = # fetch array of articles $includes = new UniqueCollection(); foreach ($articles as $article) { if (true === $request->isIncluded('author')) { $includes->addObject($article->author()); // only one } if (true === $request->isIncluded('comments')) { $includes->addObjects($article->comments()->toArray()); // multiple objects } } // note: you can also call $request->getIncludes() to retrieve array of all includes return $responder->resource($article, $includes); }
请求过滤器
如果端点需要过滤器,这是如何添加它们的。首先,通过调用allowFilters
方法允许过滤器。此方法接收一个字符串数组,实际上是过滤器的名称。允许过滤器后,您可以通过调用$request->getFilterSet()->getFilterValue('filter_name')
来读取它们的值。请注意,过滤支持处理集合的端点。
<?php declare(strict_types=1); namespace App; use Undabot\JsonApi\Definition\Model\Request\GetResourceCollectionRequestInterface; use Undabot\SymfonyJsonApi\Model\Collection\UniqueCollection; class Controller { public function list( GetResourceCollectionRequestInterface $request, Responder $responder, ): ResourceCollectionResponse { $request->allowFilters(['author.id', 'comment.ids']); // we can name this whatever we want (this could be author and comments also). $articles = $queryBus->handleQuery(new ArticlesQuery( $request->getFilterSet()?->getFilterValue('author.id'), $request->getFilterSet()?->getFilterValue('comment.ids'), )); return $responder->resource($article, $includes); }
分页
当前库可以读取基于页码和偏移量的分页。基于页码的分页会自动转换为基于偏移量的分页。因此,无论客户端发送给服务器的哪种方式,我们都可以像以下示例中那样读取它们。
<?php declare(strict_types=1); namespace App; use Undabot\JsonApi\Definition\Model\Request\GetResourceCollectionRequestInterface; use Undabot\SymfonyJsonApi\Model\Collection\UniqueCollection; class Controller { public function list( GetResourceCollectionRequestInterface $request, Responder $responder, ): ResourceCollectionResponse { $pagination = $request->getPagination(); $articles = $queryBus->handleQuery(new ArticlesQuery( $pagination?->getOffset(), $pagination?->getSize(), )); return $responder->resource($article, $includes); }
排序
与筛选类似,排序需要先允许。以下示例显示了如何启用和读取排序值。
<?php declare(strict_types=1); namespace App; use Undabot\JsonApi\Definition\Model\Request\GetResourceCollectionRequestInterface; use Undabot\SymfonyJsonApi\Model\Collection\UniqueCollection; class Controller { public function list( GetResourceCollectionRequestInterface $request, Responder $responder, ): ResourceCollectionResponse { $request->allowSorting(['article.id', 'article.createdAt', 'author.name']); // this can be any string, e.g. createdAt, article-createdAt, article_createdAt, ... $articles = $queryBus->handleQuery(new ArticlesQuery( $request->getSortSet()?->getSortsArray(), )); return $responder->resource($article, $includes); }
字段
我们可以允许客户端仅调用某些字段。
<?php declare(strict_types=1); namespace App; use Undabot\JsonApi\Definition\Model\Request\GetResourceRequestInterface; class Controller { public function get( ArticleId $id, Responder $responder, GetResourceRequestInterface $request, ): ResourceCollectionResponse { $request->allowFields(['title']); // now it's up to you will you fetch resource with only given fields // or you'll fetch entire resource and strip fields in read // model (make separate read model for all fields combination) $article = # fetch article by id return $responder->resource($article); }
响应器
响应器是我们需要用来将实体与其模型链接的粘合剂。它包含一个数组,该数组将(实体)类映射到模型内的可调用对象。
您创建的每个响应器(您可以在项目中拥有更多),都应该扩展 \Undabot\SymfonyJsonApi\Http\Service\Responder\AbstractResponder\AbstractResponder
类,并按照以下方式实现 getMap()
方法。
<?php declare(strict_types=1); namespace App; use Undabot\SymfonyJsonApi\Http\Service\Responder\AbstractResponder; final class Responder extends AbstractResponder { /** {@inheritdoc} */ public function getMap(): array { return [ SomeClass::class => [SomeReadModel::class, 'fromSomething'], ]; } }
这就是响应器如何在控制器中用于返回单个资源(getById
端点)的方式。
<?php declare(strict_types=1); namespace App; class Controller { public function get( Responder $responder, ): ResourceResponse { ... # fetch single entity return $responder->resource($singleEntity); }
这就是响应器如何在控制器中用于返回资源的集合(list
端点)。响应器会将数组中的每个实体转换为可读模型。
<?php declare(strict_types=1); namespace App; class Controller { public function list( Responder $responder, ): ResourceCollectionResponse { ... # fetch array of entities return $responder->resourceCollection($entities); }
如您所见,响应器支持几种不同的方法,您可以根据需要返回给客户端的响应来使用它们。每种方法都接受一个数据对象(或数据对象的集合/数组),以及一些其他可选参数,并构造一个表示 JSON:API 兼容响应的 DTO。以下是支持的方法列表
- \Undabot\SymfonyJsonApi\Http\Model\Response\ResourceCollectionResponse
- \Undabot\SymfonyJsonApi\Http\Model\Response\ResourceCreatedResponse
- \Undabot\SymfonyJsonApi\Http\Model\Response\ResourceUpdatedResponse
- \Undabot\SymfonyJsonApi\Http\Model\Response\ResourceResponse
- \Undabot\SymfonyJsonApi\Http\Model\Response\ResourceDeletedResponse(不接受任何内容)
ViewResponseSubscriber(\Undabot\SymfonyJsonApi\Http\EventSubscriber\ViewResponseSubscriber
)将然后编码响应器生成的响应为 JSON:API 兼容的 JSON 响应。此外,它将为响应添加正确的 HTTP 状态码,例如,如果从响应器中调用了 ResourceCreatedMethod
,则为 201
,或者如果您调用了 ResourceDeletedResponse
,则为 204
。
resourceCollection(...) 方法
接受一个数据对象数组,您已在响应器中为它定义了编码映射条目,并将其转换为 ResourceCollectionResponse。
public function resourceCollection( array $primaryData, array $includedData = null, array $meta = null, array $links = null ): ResourceCollectionResponse()
resourceObjectCollection(...) 方法
接受一个对象数组,您已在响应器中为它定义了编码映射条目,并将其转换为 ResourceCollectionResponse。
public function resourceObjectCollection( ObjectCollection $primaryModels, array $included = null, array $meta = null, array $links = null ): ResourceCollectionResponse
resource(...) 方法
接受数据,例如单个对象,该对象将被转换为 ResourceResponse。数据也可以为 null,如果没有数据存在。例如,如果某人请求 /user/1/car
,而汽车是一个关系,它不在用户上,因为用户没有汽车。在这个例子中,具有 id 1 的用户存在于数据库中。
public function resource( $primaryData, array $includedData = null, array $meta = null, array $links = null ): ResourceResponse {
resourceCreated(...) 方法
接受数据,例如单个对象,该对象将被转换为 ResourceCreatedResponse。
public function resourceCreated( $primaryData, array $includedData = null, array $meta = null, array $links = null ): ResourceCreatedResponse
resourceUpdated(...) 方法
接受数据,例如单个对象,该对象将被转换为 ResourceUpdatedResponse。
public function resourceUpdated( $primaryData, array $includedData = null, array $meta = null, array $links = null ): ResourceUpdatedResponse
resourceDeleted(...) 方法
ResourceDeletedResponse 响应基本上是 204 HTTP 状态码,没有内容。
public function resourceDeleted(): ResourceDeletedResponse
配置
异常监听器具有默认优先级 -128,但它可以通过创建 config/packages/json_api_symfony.yaml
并使用以下参数进行配置。
json_api_symfony: exception_listener_priority: 100
开发
有一个自定义的Docker镜像,您可以用它进行开发。此存储库被挂载在容器内部,对文件的任何更改都会自动传播到容器中。您可以使用容器运行测试并检查兼容性问题。由于文件系统指向两个位置,所以没有同步。
使用名为dev.sh
的脚本来管理镜像。以下是可用的命令
-
构建基于开发的基础Docker镜像,并在首次运行时安装composer和依赖项
./dev.sh build
-
启动开发容器
./dev.sh run
-
停止开发容器
./dev.sh stop
-
附加容器shell到终端,以便您可以在容器内部执行命令
./dev.sh ssh
-
在运行的容器中运行PHP单元测试
./dev.sh test
-
执行代码检查和运行测试
./dev.sh qc
-
执行composer install --optimize-autoloader
./dev.sh install
术语表
性能
性能是每个后端应用的重要部分。我们在1个vCPU(1797.917 MHz)服务器上,1 GB的RAM上测试了返回资源列表的性能。安装的PHP版本为8.0.3,Symfony包为6.0.*。xDebug已禁用。
测试的请求是一个包含关系的10个资源的列表。
示例
{
"jsonapi": {
"version": "1.0"
},
"meta": {
"total": 501
},
"data": [
{
"type": "products",
"id": "01FR7ZF0CVC7DWYAXK9NN75B2E",
"attributes": {
"name": "khlywk oingpndl",
"price": 34575
},
"relationships": {
"productVariants": {
"data": [
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403K8"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403K9"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KA"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KB"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KC"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KD"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KE"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KF"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KG"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KH"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KJ"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KK"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KM"
},
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403KN"
}
]
}
}
},
.....
"included": [
{
"type": "product-variants",
"id": "01GPBFYZCGF6Y6S99J4YG403K8",
"attributes": {
"color": "LightPink",
"size": "eum",
"multiplier": 0.8
},
"relationships": {
"product": {
"data": {
"type": "products",
"id": "01FR7ZF0CVC7DWYAXK9NN75B2E"
}
}
}
},
.....
响应大约为17.5 kb,平均响应时间为158 ms。
同样,没有使用库,通过在控制器内部循环数据创建JSON:API兼容的PHP数组,并将这些数组作为JsonResponse返回的方式进行了测试。
响应大约为17.5 kb,平均响应时间为140 ms。
同样的资源以纯文本响应返回,没有任何标准化。
示例
[
{
"id": "01FR7ZF0CVC7DWYAXK9NN75B2E",
"name": "khlywk oingpndl",
"price": 34575,
"variants": [
{
"id": "01GPBFYZCGF6Y6S99J4YG403K8",
"color": "LightPink",
"size": "eum",
"multiplier": 0.8
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403K9",
"color": "WhiteSmoke",
"size": "ut",
"multiplier": 0.4
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KA",
"color": "Aqua",
"size": "quibusdam",
"multiplier": 1
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KB",
"color": "SandyBrown",
"size": "minima",
"multiplier": 2.1
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KC",
"color": "GreenYellow",
"size": "ipsa",
"multiplier": 1.3
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KD",
"color": "PeachPuff",
"size": "sit",
"multiplier": 0.3
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KE",
"color": "LavenderBlush",
"size": "aut",
"multiplier": 0.3
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KF",
"color": "LawnGreen",
"size": "dolor",
"multiplier": 1.3
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KG",
"color": "MediumBlue",
"size": "eos",
"multiplier": 0.1
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KH",
"color": "DarkSlateGray",
"size": "esse",
"multiplier": 1.5
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KJ",
"color": "Gray",
"size": "consequatur",
"multiplier": 2.9
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KK",
"color": "CadetBlue",
"size": "fuga",
"multiplier": 1.7
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KM",
"color": "Aqua",
"size": "tenetur",
"multiplier": 1.3
},
{
"id": "01GPBFYZCGF6Y6S99J4YG403KN",
"color": "FireBrick",
"size": "esse",
"multiplier": 1.3
}
]
},
.....
响应大约为6 kb,平均响应时间为103 ms。