Skip to content

Commit

Permalink
Merge 5e9dd4d into 799d88a
Browse files Browse the repository at this point in the history
  • Loading branch information
alanpoulain committed Feb 12, 2020
2 parents 799d88a + 5e9dd4d commit 209fecd
Show file tree
Hide file tree
Showing 29 changed files with 405 additions and 165 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* GraphQL: Allow to format GraphQL errors based on exceptions (#3063)
* GraphQL: Add page-based pagination (#3175)
* OpenAPI: Add PHP default values to the documentation (#2386)
* Hydra and OpenAPI documentation can take into account the dynamic serialization groups added with the serializer context builder (set `enable_serialization_context_doc` to `true` in configuration)

## 2.5.4

Expand Down
1 change: 1 addition & 0 deletions features/bootstrap/DoctrineContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,7 @@ public function thereIsAnOrderWithSameCustomerAndRecipient()
$order = $this->isOrm() ? new Order() : new OrderDocument();
$order->recipient = $customer;
$order->customer = $customer;
$order->customerIp = '82.654.87.09';

$customer->addresses->add($address1);
$customer->addresses->add($address2);
Expand Down
2 changes: 1 addition & 1 deletion features/bootstrap/HydraContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public function assertNbPropertiesExist(int $nb, string $className)
}

/**
* @Then :prop property doesn't exist for the Hydra class :class
* @Then :prop property doesn't exist for Hydra class :class
*/
public function assertPropertyNotExist(string $propertyName, string $className)
{
Expand Down
10 changes: 10 additions & 0 deletions features/hydra/docs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Feature: Documentation support
And the Hydra class "CustomNormalizedDummy" exists
And the Hydra class "CustomWritableIdentifierDummy" exists
And the Hydra class "Dummy" exists
And the Hydra class "Order" exists
And the Hydra class "RelatedDummy" exists
And the Hydra class "RelationEmbedder" exists
And the Hydra class "ThirdLevel" exists
Expand All @@ -59,6 +60,7 @@ Feature: Documentation support
And "name" property is readable for Hydra class "Dummy"
And "name" property is writable for Hydra class "Dummy"
And "name" property is required for Hydra class "Dummy"
And "customerIp" property doesn't exist for Hydra class "Order"
And "plainPassword" property is not readable for Hydra class "User"
And "plainPassword" property is writable for Hydra class "User"
And "plainPassword" property is not required for Hydra class "User"
Expand Down Expand Up @@ -87,3 +89,11 @@ Feature: Documentation support
And the boolean value of the node "owl:deprecated" of the property "deprecatedField" of the Hydra class "DeprecatedResource" is true
And the boolean value of the node "owl:deprecated" of the property "The collection of DeprecatedResource resources" of the Hydra class "The API entrypoint" is true
And the boolean value of the node "owl:deprecated" of the operation "GET" of the Hydra class "DeprecatedResource" is true

Scenario: Retrieve the API vocabulary with dynamic groups
Given I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
And I send a "GET" request to "/docs.jsonld"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And "customerIp" property is readable for Hydra class "Order"
1 change: 1 addition & 0 deletions src/Api/OperationType.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ final class OperationType
public const ITEM = 'item';
public const COLLECTION = 'collection';
public const SUBRESOURCE = 'subresource';
public const RESOURCE = 'resource';
public const TYPES = [self::ITEM, self::COLLECTION, self::SUBRESOURCE];
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ 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->registerSwaggerConfiguration($container, $config, $loader);
$this->registerSwaggerConfiguration($container, $config, $loader, $config['enable_serialization_context_doc']);
$this->registerJsonApiConfiguration($formats, $loader);
$this->registerJsonLdHydraConfiguration($container, $formats, $loader, $config['enable_docs']);
$this->registerJsonLdHydraConfiguration($container, $formats, $loader, $config['enable_docs'], $config['enable_serialization_context_doc']);
$this->registerJsonHalConfiguration($formats, $loader);
$this->registerJsonProblemConfiguration($errorFormats, $loader);
$this->registerGraphQlConfiguration($container, $config, $loader);
Expand Down Expand Up @@ -333,7 +333,7 @@ private function registerOAuthConfiguration(ContainerBuilder $container, array $
/**
* Registers the Swagger, ReDoc and Swagger UI configuration.
*/
private function registerSwaggerConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void
private function registerSwaggerConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader, bool $useSerializationContext): void
{
$container->setParameter('api_platform.swagger.versions', $config['swagger']['versions']);

Expand All @@ -353,6 +353,11 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array
$container->setParameter('api_platform.enable_swagger_ui', $config['enable_swagger_ui']);
$container->setParameter('api_platform.enable_re_doc', $config['enable_re_doc']);
$container->setParameter('api_platform.swagger.api_keys', $config['swagger']['api_keys']);

if (!$useSerializationContext) {
$container->getDefinition('api_platform.json_schema.schema_factory')->replaceArgument(6, null);
$container->getDefinition('api_platform.swagger.normalizer.documentation')->replaceArgument(26, null);
}
}

private function registerJsonApiConfiguration(array $formats, XmlFileLoader $loader): void
Expand All @@ -364,7 +369,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, bool $docEnabled, bool $useSerializationContext): void
{
if (!isset($formats['jsonld'])) {
return;
Expand All @@ -380,6 +385,10 @@ private function registerJsonLdHydraConfiguration(ContainerBuilder $container, a
if (!$docEnabled) {
$container->removeDefinition('api_platform.hydra.listener.response.add_link_header');
}

if (!$useSerializationContext) {
$container->getDefinition('api_platform.hydra.normalizer.documentation')->replaceArgument(8, null);
}
}

private function registerJsonHalConfiguration(array $formats, XmlFileLoader $loader): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ public function getConfigTreeBuilder()
->booleanNode('enable_re_doc')->defaultValue(class_exists(TwigBundle::class))->info('Enable ReDoc')->end()
->booleanNode('enable_entrypoint')->defaultTrue()->info('Enable the entrypoint')->end()
->booleanNode('enable_docs')->defaultTrue()->info('Enable the docs')->end()
->booleanNode('enable_serialization_context_doc')->defaultFalse()->info('Enable the documentation to vary according to the serialization context')->end()
->booleanNode('enable_profiler')->defaultTrue()->info('Enable the data collector and the WebProfilerBundle integration.')->end()
->arrayNode('collection')
->addDefaultsIfNotSet()
Expand Down
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/hydra.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<argument type="service" id="api_platform.router" />
<argument type="service" id="api_platform.subresource_operation_factory" />
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
<argument type="service" id="api_platform.serializer.context_builder" />

<tag name="serializer.normalizer" priority="-800" />
</service>
Expand Down
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
<argument type="service" id="api_platform.resource_class_resolver" />
<argument type="service" id="api_platform.serializer.context_builder" />
</service>
<service id="ApiPlatform\Core\JsonSchema\SchemaFactoryInterface" alias="api_platform.json_schema.schema_factory" />

Expand Down
2 changes: 2 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/swagger.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
<argument>%api_platform.formats%</argument>
<argument>%api_platform.collection.pagination.client_enabled%</argument>
<argument>%api_platform.collection.pagination.enabled_parameter_name%</argument>
<argument type="collection" />
<argument>%api_platform.swagger.versions%</argument>
<argument type="service" id="api_platform.serializer.context_builder" />
<tag name="serializer.normalizer" priority="-790" />
</service>

Expand Down
44 changes: 11 additions & 33 deletions src/Hydra/Serializer/DocumentationNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
use ApiPlatform\Core\Serializer\PropertyFactoryOptionsTrait;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

Expand All @@ -39,6 +40,8 @@
*/
final class DocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
{
use PropertyFactoryOptionsTrait;

public const FORMAT = 'jsonld';

private $resourceMetadataFactory;
Expand All @@ -49,8 +52,9 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup
private $urlGenerator;
private $subresourceOperationFactory;
private $nameConverter;
private $serializerContextBuilder;

public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver = null, UrlGeneratorInterface $urlGenerator, SubresourceOperationFactoryInterface $subresourceOperationFactory = null, NameConverterInterface $nameConverter = null)
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver = null, UrlGeneratorInterface $urlGenerator, SubresourceOperationFactoryInterface $subresourceOperationFactory = null, NameConverterInterface $nameConverter = null, ?SerializerContextBuilderInterface $serializerContextBuilder = null)
{
if ($operationMethodResolver) {
@trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0.', OperationMethodResolverInterface::class, __METHOD__), E_USER_DEPRECATED);
Expand All @@ -64,6 +68,7 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
$this->urlGenerator = $urlGenerator;
$this->subresourceOperationFactory = $subresourceOperationFactory;
$this->nameConverter = $nameConverter;
$this->serializerContextBuilder = $serializerContextBuilder;
}

/**
Expand Down Expand Up @@ -151,35 +156,6 @@ private function getClass(string $resourceClass, ResourceMetadata $resourceMetad
return $class;
}

/**
* Gets the context for the property name factory.
*/
private function getPropertyNameCollectionFactoryContext(ResourceMetadata $resourceMetadata): array
{
$attributes = $resourceMetadata->getAttributes();
$context = [];

if (isset($attributes['normalization_context'][AbstractNormalizer::GROUPS])) {
$context['serializer_groups'] = (array) $attributes['normalization_context'][AbstractNormalizer::GROUPS];
}

if (!isset($attributes['denormalization_context'][AbstractNormalizer::GROUPS])) {
return $context;
}

if (isset($context['serializer_groups'])) {
foreach ((array) $attributes['denormalization_context'][AbstractNormalizer::GROUPS] as $groupName) {
$context['serializer_groups'][] = $groupName;
}

return $context;
}

$context['serializer_groups'] = (array) $attributes['denormalization_context'][AbstractNormalizer::GROUPS];

return $context;
}

/**
* Gets Hydra properties.
*/
Expand All @@ -202,8 +178,10 @@ private function getHydraProperties(string $resourceClass, ResourceMetadata $res
$classes = array_keys($classes);
$properties = [];
foreach ($classes as $class) {
foreach ($this->propertyNameCollectionFactory->create($class, $this->getPropertyNameCollectionFactoryContext($resourceMetadata)) as $propertyName) {
$propertyMetadata = $this->propertyMetadataFactory->create($class, $propertyName);
$propertyOptions = $this->getPropertyFactoryOptions($resourceClass);
$splitPropertyOptions = $this->getPropertyFactoryOptions($resourceClass, true);
foreach ($this->propertyNameCollectionFactory->create($class, $propertyOptions) as $propertyName) {
$propertyMetadata = $this->propertyMetadataFactory->create($class, $propertyName, $splitPropertyOptions);
if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) {
continue;
}
Expand Down
28 changes: 11 additions & 17 deletions src/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Serializer\PropertyFactoryOptionsTrait;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer;
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
use Symfony\Component\PropertyInfo\Type;
Expand All @@ -35,22 +36,25 @@
*/
final class SchemaFactory implements SchemaFactoryInterface
{
use PropertyFactoryOptionsTrait;
use ResourceClassInfoTrait;

private $typeFactory;
private $propertyNameCollectionFactory;
private $propertyMetadataFactory;
private $nameConverter;
private $distinctFormats = [];
private $serializerContextBuilder;

public function __construct(TypeFactoryInterface $typeFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null)
public function __construct(TypeFactoryInterface $typeFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null, ?SerializerContextBuilderInterface $serializerContextBuilder = null)
{
$this->typeFactory = $typeFactory;
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->nameConverter = $nameConverter;
$this->resourceClassResolver = $resourceClassResolver;
$this->serializerContextBuilder = $serializerContextBuilder;
}

/**
Expand Down Expand Up @@ -131,9 +135,10 @@ public function buildSchema(string $className, string $format = 'json', string $
$definition['externalDocs'] = ['url' => $iri];
}

$options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => (array) $serializerContext[AbstractNormalizer::GROUPS]] : [];
foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) {
$propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName);
$propertyOptions = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => (array) $serializerContext[AbstractNormalizer::GROUPS]] : [];
$splitPropertyOptions = $this->getPropertyFactoryOptions($className, true);
foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $propertyOptions) as $propertyName) {
$propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName, $splitPropertyOptions);
if (!$propertyMetadata->isReadable() && !$propertyMetadata->isWritable()) {
continue;
}
Expand Down Expand Up @@ -272,19 +277,8 @@ private function getMetadata(string $className, string $type = Schema::TYPE_OUTP

return [
$resourceMetadata,
$serializerContext ?? $this->getSerializerContext($resourceMetadata, $type, $operationType, $operationName),
$serializerContext ?? $this->getSerializationContext($className, Schema::TYPE_OUTPUT === $type, $operationType, $operationName),
$inputOrOutput['class'],
];
}

private function getSerializerContext(ResourceMetadata $resourceMetadata, string $type = Schema::TYPE_OUTPUT, ?string $operationType, ?string $operationName): array
{
$attribute = Schema::TYPE_OUTPUT === $type ? 'normalization_context' : 'denormalization_context';

if (null === $operationType || null === $operationName) {
return $resourceMetadata->getAttribute($attribute, []);
}

return $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, [], true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ private function transformLinkStatus(PropertyMetadata $propertyMetadata, array $
*
* Groups are extracted in the following order:
*
* - From the "serializer_groups" key of the $options array.
* - From the "serializer_groups" and/or "deserializer_groups" key of the $options array.
* - From metadata of the given operation ("collection_operation_name" and "item_operation_name" keys).
* - From metadata of the current resource.
*
Expand All @@ -151,10 +151,16 @@ private function transformLinkStatus(PropertyMetadata $propertyMetadata, array $
*/
private function getEffectiveSerializerGroups(array $options, string $resourceClass): array
{
$groups = null;
$deserializerGroups = null;
if (isset($options['serializer_groups'])) {
$groups = (array) $options['serializer_groups'];

return [$groups, $groups];
}
if (isset($options['deserializer_groups'])) {
$deserializerGroups = (array) $options['deserializer_groups'];
}
if (null !== $groups) {
return [$groups, $deserializerGroups ?? $groups];
}

$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
Expand Down

0 comments on commit 209fecd

Please sign in to comment.