Skip to content

Commit

Permalink
infer types from php 7.4
Browse files Browse the repository at this point in the history
  • Loading branch information
goetas committed Apr 24, 2020
1 parent 3a811f7 commit 1a74993
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 2 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;
}
}
106 changes: 106 additions & 0 deletions src/Metadata/Driver/TypedPropertiesDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?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 $customTypes;

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

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->customTypes, true)) {
return true;
}

if (interface_exists($propertyReflection->getType()->getName())) {
return false;
}

return true;
}

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;
}
}
14 changes: 14 additions & 0 deletions tests/Fixtures/TypedProperties/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties;

class User
{
private int $id;
private Role $role;
private \DateTime $created;
private \DateTimeInterface $updated;
private 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);
}
}
}

0 comments on commit 1a74993

Please sign in to comment.