[RFC] Resources defined outside of PHP code (runtime / external metadata sources)

未关闭
#8,791 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 2 天内回复

@alanpoulain 已经在做这个了。

开始于 2026年11月5日。

  • #8813 来自 @alanpoulain —— 未关闭

评估

难度
5/5
预计耗时
一周以上
新手友好度
30/100
Issue 类型
功能
描述清晰度
基本清楚
活跃度
活跃
技术栈
php, symfony

调研方向

Start by reading AbstractItemNormalizer, IriConverter, ResourceClassResolver, and the cached metadata factory implementations named in the issue. The proposal has unresolved design questions about resource identity, instantiation, and cache invalidation, so first agree on the direction with maintainers. Done means implementing the agreed runtime resource support and covering resource discovery, serialization, instantiation, and IRI behavior with tests.

由索引模型根据 Issue 内容生成。

描述

Summary

Allow API resources declared from a source other than PHP code (a database, another API, a configuration
service) to work without a PHP class per resource. Discovery and metadata can already be provided by
decorating the metadata factories; what is missing are a few runtime hooks, because the serializer, the IRI
converter and the resource class resolver derive the resource from $object::class.

Use case

Administrators define content models at runtime (fields, types, relations) without deployment or migration.
We want each model to be a real API resource:

  • GET /dynamic/faq/{id} → "@type": "DynamicFaq", properties question, answer, theme…
  • per-model JSON Schema / OpenAPI / Hydra documentation
  • per-model validation derived from field definitions
  • IRIs generated and resolved like any other resource (getIriFromResource, getResourceFromIri)
  • a new or modified model available without a deployment

How the data is persisted is up to the application (in our case a generic Doctrine entity with a JSON
column), exactly as with DTOs and stateOptions today.

Similar needs: headless CMS builders, form builders, multi-tenant schemas, schemaless document stores,
resources proxied from a remote API whose schema is discovered at runtime.

What already works

Decorating ResourceNameCollectionFactoryInterface, ResourceMetadataCollectionFactoryInterface (below the
cache) and the property factories lets an application declare resources, operations and properties from any
source. The Laravel Eloquent integration does it for properties. Types come from ApiProperty::nativeType /
schema, so no reflection is needed. Operations, routes and documentation can be generated this way,
provided a few metadata factories stop assuming the resource is a class (see below): with the resource
metadata declared at the innermost level (so that the core factories still apply their defaults), route
loading currently fails with ReflectionException: Class "DynamicFaq" does not exist.

What breaks

Once a resource is identified by a name that is not a class (DynamicFaq):

Where Code Effect
ParameterResourceMetadataCollectionFactory::createParametersFromAttributes() new \ReflectionClass($resourceClass) ReflectionException while loading routes
FiltersResourceMetadataCollectionFactory new \ReflectionClass($resourceClass) ResourceClassNotFoundException
Doctrine ORM/ODM metadata factories (resource collection, parameters, links, properties, discriminator) ManagerRegistry::getManagerForClass() ReflectionException from Doctrine on an unknown class
AbstractItemNormalizer::instantiateObject() new \ReflectionClass($class) ReflectionException on every write operation
AbstractItemNormalizer::normalize() $context['force_resource_class'] ?? $this->getObjectClass($data) resource derived from the PHP class of the object
IriConverter::getIriFromResource() same wrong or missing IRI for nested relations, cache purge, Mercure
ResourceClassResolver::getResourceClass() / isResourceClass() getObjectClass() + is_a() an object cannot belong to a resource that is not a class
CachedResourceNameCollectionFactory, Cached*MetadataFactory no invalidation a new resource is only visible after a cache clear

Workarounds without core changes:

  1. Generate one PHP class per resource at runtime: code generation in production, opcache/container
    rebuilds, multi-server synchronisation.
  2. Map all resources onto one shared class: getObjectClass() always returns that class, so the resource
    of a nested object cannot be determined.
  3. Decorate the normalizers, IriConverter and ResourceClassResolver: re-implementing part of the core
    outside the project, fragile across upgrades.

Proposal

1. Objects carrying their resource
interface ResourceAwareInterface
{
    public function getResource(): string;
}

When an object implements it, AbstractItemNormalizer, IriConverter and ResourceClassResolver use
getResource() instead of $object::class. Nested relations and getIriFromResource() outside the
operation context then work as for any resource.

Optionally, the core could ship a generic implementation for applications that do not want their own:

final class DynamicResource implements ResourceAwareInterface, \ArrayAccess, \IteratorAggregate
{
    public function __construct(private readonly string $resource, private array $data = []) {}

    public function getResource(): string { return $this->resource; }
}
2. Instantiation hook for name-identified resources

When the resource is not a class, the serializer delegates instantiation (and property read/write) instead
of calling new \ReflectionClass(): by default to DynamicResource, or to a class declared in the
resource metadata. The processor receives that object and maps it to the domain model, as DTOs with
stateOptions do today.

3. Metadata cache invalidation (optional)

Cache tags on the metadata pools + MetadataCacheInvalidatorInterface::invalidate(string $resource), so a
resource added or modified at runtime is picked up without a cache clear. Routing can rely on a catch-all
route or a router cache fingerprint, left to the application at first. GraphQL is out of scope.

Backward compatibility

No BC break: everything is opt-in and introduced through new interfaces and classes. Existing interfaces are
not modified, and objects that do not implement ResourceAwareInterface behave exactly as today. It can ship
in a 5.x minor, flagged experimental.

Known limitation: many extension points document string $resourceClass as class-string. Third-party code
assuming so (new \ReflectionClass($resourceClass), class_exists(), getRepository()) keeps working for
class-based resources but will not support name-identified resources. This should be documented for
extension authors.

Open questions

  1. Is a resource identity that is not a class name acceptable for the project?
  2. ResourceAwareInterface only, or also a generic DynamicResource shipped by the core?
  3. Resource name vs. shortName: reuse shortName as identity, or introduce a new key?
  4. Is metadata cache invalidation in scope, or left to applications?

Contribution

We can implement the ResourceAwareInterface support first, then the instantiation hook, after agreement on
the direction. We can also contribute documentation on providing resources by decorating the metadata
factories.

主要语言
PHP
星标
2.6k
派生
988
平均合并
4 天 10 小时
30 天内合并 PR
114

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

api-platform/core 的其他 Issue

查看 api-platform/core 的全部 Issue

相似的 Issue

更多 PHP Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。