From 6230a502e9174c1065415194739076a51630aa6f Mon Sep 17 00:00:00 2001 From: soyuka Date: Mon, 16 May 2022 11:12:48 +0200 Subject: [PATCH 01/10] fix(json-schema): factory backward compatibility layer link #4753 --- src/Core/Hal/JsonSchema/SchemaFactory.php | 22 - src/Core/JsonSchema/SchemaFactory.php | 424 +++++++++++++++++- .../JsonSchema/SchemaFactoryInterface.php | 31 ++ src/Core/OpenApi/Factory/OpenApiFactory.php | 2 +- .../Serializer/DocumentationNormalizer.php | 4 +- src/Hal/JsonSchema/SchemaFactory.php | 7 +- src/Hydra/JsonSchema/SchemaFactory.php | 7 +- .../Command/JsonSchemaGenerateCommand.php | 13 +- src/JsonSchema/SchemaFactory.php | 223 ++++----- src/JsonSchema/SchemaFactoryInterface.php | 4 +- src/JsonSchema/TypeFactory.php | 10 +- src/Metadata/ApiProperty.php | 16 + .../Extractor/XmlPropertyExtractor.php | 2 + .../Extractor/XmlResourceExtractor.php | 2 + .../Extractor/YamlPropertyExtractor.php | 2 + src/Metadata/Extractor/schema/properties.xsd | 1 + src/OpenApi/Factory/OpenApiFactory.php | 4 +- .../ApiPlatformExtension.php | 14 +- .../Resources/config/legacy/json_schema.xml | 30 ++ .../Resources/config/v3/json_schema.xml | 29 ++ .../Bundle/Test/ApiTestAssertionsTrait.php | 7 +- src/deprecation.php | 1 - .../OpenApi/Factory/OpenApiFactoryTest.php | 2 +- .../DocumentationNormalizerV2Test.php | 2 +- .../DocumentationNormalizerV3Test.php | 4 +- tests/Hal/JsonSchema/SchemaFactoryTest.php | 33 +- tests/Hydra/JsonSchema/SchemaFactoryTest.php | 42 +- tests/JsonSchema/SchemaFactoryTest.php | 49 +- tests/JsonSchema/TypeFactoryTest.php | 6 +- .../Extractor/Adapter/XmlPropertyAdapter.php | 5 + .../PropertyMetadataCompatibilityTest.php | 3 + .../Serializer/OpenApiNormalizerTest.php | 30 +- 32 files changed, 764 insertions(+), 267 deletions(-) delete mode 100644 src/Core/Hal/JsonSchema/SchemaFactory.php create mode 100644 src/Core/JsonSchema/SchemaFactoryInterface.php create mode 100644 src/Symfony/Bundle/Resources/config/legacy/json_schema.xml create mode 100644 src/Symfony/Bundle/Resources/config/v3/json_schema.xml diff --git a/src/Core/Hal/JsonSchema/SchemaFactory.php b/src/Core/Hal/JsonSchema/SchemaFactory.php deleted file mode 100644 index 4cba3d38752..00000000000 --- a/src/Core/Hal/JsonSchema/SchemaFactory.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Core\Hal\JsonSchema; - -class_exists(\ApiPlatform\Hal\JsonSchema\SchemaFactory::class); - -if (false) { - final class SchemaFactory extends \ApiPlatform\Hal\JsonSchema\SchemaFactory - { - } -} diff --git a/src/Core/JsonSchema/SchemaFactory.php b/src/Core/JsonSchema/SchemaFactory.php index 0e96b4e23fe..2ff09154358 100644 --- a/src/Core/JsonSchema/SchemaFactory.php +++ b/src/Core/JsonSchema/SchemaFactory.php @@ -13,10 +13,428 @@ namespace ApiPlatform\Core\JsonSchema; -class_exists(\ApiPlatform\JsonSchema\SchemaFactory::class); +use ApiPlatform\Api\ResourceClassResolverInterface; +use ApiPlatform\Core\Api\OperationType; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface as LegacyPropertyMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface as LegacyPropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\OpenApi\Factory\OpenApiFactory; +use ApiPlatform\Util\ResourceClassInfoTrait; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -if (false) { - final class SchemaFactory extends \ApiPlatform\JsonSchema\SchemaFactory +/** + * {@inheritdoc} + * + * @experimental + * + * @author Kévin Dunglas + */ +final class SchemaFactory implements SchemaFactoryInterface +{ + use ResourceClassInfoTrait; + + private $typeFactory; + /** + * @var LegacyPropertyNameCollectionFactoryInterface|PropertyNameCollectionFactoryInterface + */ + private $propertyNameCollectionFactory; + /** + * @var LegacyPropertyMetadataFactoryInterface|PropertyMetadataFactoryInterface + */ + private $propertyMetadataFactory; + private $nameConverter; + private $distinctFormats = []; + + public function __construct(TypeFactoryInterface $typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null) + { + $this->typeFactory = $typeFactory; + if (!$resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) { + trigger_deprecation('api-platform/core', '2.7', sprintf('Use "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, ResourceMetadataFactoryInterface::class)); + } + $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; + $this->propertyMetadataFactory = $propertyMetadataFactory; + $this->nameConverter = $nameConverter; + $this->resourceClassResolver = $resourceClassResolver; + } + + /** + * When added to the list, the given format will lead to the creation of a new definition. + * + * @internal + */ + public function addDistinctFormat(string $format): void + { + $this->distinctFormats[$format] = true; + } + + /** + * {@inheritdoc} + */ + public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema { + $schema = $schema ? clone $schema : new Schema(); + if (null === $metadata = $this->getMetadata($className, $type, $operationType, $operationName, $serializerContext)) { + return $schema; + } + + [$resourceMetadata, $serializerContext, $validationGroups, $inputOrOutputClass] = $metadata; + + if (null === $resourceMetadata && (null !== $operationType || null !== $operationName)) { + throw new \LogicException('The $operationType and $operationName arguments must be null for non-resource class.'); + } + + $operation = $resourceMetadata instanceof ResourceMetadataCollection ? $resourceMetadata->getOperation($operationName, OperationType::COLLECTION === $operationType) : null; + + $version = $schema->getVersion(); + $definitionName = $this->buildDefinitionName($className, $format, $inputOrOutputClass, $resourceMetadata instanceof ResourceMetadata ? $resourceMetadata : $operation, $serializerContext); + + $method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET'; + if (!$operation && (null === $operationType || null === $operationName)) { + $method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET'; + } elseif ($resourceMetadata instanceof ResourceMetadata) { + $method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET'); + } + + if (Schema::TYPE_OUTPUT !== $type && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) { + return $schema; + } + + if (!isset($schema['$ref']) && !isset($schema['type'])) { + $ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName; + + if ($forceCollection || (OperationType::COLLECTION === $operationType && 'POST' !== $method)) { + $schema['type'] = 'array'; + $schema['items'] = ['$ref' => $ref]; + } else { + $schema['$ref'] = $ref; + } + } + + $definitions = $schema->getDefinitions(); + if (isset($definitions[$definitionName])) { + // Already computed + return $schema; + } + + /** @var \ArrayObject $definition */ + $definition = new \ArrayObject(['type' => 'object']); + $definitions[$definitionName] = $definition; + + if ($resourceMetadata instanceof ResourceMetadata) { + $definition['description'] = $resourceMetadata->getDescription() ?? ''; + } else { + $definition['description'] = $operation ? ($operation->getDescription() ?? '') : ''; + } + + // additionalProperties are allowed by default, so it does not need to be set explicitly, unless allow_extra_attributes is false + // See https://json-schema.org/understanding-json-schema/reference/object.html#properties + if (false === ($serializerContext[AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES] ?? true)) { + $definition['additionalProperties'] = false; + } + + // see https://github.com/json-schema-org/json-schema-spec/pull/737 + if ( + Schema::VERSION_SWAGGER !== $version + ) { + if (($resourceMetadata instanceof ResourceMetadata && + ($operationType && $operationName ? $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true) : $resourceMetadata->getAttribute('deprecation_reason', null)) + ) || ($operation && $operation->getDeprecationReason()) + ) { + $definition['deprecated'] = true; + } + } + + // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it + // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4 + if ($resourceMetadata instanceof ResourceMetadata && $resourceMetadata->getIri()) { + $definition['externalDocs'] = ['url' => $resourceMetadata->getIri()]; + } elseif ($operation instanceof HttpOperation && ($operation->getTypes()[0] ?? null)) { + $definition['externalDocs'] = ['url' => $operation->getTypes()[0]]; + } + + // TODO: getFactoryOptions should be refactored because Item & Collection Operations don't exist anymore (API Platform 3.0) + $options = $this->getFactoryOptions($serializerContext, $validationGroups, $operationType, $operationName, $operation instanceof HttpOperation ? $operation : null); + foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) { + $propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName, $options); + if (!$propertyMetadata->isReadable() && !$propertyMetadata->isWritable()) { + continue; + } + + $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $inputOrOutputClass, $format, $serializerContext) : $propertyName; + if ($propertyMetadata->isRequired()) { + $definition['required'][] = $normalizedPropertyName; + } + + $this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format); + } + + return $schema; + } + + private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, $propertyMetadata, array $serializerContext, string $format): void + { + $version = $schema->getVersion(); + $swagger = Schema::VERSION_SWAGGER === $version; + $propertySchema = $propertyMetadata->getSchema() ?? []; + + if ($propertyMetadata instanceof ApiProperty) { + $additionalPropertySchema = $propertyMetadata->getOpenapiContext() ?? []; + } else { + switch ($version) { + case Schema::VERSION_SWAGGER: + $basePropertySchemaAttribute = 'swagger_context'; + break; + case Schema::VERSION_OPENAPI: + $basePropertySchemaAttribute = 'openapi_context'; + break; + default: + $basePropertySchemaAttribute = 'json_schema_context'; + } + + $additionalPropertySchema = $propertyMetadata->getAttributes()[$basePropertySchemaAttribute] ?? []; + } + + $propertySchema = array_merge( + $propertySchema, + $additionalPropertySchema + ); + + if (false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) { + $propertySchema['readOnly'] = true; + } + if (!$swagger && false === $propertyMetadata->isReadable()) { + $propertySchema['writeOnly'] = true; + } + if (null !== $description = $propertyMetadata->getDescription()) { + $propertySchema['description'] = $description; + } + + $deprecationReason = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getAttribute('deprecation_reason') : $propertyMetadata->getDeprecationReason(); + + // see https://github.com/json-schema-org/json-schema-spec/pull/737 + if (!$swagger && null !== $deprecationReason) { + $propertySchema['deprecated'] = true; + } + // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it + // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4 + $iri = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getIri() : $propertyMetadata->getTypes()[0] ?? null; + if (null !== $iri) { + $propertySchema['externalDocs'] = ['url' => $iri]; + } + + if (!isset($propertySchema['default']) && !empty($default = $propertyMetadata->getDefault())) { + $propertySchema['default'] = $default; + } + + if (!isset($propertySchema['example']) && !empty($example = $propertyMetadata->getExample())) { + $propertySchema['example'] = $example; + } + + if (!isset($propertySchema['example']) && isset($propertySchema['default'])) { + $propertySchema['example'] = $propertySchema['default']; + } + + $valueSchema = []; + // TODO: 3.0 support multiple types, default value of types will be [] instead of null + $type = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getType() : $propertyMetadata->getBuiltinTypes()[0] ?? null; + if (null !== $type) { + if ($isCollection = $type->isCollection()) { + $keyType = method_exists(Type::class, 'getCollectionKeyTypes') ? ($type->getCollectionKeyTypes()[0] ?? null) : $type->getCollectionKeyType(); + $valueType = method_exists(Type::class, 'getCollectionValueTypes') ? ($type->getCollectionValueTypes()[0] ?? null) : $type->getCollectionValueType(); + } else { + $keyType = null; + $valueType = $type; + } + + if (null === $valueType) { + $builtinType = 'string'; + $className = null; + } else { + $builtinType = $valueType->getBuiltinType(); + $className = $valueType->getClassName(); + } + + $valueSchema = $this->typeFactory->getType(new Type($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $format, $propertyMetadata->isReadableLink(), $serializerContext, $schema); + } + + if (\array_key_exists('type', $propertySchema) && \array_key_exists('$ref', $valueSchema)) { + $propertySchema = new \ArrayObject($propertySchema); + } else { + $propertySchema = new \ArrayObject($propertySchema + $valueSchema); + } + $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema; + } + + private function buildDefinitionName(string $className, string $format = 'json', ?string $inputOrOutputClass = null, $resourceMetadata = null, ?array $serializerContext = null): string + { + if ($resourceMetadata) { + $prefix = $resourceMetadata instanceof ResourceMetadata ? $resourceMetadata->getShortName() : $resourceMetadata->getShortName(); + } + + if (!isset($prefix)) { + $prefix = (new \ReflectionClass($className))->getShortName(); + } + + if (null !== $inputOrOutputClass && $className !== $inputOrOutputClass) { + $parts = explode('\\', $inputOrOutputClass); + $shortName = end($parts); + $prefix .= '.'.$shortName; + } + + if (isset($this->distinctFormats[$format])) { + // JSON is the default, and so isn't included in the definition name + $prefix .= '.'.$format; + } + + $definitionName = $serializerContext[OpenApiFactory::OPENAPI_DEFINITION_NAME] ?? $serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME] ?? null; + if ($definitionName) { + $name = sprintf('%s-%s', $prefix, $definitionName); + } else { + $groups = (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []); + $name = $groups ? sprintf('%s-%s', $prefix, implode('_', $groups)) : $prefix; + } + + return $this->encodeDefinitionName($name); + } + + private function encodeDefinitionName(string $name): string + { + return preg_replace('/[^a-zA-Z0-9.\-_]/', '.', $name); + } + + private function getMetadata(string $className, string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?array $serializerContext = null): ?array + { + if (!$this->isResourceClass($className)) { + return [ + null, + $serializerContext ?? [], + [], + $className, + ]; + } + + /** @var ResourceMetadata|ResourceMetadataCollection $resourceMetadata */ + $resourceMetadata = $this->resourceMetadataFactory->create($className); + $attribute = Schema::TYPE_OUTPUT === $type ? 'output' : 'input'; + $operation = ($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) ? null : $resourceMetadata->getOperation($operationName); + + if ($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) { + if (null === $operationType || null === $operationName) { + $inputOrOutput = $resourceMetadata->getAttribute($attribute, ['class' => $className]); + } else { + $inputOrOutput = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, ['class' => $className], true); + } + } elseif ($operation) { + $inputOrOutput = (Schema::TYPE_OUTPUT === $type ? $operation->getOutput() : $operation->getInput()) ?? ['class' => $className]; + } else { + $inputOrOutput = ['class' => $className]; + } + + if (null === ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null)) { + // input or output disabled + return null; + } + + return [ + $resourceMetadata, + $serializerContext ?? $this->getSerializerContext($resourceMetadata, $type, $operationType, $operationName), + $this->getValidationGroups($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface ? $resourceMetadata : $operation, $operationType, $operationName), + $inputOrOutput['class'] ?? $inputOrOutput->class, + ]; + } + + private function getSerializerContext($resourceMetadata, string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null): array + { + if ($resourceMetadata instanceof ResourceMetadata) { + $attribute = Schema::TYPE_OUTPUT === $type ? 'normalization_context' : 'denormalization_context'; + } else { + $operation = $resourceMetadata->getOperation($operationName); + } + + if (null === $operationType || null === $operationName) { + if ($resourceMetadata instanceof ResourceMetadata) { + return $resourceMetadata->getAttribute($attribute, []); + } + + return Schema::TYPE_OUTPUT === $type ? ($operation->getNormalizationContext() ?? []) : ($operation->getDenormalizationContext() ?? []); + } + + if ($resourceMetadata instanceof ResourceMetadata) { + return $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, [], true); + } + + return Schema::TYPE_OUTPUT === $type ? ($operation->getNormalizationContext() ?? []) : ($operation->getDenormalizationContext() ?? []); + } + + /** + * @param HttpOperation|ResourceMetadata|null $resourceMetadata + */ + private function getValidationGroups($resourceMetadata, ?string $operationType, ?string $operationName): array + { + if ($resourceMetadata instanceof ResourceMetadata) { + $attribute = 'validation_groups'; + + if (null === $operationType || null === $operationName) { + return \is_array($validationGroups = $resourceMetadata->getAttribute($attribute, [])) ? $validationGroups : []; + } + + return \is_array($validationGroups = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, [], true)) ? $validationGroups : []; + } + + $groups = $resourceMetadata ? ($resourceMetadata->getValidationContext()['groups'] ?? []) : []; + + return \is_array($groups) ? $groups : [$groups]; + } + + /** + * Gets the options for the property name collection / property metadata factories. + */ + private function getFactoryOptions(array $serializerContext, array $validationGroups, ?string $operationType, ?string $operationName, ?HttpOperation $operation = null): array + { + $options = [ + /* @see https://github.com/symfony/symfony/blob/v5.1.0/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php */ + 'enable_getter_setter_extraction' => true, + ]; + + if (isset($serializerContext[AbstractNormalizer::GROUPS])) { + /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */ + $options['serializer_groups'] = (array) $serializerContext[AbstractNormalizer::GROUPS]; + } + + if ($this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface && $operation) { + $options['normalization_groups'] = $operation->getNormalizationContext()['groups'] ?? null; + $options['denormalization_groups'] = $operation->getDenormalizationContext()['groups'] ?? null; + } + + if (null !== $operationType && null !== $operationName) { + switch ($operationType) { + case OperationType::COLLECTION: + $options['collection_operation_name'] = $operationName; + break; + case OperationType::ITEM: + $options['item_operation_name'] = $operationName; + break; + default: + break; + } + } + + if ($validationGroups) { + $options['validation_groups'] = $validationGroups; + } + + return $options; } } diff --git a/src/Core/JsonSchema/SchemaFactoryInterface.php b/src/Core/JsonSchema/SchemaFactoryInterface.php new file mode 100644 index 00000000000..70d79c4439f --- /dev/null +++ b/src/Core/JsonSchema/SchemaFactoryInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\JsonSchema; + +use ApiPlatform\JsonSchema\Schema; + +/** + * Factory for creating the JSON Schema document corresponding to a PHP class. + * + * @experimental + * + * @author Kévin Dunglas + */ +interface SchemaFactoryInterface +{ + /** + * Builds the JSON Schema document corresponding to the given PHP class. + */ + public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema; +} diff --git a/src/Core/OpenApi/Factory/OpenApiFactory.php b/src/Core/OpenApi/Factory/OpenApiFactory.php index 6ebdacb6111..b8e3f171703 100644 --- a/src/Core/OpenApi/Factory/OpenApiFactory.php +++ b/src/Core/OpenApi/Factory/OpenApiFactory.php @@ -16,13 +16,13 @@ use ApiPlatform\Core\Api\FilterLocatorTrait; use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\OperationType; +use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface; use ApiPlatform\JsonSchema\Schema; -use ApiPlatform\JsonSchema\SchemaFactoryInterface; use ApiPlatform\JsonSchema\TypeFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; diff --git a/src/Core/Swagger/Serializer/DocumentationNormalizer.php b/src/Core/Swagger/Serializer/DocumentationNormalizer.php index 5f9bf9b9455..6e7cae6277e 100644 --- a/src/Core/Swagger/Serializer/DocumentationNormalizer.php +++ b/src/Core/Swagger/Serializer/DocumentationNormalizer.php @@ -22,6 +22,8 @@ use ApiPlatform\Core\Api\OperationType; use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Api\UrlGeneratorInterface; +use ApiPlatform\Core\JsonSchema\SchemaFactory; +use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ApiResourceToLegacyResourceMetadataTrait; @@ -32,8 +34,6 @@ use ApiPlatform\Exception\ResourceClassNotFoundException; use ApiPlatform\Exception\RuntimeException; use ApiPlatform\JsonSchema\Schema; -use ApiPlatform\JsonSchema\SchemaFactory; -use ApiPlatform\JsonSchema\SchemaFactoryInterface; use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\JsonSchema\TypeFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; diff --git a/src/Hal/JsonSchema/SchemaFactory.php b/src/Hal/JsonSchema/SchemaFactory.php index b49a9ba1bcd..7e9ba378ea2 100644 --- a/src/Hal/JsonSchema/SchemaFactory.php +++ b/src/Hal/JsonSchema/SchemaFactory.php @@ -15,6 +15,7 @@ use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Metadata\Operation; /** * Decorator factory which adds HAL properties to the JSON Schema document. @@ -56,9 +57,9 @@ public function __construct(SchemaFactoryInterface $schemaFactory) /** * {@inheritdoc} */ - public function buildSchema(string $className, string $format = 'jsonhal', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema + public function buildSchema(string $className, string $format = 'jsonhal', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema { - $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection); + $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); if ('jsonhal' !== $format) { return $schema; } @@ -135,5 +136,3 @@ public function addDistinctFormat(string $format): void } } } - -class_alias(SchemaFactory::class, \ApiPlatform\Core\Hal\JsonSchema\SchemaFactory::class); diff --git a/src/Hydra/JsonSchema/SchemaFactory.php b/src/Hydra/JsonSchema/SchemaFactory.php index d0e0aa6b269..c2ab6d327d6 100644 --- a/src/Hydra/JsonSchema/SchemaFactory.php +++ b/src/Hydra/JsonSchema/SchemaFactory.php @@ -17,6 +17,7 @@ use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactory as BaseSchemaFactory; use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Metadata\Operation; /** * Decorator factory which adds Hydra properties to the JSON Schema document. @@ -70,9 +71,9 @@ public function __construct(SchemaFactoryInterface $schemaFactory) /** * {@inheritdoc} */ - public function buildSchema(string $className, string $format = 'jsonld', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema + public function buildSchema(string $className, string $format = 'jsonld', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema { - $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection); + $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); if ('jsonld' !== $format) { return $schema; } @@ -187,5 +188,3 @@ public function addDistinctFormat(string $format): void } } } - -class_alias(SchemaFactory::class, \ApiPlatform\Core\Hydra\JsonSchema\SchemaFactory::class); diff --git a/src/JsonSchema/Command/JsonSchemaGenerateCommand.php b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php index 10aaedf29c6..152ef33dd36 100644 --- a/src/JsonSchema/Command/JsonSchemaGenerateCommand.php +++ b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php @@ -14,8 +14,10 @@ namespace ApiPlatform\JsonSchema\Command; use ApiPlatform\Core\Api\OperationType; +use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface as LegacySchemaFactoryInterface; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Metadata\HttpOperation; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Input\InputArgument; @@ -33,10 +35,13 @@ final class JsonSchemaGenerateCommand extends Command { protected static $defaultName = 'api:json-schema:generate'; + /** + * @param $schemaFactory SchemaFactoryInterface|LegacySchemaFactoryInterface + */ private $schemaFactory; private $formats; - public function __construct(SchemaFactoryInterface $schemaFactory, array $formats) + public function __construct($schemaFactory, array $formats) { $this->schemaFactory = $schemaFactory; $this->formats = array_keys($formats); @@ -102,7 +107,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $operationName = $itemOperation ?? $collectionOperation; } - $schema = $this->schemaFactory->buildSchema($resource, $format, $type, $operationType, $operationName); + if ($this->schemaFactory instanceof LegacySchemaFactoryInterface) { + $schema = $this->schemaFactory->buildSchema($resource, $format, $type, $operationType, $operationName); + } else { + $schema = $this->schemaFactory->buildSchema($resource, $format, $type, $operationName ? (new class() extends HttpOperation {})->withName($operationName) : null); + } if (null !== $operationType && null !== $operationName && !$schema->isDefined()) { $io->error(sprintf('There is no %s defined for the operation "%s" of the resource "%s".', $type, $operationName, $resource)); diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 535fada3564..979ea72f16b 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -14,19 +14,13 @@ namespace ApiPlatform\JsonSchema; use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Core\Api\OperationType; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface as LegacyPropertyMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface as LegacyPropertyNameCollectionFactoryInterface; -use ApiPlatform\Core\Metadata\Property\PropertyMetadata; -use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; -use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\OpenApi\Factory\OpenApiFactory; use ApiPlatform\Util\ResourceClassInfoTrait; use Symfony\Component\PropertyInfo\Type; @@ -45,23 +39,14 @@ final class SchemaFactory implements SchemaFactoryInterface use ResourceClassInfoTrait; private $typeFactory; - /** - * @var LegacyPropertyNameCollectionFactoryInterface|PropertyNameCollectionFactoryInterface - */ private $propertyNameCollectionFactory; - /** - * @var LegacyPropertyMetadataFactoryInterface|PropertyMetadataFactoryInterface - */ private $propertyMetadataFactory; private $nameConverter; private $distinctFormats = []; - public function __construct(TypeFactoryInterface $typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null) + public function __construct(TypeFactoryInterface $typeFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null) { $this->typeFactory = $typeFactory; - if (!$resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) { - trigger_deprecation('api-platform/core', '2.7', sprintf('Use "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, ResourceMetadataFactoryInterface::class)); - } $this->resourceMetadataFactory = $resourceMetadataFactory; $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; @@ -82,29 +67,21 @@ public function addDistinctFormat(string $format): void /** * {@inheritdoc} */ - public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema + 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 = $schema ? clone $schema : new Schema(); - if (null === $metadata = $this->getMetadata($className, $type, $operationType, $operationName, $serializerContext)) { + if (null === $metadata = $this->getMetadata($className, $type, $operation, $serializerContext)) { return $schema; } - [$resourceMetadata, $serializerContext, $validationGroups, $inputOrOutputClass] = $metadata; - - if (null === $resourceMetadata && (null !== $operationType || null !== $operationName)) { - throw new \LogicException('The $operationType and $operationName arguments must be null for non-resource class.'); - } - - $operation = $resourceMetadata instanceof ResourceMetadataCollection ? $resourceMetadata->getOperation($operationName, OperationType::COLLECTION === $operationType) : null; + [$operation, $serializerContext, $validationGroups, $inputOrOutputClass] = $metadata; $version = $schema->getVersion(); - $definitionName = $this->buildDefinitionName($className, $format, $inputOrOutputClass, $resourceMetadata instanceof ResourceMetadata ? $resourceMetadata : $operation, $serializerContext); + $definitionName = $this->buildDefinitionName($className, $format, $inputOrOutputClass, $operation, $serializerContext); $method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET'; - if (!$operation && (null === $operationType || null === $operationName)) { + if (!$operation) { $method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET'; - } elseif ($resourceMetadata instanceof ResourceMetadata) { - $method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET'); } if (Schema::TYPE_OUTPUT !== $type && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) { @@ -113,8 +90,7 @@ public function buildSchema(string $className, string $format = 'json', string $ if (!isset($schema['$ref']) && !isset($schema['type'])) { $ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName; - - if ($forceCollection || (OperationType::COLLECTION === $operationType && 'POST' !== $method)) { + if ($forceCollection || ('POST' !== $method && $operation instanceof CollectionOperationInterface)) { $schema['type'] = 'array'; $schema['items'] = ['$ref' => $ref]; } else { @@ -131,12 +107,7 @@ public function buildSchema(string $className, string $format = 'json', string $ /** @var \ArrayObject $definition */ $definition = new \ArrayObject(['type' => 'object']); $definitions[$definitionName] = $definition; - - if ($resourceMetadata instanceof ResourceMetadata) { - $definition['description'] = $resourceMetadata->getDescription() ?? ''; - } else { - $definition['description'] = $operation ? ($operation->getDescription() ?? '') : ''; - } + $definition['description'] = $operation ? ($operation->getDescription() ?? '') : ''; // additionalProperties are allowed by default, so it does not need to be set explicitly, unless allow_extra_attributes is false // See https://json-schema.org/understanding-json-schema/reference/object.html#properties @@ -145,27 +116,17 @@ public function buildSchema(string $className, string $format = 'json', string $ } // see https://github.com/json-schema-org/json-schema-spec/pull/737 - if ( - Schema::VERSION_SWAGGER !== $version - ) { - if (($resourceMetadata instanceof ResourceMetadata && - ($operationType && $operationName ? $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true) : $resourceMetadata->getAttribute('deprecation_reason', null)) - ) || ($operation && $operation->getDeprecationReason()) - ) { - $definition['deprecated'] = true; - } + if (Schema::VERSION_SWAGGER !== $version && $operation && $operation->getDeprecationReason()) { + $definition['deprecated'] = true; } // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4 - if ($resourceMetadata instanceof ResourceMetadata && $resourceMetadata->getIri()) { - $definition['externalDocs'] = ['url' => $resourceMetadata->getIri()]; - } elseif ($operation instanceof HttpOperation && ($operation->getTypes()[0] ?? null)) { + if ($operation instanceof HttpOperation && ($operation->getTypes()[0] ?? null)) { $definition['externalDocs'] = ['url' => $operation->getTypes()[0]]; } - // TODO: getFactoryOptions should be refactored because Item & Collection Operations don't exist anymore (API Platform 3.0) - $options = $this->getFactoryOptions($serializerContext, $validationGroups, $operationType, $operationName, $operation instanceof HttpOperation ? $operation : null); + $options = $this->getFactoryOptions($serializerContext, $validationGroups, $operation instanceof HttpOperation ? $operation : null); foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) { $propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName, $options); if (!$propertyMetadata->isReadable() && !$propertyMetadata->isWritable()) { @@ -183,32 +144,19 @@ public function buildSchema(string $className, string $format = 'json', string $ return $schema; } - private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, $propertyMetadata, array $serializerContext, string $format): void + private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format): void { $version = $schema->getVersion(); $swagger = Schema::VERSION_SWAGGER === $version; - $propertySchema = $propertyMetadata->getSchema() ?? []; - - if ($propertyMetadata instanceof ApiProperty) { - $additionalPropertySchema = $propertyMetadata->getOpenapiContext() ?? []; + if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) { + $additionalPropertySchema = $propertyMetadata->getOpenapiContext(); } else { - switch ($version) { - case Schema::VERSION_SWAGGER: - $basePropertySchemaAttribute = 'swagger_context'; - break; - case Schema::VERSION_OPENAPI: - $basePropertySchemaAttribute = 'openapi_context'; - break; - default: - $basePropertySchemaAttribute = 'json_schema_context'; - } - - $additionalPropertySchema = $propertyMetadata->getAttributes()[$basePropertySchemaAttribute] ?? []; + $additionalPropertySchema = $propertyMetadata->getJsonSchemaContext(); } $propertySchema = array_merge( - $propertySchema, - $additionalPropertySchema + $propertyMetadata->getSchema() ?? [], + $additionalPropertySchema ?? [] ); if (false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) { @@ -221,7 +169,7 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $propertySchema['description'] = $description; } - $deprecationReason = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getAttribute('deprecation_reason') : $propertyMetadata->getDeprecationReason(); + $deprecationReason = $propertyMetadata->getDeprecationReason(); // see https://github.com/json-schema-org/json-schema-spec/pull/737 if (!$swagger && null !== $deprecationReason) { @@ -229,7 +177,7 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str } // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4 - $iri = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getIri() : $propertyMetadata->getTypes()[0] ?? null; + $iri = $propertyMetadata->getTypes()[0] ?? null; if (null !== $iri) { $propertySchema['externalDocs'] = ['url' => $iri]; } @@ -247,8 +195,8 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str } $valueSchema = []; - // TODO: 3.0 support multiple types, default value of types will be [] instead of null - $type = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getType() : $propertyMetadata->getBuiltinTypes()[0] ?? null; + // TODO: 3.0 support multiple types + $type = $propertyMetadata->getBuiltinTypes()[0] ?? null; if (null !== $type) { if ($isCollection = $type->isCollection()) { $keyType = method_exists(Type::class, 'getCollectionKeyTypes') ? ($type->getCollectionKeyTypes()[0] ?? null) : $type->getCollectionKeyType(); @@ -277,10 +225,10 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema; } - private function buildDefinitionName(string $className, string $format = 'json', ?string $inputOrOutputClass = null, $resourceMetadata = null, ?array $serializerContext = null): string + private function buildDefinitionName(string $className, string $format = 'json', ?string $inputOrOutputClass = null, Operation $operation = null, ?array $serializerContext = null): string { - if ($resourceMetadata) { - $prefix = $resourceMetadata instanceof ResourceMetadata ? $resourceMetadata->getShortName() : $resourceMetadata->getShortName(); + if ($operation) { + $prefix = $operation->getShortName(); } if (!isset($prefix)) { @@ -298,7 +246,7 @@ private function buildDefinitionName(string $className, string $format = 'json', $prefix .= '.'.$format; } - $definitionName = $serializerContext[OpenApiFactory::OPENAPI_DEFINITION_NAME] ?? $serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME] ?? null; + $definitionName = $serializerContext[OpenApiFactory::OPENAPI_DEFINITION_NAME] ?? null; if ($definitionName) { $name = sprintf('%s-%s', $prefix, $definitionName); } else { @@ -314,7 +262,7 @@ private function encodeDefinitionName(string $name): string return preg_replace('/[^a-zA-Z0-9.\-_]/', '.', $name); } - private function getMetadata(string $className, string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?array $serializerContext = null): ?array + private function getMetadata(string $className, string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?array $serializerContext = null): ?array { if (!$this->isResourceClass($className)) { return [ @@ -325,21 +273,39 @@ private function getMetadata(string $className, string $type = Schema::TYPE_OUTP ]; } - /** @var ResourceMetadata|ResourceMetadataCollection $resourceMetadata */ - $resourceMetadata = $this->resourceMetadataFactory->create($className); - $attribute = Schema::TYPE_OUTPUT === $type ? 'output' : 'input'; - $operation = ($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) ? null : $resourceMetadata->getOperation($operationName); + // The best here is to use an Operation when calling `buildSchema`, we try to do a smart guess otherwise + if (!$operation || !$operation->getClass()) { + $resourceMetadataCollection = $this->resourceMetadataFactory->create($className); - if ($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) { - if (null === $operationType || null === $operationName) { - $inputOrOutput = $resourceMetadata->getAttribute($attribute, ['class' => $className]); - } else { - $inputOrOutput = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, ['class' => $className], true); + foreach ($resourceMetadataCollection as $resourceMetadata) { + foreach ($resourceMetadata->getOperations() ?? [] as $op) { + if ($operation && $operation->getName() === $op->getName()) { + $operation = $op; + break 2; + } + + if ($operation instanceof CollectionOperationInterface && $op instanceof CollectionOperationInterface) { + $operation = $op; + break 2; + } + + if (Schema::TYPE_INPUT === $type && \in_array($op->getMethod(), ['POST', 'PATCH', 'PUT'], true)) { + $operation = $op; + break 2; + } + + if (!$operation) { + $operation = $op; + } + } } - } elseif ($operation) { - $inputOrOutput = (Schema::TYPE_OUTPUT === $type ? $operation->getOutput() : $operation->getInput()) ?? ['class' => $className]; - } else { - $inputOrOutput = ['class' => $className]; + } + + $attribute = Schema::TYPE_OUTPUT === $type ? 'output' : 'input'; + $inputOrOutput = ['class' => $className]; + + if ($operation) { + $inputOrOutput = Schema::TYPE_OUTPUT === $type ? ($operation->getOutput() ?? $inputOrOutput) : ($operation->getInput() ?? $inputOrOutput); } if (null === ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null)) { @@ -347,53 +313,26 @@ private function getMetadata(string $className, string $type = Schema::TYPE_OUTP return null; } + if (!$operation) { + return [$operation, $serializerContext ?? [], [], $inputOrOutput['class'] ?? $inputOrOutput->class]; + } + return [ - $resourceMetadata, - $serializerContext ?? $this->getSerializerContext($resourceMetadata, $type, $operationType, $operationName), - $this->getValidationGroups($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface ? $resourceMetadata : $operation, $operationType, $operationName), + $operation, + $serializerContext ?? $this->getSerializerContext($operation, $type), + $this->getValidationGroups($operation), $inputOrOutput['class'] ?? $inputOrOutput->class, ]; } - private function getSerializerContext($resourceMetadata, string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null): array + private function getSerializerContext(?Operation $operation, string $type = Schema::TYPE_OUTPUT): array { - if ($resourceMetadata instanceof ResourceMetadata) { - $attribute = Schema::TYPE_OUTPUT === $type ? 'normalization_context' : 'denormalization_context'; - } else { - $operation = $resourceMetadata->getOperation($operationName); - } - - if (null === $operationType || null === $operationName) { - if ($resourceMetadata instanceof ResourceMetadata) { - return $resourceMetadata->getAttribute($attribute, []); - } - - return Schema::TYPE_OUTPUT === $type ? ($operation->getNormalizationContext() ?? []) : ($operation->getDenormalizationContext() ?? []); - } - - if ($resourceMetadata instanceof ResourceMetadata) { - return $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, [], true); - } - return Schema::TYPE_OUTPUT === $type ? ($operation->getNormalizationContext() ?? []) : ($operation->getDenormalizationContext() ?? []); } - /** - * @param HttpOperation|ResourceMetadata|null $resourceMetadata - */ - private function getValidationGroups($resourceMetadata, ?string $operationType, ?string $operationName): array + private function getValidationGroups(Operation $operation): array { - if ($resourceMetadata instanceof ResourceMetadata) { - $attribute = 'validation_groups'; - - if (null === $operationType || null === $operationName) { - return \is_array($validationGroups = $resourceMetadata->getAttribute($attribute, [])) ? $validationGroups : []; - } - - return \is_array($validationGroups = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, [], true)) ? $validationGroups : []; - } - - $groups = $resourceMetadata ? ($resourceMetadata->getValidationContext()['groups'] ?? []) : []; + $groups = $operation->getValidationContext()['groups'] ?? []; return \is_array($groups) ? $groups : [$groups]; } @@ -401,7 +340,7 @@ private function getValidationGroups($resourceMetadata, ?string $operationType, /** * Gets the options for the property name collection / property metadata factories. */ - private function getFactoryOptions(array $serializerContext, array $validationGroups, ?string $operationType, ?string $operationName, ?HttpOperation $operation = null): array + private function getFactoryOptions(array $serializerContext, array $validationGroups, ?HttpOperation $operation = null): array { $options = [ /* @see https://github.com/symfony/symfony/blob/v5.1.0/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php */ @@ -413,22 +352,12 @@ private function getFactoryOptions(array $serializerContext, array $validationGr $options['serializer_groups'] = (array) $serializerContext[AbstractNormalizer::GROUPS]; } - if ($this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface && $operation) { - $options['normalization_groups'] = $operation->getNormalizationContext()['groups'] ?? null; - $options['denormalization_groups'] = $operation->getDenormalizationContext()['groups'] ?? null; + if ($operation && ($normalizationGroups = $operation->getNormalizationContext()['groups'] ?? null)) { + $options['normalization_groups'] = $normalizationGroups; } - if (null !== $operationType && null !== $operationName) { - switch ($operationType) { - case OperationType::COLLECTION: - $options['collection_operation_name'] = $operationName; - break; - case OperationType::ITEM: - $options['item_operation_name'] = $operationName; - break; - default: - break; - } + if ($operation && ($denormalizationGroups = $operation->getDenormalizationContext()['groups'] ?? null)) { + $options['denormalization_groups'] = $denormalizationGroups; } if ($validationGroups) { @@ -438,5 +367,3 @@ private function getFactoryOptions(array $serializerContext, array $validationGr return $options; } } - -class_alias(SchemaFactory::class, \ApiPlatform\Core\JsonSchema\SchemaFactory::class); diff --git a/src/JsonSchema/SchemaFactoryInterface.php b/src/JsonSchema/SchemaFactoryInterface.php index 6a5170f109d..610b5dbaca2 100644 --- a/src/JsonSchema/SchemaFactoryInterface.php +++ b/src/JsonSchema/SchemaFactoryInterface.php @@ -13,6 +13,8 @@ namespace ApiPlatform\JsonSchema; +use ApiPlatform\Metadata\Operation; + /** * Factory for creating the JSON Schema document corresponding to a PHP class. * @@ -25,5 +27,5 @@ interface SchemaFactoryInterface /** * Builds the JSON Schema document corresponding to the given PHP class. */ - public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema; + 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; } diff --git a/src/JsonSchema/TypeFactory.php b/src/JsonSchema/TypeFactory.php index b732726ad62..4e008ec134d 100644 --- a/src/JsonSchema/TypeFactory.php +++ b/src/JsonSchema/TypeFactory.php @@ -14,6 +14,7 @@ namespace ApiPlatform\JsonSchema; use ApiPlatform\Api\ResourceClassResolverInterface; +use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface as LegacySchemaFactoryInterface; use ApiPlatform\Util\ResourceClassInfoTrait; use Ramsey\Uuid\UuidInterface; use Symfony\Component\PropertyInfo\Type; @@ -41,7 +42,12 @@ public function __construct(ResourceClassResolverInterface $resourceClassResolve $this->resourceClassResolver = $resourceClassResolver; } - public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void + /** + * SchemaFactoryInterface|LegacySchemaFactoryInterface. + * + * @param mixed $schemaFactory + */ + public function setSchemaFactory($schemaFactory): void { $this->schemaFactory = $schemaFactory; } @@ -148,7 +154,7 @@ private function getClassType(?string $className, string $format, ?bool $readabl throw new \LogicException('The schema factory must be injected by calling the "setSchemaFactory" method.'); } - $subSchema = $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, null, $subSchema, $serializerContext, false); + $subSchema = $this->schemaFactory instanceof LegacySchemaFactoryInterface ? $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, null, $subSchema, $serializerContext, false) : $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, $subSchema, $serializerContext, false); return ['$ref' => $subSchema['$ref']]; } diff --git a/src/Metadata/ApiProperty.php b/src/Metadata/ApiProperty.php index af9806d9895..3550dce6672 100644 --- a/src/Metadata/ApiProperty.php +++ b/src/Metadata/ApiProperty.php @@ -73,6 +73,7 @@ final class ApiProperty private $fetchEager; private $jsonldContext; private $openapiContext; + private $jsonSchemaContext; private $push; private $security; private $securityPostDenormalize; @@ -132,6 +133,7 @@ public function __construct( ?bool $fetchEager = null, ?array $jsonldContext = null, ?array $openapiContext = null, + ?array $jsonSchemaContext = null, ?bool $push = null, ?string $security = null, ?string $securityPostDenormalize = null, @@ -160,6 +162,7 @@ public function __construct( $this->fetchEager = $fetchEager; $this->jsonldContext = $jsonldContext; $this->openapiContext = $openapiContext; + $this->jsonSchemaContext = $jsonSchemaContext; $this->push = $push; $this->security = $security; $this->openapiContext = $openapiContext; @@ -354,6 +357,19 @@ public function withOpenapiContext($openapiContext): self return $self; } + public function getJsonSchemaContext(): ?array + { + return $this->jsonSchemaContext; + } + + public function withJsonSchemaContext($jsonSchemaContext): self + { + $self = clone $this; + $self->jsonSchemaContext = $jsonSchemaContext; + + return $self; + } + public function getPush(): ?bool { return $this->push; diff --git a/src/Metadata/Extractor/XmlPropertyExtractor.php b/src/Metadata/Extractor/XmlPropertyExtractor.php index a270648c174..6b7d0333882 100644 --- a/src/Metadata/Extractor/XmlPropertyExtractor.php +++ b/src/Metadata/Extractor/XmlPropertyExtractor.php @@ -61,6 +61,7 @@ protected function extractPath(string $path) 'fetchEager' => $this->phpize($property, 'fetchEager', 'bool'), 'jsonldContext' => isset($property->jsonldContext->values) ? $this->buildValues($property->jsonldContext->values) : null, 'openapiContext' => isset($property->openapiContext->values) ? $this->buildValues($property->openapiContext->values) : null, + 'jsonSchemaContext' => isset($property->jsonSchemaContext->values) ? $this->buildValues($property->jsonSchemaContext->values) : null, 'push' => $this->phpize($property, 'push', 'bool'), 'security' => $this->phpize($property, 'security', 'string'), 'securityPostDenormalize' => $this->phpize($property, 'securityPostDenormalize', 'string'), @@ -69,6 +70,7 @@ protected function extractPath(string $path) 'schema' => isset($property->schema->values) ? $this->buildValues($property->schema->values) : null, 'initializable' => $this->phpize($property, 'initializable', 'bool'), 'extraProperties' => $this->buildExtraProperties($property, 'extraProperties'), + 'iri' => $this->phpize($property, 'iri', 'bool'), ]; } } diff --git a/src/Metadata/Extractor/XmlResourceExtractor.php b/src/Metadata/Extractor/XmlResourceExtractor.php index 534d453eb01..a1279253c09 100644 --- a/src/Metadata/Extractor/XmlResourceExtractor.php +++ b/src/Metadata/Extractor/XmlResourceExtractor.php @@ -128,6 +128,8 @@ private function buildBase(\SimpleXMLElement $resource): array 'filters' => $this->buildArrayValue($resource, 'filter'), 'order' => isset($resource->order->values) ? $this->buildValues($resource->order->values) : null, 'extraProperties' => $this->buildExtraProperties($resource, 'extraProperties'), + // 'provider' => $this->phpize($resource, 'provider', 'string'), + // 'processor' => $this->phpize($resource, 'processor', 'string'), ]; } diff --git a/src/Metadata/Extractor/YamlPropertyExtractor.php b/src/Metadata/Extractor/YamlPropertyExtractor.php index d0b7d7ac582..429f0dd2f47 100644 --- a/src/Metadata/Extractor/YamlPropertyExtractor.php +++ b/src/Metadata/Extractor/YamlPropertyExtractor.php @@ -82,8 +82,10 @@ private function buildProperties(array $resourcesYaml): void 'security' => $this->phpize($propertyValues, 'security', 'string'), 'securityPostDenormalize' => $this->phpize($propertyValues, 'securityPostDenormalize', 'string'), 'initializable' => $this->phpize($propertyValues, 'initializable', 'bool'), + 'iri' => $this->phpize($propertyValues, 'iri', 'bool'), 'jsonldContext' => $this->buildAttribute($propertyValues, 'jsonldContext'), 'openapiContext' => $this->buildAttribute($propertyValues, 'openapiContext'), + 'jsonSchemaContext' => $this->buildAttribute($propertyValues, 'jsonSchemaContext'), 'types' => $this->buildAttribute($propertyValues, 'types'), 'extraProperties' => $this->buildAttribute($propertyValues, 'extraProperties'), 'default' => $propertyValues['default'] ?? null, diff --git a/src/Metadata/Extractor/schema/properties.xsd b/src/Metadata/Extractor/schema/properties.xsd index e834835c98c..0a189f0ec4d 100644 --- a/src/Metadata/Extractor/schema/properties.xsd +++ b/src/Metadata/Extractor/schema/properties.xsd @@ -18,6 +18,7 @@ + diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 6e3fb82bcbe..58119c4c6cb 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -174,7 +174,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection $operationOutputSchemas = []; foreach ($responseMimeTypes as $operationFormat) { - $operationOutputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_OUTPUT, null, $operationName, $schema, null, $forceSchemaCollection); + $operationOutputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_OUTPUT, $operation, $schema, null, $forceSchemaCollection); $operationOutputSchemas[$operationFormat] = $operationOutputSchema; $this->appendSchemaDefinitions($schemas, $operationOutputSchema->getDefinitions()); } @@ -259,7 +259,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection } elseif (\in_array($method, [HttpOperation::METHOD_PATCH, HttpOperation::METHOD_PUT, HttpOperation::METHOD_POST], true)) { $operationInputSchemas = []; foreach ($requestMimeTypes as $operationFormat) { - $operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, null, $operationName, $schema, null, $forceSchemaCollection); + $operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, $operation, $schema, null, $forceSchemaCollection); $operationInputSchemas[$operationFormat] = $operationInputSchema; $this->appendSchemaDefinitions($schemas, $operationInputSchema->getDefinitions()); } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index b29dfc9cc26..f248738d467 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -110,7 +110,7 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerCommonConfiguration($container, $config, $loader, $formats, $patchFormats, $errorFormats); $this->registerMetadataConfiguration($container, $config, $loader); $this->registerOAuthConfiguration($container, $config); - $this->registerOpenApiConfiguration($container, $config); + $this->registerOpenApiConfiguration($container, $config, $loader); $this->registerSwaggerConfiguration($container, $config, $loader); $this->registerJsonApiConfiguration($formats, $loader); $this->registerJsonLdHydraConfiguration($container, $formats, $loader, $config['enable_docs']); @@ -470,8 +470,6 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array throw new RuntimeException('You can not enable the Swagger UI without enabling Swagger, fix this by enabling swagger via the configuration "enable_swagger: true".'); } - $loader->load('json_schema.xml'); - if (!$config['enable_swagger']) { return; } @@ -899,7 +897,7 @@ private function registerSecurityConfiguration(ContainerBuilder $container, XmlF } } - private function registerOpenApiConfiguration(ContainerBuilder $container, array $config): void + private function registerOpenApiConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void { $container->setParameter('api_platform.openapi.termsOfService', $config['openapi']['termsOfService']); $container->setParameter('api_platform.openapi.contact.name', $config['openapi']['contact']['name']); @@ -907,6 +905,14 @@ private function registerOpenApiConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.openapi.contact.email', $config['openapi']['contact']['email']); $container->setParameter('api_platform.openapi.license.name', $config['openapi']['license']['name']); $container->setParameter('api_platform.openapi.license.url', $config['openapi']['license']['url']); + + $loader->load('json_schema.xml'); + + if ($config['metadata_backward_compatibility_layer']) { + $loader->load('legacy/json_schema.xml'); + } else { + $loader->load('v3/json_schema.xml'); + } } private function registerMakerConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void diff --git a/src/Symfony/Bundle/Resources/config/legacy/json_schema.xml b/src/Symfony/Bundle/Resources/config/legacy/json_schema.xml new file mode 100644 index 00000000000..90943ab4059 --- /dev/null +++ b/src/Symfony/Bundle/Resources/config/legacy/json_schema.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/v3/json_schema.xml b/src/Symfony/Bundle/Resources/config/v3/json_schema.xml new file mode 100644 index 00000000000..21f1c5334fd --- /dev/null +++ b/src/Symfony/Bundle/Resources/config/v3/json_schema.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php index 1af59ecdfb1..57d99338405 100644 --- a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php +++ b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php @@ -13,9 +13,10 @@ namespace ApiPlatform\Symfony\Bundle\Test; -use ApiPlatform\Core\Api\OperationType; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Symfony\Bundle\Test\Constraint\ArraySubset; use ApiPlatform\Symfony\Bundle\Test\Constraint\MatchesJsonSchema; use PHPUnit\Framework\ExpectationFailedException; @@ -112,14 +113,14 @@ public static function assertMatchesJsonSchema($jsonSchema, ?int $checkMode = nu public static function assertMatchesResourceCollectionJsonSchema(string $resourceClass, ?string $operationName = null, string $format = 'jsonld'): void { - $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, OperationType::COLLECTION, $operationName, null); + $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, new GetCollection(name: $operationName), null); static::assertMatchesJsonSchema($schema->getArrayCopy()); } public static function assertMatchesResourceItemJsonSchema(string $resourceClass, ?string $operationName = null, string $format = 'jsonld'): void { - $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, OperationType::ITEM, $operationName, null); + $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, new Get(name: $operationName), null); static::assertMatchesJsonSchema($schema->getArrayCopy()); } diff --git a/src/deprecation.php b/src/deprecation.php index 1355c779556..0a7226b0789 100644 --- a/src/deprecation.php +++ b/src/deprecation.php @@ -222,7 +222,6 @@ class_alias($interfaceName, $oldInterfaceName); ApiPlatform\Core\EventListener\SerializeListener::class => ApiPlatform\Symfony\EventListener\SerializeListener::class, // Hal - ApiPlatform\Core\Hal\JsonSchema\SchemaFactory::class => ApiPlatform\Hal\JsonSchema\SchemaFactory::class, ApiPlatform\Core\Hal\Serializer\CollectionNormalizer::class => ApiPlatform\Hal\Serializer\CollectionNormalizer::class, ApiPlatform\Core\Hal\Serializer\EntrypointNormalizer::class => ApiPlatform\Hal\Serializer\EntrypointNormalizer::class, ApiPlatform\Core\Hal\Serializer\ItemNormalizer::class => ApiPlatform\Hal\Serializer\ItemNormalizer::class, diff --git a/tests/Core/OpenApi/Factory/OpenApiFactoryTest.php b/tests/Core/OpenApi/Factory/OpenApiFactoryTest.php index 0d135462797..12f9403e244 100644 --- a/tests/Core/OpenApi/Factory/OpenApiFactoryTest.php +++ b/tests/Core/OpenApi/Factory/OpenApiFactoryTest.php @@ -15,6 +15,7 @@ use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Bridge\Symfony\Routing\RouterOperationPathResolver; +use ApiPlatform\Core\JsonSchema\SchemaFactory; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; @@ -28,7 +29,6 @@ use ApiPlatform\Core\Operation\UnderscorePathSegmentNameGenerator; use ApiPlatform\Core\Tests\ProphecyTrait; use ApiPlatform\JsonSchema\Schema; -use ApiPlatform\JsonSchema\SchemaFactory; use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceNameCollection; diff --git a/tests/Core/Swagger/Serializer/DocumentationNormalizerV2Test.php b/tests/Core/Swagger/Serializer/DocumentationNormalizerV2Test.php index 58bd0c9bb32..f4c5848d31a 100644 --- a/tests/Core/Swagger/Serializer/DocumentationNormalizerV2Test.php +++ b/tests/Core/Swagger/Serializer/DocumentationNormalizerV2Test.php @@ -21,6 +21,7 @@ use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Api\UrlGeneratorInterface; use ApiPlatform\Core\Bridge\Symfony\Routing\RouterOperationPathResolver; +use ApiPlatform\Core\JsonSchema\SchemaFactory; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; @@ -34,7 +35,6 @@ use ApiPlatform\Core\Tests\ProphecyTrait; use ApiPlatform\Documentation\Documentation; use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\JsonSchema\SchemaFactory; use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\Metadata\Resource\ResourceNameCollection; use ApiPlatform\PathResolver\CustomOperationPathResolver; diff --git a/tests/Core/Swagger/Serializer/DocumentationNormalizerV3Test.php b/tests/Core/Swagger/Serializer/DocumentationNormalizerV3Test.php index c5fbdf0dc3f..53939a43505 100644 --- a/tests/Core/Swagger/Serializer/DocumentationNormalizerV3Test.php +++ b/tests/Core/Swagger/Serializer/DocumentationNormalizerV3Test.php @@ -20,6 +20,8 @@ use ApiPlatform\Core\Api\OperationType; use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Bridge\Symfony\Routing\RouterOperationPathResolver; +use ApiPlatform\Core\JsonSchema\SchemaFactory; +use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; @@ -33,8 +35,6 @@ use ApiPlatform\Core\Tests\ProphecyTrait; use ApiPlatform\Documentation\Documentation; use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\JsonSchema\SchemaFactory; -use ApiPlatform\JsonSchema\SchemaFactoryInterface; use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\JsonSchema\TypeFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceNameCollection; diff --git a/tests/Hal/JsonSchema/SchemaFactoryTest.php b/tests/Hal/JsonSchema/SchemaFactoryTest.php index 7cf6ff4cfe2..2f247d5cf6d 100644 --- a/tests/Hal/JsonSchema/SchemaFactoryTest.php +++ b/tests/Hal/JsonSchema/SchemaFactoryTest.php @@ -13,18 +13,20 @@ namespace ApiPlatform\Tests\Hal\JsonSchema; -use ApiPlatform\Core\Api\OperationType; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; -use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\ProphecyTrait; use ApiPlatform\Hal\JsonSchema\SchemaFactory; use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactory as BaseSchemaFactory; use ApiPlatform\JsonSchema\TypeFactoryInterface; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; @@ -40,8 +42,13 @@ class SchemaFactoryTest extends TestCase protected function setUp(): void { $typeFactory = $this->prophesize(TypeFactoryInterface::class); - $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactory->create(Dummy::class)->willReturn(new ResourceMetadata(Dummy::class)); + $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactory->create(Dummy::class)->willReturn( + new ResourceMetadataCollection(Dummy::class, [ + (new ApiResource())->withOperations([ + 'get' => (new Get())->withName('get'), + ]), + ])); $propertyNameCollectionFactory = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true])->willReturn(new PropertyNameCollection()); $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); @@ -63,7 +70,7 @@ public function testBuildSchema(): void $resultSchema = $this->schemaFactory->buildSchema(Dummy::class); $this->assertTrue($resultSchema->isDefined()); - $this->assertEquals(str_replace('\\', '.', Dummy::class).'.jsonhal', $resultSchema->getRootDefinitionKey()); + $this->assertEquals('Dummy.jsonhal', $resultSchema->getRootDefinitionKey()); } public function testCustomFormatBuildSchema(): void @@ -71,7 +78,7 @@ public function testCustomFormatBuildSchema(): void $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'json'); $this->assertTrue($resultSchema->isDefined()); - $this->assertEquals(str_replace('\\', '.', Dummy::class), $resultSchema->getRootDefinitionKey()); + $this->assertEquals('Dummy', $resultSchema->getRootDefinitionKey()); } public function testHasRootDefinitionKeyBuildSchema(): void @@ -105,8 +112,8 @@ public function testHasRootDefinitionKeyBuildSchema(): void public function testSchemaTypeBuildSchema(): void { - $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonhal', Schema::TYPE_OUTPUT, OperationType::COLLECTION); - $definitionName = str_replace('\\', '.', Dummy::class).'.jsonhal'; + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonhal', Schema::TYPE_OUTPUT, new GetCollection()); + $definitionName = 'Dummy.jsonhal'; $this->assertNull($resultSchema->getRootDefinitionKey()); $this->assertArrayHasKey('properties', $resultSchema); @@ -117,7 +124,7 @@ public function testSchemaTypeBuildSchema(): void $properties = $resultSchema['definitions'][$definitionName]['properties']; $this->assertArrayHasKey('_links', $properties); - $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonhal', Schema::TYPE_OUTPUT, null, null, null, null, true); + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonhal', Schema::TYPE_OUTPUT, null, null, null, true); $this->assertNull($resultSchema->getRootDefinitionKey()); $this->assertArrayHasKey('properties', $resultSchema); diff --git a/tests/Hydra/JsonSchema/SchemaFactoryTest.php b/tests/Hydra/JsonSchema/SchemaFactoryTest.php index e2b6939a755..198f00fec12 100644 --- a/tests/Hydra/JsonSchema/SchemaFactoryTest.php +++ b/tests/Hydra/JsonSchema/SchemaFactoryTest.php @@ -13,24 +13,23 @@ namespace ApiPlatform\Tests\Hydra\JsonSchema; -use ApiPlatform\Core\Api\OperationType; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; -use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\ProphecyTrait; use ApiPlatform\Hydra\JsonSchema\SchemaFactory; use ApiPlatform\JsonLd\ContextBuilder; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactory as BaseSchemaFactory; use ApiPlatform\JsonSchema\TypeFactoryInterface; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; -/** - * @group legacy - */ class SchemaFactoryTest extends TestCase { use ProphecyTrait; @@ -40,15 +39,22 @@ class SchemaFactoryTest extends TestCase protected function setUp(): void { $typeFactory = $this->prophesize(TypeFactoryInterface::class); - $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactory->create(Dummy::class)->willReturn(new ResourceMetadata(Dummy::class)); + $resourceMetadataFactoryCollection = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryCollection->create(Dummy::class)->willReturn( + new ResourceMetadataCollection(Dummy::class, [ + (new ApiResource())->withOperations([ + 'get' => (new Get())->withName('get'), + ]), + ]) + ); + $propertyNameCollectionFactory = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true])->willReturn(new PropertyNameCollection()); $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); $baseSchemaFactory = new BaseSchemaFactory( $typeFactory->reveal(), - $resourceMetadataFactory->reveal(), + $resourceMetadataFactoryCollection->reveal(), $propertyNameCollectionFactory->reveal(), $propertyMetadataFactory->reveal() ); @@ -61,7 +67,7 @@ public function testBuildSchema(): void $resultSchema = $this->schemaFactory->buildSchema(Dummy::class); $this->assertTrue($resultSchema->isDefined()); - $this->assertEquals(str_replace('\\', '.', Dummy::class).'.jsonld', $resultSchema->getRootDefinitionKey()); + $this->assertEquals('Dummy.jsonld', $resultSchema->getRootDefinitionKey()); } public function testCustomFormatBuildSchema(): void @@ -69,7 +75,7 @@ public function testCustomFormatBuildSchema(): void $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'json'); $this->assertTrue($resultSchema->isDefined()); - $this->assertEquals(str_replace('\\', '.', Dummy::class), $resultSchema->getRootDefinitionKey()); + $this->assertEquals('Dummy', $resultSchema->getRootDefinitionKey()); } public function testHasRootDefinitionKeyBuildSchema(): void @@ -111,8 +117,8 @@ public function testHasRootDefinitionKeyBuildSchema(): void public function testSchemaTypeBuildSchema(): void { - $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, OperationType::COLLECTION); - $definitionName = str_replace('\\', '.', Dummy::class).'.jsonld'; + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, new GetCollection()); + $definitionName = 'Dummy.jsonld'; $this->assertNull($resultSchema->getRootDefinitionKey()); $this->assertArrayHasKey('properties', $resultSchema); @@ -125,7 +131,7 @@ public function testSchemaTypeBuildSchema(): void $this->assertArrayHasKey('@type', $properties); $this->assertArrayHasKey('@id', $properties); - $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, null, null, null, null, true); + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, null, null, null, true); $this->assertNull($resultSchema->getRootDefinitionKey()); $this->assertArrayHasKey('properties', $resultSchema); @@ -141,7 +147,7 @@ public function testSchemaTypeBuildSchema(): void public function testHasHydraViewNavigationBuildSchema(): void { - $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, OperationType::COLLECTION); + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, new GetCollection()); $this->assertNull($resultSchema->getRootDefinitionKey()); $this->assertArrayHasKey('properties', $resultSchema); diff --git a/tests/JsonSchema/SchemaFactoryTest.php b/tests/JsonSchema/SchemaFactoryTest.php index c691115274a..9790335f2b8 100644 --- a/tests/JsonSchema/SchemaFactoryTest.php +++ b/tests/JsonSchema/SchemaFactoryTest.php @@ -14,17 +14,18 @@ namespace ApiPlatform\Tests\JsonSchema; use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Core\Api\OperationType; -use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\ProphecyTrait; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactory; use ApiPlatform\JsonSchema\TypeFactoryInterface; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\Put; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Tests\Fixtures\NotAResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\OverriddenOperationDummy; use PHPUnit\Framework\TestCase; @@ -32,9 +33,6 @@ use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -/** - * @group legacy - */ class SchemaFactoryTest extends TestCase { use ProphecyTrait; @@ -55,7 +53,7 @@ public function testBuildSchemaForNonResourceClass(): void 'type' => 'integer', ]); - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(NotAResource::class, Argument::cetera())->willReturn(new PropertyNameCollection(['foo', 'bar'])); @@ -103,20 +101,27 @@ public function testBuildSchemaForOperationWithOverriddenSerializerGroups(): voi 'type' => 'string', ]); - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(OverriddenOperationDummy::class)->willReturn(new ResourceMetadata((new \ReflectionClass(OverriddenOperationDummy::class))->getShortName(), null, null, [ - 'put' => [ - 'normalization_context' => [ - 'groups' => 'overridden_operation_dummy_put', - AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, - ], - 'validation_groups' => ['validation_groups_dummy_put'], - ], - ], [], [ - 'normalization_context' => [ - 'groups' => 'overridden_operation_dummy_read', + $shortName = (new \ReflectionClass(OverriddenOperationDummy::class))->getShortName(); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $operation = new Put( + name: 'put', + normalizationContext: [ + 'groups' => 'overridden_operation_dummy_put', + AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, ], - ])); + shortName: $shortName, + validationContext: [ + 'groups' => ['validation_groups_dummy_put'], + ] + ); + $resourceMetadataFactoryProphecy->create(OverriddenOperationDummy::class) + ->willReturn( + new ResourceMetadataCollection(OverriddenOperationDummy::class, [ + new ApiResource(operations: [ + 'put' => $operation, + ]), + ]) + ); $serializerGroup = 'overridden_operation_dummy_put'; $validationGroups = 'validation_groups_dummy_put'; @@ -141,7 +146,7 @@ public function testBuildSchemaForOperationWithOverriddenSerializerGroups(): voi $resourceClassResolverProphecy->isResourceClass(OverriddenOperationDummy::class)->willReturn(true); $schemaFactory = new SchemaFactory($typeFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), null, $resourceClassResolverProphecy->reveal()); - $resultSchema = $schemaFactory->buildSchema(OverriddenOperationDummy::class, 'json', Schema::TYPE_OUTPUT, OperationType::ITEM, 'put'); + $resultSchema = $schemaFactory->buildSchema(OverriddenOperationDummy::class, 'json', Schema::TYPE_OUTPUT); $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); $definitions = $resultSchema->getDefinitions(); @@ -193,7 +198,7 @@ public function testBuildSchemaForAssociativeArray(): void 'additionalProperties' => Type::BUILTIN_TYPE_STRING, ]); - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(NotAResource::class, Argument::cetera())->willReturn(new PropertyNameCollection(['foo', 'bar'])); diff --git a/tests/JsonSchema/TypeFactoryTest.php b/tests/JsonSchema/TypeFactoryTest.php index 44fd50fd886..a1cbe190083 100644 --- a/tests/JsonSchema/TypeFactoryTest.php +++ b/tests/JsonSchema/TypeFactoryTest.php @@ -375,10 +375,10 @@ public function testGetClassType(): void { $schemaFactoryProphecy = $this->prophesize(SchemaFactoryInterface::class); - $schemaFactoryProphecy->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, null, null, Argument::type(Schema::class), ['foo' => 'bar'], false)->will(function (array $args) { - $args[5]['$ref'] = 'ref'; + $schemaFactoryProphecy->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, null, Argument::type(Schema::class), ['foo' => 'bar'], false)->will(function (array $args) { + $args[4]['$ref'] = 'ref'; - return $args[5]; + return $args[4]; }); $typeFactory = new TypeFactory(); diff --git a/tests/Metadata/Extractor/Adapter/XmlPropertyAdapter.php b/tests/Metadata/Extractor/Adapter/XmlPropertyAdapter.php index b0a58b57e63..1163225a84c 100644 --- a/tests/Metadata/Extractor/Adapter/XmlPropertyAdapter.php +++ b/tests/Metadata/Extractor/Adapter/XmlPropertyAdapter.php @@ -117,6 +117,11 @@ private function buildOpenapiContext(\SimpleXMLElement $resource, array $values) $this->buildValues($resource->addChild('openapiContext'), $values); } + private function buildJsonSchemaContext(\SimpleXMLElement $resource, array $values): void + { + $this->buildValues($resource->addChild('jsonSchemaContext'), $values); + } + private function buildExtraProperties(\SimpleXMLElement $resource, array $values): void { $this->buildValues($resource->addChild('extraProperties'), $values); diff --git a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php index 29913e87996..a5e371363b4 100644 --- a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php @@ -62,6 +62,9 @@ final class PropertyMetadataCompatibilityTest extends TestCase 'openapiContext' => [ 'foo' => 'bar', ], + 'jsonSchemaContext' => [ + 'foo' => 'bar', + ], 'push' => true, 'security' => 'is_granted(\'IS_AUTHENTICATED_ANONYMOUSLY\')', 'securityPostDenormalize' => 'is_granted(\'ROLE_CUSTOM_ADMIN\')', diff --git a/tests/OpenApi/Serializer/OpenApiNormalizerTest.php b/tests/OpenApi/Serializer/OpenApiNormalizerTest.php index 6f9a4a322d7..ff2ce34ce7b 100644 --- a/tests/OpenApi/Serializer/OpenApiNormalizerTest.php +++ b/tests/OpenApi/Serializer/OpenApiNormalizerTest.php @@ -14,7 +14,8 @@ namespace ApiPlatform\Tests\OpenApi\Serializer; use ApiPlatform\Core\Api\IdentifiersExtractorInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Core\JsonSchema\SchemaFactory as LegacySchemaFactory; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface as LegacyPropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; @@ -26,6 +27,7 @@ use ApiPlatform\Core\Tests\ProphecyTrait; use ApiPlatform\JsonSchema\SchemaFactory; use ApiPlatform\JsonSchema\TypeFactory; +use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; @@ -33,6 +35,7 @@ use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; @@ -116,7 +119,7 @@ public function testLegacyFactoryNormalize() $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata); $resourceMetadataFactoryProphecy->create('Zorro')->shouldBeCalled()->willReturn($zorroMetadata); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy = $this->prophesize(LegacyPropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true, null, null, null, null, null, null, null)); $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, [], null, null, null, null, ['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$'])); $propertyMetadataFactoryProphecy->create(Dummy::class, 'description', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is an initializable but not writable property.', true, false, true, true, false, false, null, null, [], null, true)); @@ -133,7 +136,7 @@ public function testLegacyFactoryNormalize() $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); $typeFactory = new TypeFactory(); - $schemaFactory = new SchemaFactory($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter()); + $schemaFactory = new LegacySchemaFactory($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter()); $typeFactory->setSchemaFactory($schemaFactory); $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); @@ -295,12 +298,23 @@ public function testNormalize() $resourceCollectionMetadataFactoryProphecy->create('Zorro')->shouldBeCalled()->willReturn($zorroMetadata); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true, null, null, null, null, null, null, null)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, [], null, null, null, null, ['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$'])); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'description', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is an initializable but not writable property.', true, false, true, true, false, false, null, null, [], null, true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class), 'This is a \DateTimeInterface object.', true, true, true, true, false, false, null, null, [])); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn( + (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withDescription('This is an id.')->withReadable(true)->withWritable(false)->withIdentifier(true) + ); - $propertyMetadataFactoryProphecy->create('Zorro', 'id', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->shouldBeCalled()->willReturn( + (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withSchema(['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$']) + ); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'description', Argument::any())->shouldBeCalled()->willReturn( + (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('This is an initializable but not writable property.')->withReadable(true)->withWritable(false)->withReadableLink(true)->withWritableLink(true)->withInitializable(true) + ); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn( + (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class)])->withDescription('This is a \DateTimeInterface object.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true) + ); + + $propertyMetadataFactoryProphecy->create('Zorro', 'id', Argument::any())->shouldBeCalled()->willReturn( + (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withDescription('This is an id.')->withReadable(true)->withWritable(false)->withIdentifier(true) + ); $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); From d8d8ab5f47f39ed0260de98e935a167d71f8b028 Mon Sep 17 00:00:00 2001 From: soyuka Date: Mon, 16 May 2022 17:03:38 +0200 Subject: [PATCH 02/10] sdkjhg --- .../Serializer/DocumentationNormalizer.php | 22 +++++++++++++++---- .../Command/JsonSchemaGenerateCommand.php | 2 +- tests/Hal/JsonSchema/SchemaFactoryTest.php | 5 +++-- tests/Hydra/JsonSchema/SchemaFactoryTest.php | 5 +++-- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/Core/Swagger/Serializer/DocumentationNormalizer.php b/src/Core/Swagger/Serializer/DocumentationNormalizer.php index 6e7cae6277e..cb15a110d5e 100644 --- a/src/Core/Swagger/Serializer/DocumentationNormalizer.php +++ b/src/Core/Swagger/Serializer/DocumentationNormalizer.php @@ -22,8 +22,8 @@ use ApiPlatform\Core\Api\OperationType; use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Api\UrlGeneratorInterface; -use ApiPlatform\Core\JsonSchema\SchemaFactory; -use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Core\JsonSchema\SchemaFactory as LegacySchemaFactory; +use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface as LegacySchemaFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ApiResourceToLegacyResourceMetadataTrait; @@ -34,8 +34,11 @@ use ApiPlatform\Exception\ResourceClassNotFoundException; use ApiPlatform\Exception\RuntimeException; use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\JsonSchema\SchemaFactory; +use ApiPlatform\JsonSchema\SchemaFactoryInterface; use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\JsonSchema\TypeFactoryInterface; +use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\OpenApi\OpenApi; use ApiPlatform\OpenApi\Serializer\ApiGatewayNormalizer; @@ -94,7 +97,7 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup private $formatsProvider; /** - * @var SchemaFactoryInterface + * @var SchemaFactoryInterface|LegacySchemaFactoryInterface */ private $jsonSchemaFactory; /** @@ -135,7 +138,12 @@ public function __construct($resourceMetadataFactory, PropertyNameCollectionFact } if (null === $jsonSchemaFactory || $jsonSchemaFactory instanceof ResourceClassResolverInterface) { - $jsonSchemaFactory = new SchemaFactory($this->jsonSchemaTypeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $nameConverter); + if ($resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) { + $jsonSchemaFactory = new LegacySchemaFactory($this->jsonSchemaTypeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $nameConverter); + } else { + $jsonSchemaFactory = new SchemaFactory($this->jsonSchemaTypeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $nameConverter); + } + $this->jsonSchemaTypeFactory->setSchemaFactory($jsonSchemaFactory); } $this->jsonSchemaFactory = $jsonSchemaFactory; @@ -731,6 +739,12 @@ private function getJsonSchema(bool $v3, \ArrayObject $definitions, string $reso $schema = new Schema($v3 ? Schema::VERSION_OPENAPI : Schema::VERSION_SWAGGER); $schema->setDefinitions($definitions); + if ($this->jsonSchemaFactory instanceof SchemaFactoryInterface) { + $operation = $operationName ? (new class() extends HttpOperation {})->withName($operationName) : null; + + return $this->jsonSchemaFactory->buildSchema($resourceClass, $format, $type, $operation, $schema, $serializerContext, $forceCollection); + } + return $this->jsonSchemaFactory->buildSchema($resourceClass, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection); } diff --git a/src/JsonSchema/Command/JsonSchemaGenerateCommand.php b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php index 152ef33dd36..d637a06723d 100644 --- a/src/JsonSchema/Command/JsonSchemaGenerateCommand.php +++ b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php @@ -36,7 +36,7 @@ final class JsonSchemaGenerateCommand extends Command protected static $defaultName = 'api:json-schema:generate'; /** - * @param $schemaFactory SchemaFactoryInterface|LegacySchemaFactoryInterface + * @var SchemaFactoryInterface|LegacySchemaFactoryInterface */ private $schemaFactory; private $formats; diff --git a/tests/Hal/JsonSchema/SchemaFactoryTest.php b/tests/Hal/JsonSchema/SchemaFactoryTest.php index 2f247d5cf6d..5317ea2666e 100644 --- a/tests/Hal/JsonSchema/SchemaFactoryTest.php +++ b/tests/Hal/JsonSchema/SchemaFactoryTest.php @@ -22,6 +22,7 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; @@ -45,9 +46,9 @@ protected function setUp(): void $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $resourceMetadataFactory->create(Dummy::class)->willReturn( new ResourceMetadataCollection(Dummy::class, [ - (new ApiResource())->withOperations([ + (new ApiResource())->withOperations(new Operations([ 'get' => (new Get())->withName('get'), - ]), + ])), ])); $propertyNameCollectionFactory = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true])->willReturn(new PropertyNameCollection()); diff --git a/tests/Hydra/JsonSchema/SchemaFactoryTest.php b/tests/Hydra/JsonSchema/SchemaFactoryTest.php index 198f00fec12..ade1f37c8bd 100644 --- a/tests/Hydra/JsonSchema/SchemaFactoryTest.php +++ b/tests/Hydra/JsonSchema/SchemaFactoryTest.php @@ -22,6 +22,7 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; @@ -42,9 +43,9 @@ protected function setUp(): void $resourceMetadataFactoryCollection = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $resourceMetadataFactoryCollection->create(Dummy::class)->willReturn( new ResourceMetadataCollection(Dummy::class, [ - (new ApiResource())->withOperations([ + (new ApiResource())->withOperations(new Operations([ 'get' => (new Get())->withName('get'), - ]), + ])), ]) ); From f9bac01c48f9b86e7353038195a8a8deb0d487ea Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 19 May 2022 15:23:47 +0200 Subject: [PATCH 03/10] php 7... --- tests/JsonSchema/SchemaFactoryTest.php | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/JsonSchema/SchemaFactoryTest.php b/tests/JsonSchema/SchemaFactoryTest.php index 9790335f2b8..00d6ec86290 100644 --- a/tests/JsonSchema/SchemaFactoryTest.php +++ b/tests/JsonSchema/SchemaFactoryTest.php @@ -103,17 +103,10 @@ public function testBuildSchemaForOperationWithOverriddenSerializerGroups(): voi $shortName = (new \ReflectionClass(OverriddenOperationDummy::class))->getShortName(); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $operation = new Put( - name: 'put', - normalizationContext: [ - 'groups' => 'overridden_operation_dummy_put', - AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, - ], - shortName: $shortName, - validationContext: [ - 'groups' => ['validation_groups_dummy_put'], - ] - ); + $operation = (new Put)->withName('put')->withNormalizationContext([ + 'groups' => 'overridden_operation_dummy_put', + AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, + ])->withShortName($shortName)->withValidationContext(['groups' => ['validation_groups_dummy_put']]); $resourceMetadataFactoryProphecy->create(OverriddenOperationDummy::class) ->willReturn( new ResourceMetadataCollection(OverriddenOperationDummy::class, [ From 3fe06541cb8f5f6ffe3964a90ed35239ab311ce5 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 19 May 2022 15:53:58 +0200 Subject: [PATCH 04/10] php 7... --- tests/JsonSchema/SchemaFactoryTest.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/JsonSchema/SchemaFactoryTest.php b/tests/JsonSchema/SchemaFactoryTest.php index 00d6ec86290..d6d94f8fac6 100644 --- a/tests/JsonSchema/SchemaFactoryTest.php +++ b/tests/JsonSchema/SchemaFactoryTest.php @@ -20,6 +20,7 @@ use ApiPlatform\JsonSchema\TypeFactoryInterface; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; @@ -103,16 +104,14 @@ public function testBuildSchemaForOperationWithOverriddenSerializerGroups(): voi $shortName = (new \ReflectionClass(OverriddenOperationDummy::class))->getShortName(); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $operation = (new Put)->withName('put')->withNormalizationContext([ + $operation = (new Put())->withName('put')->withNormalizationContext([ 'groups' => 'overridden_operation_dummy_put', AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, ])->withShortName($shortName)->withValidationContext(['groups' => ['validation_groups_dummy_put']]); $resourceMetadataFactoryProphecy->create(OverriddenOperationDummy::class) ->willReturn( new ResourceMetadataCollection(OverriddenOperationDummy::class, [ - new ApiResource(operations: [ - 'put' => $operation, - ]), + (new ApiResource())->withOperations(new Operations(['put' => $operation])), ]) ); From e8c294fb973a1b975cd29806d61aaeed19d752e5 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 19 May 2022 16:05:16 +0200 Subject: [PATCH 05/10] php 7... --- src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php index 57d99338405..0d74e488bbf 100644 --- a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php +++ b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php @@ -113,14 +113,14 @@ public static function assertMatchesJsonSchema($jsonSchema, ?int $checkMode = nu public static function assertMatchesResourceCollectionJsonSchema(string $resourceClass, ?string $operationName = null, string $format = 'jsonld'): void { - $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, new GetCollection(name: $operationName), null); + $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, (new GetCollection)->withName($operationName), null); static::assertMatchesJsonSchema($schema->getArrayCopy()); } public static function assertMatchesResourceItemJsonSchema(string $resourceClass, ?string $operationName = null, string $format = 'jsonld'): void { - $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, new Get(name: $operationName), null); + $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, (new GetCollection)->withName($operationName), null); static::assertMatchesJsonSchema($schema->getArrayCopy()); } From 3791838e7e99779d01e9729cc2624c2012d7bced Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 19 May 2022 16:19:45 +0200 Subject: [PATCH 06/10] php 7... --- src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php index 0d74e488bbf..c4becbff806 100644 --- a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php +++ b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php @@ -113,14 +113,16 @@ public static function assertMatchesJsonSchema($jsonSchema, ?int $checkMode = nu public static function assertMatchesResourceCollectionJsonSchema(string $resourceClass, ?string $operationName = null, string $format = 'jsonld'): void { - $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, (new GetCollection)->withName($operationName), null); + $operation = $operationName ? (new GetCollection())->withName($operationName) : new GetCollection(); + $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null); static::assertMatchesJsonSchema($schema->getArrayCopy()); } public static function assertMatchesResourceItemJsonSchema(string $resourceClass, ?string $operationName = null, string $format = 'jsonld'): void { - $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, (new GetCollection)->withName($operationName), null); + $operation = $operationName ? (new Get())->withName($operationName) : new Get(); + $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null); static::assertMatchesJsonSchema($schema->getArrayCopy()); } From 9fa6940fe1c893b99c26005ff17dd14194fc507c Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 19 May 2022 17:19:24 +0200 Subject: [PATCH 07/10] fix --- src/Core/JsonSchema/SchemaFactory.php | 6 ++- .../Serializer/DocumentationNormalizer.php | 2 +- src/JsonSchema/SchemaFactory.php | 40 ++++++++++--------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/Core/JsonSchema/SchemaFactory.php b/src/Core/JsonSchema/SchemaFactory.php index 2ff09154358..865e85fbbee 100644 --- a/src/Core/JsonSchema/SchemaFactory.php +++ b/src/Core/JsonSchema/SchemaFactory.php @@ -27,6 +27,7 @@ use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\JsonSchema\TypeFactoryInterface; use ApiPlatform\OpenApi\Factory\OpenApiFactory; use ApiPlatform\Util\ResourceClassInfoTrait; use Symfony\Component\PropertyInfo\Type; @@ -56,7 +57,10 @@ final class SchemaFactory implements SchemaFactoryInterface private $nameConverter; private $distinctFormats = []; - public function __construct(TypeFactoryInterface $typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null) + /** + * @param TypeFactoryInterface $typeFactory + */ + public function __construct($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null) { $this->typeFactory = $typeFactory; if (!$resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) { diff --git a/src/Core/Swagger/Serializer/DocumentationNormalizer.php b/src/Core/Swagger/Serializer/DocumentationNormalizer.php index cb15a110d5e..bc7b8908275 100644 --- a/src/Core/Swagger/Serializer/DocumentationNormalizer.php +++ b/src/Core/Swagger/Serializer/DocumentationNormalizer.php @@ -115,7 +115,7 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup private $legacyMode; /** - * @param SchemaFactoryInterface|ResourceClassResolverInterface|null $jsonSchemaFactory + * @param LegacySchemaFactoryInterface|SchemaFactoryInterface|ResourceClassResolverInterface|null $jsonSchemaFactory * @param ContainerInterface|FilterCollection|null $filterLocator * @param array|OperationAwareFormatsProviderInterface $formats * @param mixed|null $jsonSchemaTypeFactory diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 979ea72f16b..67e6dc015ac 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -22,6 +22,7 @@ use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\OpenApi\Factory\OpenApiFactory; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\OverriddenOperationDummy; use ApiPlatform\Util\ResourceClassInfoTrait; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -70,6 +71,7 @@ public function addDistinctFormat(string $format): void 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 = $schema ? clone $schema : new Schema(); + if (null === $metadata = $this->getMetadata($className, $type, $operation, $serializerContext)) { return $schema; } @@ -277,25 +279,25 @@ private function getMetadata(string $className, string $type = Schema::TYPE_OUTP if (!$operation || !$operation->getClass()) { $resourceMetadataCollection = $this->resourceMetadataFactory->create($className); - foreach ($resourceMetadataCollection as $resourceMetadata) { - foreach ($resourceMetadata->getOperations() ?? [] as $op) { - if ($operation && $operation->getName() === $op->getName()) { - $operation = $op; - break 2; - } - - if ($operation instanceof CollectionOperationInterface && $op instanceof CollectionOperationInterface) { - $operation = $op; - break 2; - } - - if (Schema::TYPE_INPUT === $type && \in_array($op->getMethod(), ['POST', 'PATCH', 'PUT'], true)) { - $operation = $op; - break 2; - } - - if (!$operation) { - $operation = $op; + if ($operation && $operation->getName()) { + $operation = $resourceMetadataCollection->getOperation($operation->getName()); + } else { + // Guess the operation and use the first one that matches criterias + foreach ($resourceMetadataCollection as $resourceMetadata) { + foreach ($resourceMetadata->getOperations() ?? [] as $op) { + if ($operation instanceof CollectionOperationInterface && $op instanceof CollectionOperationInterface) { + $operation = $op; + break 2; + } + + if (Schema::TYPE_INPUT === $type && \in_array($op->getMethod(), ['POST', 'PATCH', 'PUT'], true)) { + $operation = $op; + break 2; + } + + if (!$operation) { + $operation = $op; + } } } } From 72d27dfc57c042ef037573831184aadc38efcc54 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 19 May 2022 17:53:35 +0200 Subject: [PATCH 08/10] tired --- src/Core/Hal/JsonSchema/SchemaFactory.php | 137 ++++++++++++++ src/Core/Hydra/JsonSchema/SchemaFactory.php | 173 +++++++++++++++++- src/Core/JsonSchema/SchemaFactory.php | 5 +- .../Serializer/DocumentationNormalizer.php | 10 +- src/JsonSchema/SchemaFactory.php | 3 +- .../ApiPlatformExtension.php | 22 ++- src/Symfony/Bundle/Resources/config/hal.xml | 6 - src/Symfony/Bundle/Resources/config/hydra.xml | 6 - .../Bundle/Resources/config/legacy/hal.xml | 17 ++ .../Bundle/Resources/config/legacy/hydra.xml | 17 ++ .../Bundle/Resources/config/v3/hal.xml | 17 ++ .../Bundle/Resources/config/v3/hydra.xml | 17 ++ 12 files changed, 402 insertions(+), 28 deletions(-) create mode 100644 src/Core/Hal/JsonSchema/SchemaFactory.php create mode 100644 src/Symfony/Bundle/Resources/config/legacy/hal.xml create mode 100644 src/Symfony/Bundle/Resources/config/legacy/hydra.xml create mode 100644 src/Symfony/Bundle/Resources/config/v3/hal.xml create mode 100644 src/Symfony/Bundle/Resources/config/v3/hydra.xml diff --git a/src/Core/Hal/JsonSchema/SchemaFactory.php b/src/Core/Hal/JsonSchema/SchemaFactory.php new file mode 100644 index 00000000000..eaebc028937 --- /dev/null +++ b/src/Core/Hal/JsonSchema/SchemaFactory.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Hal\JsonSchema; + +use ApiPlatform\Core\JsonSchema\Schema; +use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface; + +/** + * Decorator factory which adds HAL properties to the JSON Schema document. + * + * @experimental + * + * @author Kévin Dunglas + * @author Jachim Coudenys + */ +final class SchemaFactory implements SchemaFactoryInterface +{ + private const HREF_PROP = [ + 'href' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], + ]; + private const BASE_PROPS = [ + '_links' => [ + 'type' => 'object', + 'properties' => [ + 'self' => [ + 'type' => 'object', + 'properties' => self::HREF_PROP, + ], + ], + ], + ]; + + private $schemaFactory; + + public function __construct(SchemaFactoryInterface $schemaFactory) + { + $this->schemaFactory = $schemaFactory; + + $this->addDistinctFormat('jsonhal'); + } + + /** + * {@inheritdoc} + */ + public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema + { + $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection); + if ('jsonhal' !== $format) { + return $schema; + } + + $definitions = $schema->getDefinitions(); + if ($key = $schema->getRootDefinitionKey()) { + $definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []); + + return $schema; + } + if ($key = $schema->getItemsDefinitionKey()) { + $definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []); + } + + if (($schema['type'] ?? '') === 'array') { + $items = $schema['items']; + unset($schema['items']); + + $schema['type'] = 'object'; + $schema['properties'] = [ + '_embedded' => [ + 'type' => 'array', + 'items' => $items, + ], + 'totalItems' => [ + 'type' => 'integer', + 'minimum' => 0, + ], + 'itemsPerPage' => [ + 'type' => 'integer', + 'minimum' => 0, + ], + '_links' => [ + 'type' => 'object', + 'properties' => [ + 'self' => [ + 'type' => 'object', + 'properties' => self::HREF_PROP, + ], + 'first' => [ + 'type' => 'object', + 'properties' => self::HREF_PROP, + ], + 'last' => [ + 'type' => 'object', + 'properties' => self::HREF_PROP, + ], + 'next' => [ + 'type' => 'object', + 'properties' => self::HREF_PROP, + ], + 'previous' => [ + 'type' => 'object', + 'properties' => self::HREF_PROP, + ], + ], + ], + ]; + $schema['required'] = [ + '_links', + '_embedded', + ]; + + return $schema; + } + + return $schema; + } + + public function addDistinctFormat(string $format): void + { + if (method_exists($this->schemaFactory, 'addDistinctFormat')) { + $this->schemaFactory->addDistinctFormat($format); + } + } +} diff --git a/src/Core/Hydra/JsonSchema/SchemaFactory.php b/src/Core/Hydra/JsonSchema/SchemaFactory.php index 5af22519fb5..03a8ad49aad 100644 --- a/src/Core/Hydra/JsonSchema/SchemaFactory.php +++ b/src/Core/Hydra/JsonSchema/SchemaFactory.php @@ -13,10 +13,177 @@ namespace ApiPlatform\Core\Hydra\JsonSchema; -class_exists(\ApiPlatform\Hydra\JsonSchema\SchemaFactory::class); +use ApiPlatform\Core\JsonLd\ContextBuilder; +use ApiPlatform\Core\JsonSchema\Schema; +use ApiPlatform\Core\JsonSchema\SchemaFactory as BaseSchemaFactory; +use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface; -if (false) { - final class SchemaFactory extends \ApiPlatform\Hydra\JsonSchema\SchemaFactory +/** + * Decorator factory which adds Hydra properties to the JSON Schema document. + * + * @experimental + * + * @author Kévin Dunglas + */ +final class SchemaFactory implements SchemaFactoryInterface +{ + private const BASE_PROP = [ + 'readOnly' => true, + 'type' => 'string', + ]; + private const BASE_PROPS = [ + '@id' => self::BASE_PROP, + '@type' => self::BASE_PROP, + ]; + private const BASE_ROOT_PROPS = [ + '@context' => [ + 'readOnly' => true, + 'oneOf' => [ + ['type' => 'string'], + [ + 'type' => 'object', + 'properties' => [ + '@vocab' => [ + 'type' => 'string', + ], + 'hydra' => [ + 'type' => 'string', + 'enum' => [ContextBuilder::HYDRA_NS], + ], + ], + 'required' => ['@vocab', 'hydra'], + 'additionalProperties' => true, + ], + ], + ], + ] + self::BASE_PROPS; + + private $schemaFactory; + + public function __construct(SchemaFactoryInterface $schemaFactory) + { + $this->schemaFactory = $schemaFactory; + + $this->addDistinctFormat('jsonld'); + } + + /** + * {@inheritdoc} + */ + public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema + { + $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection); + if ('jsonld' !== $format) { + return $schema; + } + + $definitions = $schema->getDefinitions(); + if ($key = $schema->getRootDefinitionKey()) { + $definitions[$key]['properties'] = self::BASE_ROOT_PROPS + ($definitions[$key]['properties'] ?? []); + + return $schema; + } + if ($key = $schema->getItemsDefinitionKey()) { + $definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []); + } + + if (($schema['type'] ?? '') === 'array') { + // hydra:collection + $items = $schema['items']; + unset($schema['items']); + + $nullableStringDefinition = ['type' => 'string']; + + switch ($schema->getVersion()) { + case Schema::VERSION_JSON_SCHEMA: + $nullableStringDefinition = ['type' => ['string', 'null']]; + break; + case Schema::VERSION_OPENAPI: + $nullableStringDefinition = ['type' => 'string', 'nullable' => true]; + break; + } + + $schema['type'] = 'object'; + $schema['properties'] = [ + 'hydra:member' => [ + 'type' => 'array', + 'items' => $items, + ], + 'hydra:totalItems' => [ + 'type' => 'integer', + 'minimum' => 0, + ], + 'hydra:view' => [ + 'type' => 'object', + 'properties' => [ + '@id' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], + '@type' => [ + 'type' => 'string', + ], + 'hydra:first' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], + 'hydra:last' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], + 'hydra:previous' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], + 'hydra:next' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], + ], + 'example' => [ + '@id' => 'string', + 'type' => 'string', + 'hydra:first' => 'string', + 'hydra:last' => 'string', + 'hydra:previous' => 'string', + 'hydra:next' => 'string', + ], + ], + 'hydra:search' => [ + 'type' => 'object', + 'properties' => [ + '@type' => ['type' => 'string'], + 'hydra:template' => ['type' => 'string'], + 'hydra:variableRepresentation' => ['type' => 'string'], + 'hydra:mapping' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + '@type' => ['type' => 'string'], + 'variable' => ['type' => 'string'], + 'property' => $nullableStringDefinition, + 'required' => ['type' => 'boolean'], + ], + ], + ], + ], + ], + ]; + $schema['required'] = [ + 'hydra:member', + ]; + + return $schema; + } + + return $schema; + } + + public function addDistinctFormat(string $format): void { + if ($this->schemaFactory instanceof BaseSchemaFactory) { + $this->schemaFactory->addDistinctFormat($format); + } } } diff --git a/src/Core/JsonSchema/SchemaFactory.php b/src/Core/JsonSchema/SchemaFactory.php index 865e85fbbee..372eb63c3dc 100644 --- a/src/Core/JsonSchema/SchemaFactory.php +++ b/src/Core/JsonSchema/SchemaFactory.php @@ -21,13 +21,13 @@ use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer; +use ApiPlatform\JsonSchema\TypeFactoryInterface; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\JsonSchema\TypeFactoryInterface; use ApiPlatform\OpenApi\Factory\OpenApiFactory; use ApiPlatform\Util\ResourceClassInfoTrait; use Symfony\Component\PropertyInfo\Type; @@ -59,6 +59,9 @@ final class SchemaFactory implements SchemaFactoryInterface /** * @param TypeFactoryInterface $typeFactory + * @param mixed $resourceMetadataFactory + * @param mixed $propertyNameCollectionFactory + * @param mixed $propertyMetadataFactory */ public function __construct($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null) { diff --git a/src/Core/Swagger/Serializer/DocumentationNormalizer.php b/src/Core/Swagger/Serializer/DocumentationNormalizer.php index bc7b8908275..fa3709060fd 100644 --- a/src/Core/Swagger/Serializer/DocumentationNormalizer.php +++ b/src/Core/Swagger/Serializer/DocumentationNormalizer.php @@ -116,11 +116,11 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup /** * @param LegacySchemaFactoryInterface|SchemaFactoryInterface|ResourceClassResolverInterface|null $jsonSchemaFactory - * @param ContainerInterface|FilterCollection|null $filterLocator - * @param array|OperationAwareFormatsProviderInterface $formats - * @param mixed|null $jsonSchemaTypeFactory - * @param int[] $swaggerVersions - * @param mixed $resourceMetadataFactory + * @param ContainerInterface|FilterCollection|null $filterLocator + * @param array|OperationAwareFormatsProviderInterface $formats + * @param mixed|null $jsonSchemaTypeFactory + * @param int[] $swaggerVersions + * @param mixed $resourceMetadataFactory */ public function __construct($resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, $jsonSchemaFactory = null, $jsonSchemaTypeFactory = null, OperationPathResolverInterface $operationPathResolver = null, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, bool $oauthEnabled = false, string $oauthType = '', string $oauthFlow = '', string $oauthTokenUrl = '', string $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $paginationEnabled = true, string $paginationPageParameterName = 'page', bool $clientItemsPerPage = false, string $itemsPerPageParameterName = 'itemsPerPage', $formats = [], bool $paginationClientEnabled = false, string $paginationClientEnabledParameterName = 'pagination', array $defaultContext = [], array $swaggerVersions = [2, 3], IdentifiersExtractorInterface $identifiersExtractor = null, NormalizerInterface $openApiNormalizer = null, bool $legacyMode = false) { diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 67e6dc015ac..0c6c51986a4 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -22,7 +22,6 @@ use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\OpenApi\Factory\OpenApiFactory; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\OverriddenOperationDummy; use ApiPlatform\Util\ResourceClassInfoTrait; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -71,7 +70,7 @@ public function addDistinctFormat(string $format): void 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 = $schema ? clone $schema : new Schema(); - + if (null === $metadata = $this->getMetadata($className, $type, $operation, $serializerContext)) { return $schema; } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index f248738d467..f62d3a9b51e 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -113,8 +113,8 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerOpenApiConfiguration($container, $config, $loader); $this->registerSwaggerConfiguration($container, $config, $loader); $this->registerJsonApiConfiguration($formats, $loader); - $this->registerJsonLdHydraConfiguration($container, $formats, $loader, $config['enable_docs']); - $this->registerJsonHalConfiguration($formats, $loader); + $this->registerJsonLdHydraConfiguration($container, $formats, $loader, $config); + $this->registerJsonHalConfiguration($formats, $loader, $config); $this->registerJsonProblemConfiguration($errorFormats, $loader); $this->registerGraphQlConfiguration($container, $config, $loader); $this->registerLegacyBundlesConfiguration($container, $config, $loader); @@ -517,7 +517,7 @@ private function registerJsonApiConfiguration(array $formats, XmlFileLoader $loa $loader->load('jsonapi.xml'); } - private function registerJsonLdHydraConfiguration(ContainerBuilder $container, array $formats, XmlFileLoader $loader, bool $docEnabled): void + private function registerJsonLdHydraConfiguration(ContainerBuilder $container, array $formats, XmlFileLoader $loader, array $config): void { if (!isset($formats['jsonld'])) { return; @@ -526,22 +526,34 @@ private function registerJsonLdHydraConfiguration(ContainerBuilder $container, a $loader->load('jsonld.xml'); $loader->load('hydra.xml'); + if ($config['metadata_backward_compatibility_layer']) { + $loader->load('legacy/hydra.xml'); + } else { + $loader->load('v3/hydra.xml'); + } + if (!$container->has('api_platform.json_schema.schema_factory')) { $container->removeDefinition('api_platform.hydra.json_schema.schema_factory'); } - if (!$docEnabled) { + if (!$config['enable_docs']) { $container->removeDefinition('api_platform.hydra.listener.response.add_link_header'); } } - private function registerJsonHalConfiguration(array $formats, XmlFileLoader $loader): void + private function registerJsonHalConfiguration(array $formats, XmlFileLoader $loader, array $config): void { if (!isset($formats['jsonhal'])) { return; } $loader->load('hal.xml'); + + if ($config['metadata_backward_compatibility_layer']) { + $loader->load('legacy/hal.xml'); + } else { + $loader->load('v3/hal.xml'); + } } private function registerJsonProblemConfiguration(array $errorFormats, XmlFileLoader $loader): void diff --git a/src/Symfony/Bundle/Resources/config/hal.xml b/src/Symfony/Bundle/Resources/config/hal.xml index ca8043dc806..a4806bb5c22 100644 --- a/src/Symfony/Bundle/Resources/config/hal.xml +++ b/src/Symfony/Bundle/Resources/config/hal.xml @@ -54,12 +54,6 @@ - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/hydra.xml b/src/Symfony/Bundle/Resources/config/hydra.xml index bcb0d1c1725..6710e7b7eb4 100644 --- a/src/Symfony/Bundle/Resources/config/hydra.xml +++ b/src/Symfony/Bundle/Resources/config/hydra.xml @@ -76,12 +76,6 @@ - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/hal.xml b/src/Symfony/Bundle/Resources/config/legacy/hal.xml new file mode 100644 index 00000000000..dab3af931ca --- /dev/null +++ b/src/Symfony/Bundle/Resources/config/legacy/hal.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/legacy/hydra.xml b/src/Symfony/Bundle/Resources/config/legacy/hydra.xml new file mode 100644 index 00000000000..14e86ebf8da --- /dev/null +++ b/src/Symfony/Bundle/Resources/config/legacy/hydra.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/v3/hal.xml b/src/Symfony/Bundle/Resources/config/v3/hal.xml new file mode 100644 index 00000000000..6d5bc64890a --- /dev/null +++ b/src/Symfony/Bundle/Resources/config/v3/hal.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/v3/hydra.xml b/src/Symfony/Bundle/Resources/config/v3/hydra.xml new file mode 100644 index 00000000000..52c1128348d --- /dev/null +++ b/src/Symfony/Bundle/Resources/config/v3/hydra.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + From fe99e64afaedb5012a75d9b37d2267c7b5750ba6 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 19 May 2022 19:12:59 +0200 Subject: [PATCH 09/10] come on --- .../Bundle/Test/ApiTestAssertionsTrait.php | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php index c4becbff806..8583a019fdb 100644 --- a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php +++ b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php @@ -13,6 +13,8 @@ namespace ApiPlatform\Symfony\Bundle\Test; +use ApiPlatform\Core\Api\OperationType; +use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface as LegacySchemaFactoryInterface; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryInterface; use ApiPlatform\Metadata\Get; @@ -113,16 +115,28 @@ public static function assertMatchesJsonSchema($jsonSchema, ?int $checkMode = nu public static function assertMatchesResourceCollectionJsonSchema(string $resourceClass, ?string $operationName = null, string $format = 'jsonld'): void { - $operation = $operationName ? (new GetCollection())->withName($operationName) : new GetCollection(); - $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null); + $schemaFactory = self::getSchemaFactory(); + + if ($schemaFactory instanceof LegacySchemaFactoryInterface) { + $schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, OperationType::COLLECTION, $operationName, null); + } else { + $operation = $operationName ? (new GetCollection())->withName($operationName) : new GetCollection(); + $schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null); + } static::assertMatchesJsonSchema($schema->getArrayCopy()); } public static function assertMatchesResourceItemJsonSchema(string $resourceClass, ?string $operationName = null, string $format = 'jsonld'): void { - $operation = $operationName ? (new Get())->withName($operationName) : new Get(); - $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null); + $schemaFactory = self::getSchemaFactory(); + + if ($schemaFactory instanceof LegacySchemaFactoryInterface) { + $schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, OperationType::ITEM, $operationName, null); + } else { + $operation = $operationName ? (new Get())->withName($operationName) : new Get(); + $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null); + } static::assertMatchesJsonSchema($schema->getArrayCopy()); } @@ -151,12 +165,15 @@ private static function getHttpResponse(): ResponseInterface return $response; } - private static function getSchemaFactory(): SchemaFactoryInterface + /** + * @return SchemaFactoryInterface|LegacySchemaFactoryInterface + */ + private static function getSchemaFactory() { $container = method_exists(static::class, 'getContainer') ? static::getContainer() : static::$container; // @phpstan-ignore-line try { - /** @var SchemaFactoryInterface $schemaFactory */ + /** @var SchemaFactoryInterface|LegacySchemaFactoryInterface $schemaFactory */ $schemaFactory = $container->get('api_platform.json_schema.schema_factory'); } catch (ServiceNotFoundException $e) { throw new \LogicException('You cannot use the resource JSON Schema assertions if the "api_platform.swagger.versions" config is null or empty.'); From 7449989dfb9e264425c2589aae5f43c5f627952b Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 19 May 2022 23:33:16 +0200 Subject: [PATCH 10/10] come on --- src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php index 8583a019fdb..c141c828617 100644 --- a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php +++ b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php @@ -135,7 +135,7 @@ public static function assertMatchesResourceItemJsonSchema(string $resourceClass $schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, OperationType::ITEM, $operationName, null); } else { $operation = $operationName ? (new Get())->withName($operationName) : new Get(); - $schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null); + $schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null); } static::assertMatchesJsonSchema($schema->getArrayCopy());