Skip to content

Commit

Permalink
use PhpDocParser for property parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach committed Oct 9, 2024
1 parent aa7c702 commit 0e39f0a
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 79 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"ext-ctype": "*",
"nette/caching": "~3.2 || ~3.1.3",
"nette/utils": "~3.0 || ~4.0",
"nextras/dbal": "dev-main#bf717b4b02b45f44b7c25b3b1c6a14a19cc59847"
"nextras/dbal": "dev-main#bf717b4b02b45f44b7c25b3b1c6a14a19cc59847",
"phpstan/phpdoc-parser": "2.0.x-dev"
},
"require-dev": {
"nette/bootstrap": "~3.1",
Expand Down
155 changes: 102 additions & 53 deletions src/Entity/Reflection/MetadataParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,28 @@
use Nextras\Orm\Relationships\OneHasMany;
use Nextras\Orm\Relationships\OneHasOne;
use Nextras\Orm\Repository\IRepository;
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPStan\PhpDocParser\ParserConfig;
use ReflectionClass;
use function array_keys;
use function assert;
use function class_exists;
use function count;
use function is_subclass_of;
use function preg_split;
use function strlen;
use function substr;
use function trigger_error;
Expand Down Expand Up @@ -68,6 +83,9 @@ class MetadataParser implements IMetadataParser
/** @var array<string, PropertyMetadata[]> */
protected $classPropertiesCache = [];

protected PhpDocParser $phpDocParser;
protected Lexer $phpDocLexer;


/**
* @param array<string, string> $entityClassesMap
Expand All @@ -77,6 +95,12 @@ public function __construct(array $entityClassesMap)
{
$this->entityClassesMap = $entityClassesMap;
$this->modifierParser = new ModifierParser();

$config = new ParserConfig(usedAttributes: []);
$this->phpDocLexer = new Lexer($config);
$constExprParser = new ConstExprParser($config);
$typeParser = new TypeParser($config, $constExprParser);
$this->phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser);
}


Expand Down Expand Up @@ -161,43 +185,53 @@ protected function loadProperties(array|null &$fileDependencies): void
*/
protected function parseAnnotations(ReflectionClass $reflection, array $methods): array
{
preg_match_all(
'~^[ \t*]* @property(|-read|-write)[ \t]+([^\s$]+)[ \t]+\$(\w+)(.*)$~um',
(string) $reflection->getDocComment(), $matches, PREG_SET_ORDER,
);

$properties = [];
foreach ($matches as [, $access, $type, $variable, $comment]) {
$isReadonly = $access === '-read';
$docComment = $reflection->getDocComment();
if ($docComment === false) return [];

$property = new PropertyMetadata();
$property->name = $variable;
$property->containerClassname = $reflection->getName();
$property->isReadonly = $isReadonly;
$tokens = new TokenIterator($this->phpDocLexer->tokenize($docComment));
$phpDocNode = $this->phpDocParser->parse($tokens);

$this->parseAnnotationTypes($property, $type);
$this->parseAnnotationValue($property, $comment);
$this->processPropertyGettersSetters($property, $methods);
$properties = [];
foreach ($phpDocNode->getPropertyTagValues() as $propertyTagValue) {
$property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: false);
$properties[$property->name] = $property;
}
foreach ($phpDocNode->getPropertyWriteTagValues() as $propertyTagValue) {
$property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: false);
$properties[$property->name] = $property;
}
foreach ($phpDocNode->getPropertyReadTagValues() as $propertyTagValue) {
$property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: true);
$properties[$property->name] = $property;
}
return $properties;
}


protected function parseAnnotationTypes(PropertyMetadata $property, string $typesString): void
/**
* @param array<string, true> $methods
*/
protected function parseProperty(
PropertyTagValueNode $propertyNode,
string $containerClassName,
array $methods,
bool $isReadonly,
): PropertyMetadata
{
$property = new PropertyMetadata();
$property->name = substr($propertyNode->propertyName, 1);
$property->containerClassname = $containerClassName;
$property->isReadonly = $isReadonly;

$this->parseAnnotationTypes($property, $propertyNode->type);
$this->parseAnnotationValue($property, $propertyNode->description);
$this->processPropertyGettersSetters($property, $methods);
return $property;
}


protected function parseAnnotationTypes(PropertyMetadata $property, TypeNode $type): void
{
static $types = [
'array' => true,
'bool' => true,
'float' => true,
'int' => true,
'mixed' => true,
'null' => true,
'object' => true,
'string' => true,
'text' => true,
'scalar' => true,
];
static $aliases = [
'double' => 'float',
'real' => 'float',
Expand All @@ -207,38 +241,53 @@ protected function parseAnnotationTypes(PropertyMetadata $property, string $type
'boolean' => 'bool',
];

if ($type instanceof UnionTypeNode) {
$types = $type->types;
} elseif ($type instanceof IntersectionTypeNode) {
$types = $type->types;
} else {
$types = [$type];
}

$parsedTypes = [];
$isNullable = false;
$rawTypes = preg_split('#[|&]#', $typesString);
$rawTypes = $rawTypes === false ? [] : $rawTypes;
foreach ($rawTypes as $type) {
$typeLower = strtolower($type);
if (($type[0] ?? '') === '?') {
$isNullable = true;
$typeLower = substr($typeLower, 1);
$type = substr($type, 1);
foreach ($types as $subType) {
if ($subType instanceof NullableTypeNode) {
$property->isNullable = true;
$subType = $subType->type;
}
if (str_contains($type, '[')) { // string[]
$type = 'array';
} elseif (isset($types[$typeLower])) {
$type = $typeLower;
} elseif (isset($aliases[$typeLower])) {
/** @var string $type */
$type = $aliases[$typeLower];
} else {
$type = Reflection::expandClassName($type, $this->currentReflection);
if ($type === DateTime::class || is_subclass_of($type, DateTime::class)) {
throw new NotSupportedException("Type '{$type}' in {$this->currentReflection->name}::\${$property->name} property is not supported anymore. Use \DateTimeImmutable or \Nextras\Dbal\Utils\DateTimeImmutable type.");
if ($subType instanceof GenericTypeNode) {
$subType = $subType->type;
}

if ($subType instanceof IdentifierTypeNode) {
$expandedSubType = Reflection::expandClassName($subType->name, $this->currentReflection);
$expandedSubTypeLower = strtolower($expandedSubType);
if ($expandedSubTypeLower === 'null') {
$property->isNullable = true;
continue;
}
if (is_subclass_of($type, BackedEnum::class)) {
if ($expandedSubType === DateTime::class || is_subclass_of($expandedSubType, DateTime::class)) {
throw new NotSupportedException("Type '{$expandedSubType}' in {$this->currentReflection->name}::\${$property->name} property is not supported anymore. Use \DateTimeImmutable or \Nextras\Dbal\Utils\DateTimeImmutable type.");
}
if (is_subclass_of($expandedSubType, BackedEnum::class)) {
$property->wrapper = BackedEnumWrapper::class;
}
if (isset($aliases[$expandedSubTypeLower])) {
/** @var string $expandedSubType */
$expandedSubType = $aliases[$expandedSubTypeLower];
}
$parsedTypes[$expandedSubType] = true;
} elseif ($subType instanceof ArrayTypeNode) {
$parsedTypes['array'] = true;
} elseif ($subType instanceof ArrayShapeNode) {
$parsedTypes['array'] = true;
} elseif ($subType instanceof ObjectShapeNode) {
$parsedTypes['object'] = true;
} else {
throw new NotSupportedException("Type '{$type}' in {$this->currentReflection->name}::\${$property->name} property is not supported. For Nextras Orm purpose simplify it.");
}
$parsedTypes[$type] = true;
}

$property->isNullable = $isNullable || isset($parsedTypes['null']) || isset($parsedTypes['NULL']) || isset($parsedTypes['mixed']);
unset($parsedTypes['null'], $parsedTypes['NULL']);
if (count($parsedTypes) < 1) {
throw new NotSupportedException("Property {$this->currentReflection->name}::\${$property->name} without a type definition is not supported.");
}
Expand Down
25 changes: 0 additions & 25 deletions tests/cases/unit/Entity/Reflection/PropertyMetadata.isValid().phpt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ require_once __DIR__ . '/../../../../bootstrap.php';
* @property array $array1
* @property int[] $array2
* @property object $object
* @property scalar $scalar
* @property mixed $mixed
* @property ArrayHash $type
* @property bool|NULL $nullable1
Expand Down Expand Up @@ -236,30 +235,6 @@ class PropertyMetadataIsValidTest extends TestCase
}


public function testScalar(): void
{
$property = $this->metadata->getProperty('scalar');

$val = 1;
Assert::true($property->isValid($val));

$val = 1.0;
Assert::true($property->isValid($val));

$val = false;
Assert::true($property->isValid($val));

$val = 'string';
Assert::true($property->isValid($val));

$val = [];
Assert::false($property->isValid($val));

$val = (object) [];
Assert::false($property->isValid($val));
}


public function testMixed(): void
{
$property = $this->metadata->getProperty('mixed');
Expand Down

0 comments on commit 0e39f0a

Please sign in to comment.