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/ApiPlatform/JsonSchema/Metadata/Property/PropertyMetadataFactoryDecorator.php b/src/ApiPlatform/JsonSchema/Metadata/Property/PropertyMetadataFactoryDecorator.php new file mode 100644 index 0000000..6b0c64b --- /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..96e39c1 --- /dev/null +++ b/src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php @@ -0,0 +1,109 @@ + 'string', + 'format' => 'iri-reference', + 'example' => 'https://example.com/', + ]; + + 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); + $version = $schema->getVersion(); + $schemaPrefix = $this->getSchemaUriPrefix($version); + $currentReference = $schema['$ref'] ?? null; + + 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()); + } + + return $schema; + } + + /** @param ArrayObject $definitions */ + private function ensureJsonldInputPropertyForInputSchemas(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'])) { + $this->addJsonldInputProperty( + $definitions, + $schemaPrefix, + $property['$ref'], + ); + + break; + } + + foreach (self::SCHEMA_LOGICAL_OPERATORS as $operator) { + if (!isset($property[$operator])) { + continue; + } + + foreach ($property[$operator] as $subschema) { + if (!isset($subschema['$ref'])) { + continue; + } + + $this->addJsonldInputProperty( + $definitions, + $schemaPrefix, + $subschema['$ref'], + ); + } + } + } + } + + /** @param ArrayObject $definitions */ + 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; + } +} 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/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() 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)