Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infer types from PHP 7.4 type declarations #1192

Merged
merged 2 commits into from
May 4, 2020
Merged
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: 9 additions & 2 deletions src/Builder/DefaultDriverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Doctrine\Common\Annotations\Reader;
use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
use JMS\Serializer\Metadata\Driver\AnnotationDriver;
use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver;
use JMS\Serializer\Metadata\Driver\XmlDriver;
use JMS\Serializer\Metadata\Driver\YamlDriver;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
Expand Down Expand Up @@ -45,13 +46,19 @@ public function createDriver(array $metadataDirs, Reader $annotationReader): Dri
if (!empty($metadataDirs)) {
$fileLocator = new FileLocator($metadataDirs);

return new DriverChain([
$driver = new DriverChain([
new YamlDriver($fileLocator, $this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator),
new XmlDriver($fileLocator, $this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator),
new AnnotationDriver($annotationReader, $this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator),
]);
} else {
$driver = new AnnotationDriver($annotationReader, $this->propertyNamingStrategy, $this->typeParser);
}

return new AnnotationDriver($annotationReader, $this->propertyNamingStrategy, $this->typeParser);
if (PHP_VERSION_ID >= 70400) {
$driver = new TypedPropertiesDriver($driver, $this->typeParser);
}

return $driver;
}
}
3 changes: 1 addition & 2 deletions src/GraphNavigator/DeserializationGraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
use JMS\Serializer\Functions;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\HandlerRegistryInterface;
Expand Down Expand Up @@ -134,7 +133,7 @@ public function accept($data, ?array $type = null)
return $this->visitor->visitDouble($data, $type);

case 'iterable':
return $this->visitor->visitArray(Functions::iterableToArray($data), $type);
return $this->visitor->visitArray($data, $type);

case 'array':
return $this->visitor->visitArray($data, $type);
Expand Down
120 changes: 120 additions & 0 deletions src/Metadata/Driver/TypedPropertiesDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use JMS\Serializer\Metadata\ClassMetadata as SerializerClassMetadata;
use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
use JMS\Serializer\Type\Parser;
use JMS\Serializer\Type\ParserInterface;
use Metadata\ClassMetadata;
use Metadata\Driver\DriverInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionProperty;

class TypedPropertiesDriver implements DriverInterface
{
/**
* @var DriverInterface
*/
protected $delegate;

/**
* @var ParserInterface
*/
protected $typeParser;

/**
* @var string[]
*/
private $whiteList;

/**
* @param string[] $whiteList
*/
public function __construct(DriverInterface $delegate, ?ParserInterface $typeParser = null, array $whiteList = [])
{
$this->delegate = $delegate;
$this->typeParser = $typeParser ?: new Parser();
$this->whiteList = array_merge($whiteList, $this->getDefaultWhiteList());
}

private function getDefaultWhiteList(): array
{
return [
'int',
'float',
'bool',
'boolean',
'string',
'double',
'iterable',
'resource',
];
}

public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata
{
/** @var SerializerClassMetadata $classMetadata */
$classMetadata = $this->delegate->loadMetadataForClass($class);

if (null === $classMetadata) {
return null;
}
// We base our scan on the internal driver's property list so that we
// respect any internal white/blacklisting like in the AnnotationDriver
foreach ($classMetadata->propertyMetadata as $key => $propertyMetadata) {
/** @var $propertyMetadata PropertyMetadata */

// If the inner driver provides a type, don't guess anymore.
if ($propertyMetadata->type || $this->isVirtualProperty($propertyMetadata)) {
continue;
}

try {
$propertyReflection = $this->getReflection($propertyMetadata);
if ($this->shouldTypeHint($propertyReflection)) {
$propertyMetadata->setType($this->typeParser->parse($propertyReflection->getType()->getName()));
}
} catch (ReflectionException $e) {
continue;
}
}

return $classMetadata;
}

private function shouldTypeHint(ReflectionProperty $propertyReflection): bool
{
if (null === $propertyReflection->getType()) {
return false;
}

if (in_array($propertyReflection->getType()->getName(), $this->whiteList, true)) {
return true;
}

if (class_exists($propertyReflection->getType()->getName())) {
return true;
}

return false;
}

private function getReflection(PropertyMetadata $propertyMetadata): ReflectionProperty
{
return new ReflectionProperty($propertyMetadata->class, $propertyMetadata->name);
}

private function isVirtualProperty(PropertyMetadata $propertyMetadata): bool
{
return $propertyMetadata instanceof VirtualPropertyMetadata
|| $propertyMetadata instanceof StaticPropertyMetadata
|| $propertyMetadata instanceof ExpressionPropertyMetadata;
}
}
10 changes: 10 additions & 0 deletions tests/Fixtures/TypedProperties/Role.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties;

class Role
{
public int $id;
}
23 changes: 23 additions & 0 deletions tests/Fixtures/TypedProperties/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties;

use JMS\Serializer\Annotation as Serializer;

class User
{
public int $id;
public Role $role;
public \DateTime $created;

/**
* @Serializer\ReadOnly()
*/
public ?\DateTimeInterface $updated = null;
/**
* @Serializer\ReadOnly()
*/
public iterable $tags = [];
}
39 changes: 39 additions & 0 deletions tests/Metadata/Driver/DefaultDriverFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Metadata\Driver;

use Doctrine\Common\Annotations\AnnotationReader;
use JMS\Serializer\Builder\DefaultDriverFactory;
use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy;
use JMS\Serializer\Tests\Fixtures\TypedProperties\User;
use PHPUnit\Framework\TestCase;

class DefaultDriverFactoryTest extends TestCase
{
public function testDefaultDriverFactoryLoadsTypedPropertiesDriver()
{
if (PHP_VERSION_ID < 70400) {
$this->markTestSkipped(sprintf('%s requires PHP 7.4', __METHOD__));
}

$factory = new DefaultDriverFactory(new IdenticalPropertyNamingStrategy());

$driver = $factory->createDriver([], new AnnotationReader());

$m = $driver->loadMetadataForClass(new \ReflectionClass(User::class));
self::assertNotNull($m);

$expectedPropertyTypes = [
'id' => 'int',
'role' => 'JMS\Serializer\Tests\Fixtures\TypedProperties\Role',
'created' => 'DateTime',
'tags' => 'iterable',
];

foreach ($expectedPropertyTypes as $property => $type) {
self::assertEquals(['name' => $type, 'params' => []], $m->propertyMetadata[$property]->type);
}
}
}
43 changes: 43 additions & 0 deletions tests/Metadata/Driver/TypedPropertiesDriverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Metadata\Driver;

use Doctrine\Common\Annotations\AnnotationReader;
use JMS\Serializer\Metadata\Driver\AnnotationDriver;
use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver;
use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy;
use JMS\Serializer\Tests\Fixtures\TypedProperties\User;
use PHPUnit\Framework\TestCase;

class TypedPropertiesDriverTest extends TestCase
{
protected function setUp(): void
{
if (PHP_VERSION_ID < 70400) {
$this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class));
}
}

public function testInferPropertiesFromTypes()
{
$baseDriver = new AnnotationDriver(new AnnotationReader(), new IdenticalPropertyNamingStrategy());
$driver = new TypedPropertiesDriver($baseDriver);

$m = $driver->loadMetadataForClass(new \ReflectionClass(User::class));

self::assertNotNull($m);

$expectedPropertyTypes = [
'id' => 'int',
'role' => 'JMS\Serializer\Tests\Fixtures\TypedProperties\Role',
'created' => 'DateTime',
'tags' => 'iterable',
];

foreach ($expectedPropertyTypes as $property => $type) {
self::assertEquals(['name' => $type, 'params' => []], $m->propertyMetadata[$property]->type);
}
}
}
29 changes: 29 additions & 0 deletions tests/Serializer/BaseSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
use JMS\Serializer\Tests\Fixtures\Tag;
use JMS\Serializer\Tests\Fixtures\Timestamp;
use JMS\Serializer\Tests\Fixtures\Tree;
use JMS\Serializer\Tests\Fixtures\TypedProperties;
use JMS\Serializer\Tests\Fixtures\VehicleInterfaceGarage;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
Expand Down Expand Up @@ -1289,6 +1290,34 @@ public function testCustomHandler()
self::assertEquals('customly_unserialized_value', $object->someProperty);
}

public function testTypedProperties()
{
if (PHP_VERSION_ID < 70400) {
$this->markTestSkipped(sprintf('%s requires PHP 7.4', __METHOD__));
}

$user = new TypedProperties\User();
$user->id = 1;
$user->created = new \DateTime('2010-10-01 00:00:00');
$user->updated = new \DateTime('2011-10-01 00:00:00');
$user->tags = ['a', 'b'];
$role = new TypedProperties\Role();
$role->id = 5;
$user->role = $role;

$result = $this->serialize($user);

self::assertEquals($this->getContent('typed_props'), $result);

if ($this->hasDeserializer()) {
// updated is read only
$user->updated = null;
$user->tags = [];

self::assertEquals($user, $this->deserialize($this->getContent('typed_props'), get_class($user)));
}
}

/**
* @doesNotPerformAssertions
*/
Expand Down
1 change: 1 addition & 0 deletions tests/Serializer/JsonSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ protected function getContent($key)
$outputs['user_discriminator_array'] = '[{"entityName":"User"},{"entityName":"ExtendedUser"}]';
$outputs['user_discriminator'] = '{"entityName":"User"}';
$outputs['user_discriminator_extended'] = '{"entityName":"ExtendedUser"}';
$outputs['typed_props'] = '{"id":1,"role":{"id":5},"created":"2010-10-01T00:00:00+00:00","updated":"2011-10-01T00:00:00+00:00","tags":["a","b"]}';
}

if (!isset($outputs[$key])) {
Expand Down
13 changes: 13 additions & 0 deletions tests/Serializer/xml/typed_props.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<result>
<id>1</id>
<role>
<id>5</id>
</role>
<created><![CDATA[2010-10-01T00:00:00+00:00]]></created>
<updated><![CDATA[2011-10-01T00:00:00+00:00]]></updated>
<tags>
<entry><![CDATA[a]]></entry>
<entry><![CDATA[b]]></entry>
</tags>
</result>