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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/ModelDescriber/Annotations/AnnotationsReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class AnnotationsReader
{
private PropertyPhpDocReader $phpDocReader;
private OpenApiAnnotationsReader $openApiAnnotationsReader;
private SymfonyConstraintAnnotationReader $symfonyConstraintAnnotationReader;
private SymfonyAnnotationReader $symfonyAnnotationReader;
private ReflectionReader $reflectionReader;

/**
Expand All @@ -36,14 +36,14 @@ public function __construct(
) {
$this->phpDocReader = new PropertyPhpDocReader();
$this->openApiAnnotationsReader = new OpenApiAnnotationsReader($modelRegistry, $mediaTypes);
$this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($useValidationGroups);
$this->symfonyAnnotationReader = new SymfonyAnnotationReader($useValidationGroups);
$this->reflectionReader = new ReflectionReader();
}

public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): bool
{
$this->openApiAnnotationsReader->updateSchema($reflectionClass, $schema);
$this->symfonyConstraintAnnotationReader->setSchema($schema);
$this->symfonyAnnotationReader->setSchema($schema);
$this->reflectionReader->setSchema($schema);

return $this->shouldDescribeModelProperties($schema);
Expand All @@ -60,13 +60,14 @@ public function getPropertyName($reflection, string $default): string
/**
* @param \ReflectionProperty|\ReflectionMethod $reflection
* @param string[]|null $serializationGroups
* @param array<string, mixed> $context
*/
public function updateProperty($reflection, OA\Property $property, ?array $serializationGroups = null): void
public function updateProperty($reflection, OA\Property $property, array &$context, ?array $serializationGroups = null): void
{
$this->openApiAnnotationsReader->updateProperty($reflection, $property, $serializationGroups);
$this->phpDocReader->updateProperty($reflection, $property);
$this->reflectionReader->updateProperty($reflection, $property);
$this->symfonyConstraintAnnotationReader->updateProperty($reflection, $property, $serializationGroups);
$this->symfonyAnnotationReader->updateProperty($reflection, $property, $context, $serializationGroups);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/ModelDescriber/Annotations/ReflectionReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/**
* Read default values of a property from the function or property signature.
*
* This needs to be called before the {@see SymfonyConstraintAnnotationReader},
* This needs to be called before the {@see SymfonyAnnotationReader},
* otherwise required properties might be considered wrongly.
*
* @internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
/**
* @internal
*/
class SymfonyConstraintAnnotationReader
class SymfonyAnnotationReader
{
use SetsContextTrait;

Expand All @@ -39,27 +39,35 @@ public function __construct(bool $useValidationGroups = false)
}

/**
* Update the given property and schema with defined Symfony constraints.
* Update the given property and schema with defined Symfony attributes.
*
* @param \ReflectionProperty|\ReflectionMethod $reflection
* @param array<string, mixed> $context
* @param string[]|null $validationGroups
*/
public function updateProperty($reflection, OA\Property $property, ?array $validationGroups = null): void
public function updateProperty($reflection, OA\Property $property, array &$context = [], ?array $validationGroups = null): void
{
foreach ($this->getAttributes($property->_context, $reflection, $validationGroups) as $outerAttribute) {
// Handle constraints
foreach ($this->getConstraintAttributes($property->_context, $reflection, $validationGroups) as $outerAttribute) {
$innerAttributes = $outerAttribute instanceof Assert\Compound || $outerAttribute instanceof Assert\Sequentially
? $outerAttribute->constraints
: [$outerAttribute];

$this->processPropertyAttributes($reflection, $property, $innerAttributes);
$this->processConstraintPropertyAttributes($reflection, $property, $innerAttributes);
}

// Handle context
$context = $reflection->getAttributes(\Symfony\Component\Serializer\Attribute\Context::class);
if (1 === \count($context)) {
$context['symfony_context'] = $context[0]->getArguments()[0];
}
}

/**
* @param \ReflectionProperty|\ReflectionMethod $reflection
* @param Constraint[] $attributes
*/
private function processPropertyAttributes($reflection, OA\Property $property, array $attributes): void
private function processConstraintPropertyAttributes($reflection, OA\Property $property, array $attributes): void
{
foreach ($attributes as $attribute) {
if ($attribute instanceof Assert\NotBlank || $attribute instanceof Assert\NotNull) {
Expand Down Expand Up @@ -179,7 +187,7 @@ private function applyEnumFromChoiceConstraint(OA\Schema $property, Assert\Choic
*
* @return iterable<Constraint>
*/
private function getAttributes(Context $parentContext, $reflection, ?array $validationGroups): iterable
private function getConstraintAttributes(Context $parentContext, $reflection, ?array $validationGroups): iterable
{
// To correctly load OA attributes
$this->setContextFromReflection($parentContext, $reflection);
Expand Down
5 changes: 3 additions & 2 deletions src/ModelDescriber/JMSModelDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public function describe(Model $model, OA\Schema $schema): void
$context = $this->getSerializationContext($model);
$context->pushClassMetadata($metadata);
foreach ($metadata->propertyMetadata as $item) {
$propertyContext = $model->getSerializationContext();
// filter groups
if (null !== $context->getExclusionStrategy() && $context->getExclusionStrategy()->shouldSkipProperty($item, $context)) {
continue;
Expand Down Expand Up @@ -176,7 +177,7 @@ public function describe(Model $model, OA\Schema $schema): void
$property = Util::getProperty($schema, $name);

foreach ($reflections as $reflection) {
$annotationsReader->updateProperty($reflection, $property, $groups);
$annotationsReader->updateProperty($reflection, $property, $propertyContext, $groups);
}

if (Generator::UNDEFINED !== $property->type || Generator::UNDEFINED !== $property->ref) {
Expand All @@ -197,7 +198,7 @@ public function describe(Model $model, OA\Schema $schema): void
continue;
}

$this->describeItem($item->type, $property, $context, $model->getSerializationContext());
$this->describeItem($item->type, $property, $context, $propertyContext);
$context->popPropertyMetadata();
}
$context->popClassMetadata();
Expand Down
15 changes: 9 additions & 6 deletions src/ModelDescriber/ObjectModelDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public function describe(Model $model, OA\Schema $schema): void
$propertyInfoProperties = array_intersect($propertyInfoProperties, $this->propertyInfo->getProperties($class, []) ?? []);

foreach ($propertyInfoProperties as $propertyName) {
$propertyContext = [];
$serializedName = null !== $this->nameConverter ? $this->nameConverter->normalize($propertyName, $class, null, $model->getSerializationContext()) : $propertyName;

$reflections = $this->getReflections($reflClass, $propertyName);
Expand All @@ -134,7 +135,7 @@ public function describe(Model $model, OA\Schema $schema): void
$groups = $model->getGroups()[$propertyName];
}
foreach ($reflections as $reflection) {
$annotationsReader->updateProperty($reflection, $property, $groups);
$annotationsReader->updateProperty($reflection, $property, $propertyContext, $groups);
}

// If type manually defined
Expand All @@ -152,7 +153,7 @@ public function describe(Model $model, OA\Schema $schema): void
throw new \LogicException(\sprintf('The PropertyInfo component was not able to guess the type of %s::$%s. You may need to add a `@var` annotation or use `#[OA\Property(type="")]` to make its type explicit.', $class, $propertyName));
}

$this->describeProperty($types, $model, $property, $propertyName);
$this->describeProperty($types, $model, $property, $propertyName, $propertyContext);
}

$this->markRequiredProperties($schema);
Expand Down Expand Up @@ -187,15 +188,17 @@ private function camelize(string $string): string
}

/**
* @param LegacyType[]|Type $types
* @param LegacyType[]|Type $types
* @param array<string, mixed> $context
*/
private function describeProperty(array|Type $types, Model $model, OA\Schema $property, string $propertyName): void
private function describeProperty(array|Type $types, Model $model, OA\Schema $property, string $propertyName, array $context): void
{
if ($this->propertyDescriber instanceof ModelRegistryAwareInterface) {
$this->propertyDescriber->setModelRegistry($this->modelRegistry);
}
if ($this->propertyDescriber->supports($types, $model->getSerializationContext())) {
$this->propertyDescriber->describe($types, $property, $model->getSerializationContext());
$context = array_merge($context, $model->getSerializationContext());
if ($this->propertyDescriber->supports($types, $context)) {
$this->propertyDescriber->describe($types, $property, $context);

return;
}
Expand Down
6 changes: 6 additions & 0 deletions src/PropertyDescriber/DateTimePropertyDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ public function describe(array $types, OA\Schema $property, array $context = [])
{
$property->type = 'string';
$property->format = 'date-time';
if (
\array_key_exists('symfony_context', $context)
&& 'Y-m-d' === ($context['symfony_context']['datetime_format'] ?? null)
) {
$property->format = 'date';
}
}

public function supports(array $types, array $context = []): bool
Expand Down
26 changes: 26 additions & 0 deletions tests/Functional/Entity/SymfonyContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;

use Symfony\Component\Serializer\Attribute\Context;

class SymfonyContext
{
/**
* @var \DateTime
*/
#[Context(['datetime_format' => 'Y-m-d'])]
public $date;

#[Context(['datetime_format' => 'Y-m-d'])]
public ?\DateTime $nullableDate = null;
}
23 changes: 23 additions & 0 deletions tests/Functional/FunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,29 @@ public function testPrivateProtectedExposure(): void
$this->assertNotHasProperty('protected', $model);
}

public function testContextSupport(): void
{
self::assertEquals(
[
'schema' => 'SymfonyContext',
'type' => 'object',
'required' => ['date'],
'properties' => [
'date' => [
'type' => 'string',
'format' => 'date',
],
'nullableDate' => [
'type' => 'string',
'format' => 'date',
'nullable' => true,
],
],
],
json_decode($this->getModel('SymfonyContext')->toJson(), true)
);
}

public function testModelsWithDiscriminatorMapAreLoadedWithOpenApiPolymorphism(): void
{
$model = $this->getModel('SymfonyDiscriminator');
Expand Down
6 changes: 6 additions & 0 deletions tests/Functional/TestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\PrivateProtectedExposure;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyContext;
use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\NameConverter;
use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\VirtualTypeClassDoesNotExistsHandlerDefinedDescriber;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
Expand Down Expand Up @@ -177,6 +178,11 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
'type' => BazingaUser::class,
'groups' => ['foo'],
],
[
'alias' => 'SymfonyContext',
'type' => SymfonyContext::class,
'groups' => null,
],
[
'alias' => 'SymfonyConstraintsTestGroup',
'type' => SymfonyConstraintsWithValidationGroups::class,
Expand Down
Loading