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

Resolve collections from DocBlock #1214

Merged
Merged
Show file tree
Hide file tree
Changes from 11 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
9 changes: 9 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@
<property name="searchAnnotations" type="boolean" value="true"/>
</properties>
</rule>
<rule ref="SlevomatCodingStandard.Namespaces.MultipleUsesPerLine.MultipleUsesPerLine">
goetas marked this conversation as resolved.
Show resolved Hide resolved
<exclude-pattern>tests/*</exclude-pattern>
</rule>
<rule ref="PSR2.Namespaces.UseDeclaration.MultipleDeclarations">
<exclude-pattern>tests/*</exclude-pattern>
</rule>
<rule ref="SlevomatCodingStandard.Namespaces.DisallowGroupUse.DisallowedGroupUse">
<exclude-pattern>tests/*</exclude-pattern>
</rule>

<rule ref="SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing">
<properties>
Expand Down
3 changes: 2 additions & 1 deletion 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\DocBlockTypeResolver;
use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver;
use JMS\Serializer\Metadata\Driver\XmlDriver;
use JMS\Serializer\Metadata\Driver\YamlDriver;
Expand Down Expand Up @@ -56,7 +57,7 @@ public function createDriver(array $metadataDirs, Reader $annotationReader): Dri
}

if (PHP_VERSION_ID >= 70400) {
$driver = new TypedPropertiesDriver($driver, $this->typeParser);
$driver = new TypedPropertiesDriver($driver, new DocBlockTypeResolver(), $this->typeParser);
}

return $driver;
Expand Down
124 changes: 124 additions & 0 deletions src/Metadata/Driver/DocBlockTypeResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

class DocBlockTypeResolver
{
/** resolve type hints from property */
private const CLASS_PROPERTY_TYPE_HINT_REGEX = '#@var[\s]*([^\n\$\s]*)#';
/** resolve single use statements */
private const SINGLE_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[\s]*([^;\n]*)[\s]*;$/m';
/** resolve group use statements */
private const GROUP_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[[\s]*([^;\n]*)[\s]*{([a-zA-Z0-9\s\n\r,]*)};$/m';
private const GLOBAL_NAMESPACE_PREFIX = '\\';

public function getPropertyDocblockTypeHint(\ReflectionProperty $reflectionProperty): ?string
{
preg_match_all(self::CLASS_PROPERTY_TYPE_HINT_REGEX, $reflectionProperty->getDocComment(), $matchedDocBlockParameterTypes);
$typeHint = trim($matchedDocBlockParameterTypes[1][0]);

$unionTypeHint = [];
foreach (explode('|', $typeHint) as $singleTypeHint) {
if ('null' !== $singleTypeHint) {
$unionTypeHint[] = $singleTypeHint;
}
}
$typeHint = implode('|', $unionTypeHint);
if (count($unionTypeHint) > 1) {
throw new \InvalidArgumentException(sprintf("Can't use union type %s for collection in %s:%s", $typeHint, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
}

if (false === strpos($typeHint, '[]')) {
throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
}

$typeHint = rtrim($typeHint, '[]');
if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) {
$typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflectionProperty), $reflectionProperty);
}

return 'array<' . ltrim($typeHint, '\\') . '>';
}

private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, \ReflectionProperty $reflectionProperty): string
{
$expandedClassName = $declaringClass->getNamespaceName() . '\\' . $typeHint;
if (class_exists($expandedClassName)) {
return $expandedClassName;
}

$classContents = file_get_contents($declaringClass->getFileName());
$foundUseStatements = $this->gatherGroupUseStatements($classContents);
$foundUseStatements = array_merge($this->gatherSingleUseStatements($classContents), $foundUseStatements);

foreach ($foundUseStatements as $statementClassName) {
if ($alias = explode('as', $statementClassName)) {
if (array_key_exists(1, $alias) && trim($alias[1]) === $typeHint) {
return trim($alias[0]);
}
}

if ($this->endsWith($statementClassName, $typeHint)) {
return $statementClassName;
}
}

throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $declaringClass->getName(), $reflectionProperty->getName()));
}

private function endsWith(string $statementClassToCheck, string $typeHintToSearchFor): bool
{
$typeHintToSearchFor = '\\' . $typeHintToSearchFor;

return substr($statementClassToCheck, -strlen($typeHintToSearchFor)) === $typeHintToSearchFor;
}

private function isPrimitiveType(string $type): bool
{
return in_array($type, ['int', 'float', 'bool', 'string']);
}

private function hasGlobalNamespacePrefix(string $typeHint): bool
{
return self::GLOBAL_NAMESPACE_PREFIX === $typeHint[0];
}

private function gatherGroupUseStatements(string $classContents): array
{
$foundUseStatements = [];
preg_match_all(self::GROUP_USE_STATEMENTS_REGEX, $classContents, $foundGroupUseStatements);
for ($useStatementIndex = 0; $useStatementIndex < count($foundGroupUseStatements[0]); $useStatementIndex++) {
foreach (explode(',', $foundGroupUseStatements[2][$useStatementIndex]) as $singleUseStatement) {
$foundUseStatements[] = trim($foundGroupUseStatements[1][$useStatementIndex]) . trim($singleUseStatement);
}
}

return $foundUseStatements;
}

private function gatherSingleUseStatements(string $classContents): array
{
$foundUseStatements = [];
preg_match_all(self::SINGLE_USE_STATEMENTS_REGEX, $classContents, $foundSingleUseStatements);
for ($useStatementIndex = 0; $useStatementIndex < count($foundSingleUseStatements[0]); $useStatementIndex++) {
$foundUseStatements[] = trim($foundSingleUseStatements[1][$useStatementIndex]);
}

return $foundUseStatements;
}

private function getDeclaringClassOrTrait(\ReflectionProperty $reflectionProperty): \ReflectionClass
{
foreach ($reflectionProperty->getDeclaringClass()->getTraits() as $trait) {
foreach ($trait->getProperties() as $traitProperty) {
if ($traitProperty->getName() === $reflectionProperty->getName()) {
return $this->getDeclaringClassOrTrait($traitProperty);
}
}
}

return $reflectionProperty->getDeclaringClass();
}
}
39 changes: 25 additions & 14 deletions src/Metadata/Driver/TypedPropertiesDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,20 @@ class TypedPropertiesDriver implements DriverInterface
* @var string[]
*/
private $whiteList;
/**
* @var DocBlockTypeResolver
*/
private $docBlockTypeResolver;

/**
* @param string[] $whiteList
*/
public function __construct(DriverInterface $delegate, ?ParserInterface $typeParser = null, array $whiteList = [])
public function __construct(DriverInterface $delegate, DocBlockTypeResolver $docBlockTypeResolver, ?ParserInterface $typeParser = null, array $whiteList = [])
dgafka marked this conversation as resolved.
Show resolved Hide resolved
{
$this->delegate = $delegate;
$this->typeParser = $typeParser ?: new Parser();
$this->whiteList = array_merge($whiteList, $this->getDefaultWhiteList());
$this->docBlockTypeResolver = $docBlockTypeResolver;
}

private function getDefaultWhiteList(): array
Expand All @@ -55,6 +60,7 @@ private function getDefaultWhiteList(): array
'double',
'iterable',
'resource',
'array',
];
}

Expand All @@ -79,7 +85,12 @@ public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata
try {
$propertyReflection = $this->getReflection($propertyMetadata);
if ($this->shouldTypeHint($propertyReflection)) {
$propertyMetadata->setType($this->typeParser->parse($propertyReflection->getType()->getName()));
$type = $propertyReflection->getType()->getName();
if ('array' === $type) {
$type = $this->docBlockTypeResolver->getPropertyDocblockTypeHint($propertyReflection);
dgafka marked this conversation as resolved.
Show resolved Hide resolved
}

$propertyMetadata->setType($this->typeParser->parse($type));
}
} catch (ReflectionException $e) {
continue;
Expand All @@ -89,6 +100,18 @@ public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata
return $classMetadata;
}

private function isVirtualProperty(PropertyMetadata $propertyMetadata): bool
{
return $propertyMetadata instanceof VirtualPropertyMetadata
|| $propertyMetadata instanceof StaticPropertyMetadata
|| $propertyMetadata instanceof ExpressionPropertyMetadata;
}

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

private function shouldTypeHint(ReflectionProperty $propertyReflection): bool
{
if (null === $propertyReflection->getType()) {
Expand All @@ -105,16 +128,4 @@ private function shouldTypeHint(ReflectionProperty $propertyReflection): bool

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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties\Collection;

use JMS\Serializer\Tests\Fixtures\TypedProperties\Collection\Details\ProductDescription;

class CollectionOfClassesFromDifferentNamespace
{
/**
* @var ProductDescription[]
*/
public array $productDescriptions;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties\Collection;

use JMS\Serializer\Tests\Fixtures\TypedProperties\Collection\Details\{ProductDescription as Description, ProductName};

class CollectionOfClassesFromDifferentNamespaceUsingGroupAlias
{
/**
* @var Description[]
*/
public array $productDescriptions;
/**
* @var ProductName[]
*/
public array $productNames;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties\Collection;

use JMS\Serializer\Tests\Fixtures\TypedProperties\Collection\Details\ProductDescription as Description;

class CollectionOfClassesFromDifferentNamespaceUsingSingleAlias
{
/**
* @var Description[]
*/
public array $productDescriptions;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties\Collection;

class CollectionOfClassesFromGlobalNamespace
{
/**
* @var \stdClass[]
*/
public array $products;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties\Collection;

class CollectionOfClassesFromSameNamespace
{
/**
* @var Product[]
*/
public array $productIds;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties\Collection;

use JMS\Serializer\Tests\Fixtures\TypedProperties\Collection\Details\WithProductDescriptionTrait;

class CollectionOfClassesFromTrait
{
use WithProductDescriptionTrait;
use WithProductNameTrait;
}
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\Collection;

class CollectionOfClassesFromTraitInsideTrait
{
use WithTraitInsideTrait;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties\Collection;

class CollectionOfClassesWithNull
{
/**
* @var Product[]|null
*/
public ?array $productIds;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties\Collection;

class CollectionOfNotExistingClasses
{
/**
* @var NotExistingClass[]
*/
public array $productIds;
}
13 changes: 13 additions & 0 deletions tests/Fixtures/TypedProperties/Collection/CollectionOfScalars.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\TypedProperties\Collection;

class CollectionOfScalars
{
/**
* @var string[]
*/
public array $productIds;
}
Loading