我不确定为什么每次更改 PHP 注释(更新验证约束 #[Assert/Type]、更新 #[ApiResource] 中的操作...),然后向任何 api 端点发送新请求时,都需要返回响应的时间很多(大约8秒)。
此后,后续请求的响应时间恢复正常(约500ms)。似乎 symfony/api 平台每次 PHP 注释发生任何更改时都会重建整个缓存,并且不知何故这个过程需要很长时间才能完成。
我使用的是最新的Symfony 6.3和Api平台3.2。 对于 Web 服务器,我使用内置的 symfony 服务服务器。
这是我的 api_platform.yaml 和 env.local 配置
api_platform:
title: Hello API Platform
version: 1.0.0
formats:
jsonld: ['application/ld+json']
json: ['application/json']
html: ['text/html']
jsonhal: ['application/hal+json']
docs_formats:
jsonld: ['application/ld+json']
jsonopenapi: ['application/vnd.openapi+json']
html: ['text/html']
defaults:
pagination_items_per_page: 5
pagination_client_items_per_page: true
pagination_client_enabled: true
collection:
pagination:
items_per_page_parameter_name: itemsPerPage
stateless: true
cache_headers:
vary: ['Content-Type', 'Authorization', 'Origin']
extra_properties:
standard_put: true
rfc_7807_compliant_errors: true
event_listeners_backward_compatibility_layer: false
keep_legacy_inflector: false
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=54e7442b72dbc7099aef4ae1aae2300b
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
DATABASE_URL="mysql://[email protected]:3306/api_platform?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
#DATABASE_URL="postgresql://app:[email protected]:5432/app?serverVersion=15&charset=utf8"
###< doctrine/doctrine-bundle ###
###> nelmio/cors-bundle ###
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
###< nelmio/cors-bundle ###
这是用户实体
<?php
namespace App\Entity;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\Entity\Traits\Timestamp;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;
use Gedmo\Mapping\Annotation as Gedmo;
use Gedmo\Mapping\Annotation\Timestampable;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Constraints\NotBlank;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ApiResource(
operations: [
new Get(normalizationContext: ['groups' => ['users:read', 'users:item:read']]),
new GetCollection(),
new Post(),
new Put(),
new Patch(),
new Delete()
],
normalizationContext: ['groups' => 'users:read'],
denormalizationContext: ['groups' => 'users:write']
)]
#[UniqueEntity(fields: 'email', message: 'There is already an account with this email')]
#[UniqueEntity(fields: 'name', message: 'There is already an account with this name')]
#[ApiFilter(PropertyFilter::class)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
use Timestamp;
/**
* @var \DateTime|null
* @Timestampable(on="create")
* @Column(type="datetime")
*/
#[Timestampable(on: 'create')]
#[Column(type: Types::DATETIME_MUTABLE)]
#[Groups(['users:read'])]
protected $createdAt;
/**
* @var \DateTime|null
* @Gedmo\Timestampable(on="update")
* @ORM\Column(type="datetime")
*/
#[Timestampable(on: 'update')]
#[Column(type: Types::DATETIME_MUTABLE)]
#[Groups(['users:read'])]
protected $updatedAt;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[Assert\Type('string')]
#[Assert\Email()]
#[ORM\Column(length: 180, unique: true)]
#[Groups(['users:read', 'users:write', 'treasure:item:read'])]
#[NotBlank]
private ?string $email = null;
#[ORM\Column]
private array $roles = [];
/**
* @var string The hashed password
*/
#[ORM\Column]
#[Groups(['users:write'])]
#[NotBlank]
private ?string $password = null;
#[ORM\Column(length: 255)]
#[Groups(['users:read', 'users:write', 'treasure:item:read'])]
#[NotBlank]
private ?string $name = null;
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: DragonTreasure::class, cascade: ['persist'])]
#[Groups(['users:read', 'users:write'])]
#[Assert\Valid]
private Collection $dragonTreasures;
public function __construct()
{
$this->dragonTreasures = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
/**
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
/**
* @see UserInterface
*/
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
/**
* @return Collection<int, DragonTreasure>
*/
public function getDragonTreasures(): Collection
{
return $this->dragonTreasures;
}
public function addDragonTreasure(DragonTreasure $dragonTreasure): static
{
if (!$this->dragonTreasures->contains($dragonTreasure)) {
$this->dragonTreasures->add($dragonTreasure);
$dragonTreasure->setOwner($this);
}
return $this;
}
public function removeDragonTreasure(DragonTreasure $dragonTreasure): static
{
if ($this->dragonTreasures->removeElement($dragonTreasure)) {
// set the owning side to null (unless already changed)
if ($dragonTreasure->getOwner() === $this) {
$dragonTreasure->setOwner(null);
}
}
return $this;
}
}
这是龙宝实体
<?php
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\Entity\Traits\Timestamp;
use App\Repository\DragonTreasureRepository;
use Carbon\Carbon;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;
use Gedmo\Mapping\Annotation as Gedmo;
use Gedmo\Mapping\Annotation\Timestampable;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
use Symfony\Component\Validator\Constraints\LessThanOrEqual;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Contracts\Service\Attribute\Required;
use function Symfony\Component\String\u;
#[ApiResource(
shortName: 'Treasure',
description: 'Rare and valuable resources',
operations: [
new Get(normalizationContext: ['groups' => ['treasure:read', 'treasure:item:read']]),
new GetCollection(),
new Post(),
new Put(),
new Patch(),
new Delete()
],
formats: [
'jsonld',
'json',
'html',
'jsonhal',
'csv' => 'text/csv'
],
normalizationContext: [
'groups' => ['treasure:read']
],
denormalizationContext: [
'groups' => ['treasure:write']
],
paginationItemsPerPage: 10
)]
#[ApiResource(
uriTemplate: 'users/{user_id}/treasures.{_format}',
shortName: 'Treasure',
operations: [
new GetCollection()
],
uriVariables: [
'user_id' => new Link(
fromProperty: 'dragonTreasures', fromClass: User::class,
// toProperty: 'owner'
)
],
normalizationContext: [
'groups' => ['treasure:read']
],
)]
#[ApiFilter(BooleanFilter::class, properties: ['isPublished'])]
#[ApiFilter(PropertyFilter::class)]
#[ORM\Entity(repositoryClass: DragonTreasureRepository::class)]
#[ApiFilter(SearchFilter::class, properties: ['owner.name' => 'partial'])]
class DragonTreasure
{
use Timestamp;
/**
* @var \DateTime|null
* @Timestampable(on="create")
* @Column(type="datetime")
*/
#[Timestampable(on: 'create')]
#[Column(type: Types::DATETIME_MUTABLE)]
#[Groups(['treasure:read'])]
protected $createdAt;
/**
* @var \DateTime|null
* @Gedmo\Timestampable(on="update")
* @ORM\Column(type="datetime")
*/
#[Timestampable(on: 'update')]
#[Column(type: Types::DATETIME_MUTABLE)]
#[Groups(['treasure:read'])]
protected $updatedAt;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['treasure:read'])]
private ?int $id = null;
#[Groups(['treasure:read', 'treasure:write', 'users:item:read'])]
#[ORM\Column(length: 255)]
#[ApiFilter(SearchFilter::class, strategy: SearchFilter::STRATEGY_IPARTIAL)]
#[NotBlank]
#[NotNull]
private ?string $name = null;
#[Groups(['treasure:read', 'users:item:read'])]
#[ORM\Column(type: Types::TEXT)]
#[ApiFilter(SearchFilter::class, strategy: SearchFilter::STRATEGY_IPARTIAL)]
private ?string $description = null;
/**
* Value of the treasure
*/
#[Groups(['treasure:read', 'treasure:write', 'users:item:read'])]
#[ORM\Column]
#[ApiFilter(RangeFilter::class)]
#[GreaterThanOrEqual(0)]
#[NotBlank]
#[NotNull]
#[Type('integer')]
private ?int $value = 0;
#[Groups(['treasure:read', 'treasure:write'])]
#[ORM\Column]
#[GreaterThanOrEqual(0)]
#[LessThanOrEqual(10)]
#[NotBlank]
#[NotNull]
#[Type('numeric')]
private ?int $coolFactor = 0;
#[Groups(['treasure:read', 'treasure:write'])]
#[ORM\Column]
private ?bool $isPublished = false;
#[Groups(['treasure:read'])]
#[ORM\ManyToOne(inversedBy: 'dragonTreasures')]
#[ORM\JoinColumn(name: 'owner_id', onDelete: 'CASCADE')]
#[ApiFilter(SearchFilter::class, 'exact')]
private ?User $owner = null;
public function __construct(string $name = null)
{
$this->name = $name;
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
#[Groups(['treasure:read'])]
public function getShortDescription(): ?string
{
return u($this->description)->truncate(10, '...');
}
public function getDescription(): ?string
{
return $this->description;
}
#[Groups(['treasure:read'])]
public function setDescription(string $description): static
{
$this->description = $description;
return $this;
}
#[Groups(['treasure:read'])]
public function getCreatedAtAgo(): ?string
{
return Carbon::parse($this->createdAt)->diffForHumans();
}
#[Groups(['treasure:write'])]
#[SerializedName('description')]
public function setTextDescription(string $description): static
{
$this->description = nl2br($description);
return $this;
}
public function getValue(): ?int
{
return $this->value;
}
public function setValue(int $value): static
{
$this->value = $value;
return $this;
}
public function getCoolFactor(): ?int
{
return $this->coolFactor;
}
public function setCoolFactor(int $coolFactor): static
{
$this->coolFactor = $coolFactor;
return $this;
}
public function getIsPublished(): ?bool
{
return $this->isPublished;
}
public function setIsPublished(bool $isPublished): static
{
$this->isPublished = $isPublished;
return $this;
}
public function getOwner(): ?User
{
return $this->owner;
}
public function setOwner(?User $owner): static
{
$this->owner = $owner;
return $this;
}
}
如果您对我有任何建议,我将不胜感激。
出于性能原因,Symfony 预处理并缓存了很多东西,包括。注释驱动的行为,因此数据中的任何更改都会使缓存失效,并将迫使 Symfony 重建它,这可能需要一段时间,具体取决于您的项目。 8 秒的观察是一个明显的延迟,但我不知道你的运行时环境是什么,所以很难对此做出评论。另请注意,内置服务器不适用于生产用途。
最后,您可以手动管理缓存,因为有多个命令可以帮助完成该任务,包括在启动应用程序之前进行重建:
$ bin/console cache:warmup
请参阅以下内容了解更多:
$ bin/console list cache
$ bin/console list doctrine:cache