undabot/json-api-symfony

允许 symfony 应用轻松处理与 JSON API 兼容的请求和响应

安装数: 25,322

依赖者: 1

建议者: 0

安全性: 0

星标: 1

关注者: 7

分支: 0

开放问题: 3

类型:symfony-bundle


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 注解表示该属性是关系。关系必须在 ToOneToMany 注解中包含名称和类型值,例如:

/**
  * @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,
    ) {
    }
}

此类完全由请求数据创建,而不是由另一个类创建,因此它只有一个构造函数。每个属性都有一个注解,表示它是一个关系或属性。每个关系在 ToOneToMany 注解中都必须有名称和类型值。
正如您稍后将看到的那样,我们通常在读取和写入模型中具有相同的属性。因此,如果您遇到这种情况,您可以将它们合并到同一个模型中,例如 ArticleModel。此外,如果您的更新模型与写入模型相同,则可以将它们合并为一个,并有一个写入模型(用于创建和更新),以及一个读取模型。

现在,当您知道如何创建写入侧的模型时,让我们看看还需要什么。假设您已经有一个文章实体,这里缺少的是控制器。
要从请求中提取数据到模型中,我们需要注入整个请求。下面是一个示例,我们将使用 SimpleResourceHandlerCreateResourceRequestInterface(及其具体实现)来帮助。

<?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命名。此外,我们经常使用其他命名来表示此方法,例如fromValueObjectfromAggregate。我们通常使用的数据流与此类似

  1. 我们从数据库中以实体的形式获取数据,并通过我们的应用程序处理该实体。
  2. 我们使用实体,直到我们需要返回响应的那一刻。
  3. 在那个时刻,我们将实体传递给这个库以创建适当的读取模型。

在前面写下的所有内容中,我们已经涵盖了基本模型。如果模型不是那么基础,并且需要包含、排序、过滤等,怎么办?

包含

我们经常需要返回与给定资源相关的对象。也许我们的客户端需要的不仅仅是关系的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。以下是支持的方法列表

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。