From ab56395ce214e739b9d9118c310e8926f5e082e3 Mon Sep 17 00:00:00 2001 From: Jakov Knezovic Date: Mon, 19 Jan 2026 10:09:26 +0100 Subject: [PATCH 1/5] NGSTACK-1017 update readme and config for additional functionalities under feature --- README.md | 22 ++++++++++++++++------ src/DependencyInjection/Configuration.php | 10 ++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dbb4129..e2667ca 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,22 @@ Configuration (config/packages/api_platform_extras.yaml): ```yaml api_platform_extras: features: - http_cache: { enabled: false } - schema_decoration: { enabled: false } - simple_normalizer: { enabled: false } - jwt_refresh: { enabled: false } - iri_template_generator: { enabled: false } - schema_processor: { enabled: false } + http_cache: + enabled: false + schema_decoration: + enabled: false + #Mark schema properties as required by default when the type is not nullable. + default_required_properties: false + #Add @id as an optional property to all POST, PUT and PATCH schemas. + jsonld_update_schema: false + simple_normalizer: + enabled: false + jwt_refresh: + enabled: false + iri_template_generator: + enabled: false + schema_processor: + enabled: false ``` Enable features by setting the corresponding flag to true. diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index a6c490f..ef9be7a 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -26,6 +26,16 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->arrayNode('schema_decoration') ->canBeEnabled() + ->children() + ->booleanNode('default_required_properties') + ->defaultFalse() + ->info('Mark schema properties as required by default when type is not nullable.') + ->end() + ->booleanNode('jsonld_update_schema') + ->defaultFalse() + ->info('Add @id as optional property to all POST, PUT and PATCH schemas.') + ->end() + ->end() ->end() ->arrayNode('simple_normalizer') ->canBeEnabled() From 3034a44e0cd7fd515c2080d87bcd4a89674ca11e Mon Sep 17 00:00:00 2001 From: Jakov Knezovic Date: Mon, 19 Jan 2026 10:10:18 +0100 Subject: [PATCH 2/5] NGSTACK-1017 update add decorators for default required propertyes and jsonld update schema --- .../PropertyMetadataFactoryDecorator.php | 34 ++++++++ .../JsonSchema/SchemaFactoryDecorator.php | 86 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/ApiPlatform/JsonSchema/Metadata/Property/PropertyMetadataFactoryDecorator.php create mode 100644 src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php diff --git a/src/ApiPlatform/JsonSchema/Metadata/Property/PropertyMetadataFactoryDecorator.php b/src/ApiPlatform/JsonSchema/Metadata/Property/PropertyMetadataFactoryDecorator.php new file mode 100644 index 0000000..00798bd --- /dev/null +++ b/src/ApiPlatform/JsonSchema/Metadata/Property/PropertyMetadataFactoryDecorator.php @@ -0,0 +1,34 @@ +decorated->create($resourceClass, $property, $options); + + $type = $propertyMetadata->getNativeType(); + + if ( + ($options['schema_type'] ?? null) === Schema::TYPE_OUTPUT + + && $type !== null && $type::class !== NullableType::class + ) { + return $propertyMetadata->withRequired(true); + } + + return $propertyMetadata; + } +} diff --git a/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php b/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php new file mode 100644 index 0000000..28fb622 --- /dev/null +++ b/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php @@ -0,0 +1,86 @@ + 'string', + 'format' => 'iri-reference', + 'example' => 'https://example.com/', + ]; + + public function __construct( + private SchemaFactoryInterface $decorated, + ) {} + + public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema + { + $schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); + $version = $schema->getVersion(); + $schemaPrefix = $this->getSchemaUriPrefix($version); + $currentReference = $schema['$ref'] ?? null; + + if ( + is_string($currentReference) + && $type === Schema::TYPE_INPUT + && in_array($operation::class, [Put::class, Post::class, Patch::class], true) + ) { + $this->addJsonldUpdatePropertyToObjectSchemaDefinitions($currentReference, $schemaPrefix, $schema->getDefinitions()); + } + + return $schema; + } + + private function addJsonldUpdatePropertyToObjectSchemaDefinitions(string $reference, string $schemaPrefix, ArrayObject $definitions): void + { + $definitionName = str_replace($schemaPrefix, '', $reference); + + foreach ($definitions[$definitionName]['properties'] ?? [] as $property) { + if (isset($property['type'])) { + continue; + } + + if (isset($property['$ref']) && !isset($definitions[str_replace($schemaPrefix, '', $property['$ref'])]['properties'][self::JSONLD_INPUT_OBJECT_PROPERTY_NAME])) { + $definitions[str_replace($schemaPrefix, '', $property['$ref'])]['properties'][self::JSONLD_INPUT_OBJECT_PROPERTY_NAME] = self::JSONLD_INPUT_OBJECT_PROPERTY; + + break; + } + + foreach (self::SCHEMA_LOGICAL_OPERATORS as $operator) { + if (!isset($property[$operator])) { + continue; + } + + foreach ($property[$operator] as $subschema) { + if (!isset($subschema['$ref'])) { + continue; + } + + $definitions[str_replace($schemaPrefix, '', $subschema['$ref'])]['properties'][self::JSONLD_INPUT_OBJECT_PROPERTY_NAME] = self::JSONLD_INPUT_OBJECT_PROPERTY; + } + } + } + } +} From 0eb5ce950430db39cc85171f25e3ec33cef3f8e9 Mon Sep 17 00:00:00 2001 From: Jakov Knezovic Date: Mon, 19 Jan 2026 10:12:00 +0100 Subject: [PATCH 3/5] NGSTACK-1017 add compiler pass for schema decoration feature --- .../SchemaDecorationCompilerPass.php | 56 +++++++++++++++++++ src/NetgenApiPlatformExtrasBundle.php | 4 ++ 2 files changed, 60 insertions(+) create mode 100644 src/DependencyInjection/CompilerPass/SchemaDecorationCompilerPass.php diff --git a/src/DependencyInjection/CompilerPass/SchemaDecorationCompilerPass.php b/src/DependencyInjection/CompilerPass/SchemaDecorationCompilerPass.php new file mode 100644 index 0000000..38deb6a --- /dev/null +++ b/src/DependencyInjection/CompilerPass/SchemaDecorationCompilerPass.php @@ -0,0 +1,56 @@ +hasParameter($featureEnabledParameter) + || $container->getParameter($featureEnabledParameter) === false + ) { + return; + } + + $jsonldUpdateSchemaParameter = sprintf('%s.jsonld_update_schema', self::BASE_FEATURE_PATH); + if ( + $container->hasParameter($jsonldUpdateSchemaParameter) + && $container->getParameter($jsonldUpdateSchemaParameter) === true + ) { + $container + ->setDefinition('netgen.api_platform_extras.json_schema.schema_factory', new Definition(SchemaFactoryDecorator::class)) + ->setArguments([ + new Reference('netgen.api_platform_extras.json_schema.schema_factory.inner'), + ]) + ->setDecoratedService('api_platform.json_schema.schema_factory'); + } + + $defaultRequiredPropertiesParameter = sprintf('%s.default_required_properties', self::BASE_FEATURE_PATH); + if ( + $container->hasParameter($defaultRequiredPropertiesParameter) + && $container->getParameter($defaultRequiredPropertiesParameter) === true + ) { + $container + ->setDefinition('netgen.api_platform_extras.metadata.property.metadata_factory', new Definition(PropertyMetadataFactoryDecorator::class)) + ->setArguments([ + new Reference('netgen.api_platform_extras.metadata.property.metadata_factory.inner'), + ]) + ->setDecoratedService('api_platform.metadata.property.metadata_factory', null, 19); + } + } +} diff --git a/src/NetgenApiPlatformExtrasBundle.php b/src/NetgenApiPlatformExtrasBundle.php index f73e626..16f059c 100644 --- a/src/NetgenApiPlatformExtrasBundle.php +++ b/src/NetgenApiPlatformExtrasBundle.php @@ -5,6 +5,7 @@ namespace Netgen\ApiPlatformExtras; use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\IriTemplateGeneratorCompilerPass; +use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\SchemaDecorationCompilerPass; use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\SchemaProcessorCompilerPass; use Netgen\ApiPlatformExtras\OpenApi\Processor\OpenApiProcessorInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -20,6 +21,9 @@ public function build(ContainerBuilder $container): void ) ->addCompilerPass( new SchemaProcessorCompilerPass(), + ) + ->addCompilerPass( + new SchemaDecorationCompilerPass(), ); $container->registerForAutoconfiguration(OpenApiProcessorInterface::class) From 2f1dcd7835eded634f7b667c25e3cc5d09a2491c Mon Sep 17 00:00:00 2001 From: Jakov Knezovic Date: Mon, 19 Jan 2026 10:27:37 +0100 Subject: [PATCH 4/5] NGSTACK-1017 remove readonly from decorator classes and refactor for readability --- .../PropertyMetadataFactoryDecorator.php | 2 +- .../JsonSchema/SchemaFactoryDecorator.php | 31 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/ApiPlatform/JsonSchema/Metadata/Property/PropertyMetadataFactoryDecorator.php b/src/ApiPlatform/JsonSchema/Metadata/Property/PropertyMetadataFactoryDecorator.php index 00798bd..6b0c64b 100644 --- a/src/ApiPlatform/JsonSchema/Metadata/Property/PropertyMetadataFactoryDecorator.php +++ b/src/ApiPlatform/JsonSchema/Metadata/Property/PropertyMetadataFactoryDecorator.php @@ -9,7 +9,7 @@ use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use Symfony\Component\TypeInfo\Type\NullableType; -final readonly class PropertyMetadataFactoryDecorator implements PropertyMetadataFactoryInterface +final class PropertyMetadataFactoryDecorator implements PropertyMetadataFactoryInterface { public function __construct( private PropertyMetadataFactoryInterface $decorated, diff --git a/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php b/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php index 28fb622..9f2baaa 100644 --- a/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php +++ b/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php @@ -17,7 +17,7 @@ use function is_string; use function str_replace; -final readonly class SchemaFactoryDecorator implements SchemaFactoryInterface +final class SchemaFactoryDecorator implements SchemaFactoryInterface { use SchemaUriPrefixTrait; @@ -47,13 +47,13 @@ public function buildSchema(string $className, string $format = 'json', string $ && $type === Schema::TYPE_INPUT && in_array($operation::class, [Put::class, Post::class, Patch::class], true) ) { - $this->addJsonldUpdatePropertyToObjectSchemaDefinitions($currentReference, $schemaPrefix, $schema->getDefinitions()); + $this->ensureJsonldInputPropertyForInputSchemas($currentReference, $schemaPrefix, $schema->getDefinitions()); } return $schema; } - private function addJsonldUpdatePropertyToObjectSchemaDefinitions(string $reference, string $schemaPrefix, ArrayObject $definitions): void + private function ensureJsonldInputPropertyForInputSchemas(string $reference, string $schemaPrefix, ArrayObject $definitions): void { $definitionName = str_replace($schemaPrefix, '', $reference); @@ -62,8 +62,12 @@ private function addJsonldUpdatePropertyToObjectSchemaDefinitions(string $refere continue; } - if (isset($property['$ref']) && !isset($definitions[str_replace($schemaPrefix, '', $property['$ref'])]['properties'][self::JSONLD_INPUT_OBJECT_PROPERTY_NAME])) { - $definitions[str_replace($schemaPrefix, '', $property['$ref'])]['properties'][self::JSONLD_INPUT_OBJECT_PROPERTY_NAME] = self::JSONLD_INPUT_OBJECT_PROPERTY; + if (isset($property['$ref'])) { + $this->addJsonldInputProperty( + $definitions, + $schemaPrefix, + $property['$ref'], + ); break; } @@ -78,9 +82,24 @@ private function addJsonldUpdatePropertyToObjectSchemaDefinitions(string $refere continue; } - $definitions[str_replace($schemaPrefix, '', $subschema['$ref'])]['properties'][self::JSONLD_INPUT_OBJECT_PROPERTY_NAME] = self::JSONLD_INPUT_OBJECT_PROPERTY; + $this->addJsonldInputProperty( + $definitions, + $schemaPrefix, + $subschema['$ref'], + ); } } } } + + private function addJsonldInputProperty( + ArrayObject $definitions, + string $schemaPrefix, + string $ref, + ): void { + $definitionKey = str_replace($schemaPrefix, '', $ref); + + $definitions[$definitionKey]['properties'][self::JSONLD_INPUT_OBJECT_PROPERTY_NAME] + ??= self::JSONLD_INPUT_OBJECT_PROPERTY; + } } From 1af3425b101ed91b5aa2c2ed869f19122eefe4fa Mon Sep 17 00:00:00 2001 From: Jakov Knezovic Date: Mon, 19 Jan 2026 11:48:29 +0100 Subject: [PATCH 5/5] NGSTACK-1017 update php doc to hint array object type for phpstan --- src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php b/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php index 9f2baaa..96e39c1 100644 --- a/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php +++ b/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php @@ -35,6 +35,7 @@ public function __construct( private SchemaFactoryInterface $decorated, ) {} + /** @param array $serializerContext */ public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema { $schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); @@ -45,6 +46,7 @@ public function buildSchema(string $className, string $format = 'json', string $ if ( is_string($currentReference) && $type === Schema::TYPE_INPUT + && $operation instanceof Operation && in_array($operation::class, [Put::class, Post::class, Patch::class], true) ) { $this->ensureJsonldInputPropertyForInputSchemas($currentReference, $schemaPrefix, $schema->getDefinitions()); @@ -53,6 +55,7 @@ public function buildSchema(string $className, string $format = 'json', string $ return $schema; } + /** @param ArrayObject $definitions */ private function ensureJsonldInputPropertyForInputSchemas(string $reference, string $schemaPrefix, ArrayObject $definitions): void { $definitionName = str_replace($schemaPrefix, '', $reference); @@ -92,6 +95,7 @@ private function ensureJsonldInputPropertyForInputSchemas(string $reference, str } } + /** @param ArrayObject $definitions */ private function addJsonldInputProperty( ArrayObject $definitions, string $schemaPrefix,