Skip to content

Commit

Permalink
Support php7.4 nullable typed properties for JMS serializer. (#2103)
Browse files Browse the repository at this point in the history
In the case of php7.4 typed properties for JMS serializer there is no
difference between nullable typed property and required one:
```php
    /**
     * @Serializer\Type("integer")
     */
    private int $id;

    /**
     * @Serializer\Type("string")
     */
    private ?string $name;
```
Spec is generated for both properties as optional, but the `$id` must be
required.

This PR adds support for marking fields as required automatically for
php7.4 typed properties and for virtual properties based on their return
type's nullability.

Co-authored-by: Djordy Koert <[email protected]>
  • Loading branch information
zviryatko and DjordyKoert authored Jun 12, 2024
1 parent c20a32e commit c21aead
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 0 deletions.
31 changes: 31 additions & 0 deletions src/ModelDescriber/JMSModelDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ public function describe(Model $model, OA\Schema $schema)
} catch (\ReflectionException $ignored) {
}
}
$this->checkRequiredFields($reflections, $schema, $name);
if (null !== $item->setter) {
try {
$reflections[] = new \ReflectionMethod($item->class, $item->setter);
Expand Down Expand Up @@ -397,4 +398,34 @@ private function propertyTypeUsesGroups(array $type): ?bool
return null;
}
}

/**
* Mark property as required if it is not nullable.
*
* @param array<\ReflectionProperty|\ReflectionMethod> $reflections
*/
private function checkRequiredFields(array $reflections, OA\Schema $schema, string $name): void
{
foreach ($reflections as $reflection) {
$nullable = false;
if ($reflection instanceof \ReflectionProperty) {
$type = PHP_VERSION_ID >= 70400 ? $reflection->getType() : null;
if (null !== $type && !$type->allowsNull()) {
$nullable = true;
}
} elseif ($reflection instanceof \ReflectionMethod) {
$returnType = $reflection->getReturnType();
if (null !== $returnType && !$returnType->allowsNull()) {
$nullable = true;
}
}
if ($nullable) {
$required = Generator::UNDEFINED !== $schema->required ? $schema->required : [];
$required[] = $name;

$schema->required = $required;
break;
}
}
}
}
15 changes: 15 additions & 0 deletions tests/Functional/Controller/JMSController80.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex80;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSDualComplex;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSNamingStrategyConstraints;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped80;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSUser;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChat;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatRoomUser;
Expand Down Expand Up @@ -166,4 +167,18 @@ public function minUserNestedAction()
public function discriminatorMapAction()
{
}

/**
* @Route("/api/jms_typed", methods={"GET"})
*
* @OA\Response(
* response=200,
* description="Success",
*
* @Model(type=JMSTyped80::class)
* )
*/
public function typedAction()
{
}
}
11 changes: 11 additions & 0 deletions tests/Functional/Controller/JMSController81.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex81;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSDualComplex;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSNamingStrategyConstraints;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped81;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSUser;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChat;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatRoomUser;
Expand Down Expand Up @@ -141,4 +142,14 @@ public function enum()
public function discriminatorMapAction()
{
}

#[Route('/api/jms_typed', methods: ['GET'])]
#[OA\Response(
response: 200,
description: 'Success',
content: new Model(type: JMSTyped81::class))
]
public function typedAction()
{
}
}
46 changes: 46 additions & 0 deletions tests/Functional/Entity/JMSTyped80.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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 JMS\Serializer\Annotation as Serializer;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations as OA;

class JMSTyped80
{
/**
* @Serializer\Type("integer")
*/
private int $id;

/**
* @OA\Property(ref=@Model(type=JMSUser::class))
*
* @Serializer\SerializedName("user")
*/
private JMSUser $User;

/**
* @Serializer\Type("string")
*/
private ?string $name;

/**
* @Serializer\VirtualProperty
*
* @OA\Property(ref=@Model(type=JMSUser::class))
*/
public function getVirtualFriend(): JMSUser
{
return new JMSUser();
}
}
36 changes: 36 additions & 0 deletions tests/Functional/Entity/JMSTyped81.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?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 JMS\Serializer\Annotation as Serializer;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Attributes as OA;

class JMSTyped81
{
#[Serializer\Type('integer')]
private int $id;

#[OA\Property(ref: new Model(type: JMSUser::class))]
#[Serializer\SerializedName('user')]
private JMSUser $User;

#[Serializer\Type('string')]
private ?string $name;

#[Serializer\VirtualProperty]
#[OA\Property(ref: new Model(type: JMSUser::class))]
public function getVirtualFriend(): JMSUser
{
return new JMSUser();
}
}
25 changes: 25 additions & 0 deletions tests/Functional/JMSFunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,12 @@ public function testEnumSupport(): void
],
],
'schema' => 'Article81',
'required' => [
'id',
'type',
'int_backed_type',
'not_backed_type',
],
], json_decode($this->getModel('Article81')->toJson(), true));

self::assertEquals([
Expand Down Expand Up @@ -420,4 +426,23 @@ protected static function createKernel(array $options = []): KernelInterface
{
return new TestKernel(TestKernel::USE_JMS);
}

public function testModelTypedDocumentation(): void
{
self::assertEquals([
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'user' => ['$ref' => '#/components/schemas/JMSUser'],
'name' => ['type' => 'string'],
'virtual_friend' => ['$ref' => '#/components/schemas/JMSUser'],
],
'required' => [
'virtual_friend',
'id',
'user',
],
'schema' => 'JMSTyped',
], json_decode($this->getModel('JMSTyped')->toJson(), true));
}
}
10 changes: 10 additions & 0 deletions tests/Functional/TestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
use Nelmio\ApiDocBundle\Tests\Functional\Entity\BazingaUser;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex80;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex81;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped80;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped81;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\PrivateProtectedExposure;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups;
Expand Down Expand Up @@ -221,6 +223,10 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
'type' => JMSComplex80::class,
'groups' => null,
],
[
'alias' => 'JMSTyped',
'type' => JMSTyped80::class,
],
]);
} elseif (self::isAttributesAvailable()) {
$models = array_merge($models, [
Expand All @@ -238,6 +244,10 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
'type' => JMSComplex81::class,
'groups' => null,
],
[
'alias' => 'JMSTyped',
'type' => JMSTyped81::class,
],
]);
}

Expand Down

0 comments on commit c21aead

Please sign in to comment.