Skip to content

Commit 29aab02

Browse files
Handle Symfony serializer datetime_format context for date formatting
1 parent dcde65f commit 29aab02

File tree

8 files changed

+121
-53
lines changed

8 files changed

+121
-53
lines changed

src/ModelDescriber/Annotations/AnnotationsReader.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class AnnotationsReader
2323
{
2424
private PropertyPhpDocReader $phpDocReader;
2525
private OpenApiAnnotationsReader $openApiAnnotationsReader;
26-
private SymfonyConstraintAnnotationReader $symfonyConstraintAnnotationReader;
26+
private SymfonyAnnotationReader $symfonyAnnotationReader;
2727
private ReflectionReader $reflectionReader;
2828

2929
/**
@@ -36,14 +36,14 @@ public function __construct(
3636
) {
3737
$this->phpDocReader = new PropertyPhpDocReader();
3838
$this->openApiAnnotationsReader = new OpenApiAnnotationsReader($modelRegistry, $mediaTypes);
39-
$this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($useValidationGroups);
39+
$this->symfonyAnnotationReader = new SymfonyAnnotationReader($useValidationGroups);
4040
$this->reflectionReader = new ReflectionReader();
4141
}
4242

4343
public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): bool
4444
{
4545
$this->openApiAnnotationsReader->updateSchema($reflectionClass, $schema);
46-
$this->symfonyConstraintAnnotationReader->setSchema($schema);
46+
$this->symfonyAnnotationReader->setSchema($schema);
4747
$this->reflectionReader->setSchema($schema);
4848

4949
return $this->shouldDescribeModelProperties($schema);
@@ -66,7 +66,7 @@ public function updateProperty($reflection, OA\Property $property, ?array $seria
6666
$this->openApiAnnotationsReader->updateProperty($reflection, $property, $serializationGroups);
6767
$this->phpDocReader->updateProperty($reflection, $property);
6868
$this->reflectionReader->updateProperty($reflection, $property);
69-
$this->symfonyConstraintAnnotationReader->updateProperty($reflection, $property, $serializationGroups);
69+
$this->symfonyAnnotationReader->updateProperty($reflection, $property, $serializationGroups);
7070
}
7171

7272
/**

src/ModelDescriber/Annotations/ReflectionReader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
/**
1919
* Read default values of a property from the function or property signature.
2020
*
21-
* This needs to be called before the {@see SymfonyConstraintAnnotationReader},
21+
* This needs to be called before the {@see SymfonyAnnotationReader},
2222
* otherwise required properties might be considered wrongly.
2323
*
2424
* @internal

src/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php renamed to src/ModelDescriber/Annotations/SymfonyAnnotationReader.php

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
/**
2323
* @internal
2424
*/
25-
class SymfonyConstraintAnnotationReader
25+
class SymfonyAnnotationReader
2626
{
2727
use SetsContextTrait;
2828

@@ -39,27 +39,37 @@ public function __construct(bool $useValidationGroups = false)
3939
}
4040

4141
/**
42-
* Update the given property and schema with defined Symfony constraints.
42+
* Update the given property and schema with defined Symfony attributes.
4343
*
4444
* @param \ReflectionProperty|\ReflectionMethod $reflection
4545
* @param string[]|null $validationGroups
4646
*/
4747
public function updateProperty($reflection, OA\Property $property, ?array $validationGroups = null): void
4848
{
49-
foreach ($this->getAttributes($property->_context, $reflection, $validationGroups) as $outerAttribute) {
49+
// Handle constraints
50+
foreach ($this->getConstraintAttributes($property->_context, $reflection, $validationGroups) as $outerAttribute) {
5051
$innerAttributes = $outerAttribute instanceof Assert\Compound || $outerAttribute instanceof Assert\Sequentially
5152
? $outerAttribute->constraints
5253
: [$outerAttribute];
5354

54-
$this->processPropertyAttributes($reflection, $property, $innerAttributes);
55+
$this->processConstraintPropertyAttributes($reflection, $property, $innerAttributes);
56+
}
57+
58+
// Handle context
59+
$context = $reflection->getAttributes(\Symfony\Component\Serializer\Attribute\Context::class);
60+
if (1 === \count($context)) {
61+
$contextArgs = $context[0]->getArguments()[0];
62+
if ('Y-m-d' === ($contextArgs['datetime_format'] ?? null)) {
63+
$property->format = 'date';
64+
}
5565
}
5666
}
5767

5868
/**
5969
* @param \ReflectionProperty|\ReflectionMethod $reflection
6070
* @param Constraint[] $attributes
6171
*/
62-
private function processPropertyAttributes($reflection, OA\Property $property, array $attributes): void
72+
private function processConstraintPropertyAttributes($reflection, OA\Property $property, array $attributes): void
6373
{
6474
foreach ($attributes as $attribute) {
6575
if ($attribute instanceof Assert\NotBlank || $attribute instanceof Assert\NotNull) {
@@ -179,7 +189,7 @@ private function applyEnumFromChoiceConstraint(OA\Schema $property, Assert\Choic
179189
*
180190
* @return iterable<Constraint>
181191
*/
182-
private function getAttributes(Context $parentContext, $reflection, ?array $validationGroups): iterable
192+
private function getConstraintAttributes(Context $parentContext, $reflection, ?array $validationGroups): iterable
183193
{
184194
// To correctly load OA attributes
185195
$this->setContextFromReflection($parentContext, $reflection);

src/PropertyDescriber/DateTimePropertyDescriber.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Nelmio\ApiDocBundle\PropertyDescriber;
1313

1414
use OpenApi\Annotations as OA;
15+
use OpenApi\Generator;
1516
use Symfony\Component\PropertyInfo\Type;
1617

1718
final class DateTimePropertyDescriber implements PropertyDescriberInterface
@@ -22,7 +23,9 @@ final class DateTimePropertyDescriber implements PropertyDescriberInterface
2223
public function describe(array $types, OA\Schema $property, array $context = []): void
2324
{
2425
$property->type = 'string';
25-
$property->format = 'date-time';
26+
if (Generator::UNDEFINED === $property->format) {
27+
$property->format = 'date-time';
28+
}
2629
}
2730

2831
public function supports(array $types, array $context = []): bool
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the NelmioApiDocBundle package.
5+
*
6+
* (c) Nelmio
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
13+
14+
use Symfony\Component\Serializer\Attribute\Context;
15+
16+
class SymfonyContext
17+
{
18+
/**
19+
* @var \DateTime
20+
*/
21+
#[Context(['datetime_format' => 'Y-m-d'])]
22+
public $date;
23+
24+
#[Context(['datetime_format' => 'Y-m-d'])]
25+
public ?\DateTime $nullableDate = null;
26+
}

tests/Functional/FunctionalTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,29 @@ public function testPrivateProtectedExposure(): void
780780
$this->assertNotHasProperty('protected', $model);
781781
}
782782

783+
public function testContextSupport(): void
784+
{
785+
self::assertEquals(
786+
[
787+
'schema' => 'SymfonyContext',
788+
'type' => 'object',
789+
'required' => ['date'],
790+
'properties' => [
791+
'date' => [
792+
'type' => 'string',
793+
'format' => 'date',
794+
],
795+
'nullableDate' => [
796+
'type' => 'string',
797+
'format' => 'date',
798+
'nullable' => true,
799+
],
800+
],
801+
],
802+
json_decode($this->getModel('SymfonyContext')->toJson(), true)
803+
);
804+
}
805+
783806
public function testModelsWithDiscriminatorMapAreLoadedWithOpenApiPolymorphism(): void
784807
{
785808
$model = $this->getModel('SymfonyDiscriminator');

tests/Functional/TestKernel.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture;
2323
use Nelmio\ApiDocBundle\Tests\Functional\Entity\PrivateProtectedExposure;
2424
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups;
25+
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyContext;
2526
use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\NameConverter;
2627
use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\VirtualTypeClassDoesNotExistsHandlerDefinedDescriber;
2728
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
@@ -177,6 +178,11 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
177178
'type' => BazingaUser::class,
178179
'groups' => ['foo'],
179180
],
181+
[
182+
'alias' => 'SymfonyContext',
183+
'type' => SymfonyContext::class,
184+
'groups' => null,
185+
],
180186
[
181187
'alias' => 'SymfonyConstraintsTestGroup',
182188
'type' => SymfonyConstraintsWithValidationGroups::class,

0 commit comments

Comments
 (0)