IriConverter: localOperationCache is always written but never read
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 85/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Ambito
- api, backend, performance
Direzione di ricerca
Inizia in src/Symfony/Routing/IriConverter.php, concentrandoti su getIriFromResource() e sulla ricerca locale in operation-cache intorno alla chiamata a metadata-factory. Esamina IriConverterTest::testGetIriFromItemWithoutOperation, aggiungi una copertura che dimostri che le chiamate ripetute riutilizzano la cache ed esegui la suite di test di IriConverter per verificare che la costosa ricerca dei metadati venga evitata.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
API Platform version(s) affected: 4.3.3
Description
In a getCollection, the AbstractItemNormalizer will use the IriConverter to find the IRIs for all entities in the collection, including related entities. To find these IRI's, the resource metadata has to be created to find what operation to use to generate the IRI. This resource metadata is cached in the IriConverter, but will only be read if an $operation was specified. The AbstractItemNormalizer does not specify this, so the cache is always written but never read.
This is the relevant method in ApiPlatform\Symfony\Routing\IriConverter:
public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): string
{
$resourceClass = $context['force_resource_class'] ?? (\is_string($resource) ? $resource : $this->getObjectClass($resource));
if ($this->operationMetadataFactory && isset($context['item_uri_template'])) {
$operation = $this->operationMetadataFactory->create($context['item_uri_template']);
}
$localOperationCacheKey = ($operation?->getName() ?? '').$resourceClass.(\is_string($resource) ? '_s' : '_o').($operation instanceof CollectionOperationInterface ? '_c' : '_i');
// ❗️This short-circuits on $operation, so if no $operation is present the localOperationCache will never be used
if ($operation && isset($this->localOperationCache[$localOperationCacheKey])) {
return $this->generateSymfonyRoute($resource, $referenceType, $this->localOperationCache[$localOperationCacheKey], $context, $this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] ?? null);
}
if (!($isResourceClass = $this->resourceClassResolver->isResourceClass($resourceClass)) && !isset($context['item_uri_template'])) {
return $this->generateSkolemIri($resource, $referenceType, $operation, $context, $resourceClass);
}
$context['is_resource_class'] = $isResourceClass;
$context['current_resource_class'] = $resourceClass;
// This is only for when a class (that is not a resource) extends another one that is a resource, we should remove this behavior
if (!\is_string($resource) && !isset($context['force_resource_class']) && !isset($context['item_uri_template'])) {
$resourceClass = $this->getResourceClass($resource, true);
}
if (!$operation) {
$operation = (new Get())->withClass($resourceClass);
}
if ($operation instanceof HttpOperation && 301 === $operation->getStatus()) {
$operation = ($operation instanceof CollectionOperationInterface ? new GetCollection() : new Get())->withClass($operation->getClass());
unset($context['uri_variables']);
}
$identifiersExtractorOperation = $operation;
// In symfony the operation name is the route name, try to find one if none provided
if (
!$operation->getName()
|| ($operation instanceof HttpOperation && 'POST' === $operation->getMethod())
) {
$forceCollection = $operation instanceof CollectionOperationInterface;
try {
// ❗️This is the actual expensive call
$operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(null, $forceCollection, true);
$identifiersExtractorOperation = $operation;
} catch (OperationNotFoundException) {
}
}
if (!$operation->getName() || ($operation instanceof HttpOperation && $operation->getUriTemplate() && str_starts_with($operation->getUriTemplate(), SkolemIriConverter::$skolemUriTemplate))) {
return $this->generateSkolemIri($resource, $referenceType, $operation, $context, $resourceClass);
}
$this->localOperationCache[$localOperationCacheKey] = $operation;
$this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] = $identifiersExtractorOperation;
return $this->generateSymfonyRoute($resource, $referenceType, $operation, $context, $identifiersExtractorOperation);
}
How to reproduce
IriConverterTest::testGetIriFromItemWithoutOperation touches on the logic that's affected by this bug, but does not assert the cache was used.
Possible Solution
Do not check if $operation is truthy before trying to read the cache:
--- a/src/Symfony/Routing/IriConverter.php
+++ b/src/Symfony/Routing/IriConverter.php
@@ -160,10 +160,15 @@
!$operation->getName()
|| ($operation instanceof HttpOperation && 'POST' === $operation->getMethod())
) {
- $forceCollection = $operation instanceof CollectionOperationInterface;
- try {
- $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(null, $forceCollection, true);
- $identifiersExtractorOperation = $operation;
- } catch (OperationNotFoundException) {
+ if (isset($this->localOperationCache[$localOperationCacheKey])) {
+ $operation = $this->localOperationCache[$localOperationCacheKey];
+ $identifiersExtractorOperation = $this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] ?? $operation;
+ } else {
+ $forceCollection = $operation instanceof CollectionOperationInterface;
+ try {
+ $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(null, $forceCollection, true);
+ $identifiersExtractorOperation = $operation;
+ } catch (OperationNotFoundException) {
+ }
}
}
Additional Context
Entry points:
- for each item's own '@id':
JsonLd\Serializer\ItemNormalizer::normalize():121does try to supply
one via$context['operation'] ?? null, but the context no longer has it —
AbstractCollectionNormalizerbuilds the per-item context throughcreateOperationContext(),
which ends atOperationContextTrait.php:53with
unset($context['operation'], $context['operation_name']);. - for related objects:
AbstractItemNormalizer::normalizeRelation():982passes only two named arguments,
so$operationdefaults tonull:$context['iri'] = $iri = $this->iriConverter->getIriFromResource(resource: $relatedObject, context: $context);
On a real endpoint, 2,337 items with ~6 IRIs each, Xdebug profile, PHP 8.4.20, Symfony 7.4.8, api-platform/core 4.3.3: getIriFromResource() is called 14,362 times, and fell through to $this->resourceMetadataCollectionFactory->create 14,360 times.
- Lingua principale
- PHP
- Stelle
- 2.6k
- Fork
- 987
- Merge medio
- 1g 10h
- PR unite (30g)
- 76
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di api-platform/core
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
api-platform/core#8665 ·
I maintainer di solito rispondono entro 1 giorno
-
Doctrine\Orm\OrderExtension fails on SortDirectionForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
api-platform/core#8660 ·
I maintainer di solito rispondono entro 1 giorno
-
DeserializeProvider calls PartialDenormalizationException::getErrors(), deprecated in Symfony 8.1Forse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
api-platform/core#8650 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
api-platform/core#8649 ·
I maintainer di solito rispondono entro 1 giorno
-
`OrderExtension` and `OrderFilter` pass string sort directions, deprecated since `doctrine/orm` 3.7Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
api-platform/core#8648 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di api-platform/core
Issue simili
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 84/100
WordPress/hosting-handbook#506 ·
I maintainer di solito rispondono entro 2 giorni
-
Installing a plugin with composer_packages installs dev dependencies and breaks the autoloader (Error 500)Forse già presa @Boy132 l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
I maintainer di solito rispondono entro 1 giorno
-
nginx: static-file locations drop the security headers, and the dotfile deny is matched after themApertabug Low Security
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 78/100
backdrop-contrib/commerce#60 ·