Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Netgen\ApiPlatformExtras\ApiPlatform\JsonSchema\Metadata\Property;

use ApiPlatform\JsonSchema\Schema;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use Symfony\Component\TypeInfo\Type\NullableType;

final class PropertyMetadataFactoryDecorator implements PropertyMetadataFactoryInterface
{
public function __construct(
private PropertyMetadataFactoryInterface $decorated,
) {}

public function create(string $resourceClass, string $property, array $options = []): ApiProperty
{
$propertyMetadata = $this->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;
}
}
109 changes: 109 additions & 0 deletions src/ApiPlatform/JsonSchema/SchemaFactoryDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace Netgen\ApiPlatformExtras\ApiPlatform\JsonSchema;

use ApiPlatform\JsonSchema\Schema;
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
use ApiPlatform\JsonSchema\SchemaUriPrefixTrait;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ArrayObject;

use function in_array;
use function is_string;
use function str_replace;

final class SchemaFactoryDecorator implements SchemaFactoryInterface
{
use SchemaUriPrefixTrait;

private const array SCHEMA_LOGICAL_OPERATORS = ['anyOf', 'oneOf', 'allOf'];

private const string JSONLD_INPUT_OBJECT_PROPERTY_NAME = '@id';

private const array JSONLD_INPUT_OBJECT_PROPERTY = [
'type' => 'string',
'format' => 'iri-reference',
'example' => 'https://example.com/',
];

public function __construct(
private SchemaFactoryInterface $decorated,
) {}

/** @param array<mixed> $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<string, mixed> $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<string, mixed> $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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass;

use Netgen\ApiPlatformExtras\ApiPlatform\JsonSchema\Metadata\Property\PropertyMetadataFactoryDecorator;
use Netgen\ApiPlatformExtras\ApiPlatform\JsonSchema\SchemaFactoryDecorator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

use function sprintf;

final class SchemaDecorationCompilerPass implements CompilerPassInterface
{
private const string BASE_FEATURE_PATH = 'netgen_api_platform_extras.features.schema_decoration';

public function process(ContainerBuilder $container): void
{
$featureEnabledParameter = sprintf('%s.enabled', self::BASE_FEATURE_PATH);
if (
!$container->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);
}
}
}
10 changes: 10 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions src/NetgenApiPlatformExtrasBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +21,9 @@ public function build(ContainerBuilder $container): void
)
->addCompilerPass(
new SchemaProcessorCompilerPass(),
)
->addCompilerPass(
new SchemaDecorationCompilerPass(),
);

$container->registerForAutoconfiguration(OpenApiProcessorInterface::class)
Expand Down