我不确定为什么每次我更改PHP注解(更新验证约束#[Assert/Type],更新#[ApiResource]中的操作......),然后向任何api端点发送新请求时,都需要花费大量时间才能返回响应(大约8秒)。
之后,后续请求的响应时间恢复正常(约500 ms)。似乎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;
}
}
如果你能给我任何建议,我将不胜感激。
2条答案
按热度按时间dced5bon1#
由于性能原因,Symfony预处理和缓存了很多东西,包括注解驱动的行为,因此数据中的任何更改都会使该高速缓存无效,并将迫使Symfony重新构建它,这可能需要一段时间,具体取决于您的项目。8秒的观察是一个明显的延迟,但是我不知道你的运行环境是什么,所以很难评论。另外请注意,内置的服务器并不适合生产使用。
作为对策,您可以尝试手动管理该高速缓存,因为有多个CLI命令可以帮助完成该任务,包括在启动应用程序之前重建它:
查看更多可能性:
s71maibg2#
缓存前的缓存往往适得其反。
假设Symphony进入MySQL,请注意数据库本身做了很多缓存工作。这种加倍的工作 * 可能 * 会减慢整个过程。
此外,Symphony可能缓存的内容远远超过了它的需要。
尝试关闭Symphony的全部(或部分)缓存。在MySQL中尝试同样的做法是不合理的,除非你的RAM已经用完了。交换对性能来说是可怕的;最好收缩chaches而不是依赖于交换来获得额外的空间。