Skip to content

Commit

Permalink
Merge pull request #1192 from schmittjoh/infer-types-from-php-7.4
Browse files Browse the repository at this point in the history
Infer types from PHP 7.4 type declarations
  • Loading branch information
goetas authored May 4, 2020
2 parents 3a811f7 + 02431ce commit 16f30f3
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 4 deletions.
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>

0 comments on commit 16f30f3

Please sign in to comment.