Skip to content

Commit d991b92

Browse files
authored
Merge pull request #4 from netgen/NGSTACK-1017-schema-decoration
Ngstack 1017 schema decoration
2 parents ffdf716 + 1af3425 commit d991b92

6 files changed

Lines changed: 229 additions & 6 deletions

File tree

README.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,22 @@ Configuration (config/packages/api_platform_extras.yaml):
55
```yaml
66
api_platform_extras:
77
features:
8-
http_cache: { enabled: false }
9-
schema_decoration: { enabled: false }
10-
simple_normalizer: { enabled: false }
11-
jwt_refresh: { enabled: false }
12-
iri_template_generator: { enabled: false }
13-
schema_processor: { enabled: false }
8+
http_cache:
9+
enabled: false
10+
schema_decoration:
11+
enabled: false
12+
#Mark schema properties as required by default when the type is not nullable.
13+
default_required_properties: false
14+
#Add @id as an optional property to all POST, PUT and PATCH schemas.
15+
jsonld_update_schema: false
16+
simple_normalizer:
17+
enabled: false
18+
jwt_refresh:
19+
enabled: false
20+
iri_template_generator:
21+
enabled: false
22+
schema_processor:
23+
enabled: false
1424
```
1525
1626
Enable features by setting the corresponding flag to true.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Netgen\ApiPlatformExtras\ApiPlatform\JsonSchema\Metadata\Property;
6+
7+
use ApiPlatform\JsonSchema\Schema;
8+
use ApiPlatform\Metadata\ApiProperty;
9+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
10+
use Symfony\Component\TypeInfo\Type\NullableType;
11+
12+
final class PropertyMetadataFactoryDecorator implements PropertyMetadataFactoryInterface
13+
{
14+
public function __construct(
15+
private PropertyMetadataFactoryInterface $decorated,
16+
) {}
17+
18+
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
19+
{
20+
$propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
21+
22+
$type = $propertyMetadata->getNativeType();
23+
24+
if (
25+
($options['schema_type'] ?? null) === Schema::TYPE_OUTPUT
26+
27+
&& $type !== null && $type::class !== NullableType::class
28+
) {
29+
return $propertyMetadata->withRequired(true);
30+
}
31+
32+
return $propertyMetadata;
33+
}
34+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Netgen\ApiPlatformExtras\ApiPlatform\JsonSchema;
6+
7+
use ApiPlatform\JsonSchema\Schema;
8+
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
9+
use ApiPlatform\JsonSchema\SchemaUriPrefixTrait;
10+
use ApiPlatform\Metadata\Operation;
11+
use ApiPlatform\Metadata\Patch;
12+
use ApiPlatform\Metadata\Post;
13+
use ApiPlatform\Metadata\Put;
14+
use ArrayObject;
15+
16+
use function in_array;
17+
use function is_string;
18+
use function str_replace;
19+
20+
final class SchemaFactoryDecorator implements SchemaFactoryInterface
21+
{
22+
use SchemaUriPrefixTrait;
23+
24+
private const array SCHEMA_LOGICAL_OPERATORS = ['anyOf', 'oneOf', 'allOf'];
25+
26+
private const string JSONLD_INPUT_OBJECT_PROPERTY_NAME = '@id';
27+
28+
private const array JSONLD_INPUT_OBJECT_PROPERTY = [
29+
'type' => 'string',
30+
'format' => 'iri-reference',
31+
'example' => 'https://example.com/',
32+
];
33+
34+
public function __construct(
35+
private SchemaFactoryInterface $decorated,
36+
) {}
37+
38+
/** @param array<mixed> $serializerContext */
39+
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
40+
{
41+
$schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
42+
$version = $schema->getVersion();
43+
$schemaPrefix = $this->getSchemaUriPrefix($version);
44+
$currentReference = $schema['$ref'] ?? null;
45+
46+
if (
47+
is_string($currentReference)
48+
&& $type === Schema::TYPE_INPUT
49+
&& $operation instanceof Operation
50+
&& in_array($operation::class, [Put::class, Post::class, Patch::class], true)
51+
) {
52+
$this->ensureJsonldInputPropertyForInputSchemas($currentReference, $schemaPrefix, $schema->getDefinitions());
53+
}
54+
55+
return $schema;
56+
}
57+
58+
/** @param ArrayObject<string, mixed> $definitions */
59+
private function ensureJsonldInputPropertyForInputSchemas(string $reference, string $schemaPrefix, ArrayObject $definitions): void
60+
{
61+
$definitionName = str_replace($schemaPrefix, '', $reference);
62+
63+
foreach ($definitions[$definitionName]['properties'] ?? [] as $property) {
64+
if (isset($property['type'])) {
65+
continue;
66+
}
67+
68+
if (isset($property['$ref'])) {
69+
$this->addJsonldInputProperty(
70+
$definitions,
71+
$schemaPrefix,
72+
$property['$ref'],
73+
);
74+
75+
break;
76+
}
77+
78+
foreach (self::SCHEMA_LOGICAL_OPERATORS as $operator) {
79+
if (!isset($property[$operator])) {
80+
continue;
81+
}
82+
83+
foreach ($property[$operator] as $subschema) {
84+
if (!isset($subschema['$ref'])) {
85+
continue;
86+
}
87+
88+
$this->addJsonldInputProperty(
89+
$definitions,
90+
$schemaPrefix,
91+
$subschema['$ref'],
92+
);
93+
}
94+
}
95+
}
96+
}
97+
98+
/** @param ArrayObject<string, mixed> $definitions */
99+
private function addJsonldInputProperty(
100+
ArrayObject $definitions,
101+
string $schemaPrefix,
102+
string $ref,
103+
): void {
104+
$definitionKey = str_replace($schemaPrefix, '', $ref);
105+
106+
$definitions[$definitionKey]['properties'][self::JSONLD_INPUT_OBJECT_PROPERTY_NAME]
107+
??= self::JSONLD_INPUT_OBJECT_PROPERTY;
108+
}
109+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass;
6+
7+
use Netgen\ApiPlatformExtras\ApiPlatform\JsonSchema\Metadata\Property\PropertyMetadataFactoryDecorator;
8+
use Netgen\ApiPlatformExtras\ApiPlatform\JsonSchema\SchemaFactoryDecorator;
9+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
10+
use Symfony\Component\DependencyInjection\ContainerBuilder;
11+
use Symfony\Component\DependencyInjection\Definition;
12+
use Symfony\Component\DependencyInjection\Reference;
13+
14+
use function sprintf;
15+
16+
final class SchemaDecorationCompilerPass implements CompilerPassInterface
17+
{
18+
private const string BASE_FEATURE_PATH = 'netgen_api_platform_extras.features.schema_decoration';
19+
20+
public function process(ContainerBuilder $container): void
21+
{
22+
$featureEnabledParameter = sprintf('%s.enabled', self::BASE_FEATURE_PATH);
23+
if (
24+
!$container->hasParameter($featureEnabledParameter)
25+
|| $container->getParameter($featureEnabledParameter) === false
26+
) {
27+
return;
28+
}
29+
30+
$jsonldUpdateSchemaParameter = sprintf('%s.jsonld_update_schema', self::BASE_FEATURE_PATH);
31+
if (
32+
$container->hasParameter($jsonldUpdateSchemaParameter)
33+
&& $container->getParameter($jsonldUpdateSchemaParameter) === true
34+
) {
35+
$container
36+
->setDefinition('netgen.api_platform_extras.json_schema.schema_factory', new Definition(SchemaFactoryDecorator::class))
37+
->setArguments([
38+
new Reference('netgen.api_platform_extras.json_schema.schema_factory.inner'),
39+
])
40+
->setDecoratedService('api_platform.json_schema.schema_factory');
41+
}
42+
43+
$defaultRequiredPropertiesParameter = sprintf('%s.default_required_properties', self::BASE_FEATURE_PATH);
44+
if (
45+
$container->hasParameter($defaultRequiredPropertiesParameter)
46+
&& $container->getParameter($defaultRequiredPropertiesParameter) === true
47+
) {
48+
$container
49+
->setDefinition('netgen.api_platform_extras.metadata.property.metadata_factory', new Definition(PropertyMetadataFactoryDecorator::class))
50+
->setArguments([
51+
new Reference('netgen.api_platform_extras.metadata.property.metadata_factory.inner'),
52+
])
53+
->setDecoratedService('api_platform.metadata.property.metadata_factory', null, 19);
54+
}
55+
}
56+
}

src/DependencyInjection/Configuration.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ public function getConfigTreeBuilder(): TreeBuilder
2626
->end()
2727
->arrayNode('schema_decoration')
2828
->canBeEnabled()
29+
->children()
30+
->booleanNode('default_required_properties')
31+
->defaultFalse()
32+
->info('Mark schema properties as required by default when type is not nullable.')
33+
->end()
34+
->booleanNode('jsonld_update_schema')
35+
->defaultFalse()
36+
->info('Add @id as optional property to all POST, PUT and PATCH schemas.')
37+
->end()
38+
->end()
2939
->end()
3040
->arrayNode('simple_normalizer')
3141
->canBeEnabled()

src/NetgenApiPlatformExtrasBundle.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Netgen\ApiPlatformExtras;
66

77
use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\IriTemplateGeneratorCompilerPass;
8+
use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\SchemaDecorationCompilerPass;
89
use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\SchemaProcessorCompilerPass;
910
use Netgen\ApiPlatformExtras\OpenApi\Processor\OpenApiProcessorInterface;
1011
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -20,6 +21,9 @@ public function build(ContainerBuilder $container): void
2021
)
2122
->addCompilerPass(
2223
new SchemaProcessorCompilerPass(),
24+
)
25+
->addCompilerPass(
26+
new SchemaDecorationCompilerPass(),
2327
);
2428

2529
$container->registerForAutoconfiguration(OpenApiProcessorInterface::class)

0 commit comments

Comments
 (0)