Skip to content

Instantly share code, notes, and snippets.

@vincentchalamon
Last active February 2, 2025 09:45
Show Gist options
  • Save vincentchalamon/456fb84af4ddf1281a63a0a06e633c60 to your computer and use it in GitHub Desktop.
Save vincentchalamon/456fb84af4ddf1281a63a0a06e633c60 to your computer and use it in GitHub Desktop.
Polymorphism with API Platform
<?php
#[ApiResource(
operations: [
// no GetCollection operation
new Get(...),
new Post(...),
new Patch(...),
],
)]
final class CustomCalendarPeriod implements Period
{
public const string PERIOD_TYPE = 'custom';
#[Groups(groups: ['period:read'])]
public function getPeriodType(): string
{
return self::PERIOD_TYPE;
}
// ...
}
<?php
#[ApiResource(
operations: [
new GetCollection(
uriTemplate: '/periods',
provider: PeriodCollectionProvider::class,
normalizationContext: [
'groups' => ['period:read'],
],
),
],
)]
interface Period
{
public function getPeriodType(): string;
}
<?php
#[AsDecorator(decorates: 'api_platform.jsonld.context_builder')]
final readonly class PeriodsContextBuilder implements AnonymousContextBuilderInterface
{
public function __construct(private AnonymousContextBuilderInterface $decorated)
{
}
/**
* Overrides Period context with external contexts for polymorphism.
*
* @see https://json-ld.org/spec/ED/json-ld-syntax/20120122/#external-contexts
* @see https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/
*
* @phpstan-ignore-next-line return type has no value type specified in iterable type array
*/
public function getResourceContext(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): array
{
$context = $this->decorated->getResourceContext($resourceClass, $referenceType);
if ($resourceClass === Period::class) {
// keep "@vocab" and "hydra" from API Platform JSON-LD context as generic keys
// override the rest of the context to disable the generated schema and implement polymorphism
return [
['@vocab' => $context['@vocab']],
['hydra' => $context['hydra']],
'/contexts/SubscribedCalendarPeriod',
'/contexts/CustomCalendarPeriod',
];
}
return $context;
}
/**
* @phpstan-ignore-next-line $context has no value type specified in iterable type array
*/
public function getAnonymousResourceContext(object $object, array $context = [], int $referenceType = UrlGeneratorInterface::ABS_PATH): array
{
return $this->decorated->getAnonymousResourceContext($object, $context, $referenceType);
}
/**
* @phpstan-ignore-next-line return type has no value type specified in iterable type array
*/
public function getBaseContext(int $referenceType = UrlGeneratorInterface::ABS_PATH): array
{
return $this->decorated->getBaseContext($referenceType);
}
/**
* @phpstan-ignore-next-line return type has no value type specified in iterable type array
*/
public function getEntrypointContext(int $referenceType = UrlGeneratorInterface::ABS_PATH): array
{
return $this->decorated->getEntrypointContext($referenceType);
}
public function getResourceContextUri(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
{
return $this->decorated->getResourceContextUri($resourceClass, $referenceType);
}
}
<?php
#[AsDecorator(decorates: 'api_platform.openapi.factory')]
final readonly class PeriodsOpenApiFactory implements OpenApiFactoryInterface
{
public function __construct(private OpenApiFactoryInterface $decorated)
{
}
public function __invoke(array $context = []): OpenApi
{
return $this->overrideSchema($this->decorated->__invoke($context));
}
/**
* Overrides Period schema with polymorphism.
*
* @see https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/
*/
private function overrideSchema(OpenApi $openApi): OpenApi
{
$components = $openApi->getComponents();
$schema = $components->getSchemas() ?? new \ArrayObject();
$schema['Period.jsonld-period.read'] = [
'oneOf' => [
['$ref' => '#/components/schemas/SubscribedCalendarPeriod.jsonld-subscribed-calendar-period.read'],
['$ref' => '#/components/schemas/CustomCalendarPeriod.jsonld-custom-calendar-period.read'],
],
'discriminator' => [
'propertyName' => 'periodType',
'mapping' => [
SubscribedCalendarPeriod::PERIOD_TYPE => '#/components/schemas/SubscribedCalendarPeriod.jsonld-subscribed-calendar-period.read',
CustomCalendarPeriod::PERIOD_TYPE => '#/components/schemas/CustomCalendarPeriod.jsonld-custom-calendar-period.read',
],
],
];
return $openApi->withComponents($components->withSchemas($schema));
}
}
<?php
#[ApiResource(
operations: [
// no GetCollection operation
new Get(...),
new Post(...),
new Patch(...),
],
)]
final class SubscribedCalendarPeriod implements Period
{
public const string PERIOD_TYPE = 'subscribed';
#[Groups(groups: ['period:read'])]
public function getPeriodType(): string
{
return self::PERIOD_TYPE;
}
// ...
}
@pille1842
Copy link

Thanks! It seems to work fine, but in my Swagger UI, I get the following error messages:

Resolver error at responses.200.content.application/ld+json.schema.properties.member.items.$ref
Could not resolve reference: JSON Pointer evaluation failed while evaluating token "HtmlEmbeddable.jsonld-html-embeddable.read" against an ObjectElement
Resolver error at responses.200.content.application/ld+json.schema.properties.member.items.oneOf.0.$ref
Could not resolve reference: JSON Pointer evaluation failed while evaluating token "HtmlEmbeddable.jsonld-html-embeddable.read" against an ObjectElement
Resolver error at responses.200.content.application/ld+json.schema.properties.member.items.$ref
Could not resolve reference: JSON Pointer evaluation failed while evaluating token "HtmlEmbeddable.jsonld-html-embeddable.read" against an ObjectElement
Resolver error at responses.200.content.application/ld+json.schema.properties.member.items.oneOf.0.$ref
Could not resolve reference: JSON Pointer evaluation failed while evaluating token "HtmlEmbeddable.jsonld-html-embeddable.read" against an ObjectElement

Any idea how to fix this? Your help is greatly appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment