为什么每次更新PHP注解(PHP属性)后第一次请求都很慢?

问题描述 投票:0回答:1

我不确定为什么每次更改 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;
    }
}

如果您对我有任何建议,我将不胜感激。

php rest symfony api-platform.com
1个回答
0
投票

出于性能原因,Symfony 预处理并缓存了很多东西,包括。注释驱动的行为,因此数据中的任何更改都会使缓存失效,并将迫使 Symfony 重建它,这可能需要一段时间,具体取决于您的项目。 8 秒的观察是一个明显的延迟,但我不知道你的运行时环境是什么,所以很难对此做出评论。另请注意,内置服务器不适用于生产用途。

最后,您可以手动管理缓存,因为有多个命令可以帮助完成该任务,包括在启动应用程序之前进行重建:

$ bin/console cache:warmup

请参阅以下内容了解更多:

$ bin/console list cache
$ bin/console list doctrine:cache
© www.soinside.com 2019 - 2024. All rights reserved.