-
-
Notifications
You must be signed in to change notification settings - Fork 586
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Resolve collections from DocBlock (#1214)
enable docblock type resolver as opt-in feature
- Loading branch information
Showing
31 changed files
with
928 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
<?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 DocBlockDriver implements DriverInterface | ||
{ | ||
/** | ||
* @var DriverInterface | ||
*/ | ||
protected $delegate; | ||
|
||
/** | ||
* @var ParserInterface | ||
*/ | ||
protected $typeParser; | ||
/** | ||
* @var DocBlockTypeResolver | ||
*/ | ||
private $docBlockTypeResolver; | ||
|
||
public function __construct(DriverInterface $delegate, ?ParserInterface $typeParser = null) | ||
{ | ||
$this->delegate = $delegate; | ||
$this->typeParser = $typeParser ?: new Parser(); | ||
$this->docBlockTypeResolver = new DocBlockTypeResolver(); | ||
} | ||
|
||
public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata | ||
{ | ||
$classMetadata = $this->delegate->loadMetadataForClass($class); | ||
\assert($classMetadata instanceof SerializerClassMetadata); | ||
|
||
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) { | ||
// If the inner driver provides a type, don't guess anymore. | ||
if ($propertyMetadata->type || $this->isVirtualProperty($propertyMetadata)) { | ||
continue; | ||
} | ||
|
||
try { | ||
$propertyReflection = $this->getReflection($propertyMetadata); | ||
|
||
$type = $this->docBlockTypeResolver->getPropertyDocblockTypeHint($propertyReflection); | ||
if ($type) { | ||
$propertyMetadata->setType($this->typeParser->parse($type)); | ||
} | ||
} catch (ReflectionException $e) { | ||
continue; | ||
} | ||
} | ||
|
||
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace JMS\Serializer\Metadata\Driver; | ||
|
||
use Doctrine\Common\Annotations\Reader; | ||
use JMS\Serializer\Builder\DriverFactoryInterface; | ||
use JMS\Serializer\Type\ParserInterface; | ||
use Metadata\Driver\DriverInterface; | ||
|
||
class DocBlockDriverFactory implements DriverFactoryInterface | ||
{ | ||
/** | ||
* @var DriverFactoryInterface | ||
*/ | ||
private $driverFactoryToDecorate; | ||
/** | ||
* @var ParserInterface|null | ||
*/ | ||
private $typeParser; | ||
|
||
public function __construct(DriverFactoryInterface $driverFactoryToDecorate, ?ParserInterface $typeParser = null) | ||
{ | ||
$this->driverFactoryToDecorate = $driverFactoryToDecorate; | ||
$this->typeParser = $typeParser; | ||
} | ||
|
||
public function createDriver(array $metadataDirs, Reader $annotationReader): DriverInterface | ||
{ | ||
$driver = $this->driverFactoryToDecorate->createDriver($metadataDirs, $annotationReader); | ||
|
||
return new DocBlockDriver($driver, $this->typeParser); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
<?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\$]*)#'; | ||
/** 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 | ||
{ | ||
if (!$reflectionProperty->getDocComment()) { | ||
return null; | ||
} | ||
|
||
preg_match_all(self::CLASS_PROPERTY_TYPE_HINT_REGEX, $reflectionProperty->getDocComment(), $matchedDocBlockParameterTypes); | ||
if (!isset($matchedDocBlockParameterTypes[1][0])) { | ||
return null; | ||
} | ||
|
||
$typeHint = trim($matchedDocBlockParameterTypes[1][0]); | ||
if ($this->isArrayWithoutAnyType($typeHint)) { | ||
return null; | ||
} | ||
|
||
$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, 'array<')) { | ||
$resolvedTypes = []; | ||
preg_match_all('#array<(.*)>#', $typeHint, $genericTypesToResolve); | ||
$genericTypesToResolve = $genericTypesToResolve[1][0]; | ||
foreach (explode(',', $genericTypesToResolve) as $genericTypeToResolve) { | ||
$resolvedTypes[] = $this->resolveType(trim($genericTypeToResolve), $reflectionProperty); | ||
} | ||
|
||
return 'array<' . implode(',', $resolvedTypes) . '>'; | ||
} elseif (false !== strpos($typeHint, '[]')) { | ||
$typeHint = rtrim($typeHint, '[]'); | ||
$typeHint = $this->resolveType($typeHint, $reflectionProperty); | ||
|
||
return 'array<' . $typeHint . '>'; | ||
} | ||
|
||
return $this->resolveType($typeHint, $reflectionProperty); | ||
} | ||
|
||
private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, \ReflectionProperty $reflectionProperty): string | ||
{ | ||
if (class_exists($typeHint)) { | ||
return $typeHint; | ||
} | ||
|
||
$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(); | ||
} | ||
|
||
private function resolveType(string $typeHint, \ReflectionProperty $reflectionProperty): string | ||
{ | ||
if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) { | ||
$typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflectionProperty), $reflectionProperty); | ||
} | ||
|
||
return ltrim($typeHint, '\\'); | ||
} | ||
|
||
private function isArrayWithoutAnyType(string $typeHint): bool | ||
{ | ||
return 'array' === $typeHint; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.