From bcd324297546956afec77429647a3aadca212bfa Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Wed, 15 Dec 2021 10:29:10 +0100 Subject: [PATCH 01/27] refactor token parser --- composer.json | 3 +- example/classes/SomeBaseClass.php | 1 - src/Parser/ClassParser.php | 231 +++++++++++++++++++++ src/Parser/PropertyAttributes.php | 65 ++++++ src/Parser/PropertyAttributesInterface.php | 33 +++ src/PhpClass/PhpClassProperty.php | 44 ++-- src/PhpClass/PhpClassPropertyInterface.php | 14 +- 7 files changed, 362 insertions(+), 29 deletions(-) create mode 100644 src/Parser/ClassParser.php create mode 100644 src/Parser/PropertyAttributes.php create mode 100644 src/Parser/PropertyAttributesInterface.php diff --git a/composer.json b/composer.json index 4a93cdb..7948180 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "require": { "ext-json": "*", "flix-tech/avro-php": "^3.0|^4.0", - "symfony/console": "^4.3|^5.1" + "symfony/console": "^4.3|^5.1", + "nikic/php-parser": "^4.13" }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.15", diff --git a/example/classes/SomeBaseClass.php b/example/classes/SomeBaseClass.php index 1079adf..4e12dc5 100644 --- a/example/classes/SomeBaseClass.php +++ b/example/classes/SomeBaseClass.php @@ -8,7 +8,6 @@ abstract class SomeBaseClass { - /** * @var Wonderland */ diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php new file mode 100644 index 0000000..27644f1 --- /dev/null +++ b/src/Parser/ClassParser.php @@ -0,0 +1,231 @@ + 'null', + 'bool' => 'boolean', + 'boolean' => 'boolean', + 'string' => 'string', + 'int' => 'int', + 'integer' => 'int', + 'float' => 'float', + 'double' => 'double', + 'array' => 'array', + 'object' => 'object', + 'callable' => 'callable', + 'resource' => 'resource', + 'mixed' => 'mixed', + 'Collection' => 'array', + ); + + public function __construct(ParserFactory $parserFactory) + { + $this->parserFactory = $parserFactory; + $this->parser = $parserFactory->create(ParserFactory::PREFER_PHP7); + + } + + public function setCode(string $code) + { + $this->code = $code; + $this->statements = $this->parser->parse($code); + } + + /** + * @return string|null + */ + public function getClassName(): ?string + { + if (null === $this->statements) { + return null; + } + + foreach ($this->statements as $statement) { + if ($statement instanceof Namespace_) { + foreach ($statement->stmts as $nsStatement) { + if ($nsStatement instanceof Class_) { + return $nsStatement->name->name; + } + } + } + } + + return null; + } + + /** + * @return string + */ + public function getNamespace(): ?string + { + if (null === $this->statements) { + return null; + } + + foreach ($this->statements as $statement) { + if ($statement instanceof Namespace_) { + return implode('\\', $statement->name->parts); + } + } + + return null; + } + + /** + * @return PhpClassProperty[] + */ + public function getProperties(): array + { + $properties = []; + + foreach ($this->statements as $statement) { + if ($statement instanceof Namespace_) { + foreach ($statement->stmts as $nsStatement) { + if ($nsStatement instanceof Class_) { + foreach ($nsStatement->stmts as $pStatement) { + if ($pStatement instanceof Property) { + $propertyAttributes = $this->getPropertyAttributes($pStatement); + $properties[] = new PhpClassProperty($propertyAttributes); + } + } + } + } + } + } + + return $properties; + } + + private function getPropertyAttributes(Property $property): PropertyAttributesInterface + { + $name = $property->props[0]->name->name; + $attributes = $this->getAvroAttributesFromCode($property); + var_dump($attributes); + + return new PropertyAttributes( + $name, + $attributes['types'], + $attributes['default'], + $attributes['logicalType'], + $attributes['doc'] + ); + } + + private function getAvroAttributesFromCode(Property $property): array + { + $attributes = $this->getEmptyAttributesArray(); + $attributes['types'] = $this->getPropertyType($property); + + return $attributes; + } + + private function getPropertyDocComments(Property $property): array + { + $docComments = []; + + foreach ($property->getAttributes() as $attributeName => $attributeValue) { + if ('comments' === $attributeName) { + /** @var Doc $comment */ + foreach ($attributeValue as $comment) { + $docComments[] = $comment->getText(); + } + } + + } + + return $docComments; + } + + private function getPropertyType(Property $property): string + { + if ($property->type instanceof Identifier) { + return $this->mappedTypes[$property->type->name] ?? $property->type->name; + } elseif ($property->type instanceof UnionType) { + $types = ''; + $separator = ''; + /** @var Identifier $type */ + foreach ($property->type->types as $type) { + $types .= $separator . $this->mappedTypes[$type->name] ?? $type->name; + $separator = ','; + } + + return $types; + } + + return 'string'; + } + + private function getTypeFromDocComment(array $docComments): string + { + foreach ($docComments as $doc) { + if (false !== $varPos = strpos($doc, '@var')) { + if (false !== $eolPos = strpos($doc, PHP_EOL, $varPos)) { + $varDoc = substr($doc, $varPos, ($eolPos - $varPos)); + } else { + $varDoc = substr($doc, $varPos); + } + $rawTypes = trim(str_replace(['[]', '*/', '@var'], '', $varDoc)); + + $types = explode('|', $rawTypes); + + foreach ($types as $type) { + if ('array' === $type) { + continue; + } + + return $this->mappedTypes[$type] ?? $type; + } + } + } + + return 'string'; + } + + private function getDefaultFromDocComment(): ?string + { + + } + + private function getLogicalTypeFromDocComment(): ?string + { + + } + + private function getDocFromDocComment(): ?string + { + + } + + private function getEmptyAttributesArray(): array + { + return [ + 'name' => null, + 'types' => null, + 'default' => null, + 'doc' => null + ]; + } +} \ No newline at end of file diff --git a/src/Parser/PropertyAttributes.php b/src/Parser/PropertyAttributes.php new file mode 100644 index 0000000..980acc8 --- /dev/null +++ b/src/Parser/PropertyAttributes.php @@ -0,0 +1,65 @@ +name = $name; + $this->types = $types; + $this->default = $default; + $this->logicalType = $logicalType; + $this->doc = $doc; + } + + public function getName(): string + { + return $this->name; + } + + public function getTypes(): string + { + return $this->types; + } + + /** + * @return null|mixed + */ + public function getDefault() + { + return $this->default; + } + + public function getLogicalType(): ?string + { + return $this->logicalType; + } + + public function getDoc(): ?string + { + return $this->doc; + } +} \ No newline at end of file diff --git a/src/Parser/PropertyAttributesInterface.php b/src/Parser/PropertyAttributesInterface.php new file mode 100644 index 0000000..b9ccca5 --- /dev/null +++ b/src/Parser/PropertyAttributesInterface.php @@ -0,0 +1,33 @@ +propertyName = $propertyName; - $this->propertyType = $propertyType; - $this->propertyArrayType = $propertyArrayType; + $this->propertyAttributes = $propertyAttributes; } /** @@ -33,7 +20,7 @@ public function __construct(string $propertyName, string $propertyType, ?string */ public function getPropertyName(): string { - return $this->propertyName; + return $this->propertyAttributes->getName(); } /** @@ -41,14 +28,21 @@ public function getPropertyName(): string */ public function getPropertyType(): string { - return $this->propertyType; + return $this->propertyAttributes->getTypes(); } - /** - * @return string|null - */ - public function getPropertyArrayType(): ?string + public function getPropertyDefault() + { + return $this->propertyAttributes->getDefault(); + } + + public function getPropertyLogicalType(): ?string + { + return $this->propertyAttributes->getLogicalType(); + } + + public function getPropertyDoc(): ?string { - return $this->propertyArrayType; + return $this->propertyAttributes->getDoc(); } } diff --git a/src/PhpClass/PhpClassPropertyInterface.php b/src/PhpClass/PhpClassPropertyInterface.php index 120dc2e..3ca549d 100644 --- a/src/PhpClass/PhpClassPropertyInterface.php +++ b/src/PhpClass/PhpClassPropertyInterface.php @@ -17,7 +17,17 @@ public function getPropertyName(): string; public function getPropertyType(): string; /** - * @return string|null + * @return mixed */ - public function getPropertyArrayType(): ?string; + public function getPropertyDefault(); + + /** + * @return ?string + */ + public function getPropertyLogicalType(): ?string; + + /** + * @return ?string + */ + public function getPropertyDoc(): ?string; } From a52773f37b3e6bd27c1c3b6065d729d2566591fc Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Wed, 15 Dec 2021 13:12:08 +0100 Subject: [PATCH 02/27] save work --- src/Parser/ClassParser.php | 121 +----------------- src/Parser/ClassParserInterface.php | 19 +++ src/Parser/ClassPropertyParser.php | 135 ++++++++++++++++++++ src/Parser/ClassPropertyParserInterface.php | 12 ++ src/Parser/PropertyAttributes.php | 65 ---------- src/Parser/PropertyAttributesInterface.php | 33 ----- src/PhpClass/PhpClassProperty.php | 54 +++++--- src/PhpClass/PhpClassPropertyInterface.php | 22 +--- src/Registry/ClassRegistry.php | 3 +- 9 files changed, 215 insertions(+), 249 deletions(-) create mode 100644 src/Parser/ClassParserInterface.php create mode 100644 src/Parser/ClassPropertyParser.php create mode 100644 src/Parser/ClassPropertyParserInterface.php delete mode 100644 src/Parser/PropertyAttributes.php delete mode 100644 src/Parser/PropertyAttributesInterface.php diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index 27644f1..620bb41 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -5,6 +5,7 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Parser; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassProperty; +use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyInterface; use PhpParser\Comment\Doc; use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\Class_; @@ -14,9 +15,10 @@ use PhpParser\ParserFactory; use PhpParser\Parser; -class ClassParser +class ClassParser implements ClassParserInterface { private ParserFactory $parserFactory; + private ClassPropertyParserInterface $propertyParser; private Parser $parser; private string $code; private array $statements; @@ -41,11 +43,11 @@ class ClassParser 'Collection' => 'array', ); - public function __construct(ParserFactory $parserFactory) + public function __construct(ParserFactory $parserFactory, ClassPropertyParserInterface $propertyParser) { $this->parserFactory = $parserFactory; $this->parser = $parserFactory->create(ParserFactory::PREFER_PHP7); - + $this->propertyParser = $propertyParser; } public function setCode(string $code) @@ -107,8 +109,7 @@ public function getProperties(): array if ($nsStatement instanceof Class_) { foreach ($nsStatement->stmts as $pStatement) { if ($pStatement instanceof Property) { - $propertyAttributes = $this->getPropertyAttributes($pStatement); - $properties[] = new PhpClassProperty($propertyAttributes); + $properties[] = $this->propertyParser->parseProperty($pStatement); } } } @@ -118,114 +119,4 @@ public function getProperties(): array return $properties; } - - private function getPropertyAttributes(Property $property): PropertyAttributesInterface - { - $name = $property->props[0]->name->name; - $attributes = $this->getAvroAttributesFromCode($property); - var_dump($attributes); - - return new PropertyAttributes( - $name, - $attributes['types'], - $attributes['default'], - $attributes['logicalType'], - $attributes['doc'] - ); - } - - private function getAvroAttributesFromCode(Property $property): array - { - $attributes = $this->getEmptyAttributesArray(); - $attributes['types'] = $this->getPropertyType($property); - - return $attributes; - } - - private function getPropertyDocComments(Property $property): array - { - $docComments = []; - - foreach ($property->getAttributes() as $attributeName => $attributeValue) { - if ('comments' === $attributeName) { - /** @var Doc $comment */ - foreach ($attributeValue as $comment) { - $docComments[] = $comment->getText(); - } - } - - } - - return $docComments; - } - - private function getPropertyType(Property $property): string - { - if ($property->type instanceof Identifier) { - return $this->mappedTypes[$property->type->name] ?? $property->type->name; - } elseif ($property->type instanceof UnionType) { - $types = ''; - $separator = ''; - /** @var Identifier $type */ - foreach ($property->type->types as $type) { - $types .= $separator . $this->mappedTypes[$type->name] ?? $type->name; - $separator = ','; - } - - return $types; - } - - return 'string'; - } - - private function getTypeFromDocComment(array $docComments): string - { - foreach ($docComments as $doc) { - if (false !== $varPos = strpos($doc, '@var')) { - if (false !== $eolPos = strpos($doc, PHP_EOL, $varPos)) { - $varDoc = substr($doc, $varPos, ($eolPos - $varPos)); - } else { - $varDoc = substr($doc, $varPos); - } - $rawTypes = trim(str_replace(['[]', '*/', '@var'], '', $varDoc)); - - $types = explode('|', $rawTypes); - - foreach ($types as $type) { - if ('array' === $type) { - continue; - } - - return $this->mappedTypes[$type] ?? $type; - } - } - } - - return 'string'; - } - - private function getDefaultFromDocComment(): ?string - { - - } - - private function getLogicalTypeFromDocComment(): ?string - { - - } - - private function getDocFromDocComment(): ?string - { - - } - - private function getEmptyAttributesArray(): array - { - return [ - 'name' => null, - 'types' => null, - 'default' => null, - 'doc' => null - ]; - } } \ No newline at end of file diff --git a/src/Parser/ClassParserInterface.php b/src/Parser/ClassParserInterface.php new file mode 100644 index 0000000..5fc6a13 --- /dev/null +++ b/src/Parser/ClassParserInterface.php @@ -0,0 +1,19 @@ +getPropertyAttributes($pStatement); + + return new PhpClassProperty( + $propertyAttributes['name'], + $propertyAttributes['types'], + $propertyAttributes['default'], + $propertyAttributes['doc'], + $propertyAttributes['logicalType'] + ); + } + + + private function getPropertyAttributes(Property $property): array + { + $attributes = $this->getAvroAttributesFromCode($property); + $attributes['name'] = $property->props[0]->name->name; + + return $attributes; + } + + private function getAvroAttributesFromCode(Property $property): array + { + $attributes = $this->getEmptyAttributesArray(); + $attributes['types'] = $this->getPropertyType($property); + + return $attributes; + } + + private function getPropertyDocComments(Property $property): array + { + $docComments = []; + + foreach ($property->getAttributes() as $attributeName => $attributeValue) { + if ('comments' === $attributeName) { + /** @var Doc $comment */ + foreach ($attributeValue as $comment) { + $docComments[] = $comment->getText(); + } + } + + } + + return $docComments; + } + + private function getPropertyType(Property $property): string + { + if ($property->type instanceof Identifier) { + return $this->mappedTypes[$property->type->name] ?? $property->type->name; + } elseif ($property->type instanceof UnionType) { + $types = ''; + $separator = ''; + /** @var Identifier $type */ + foreach ($property->type->types as $type) { + $types .= $separator . $this->mappedTypes[$type->name] ?? $type->name; + $separator = ','; + } + + return $types; + } + + return 'string'; + } + + private function getTypeFromDocComment(array $docComments): string + { + foreach ($docComments as $doc) { + if (false !== $varPos = strpos($doc, '@var')) { + if (false !== $eolPos = strpos($doc, PHP_EOL, $varPos)) { + $varDoc = substr($doc, $varPos, ($eolPos - $varPos)); + } else { + $varDoc = substr($doc, $varPos); + } + $rawTypes = trim(str_replace(['[]', '*/', '@var'], '', $varDoc)); + + $types = explode('|', $rawTypes); + + foreach ($types as $type) { + if ('array' === $type) { + continue; + } + + return $this->mappedTypes[$type] ?? $type; + } + } + } + + return 'string'; + } + + private function getDefaultFromDocComment(): ?string + { + + } + + private function getLogicalTypeFromDocComment(): ?string + { + + } + + private function getDocFromDocComment(): ?string + { + + } + + private function getEmptyAttributesArray(): array + { + return [ + 'name' => null, + 'types' => null, + 'default' => PhpClassPropertyInterface::NO_DEFAULT, + 'logicalType' => null, + 'doc' => null + ]; + } + +} \ No newline at end of file diff --git a/src/Parser/ClassPropertyParserInterface.php b/src/Parser/ClassPropertyParserInterface.php new file mode 100644 index 0000000..646510c --- /dev/null +++ b/src/Parser/ClassPropertyParserInterface.php @@ -0,0 +1,12 @@ +name = $name; - $this->types = $types; - $this->default = $default; - $this->logicalType = $logicalType; - $this->doc = $doc; - } - - public function getName(): string - { - return $this->name; - } - - public function getTypes(): string - { - return $this->types; - } - - /** - * @return null|mixed - */ - public function getDefault() - { - return $this->default; - } - - public function getLogicalType(): ?string - { - return $this->logicalType; - } - - public function getDoc(): ?string - { - return $this->doc; - } -} \ No newline at end of file diff --git a/src/Parser/PropertyAttributesInterface.php b/src/Parser/PropertyAttributesInterface.php deleted file mode 100644 index b9ccca5..0000000 --- a/src/Parser/PropertyAttributesInterface.php +++ /dev/null @@ -1,33 +0,0 @@ -propertyAttributes = $propertyAttributes; - } + /** @var mixed */ + private $propertyDefault; + private ?string $propertyDoc; + private ?string $propertyLogicalType; + private string $propertyName; + private string $propertyType; /** - * @return string + * @param mixed $propertyDefault + * @param string $propertyDoc + * @param string $propertyLogicalType + * @param string $propertyName + * @param string $propertyType */ - public function getPropertyName(): string - { - return $this->propertyAttributes->getName(); + public function __construct( + string $propertyName, + string $propertyType, + mixed $propertyDefault = self::NO_DEFAULT, + ?string $propertyDoc = null, + ?string $propertyLogicalType = null + ) { + $this->propertyDefault = $propertyDefault; + $this->propertyDoc = $propertyDoc; + $this->propertyLogicalType = $propertyLogicalType; + $this->propertyName = $propertyName; + $this->propertyType = $propertyType; } /** - * @return string + * @return mixed */ - public function getPropertyType(): string + public function getPropertyDefault() { - return $this->propertyAttributes->getTypes(); + return $this->propertyDefault; } - public function getPropertyDefault() + public function getPropertyDoc(): ?string { - return $this->propertyAttributes->getDefault(); + return $this->propertyDoc; } public function getPropertyLogicalType(): ?string { - return $this->propertyAttributes->getLogicalType(); + return $this->propertyLogicalType; } - public function getPropertyDoc(): ?string + public function getPropertyName(): string + { + return $this->propertyName; + } + + public function getPropertyType(): string { - return $this->propertyAttributes->getDoc(); + return $this->propertyType; } } diff --git a/src/PhpClass/PhpClassPropertyInterface.php b/src/PhpClass/PhpClassPropertyInterface.php index 3ca549d..98d06d2 100644 --- a/src/PhpClass/PhpClassPropertyInterface.php +++ b/src/PhpClass/PhpClassPropertyInterface.php @@ -6,28 +6,18 @@ interface PhpClassPropertyInterface { - /** - * @return string - */ - public function getPropertyName(): string; - - /** - * @return string - */ - public function getPropertyType(): string; + public const NO_DEFAULT = 'there-was-no-default-set'; /** * @return mixed */ public function getPropertyDefault(); - /** - * @return ?string - */ + public function getPropertyDoc(): ?string; + public function getPropertyLogicalType(): ?string; - /** - * @return ?string - */ - public function getPropertyDoc(): ?string; + public function getPropertyName(): string; + + public function getPropertyType(): string; } diff --git a/src/Registry/ClassRegistry.php b/src/Registry/ClassRegistry.php index e768895..c4d9f50 100644 --- a/src/Registry/ClassRegistry.php +++ b/src/Registry/ClassRegistry.php @@ -103,8 +103,7 @@ private function registerClassFile(SplFileInfo $fileInfo): void } /** @var class-string $className */ - $className = $parser->getNamespace() . '\\' . $parser->getClassName(); - $properties = $parser->getProperties($className); + $properties = $parser->getProperties(); $this->classes[] = new PhpClass( $parser->getClassName(), $parser->getNamespace(), From 53b121132e8f59c37ad67a43b12dd806361676f5 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Wed, 15 Dec 2021 14:21:59 +0100 Subject: [PATCH 03/27] save work --- src/Parser/ClassParser.php | 24 ----- src/Parser/ClassPropertyParser.php | 125 +++++++++++++---------- src/Parser/DocCommentParser.php | 55 ++++++++++ src/Parser/DocCommentParserInterface.php | 12 +++ 4 files changed, 140 insertions(+), 76 deletions(-) create mode 100644 src/Parser/DocCommentParser.php create mode 100644 src/Parser/DocCommentParserInterface.php diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index 620bb41..b5fc44c 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -5,13 +5,9 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Parser; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassProperty; -use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyInterface; -use PhpParser\Comment\Doc; -use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Property; -use PhpParser\Node\UnionType; use PhpParser\ParserFactory; use PhpParser\Parser; @@ -23,26 +19,6 @@ class ClassParser implements ClassParserInterface private string $code; private array $statements; - /** - * @var string[] - */ - private $mappedTypes = array( - 'null' => 'null', - 'bool' => 'boolean', - 'boolean' => 'boolean', - 'string' => 'string', - 'int' => 'int', - 'integer' => 'int', - 'float' => 'float', - 'double' => 'double', - 'array' => 'array', - 'object' => 'object', - 'callable' => 'callable', - 'resource' => 'resource', - 'mixed' => 'mixed', - 'Collection' => 'array', - ); - public function __construct(ParserFactory $parserFactory, ClassPropertyParserInterface $propertyParser) { $this->parserFactory = $parserFactory; diff --git a/src/Parser/ClassPropertyParser.php b/src/Parser/ClassPropertyParser.php index 85e188e..9750ac0 100644 --- a/src/Parser/ClassPropertyParser.php +++ b/src/Parser/ClassPropertyParser.php @@ -10,13 +10,44 @@ use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\Property; use PhpParser\Node\UnionType; +use RuntimeException; class ClassPropertyParser implements ClassPropertyParserInterface { + private DocCommentParserInterface $docParser; + + /** + * @var string[] + */ + private $mappedTypes = array( + 'null' => 'null', + 'bool' => 'boolean', + 'boolean' => 'boolean', + 'string' => 'string', + 'int' => 'int', + 'integer' => 'int', + 'float' => 'float', + 'double' => 'double', + 'array' => 'array', + 'object' => 'object', + 'callable' => 'callable', + 'resource' => 'resource', + 'mixed' => 'mixed', + 'Collection' => 'array', + ); + + public function __construct(DocCommentParserInterface $docParser) + { + $this->docParser = $docParser; + } public function parseProperty($property): PhpClassPropertyInterface { - $propertyAttributes = $this->getPropertyAttributes($pStatement); + if (false === $property instanceof Property) { + throw new RuntimeException(sprintf('Property must be of type: %s', Property::class)); + } + + $propertyAttributes = $this->getPropertyAttributes($property); return new PhpClassProperty( $propertyAttributes['name'], @@ -30,38 +61,27 @@ public function parseProperty($property): PhpClassPropertyInterface private function getPropertyAttributes(Property $property): array { - $attributes = $this->getAvroAttributesFromCode($property); - $attributes['name'] = $property->props[0]->name->name; + $attributes = $this->getEmptyAttributesArray(); + $docComments = $this->getAllPropertyDocComments($property); + $attributes['name'] = $this->getPropertyName($property); - return $attributes; - } + $attributes['types'] = $this->getTypeFromDocComment($docComments); + if (null === $attributes['types']) { + $attributes['types'] = $this->getPropertyType($property, $docComments); + } - private function getAvroAttributesFromCode(Property $property): array - { - $attributes = $this->getEmptyAttributesArray(); - $attributes['types'] = $this->getPropertyType($property); + $attributes['default'] = $this->getDefaultFromDocComment($docComments); + $attributes['doc'] = $this->getDocFromDocComment($docComments); + $attributes['logicalType'] = $this->getLogicalTypeFromDocComment($docComments); return $attributes; } - private function getPropertyDocComments(Property $property): array - { - $docComments = []; - - foreach ($property->getAttributes() as $attributeName => $attributeValue) { - if ('comments' === $attributeName) { - /** @var Doc $comment */ - foreach ($attributeValue as $comment) { - $docComments[] = $comment->getText(); - } - } - - } - - return $docComments; + private function getPropertyName(Property $property) { + return $property->props[0]->name->name; } - private function getPropertyType(Property $property): string + private function getPropertyType(Property $property, array $docComments): string { if ($property->type instanceof Identifier) { return $this->mappedTypes[$property->type->name] ?? $property->type->name; @@ -77,48 +97,49 @@ private function getPropertyType(Property $property): string return $types; } - return 'string'; + return $this->getDocCommentByType($docComments, 'var') ?? 'string'; } - private function getTypeFromDocComment(array $docComments): string + private function getDocCommentByType(array $docComments, string $type) { - foreach ($docComments as $doc) { - if (false !== $varPos = strpos($doc, '@var')) { - if (false !== $eolPos = strpos($doc, PHP_EOL, $varPos)) { - $varDoc = substr($doc, $varPos, ($eolPos - $varPos)); - } else { - $varDoc = substr($doc, $varPos); - } - $rawTypes = trim(str_replace(['[]', '*/', '@var'], '', $varDoc)); - - $types = explode('|', $rawTypes); - - foreach ($types as $type) { - if ('array' === $type) { - continue; - } - - return $this->mappedTypes[$type] ?? $type; - } - } - } - - return 'string'; + return $docComments[$type] ?? null; } - private function getDefaultFromDocComment(): ?string + private function getTypeFromDocComment(array $docComments): ?string { + return $docComments['avro-type'] ?? null; + } + private function getDefaultFromDocComment(array $docComments): ?string + { + return $docComments['avro-default'] ?? null; } - private function getLogicalTypeFromDocComment(): ?string + private function getLogicalTypeFromDocComment(array $docComments): ?string { + return $docComments['avro-logical-type'] ?? null; + } + private function getDocFromDocComment(array $docComments): ?string + { + return $docComments['avro-doc'] ?? $docComments[DocCommentParserInterface::DOC_DESCRIPTION] ?? null; } - private function getDocFromDocComment(): ?string + private function getAllPropertyDocComments(Property $property): array { + $docComments = []; + foreach ($property->getAttributes() as $attributeName => $attributeValue) { + if ('comments' === $attributeName) { + /** @var Doc $comment */ + foreach ($attributeValue as $comment) { + $docComments += $this->docParser->parseDoc($comment->getText()); + } + } + + } + + return $docComments; } private function getEmptyAttributesArray(): array diff --git a/src/Parser/DocCommentParser.php b/src/Parser/DocCommentParser.php new file mode 100644 index 0000000..919fec7 --- /dev/null +++ b/src/Parser/DocCommentParser.php @@ -0,0 +1,55 @@ +cleanDocLines($docLines); + + foreach ($cleanLines as $idx => $line) { + if (true === str_starts_with($line, '@')) { + $nextSpace = strpos($line, ' '); + $doc[substr($line, 1, $nextSpace)] = substr($line, $nextSpace + 1); + unset($cleanLines[$idx]); + } + } + + $doc[self::DOC_DESCRIPTION] = implode(' ', $cleanLines); + + return $doc; + } + + private function cleanDocLines(array $docLines): array + { + foreach ($docLines as $idx => $docLine) { + $docLines[$idx] = $this->cleanDocLine($docLine); + } + + return $docLines; + } + + private function cleanDocLine(string $docLine): string + { + $trimmedString = ltrim(rtrim($docLine)); + + if (true === str_starts_with($docLine, '/**')) { + $trimmedString = substr($trimmedString, 3); + } + + if (true === str_ends_with($docLine, '*/')) { + $trimmedString = substr($trimmedString, 0, strlen($trimmedString) - 2); + } + + if (true === str_starts_with($docLine, '*')) { + $trimmedString = substr($trimmedString, 1); + } + + return ltrim(rtrim($trimmedString)); + } +} \ No newline at end of file diff --git a/src/Parser/DocCommentParserInterface.php b/src/Parser/DocCommentParserInterface.php new file mode 100644 index 0000000..8313a2b --- /dev/null +++ b/src/Parser/DocCommentParserInterface.php @@ -0,0 +1,12 @@ + Date: Wed, 15 Dec 2021 14:31:03 +0100 Subject: [PATCH 04/27] save work --- src/Parser/DocCommentParser.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Parser/DocCommentParser.php b/src/Parser/DocCommentParser.php index 919fec7..e1fc28a 100644 --- a/src/Parser/DocCommentParser.php +++ b/src/Parser/DocCommentParser.php @@ -11,12 +11,18 @@ public function parseDoc(string $docComment): array $doc = []; $docLines = explode(PHP_EOL, $docComment); $cleanLines = $this->cleanDocLines($docLines); + $foundFirstAt = false; foreach ($cleanLines as $idx => $line) { if (true === str_starts_with($line, '@')) { + $foundFirstAt = true; $nextSpace = strpos($line, ' '); $doc[substr($line, 1, $nextSpace)] = substr($line, $nextSpace + 1); unset($cleanLines[$idx]); + } elseif (true === $foundFirstAt) { + //ignore other stuff for now + //TODO: Improve multiline @ doc comment + unset($cleanLines[$idx]); } } @@ -29,6 +35,10 @@ private function cleanDocLines(array $docLines): array { foreach ($docLines as $idx => $docLine) { $docLines[$idx] = $this->cleanDocLine($docLine); + + if ('' === $docLines[$idx]) { + unset($docLines[$idx]); + } } return $docLines; From 2f09390cb3e542d0c71e7bb65abad5d353c49723 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Wed, 15 Dec 2021 15:27:14 +0100 Subject: [PATCH 05/27] save work --- src/Parser/ClassParser.php | 23 +++++++++++++++++++++++ src/Parser/ClassParserInterface.php | 5 +++++ src/Parser/DocCommentParser.php | 13 ++++++++----- src/PhpClass/PhpClassProperty.php | 8 ++++---- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index b5fc44c..239e8d7 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -8,6 +8,8 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Property; +use PhpParser\Node\Stmt\Use_; +use PhpParser\Node\Stmt\UseUse; use PhpParser\ParserFactory; use PhpParser\Parser; @@ -54,6 +56,27 @@ public function getClassName(): ?string return null; } + public function getUsedClasses(): array + { + $usedClasses = []; + + foreach ($this->statements as $statement) { + if ($statement instanceof Namespace_) { + foreach ($statement->stmts as $nStatement) { + if ($nStatement instanceof Use_) { + /** @var UseUse $use */ + foreach ($nStatement->uses as $use) { + $className = $use->name->parts[array_key_last($use->name->parts)]; + $usedClasses[$className] = implode('\\', $use->name->parts); + } + } + } + } + } + + return $usedClasses; + } + /** * @return string */ diff --git a/src/Parser/ClassParserInterface.php b/src/Parser/ClassParserInterface.php index 5fc6a13..7146be5 100644 --- a/src/Parser/ClassParserInterface.php +++ b/src/Parser/ClassParserInterface.php @@ -16,4 +16,9 @@ public function getNamespace(): ?string; * @return PhpClassProperty[] */ public function getProperties(): array; + + /** + * @return array + */ + public function getUsedClasses(): array; } \ No newline at end of file diff --git a/src/Parser/DocCommentParser.php b/src/Parser/DocCommentParser.php index e1fc28a..6d689e9 100644 --- a/src/Parser/DocCommentParser.php +++ b/src/Parser/DocCommentParser.php @@ -17,7 +17,7 @@ public function parseDoc(string $docComment): array if (true === str_starts_with($line, '@')) { $foundFirstAt = true; $nextSpace = strpos($line, ' '); - $doc[substr($line, 1, $nextSpace)] = substr($line, $nextSpace + 1); + $doc[substr($line, 1, $nextSpace -1 )] = substr($line, $nextSpace + 1); unset($cleanLines[$idx]); } elseif (true === $foundFirstAt) { //ignore other stuff for now @@ -27,7 +27,7 @@ public function parseDoc(string $docComment): array } $doc[self::DOC_DESCRIPTION] = implode(' ', $cleanLines); - + return $doc; } @@ -48,16 +48,19 @@ private function cleanDocLine(string $docLine): string { $trimmedString = ltrim(rtrim($docLine)); - if (true === str_starts_with($docLine, '/**')) { + if (true === str_starts_with($trimmedString, '/**')) { $trimmedString = substr($trimmedString, 3); + $trimmedString = ltrim($trimmedString); } - if (true === str_ends_with($docLine, '*/')) { + if (true === str_ends_with($trimmedString, '*/')) { $trimmedString = substr($trimmedString, 0, strlen($trimmedString) - 2); + $trimmedString = rtrim($trimmedString); } - if (true === str_starts_with($docLine, '*')) { + if (true === str_starts_with($trimmedString, '*')) { $trimmedString = substr($trimmedString, 1); + $trimmedString = ltrim($trimmedString); } return ltrim(rtrim($trimmedString)); diff --git a/src/PhpClass/PhpClassProperty.php b/src/PhpClass/PhpClassProperty.php index 7f4d076..89e0807 100644 --- a/src/PhpClass/PhpClassProperty.php +++ b/src/PhpClass/PhpClassProperty.php @@ -16,16 +16,16 @@ class PhpClassProperty implements PhpClassPropertyInterface private string $propertyType; /** - * @param mixed $propertyDefault - * @param string $propertyDoc - * @param string $propertyLogicalType * @param string $propertyName * @param string $propertyType + * @param null|mixed $propertyDefault + * @param null|string $propertyDoc + * @param null|string $propertyLogicalType */ public function __construct( string $propertyName, string $propertyType, - mixed $propertyDefault = self::NO_DEFAULT, + $propertyDefault = self::NO_DEFAULT, ?string $propertyDoc = null, ?string $propertyLogicalType = null ) { From dc292dc773114d5211356fcd5e6346556fc4db3a Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Wed, 15 Dec 2021 15:48:07 +0100 Subject: [PATCH 06/27] save work --- src/Parser/ClassParser.php | 54 ++++++++++++++++++++++++++++- src/Parser/ClassParserInterface.php | 2 ++ src/Parser/DocCommentParser.php | 2 +- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index 239e8d7..dcc930c 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -12,6 +12,8 @@ use PhpParser\Node\Stmt\UseUse; use PhpParser\ParserFactory; use PhpParser\Parser; +use ReflectionClass; +use ReflectionException; class ClassParser implements ClassParserInterface { @@ -56,6 +58,30 @@ public function getClassName(): ?string return null; } + /** + * @return string|null + */ + public function getParentClassName(): ?string + { + if (null === $this->statements) { + return null; + } + + foreach ($this->statements as $statement) { + if ($statement instanceof Namespace_) { + foreach ($statement->stmts as $nsStatement) { + if ($nsStatement instanceof Class_) { + if (null !== $nsStatement->extends) { + return implode('\\', $nsStatement->extends->parts); + } + } + } + } + } + + return null; + } + public function getUsedClasses(): array { $usedClasses = []; @@ -99,10 +125,23 @@ public function getNamespace(): ?string * @return PhpClassProperty[] */ public function getProperties(): array + { + $properties = $this->getClassProperties($this->statements); + + $parentStatements = $this->getParentClassStatements(); + + if (true === is_array($parentStatements)) { + $properties = array_merge($properties, $this->getClassProperties($parentStatements)); + } + + return $properties; + } + + private function getClassProperties(array $statements): array { $properties = []; - foreach ($this->statements as $statement) { + foreach ($statements as $statement) { if ($statement instanceof Namespace_) { foreach ($statement->stmts as $nsStatement) { if ($nsStatement instanceof Class_) { @@ -118,4 +157,17 @@ public function getProperties(): array return $properties; } + + /** + * @return array + * @throws ReflectionException + */ + private function getParentClassStatements(): ?array + { + $usedClasses = $this->getUsedClasses(); + $rc = new ReflectionClass($usedClasses[$this->getParentClassName()]); + $filename = $rc->getFileName(); + + return $this->parser->parse(file_get_contents($filename)); + } } \ No newline at end of file diff --git a/src/Parser/ClassParserInterface.php b/src/Parser/ClassParserInterface.php index 7146be5..879e6d7 100644 --- a/src/Parser/ClassParserInterface.php +++ b/src/Parser/ClassParserInterface.php @@ -21,4 +21,6 @@ public function getProperties(): array; * @return array */ public function getUsedClasses(): array; + + public function getParentClassName(): ?string; } \ No newline at end of file diff --git a/src/Parser/DocCommentParser.php b/src/Parser/DocCommentParser.php index 6d689e9..e3abc91 100644 --- a/src/Parser/DocCommentParser.php +++ b/src/Parser/DocCommentParser.php @@ -27,7 +27,7 @@ public function parseDoc(string $docComment): array } $doc[self::DOC_DESCRIPTION] = implode(' ', $cleanLines); - + return $doc; } From 5fe4d798fb429f321ce799049d6ac7741ea91145 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Wed, 15 Dec 2021 15:52:12 +0100 Subject: [PATCH 07/27] save work --- src/Parser/ClassPropertyParser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Parser/ClassPropertyParser.php b/src/Parser/ClassPropertyParser.php index 9750ac0..75b85f5 100644 --- a/src/Parser/ClassPropertyParser.php +++ b/src/Parser/ClassPropertyParser.php @@ -110,9 +110,9 @@ private function getTypeFromDocComment(array $docComments): ?string return $docComments['avro-type'] ?? null; } - private function getDefaultFromDocComment(array $docComments): ?string + private function getDefaultFromDocComment(array $docComments): string { - return $docComments['avro-default'] ?? null; + return $docComments['avro-default'] ?? PhpClassPropertyInterface::NO_DEFAULT; } private function getLogicalTypeFromDocComment(array $docComments): ?string From 4297aa52fd16c9fee1ddd0123c196068d78e9685 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Wed, 15 Dec 2021 22:55:16 +0100 Subject: [PATCH 08/27] save work --- composer.json | 3 +- src/AppContainer.php | 36 ++++++++++ src/Command/SchemaGenerateCommand.php | 28 +++++--- src/Command/SubSchemaMergeCommand.php | 29 ++++++-- src/Converter/PhpClassConverter.php | 71 +++++++++++++++++++ src/Converter/PhpClassConverterInterface.php | 12 ++++ src/Generator/SchemaGenerator.php | 56 ++++++++++----- src/Generator/SchemaGeneratorInterface.php | 14 +++- src/Merger/SchemaMerger.php | 40 +++++++---- src/Merger/SchemaMergerInterface.php | 14 +++- src/Parser/ClassParser.php | 8 +-- src/Parser/ClassParserInterface.php | 2 + src/Registry/ClassRegistry.php | 24 +++---- .../CommandServiceProvider.php | 37 ++++++++++ .../ConverterServiceProvider.php | 23 ++++++ .../GeneratorServiceProvider.php | 21 ++++++ src/ServiceProvider/MergerServiceProvider.php | 21 ++++++ src/ServiceProvider/ParserServiceProvider.php | 43 +++++++++++ .../RegistryServiceProvider.php | 28 ++++++++ src/console | 9 +-- 20 files changed, 447 insertions(+), 72 deletions(-) create mode 100644 src/AppContainer.php create mode 100644 src/Converter/PhpClassConverter.php create mode 100644 src/Converter/PhpClassConverterInterface.php create mode 100644 src/ServiceProvider/CommandServiceProvider.php create mode 100644 src/ServiceProvider/ConverterServiceProvider.php create mode 100644 src/ServiceProvider/GeneratorServiceProvider.php create mode 100644 src/ServiceProvider/MergerServiceProvider.php create mode 100644 src/ServiceProvider/ParserServiceProvider.php create mode 100644 src/ServiceProvider/RegistryServiceProvider.php diff --git a/composer.json b/composer.json index 7948180..8eb2900 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "ext-json": "*", "flix-tech/avro-php": "^3.0|^4.0", "symfony/console": "^4.3|^5.1", - "nikic/php-parser": "^4.13" + "nikic/php-parser": "^4.13", + "pimple/pimple": "^3.5" }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.15", diff --git a/src/AppContainer.php b/src/AppContainer.php new file mode 100644 index 0000000..313300f --- /dev/null +++ b/src/AppContainer.php @@ -0,0 +1,36 @@ +register(new GeneratorServiceProvider()) + ->register(new MergerServiceProvider()) + ->register(new ParserServiceProvider()) + ->register(new ConverterServiceProvider()) + ->register(new RegistryServiceProvider()) + ->register(new CommandServiceProvider()); + + + return $container; + } +} \ No newline at end of file diff --git a/src/Command/SchemaGenerateCommand.php b/src/Command/SchemaGenerateCommand.php index bed3639..b88541d 100644 --- a/src/Command/SchemaGenerateCommand.php +++ b/src/Command/SchemaGenerateCommand.php @@ -5,7 +5,9 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Command; use PhpKafka\PhpAvroSchemaGenerator\Generator\SchemaGenerator; +use PhpKafka\PhpAvroSchemaGenerator\Generator\SchemaGeneratorInterface; use PhpKafka\PhpAvroSchemaGenerator\Registry\ClassRegistry; +use PhpKafka\PhpAvroSchemaGenerator\Registry\ClassRegistryInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -13,6 +15,19 @@ class SchemaGenerateCommand extends Command { + private SchemaGeneratorInterface $generator; + private ClassRegistryInterface $classRegistry; + + public function __construct( + ClassRegistryInterface $classRegistry, + SchemaGeneratorInterface $generator, + string $name = null + ) { + $this->classRegistry = $classRegistry; + $this->generator = $generator; + parent::__construct($name); + } + protected function configure(): void { $this @@ -36,15 +51,12 @@ public function execute(InputInterface $input, OutputInterface $output): int $classDirectory = $this->getPath($classDirectoryArg); $outputDirectory = $this->getPath($outputDirectoryArg); - $registry = (new ClassRegistry()) - ->addClassDirectory($classDirectory) - ->load(); - - $generator = new SchemaGenerator($registry, $outputDirectory); - - $schemas = $generator->generate(); + $registry = $this->classRegistry->addClassDirectory($classDirectory)->load(); + $this->generator->setOutputDirectory($outputDirectory); + $this->generator->setClassRegistry($registry); - $result = $generator->exportSchemas($schemas); + $schemas = $this->generator->generate(); + $result = $this->generator->exportSchemas($schemas); // retrieve the argument value using getArgument() $output->writeln(sprintf('Generated %d schema files', $result)); diff --git a/src/Command/SubSchemaMergeCommand.php b/src/Command/SubSchemaMergeCommand.php index d518efa..f318984 100644 --- a/src/Command/SubSchemaMergeCommand.php +++ b/src/Command/SubSchemaMergeCommand.php @@ -4,12 +4,14 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Command; +use PhpKafka\PhpAvroSchemaGenerator\Merger\SchemaMergerInterface; use PhpKafka\PhpAvroSchemaGenerator\Optimizer\FieldOrderOptimizer; use PhpKafka\PhpAvroSchemaGenerator\Optimizer\FullNameOptimizer; use PhpKafka\PhpAvroSchemaGenerator\Optimizer\OptimizerInterface; use PhpKafka\PhpAvroSchemaGenerator\Optimizer\PrimitiveSchemaOptimizer; use PhpKafka\PhpAvroSchemaGenerator\Registry\SchemaRegistry; use PhpKafka\PhpAvroSchemaGenerator\Merger\SchemaMerger; +use PhpKafka\PhpAvroSchemaGenerator\Registry\SchemaRegistryInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -18,12 +20,26 @@ class SubSchemaMergeCommand extends Command { + private SchemaRegistryInterface $schemaRegistry; + private SchemaMergerInterface $schemaMerger; + /** @var string[] */ - protected $optimizerOptionMapping = [ + protected array $optimizerOptionMapping = [ 'optimizeFieldOrder' => FieldOrderOptimizer::class, 'optimizeFullNames' => FullNameOptimizer::class, 'optimizePrimitiveSchemas' => PrimitiveSchemaOptimizer::class, ]; + + public function __construct( + SchemaMergerInterface $schemaMerger, + SchemaRegistryInterface $schemaRegistry, + string $name = null + ) { + $this->schemaMerger = $schemaMerger; + $this->schemaRegistry = $schemaRegistry; + parent::__construct($name); + } + protected function configure(): void { $this @@ -71,20 +87,19 @@ public function execute(InputInterface $input, OutputInterface $output): int $templateDirectory = $this->getPath($templateDirectoryArg); $outputDirectory = $this->getPath($outputDirectoryArg); - $registry = (new SchemaRegistry()) - ->addSchemaTemplateDirectory($templateDirectory) - ->load(); + $registry = $this->schemaRegistry->addSchemaTemplateDirectory($templateDirectory)->load(); - $merger = new SchemaMerger($registry, $outputDirectory); + $this->schemaMerger->setSchemaRegistry($registry); + $this->schemaMerger->setOutputDirectory($outputDirectory); /** @var OptimizerInterface $optimizerClass */ foreach ($this->optimizerOptionMapping as $optionName => $optimizerClass) { if (true === (bool) $input->getOption($optionName)) { - $merger->addOptimizer(new $optimizerClass()); + $this->schemaMerger->addOptimizer(new $optimizerClass()); } } - $result = $merger->merge( + $result = $this->schemaMerger->merge( (bool) $input->getOption('prefixWithNamespace'), (bool) $input->getOption('useFilenameAsSchemaName') ); diff --git a/src/Converter/PhpClassConverter.php b/src/Converter/PhpClassConverter.php new file mode 100644 index 0000000..4d94014 --- /dev/null +++ b/src/Converter/PhpClassConverter.php @@ -0,0 +1,71 @@ +parser = $parser; + } + + + public function convert(string $phpClass): ?PhpClassInterface + { + $this->parser->setCode($phpClass); + + if (null === $this->parser->getClassName()) { + return null; + } + + $convertedProperties = $this->getConvertedProperties($this->parser->getProperties()); + + return new PhpClass( + $this->parser->getClassName(), + $this->parser->getNamespace(), + $phpClass, + $convertedProperties + ); + } + + /** + * @param PhpClassPropertyInterface[] $properties + * @return array + */ + private function getConvertedProperties(array $properties): array + { + $convertedProperties = []; + foreach ($properties as $property) { + $convertedType = $this->getConvertedType($property->getPropertyType()); + $convertedProperties[] = new PhpClassProperty( + $property->getPropertyName(), + $convertedType, + $property->getPropertyDefault(), + $property->getPropertyDoc(), + $property->getPropertyLogicalType() + ); + } + + return $convertedProperties; + } + + private function getConvertedType(string $type): string + { + + return $type; + } +} \ No newline at end of file diff --git a/src/Converter/PhpClassConverterInterface.php b/src/Converter/PhpClassConverterInterface.php new file mode 100644 index 0000000..ee37b40 --- /dev/null +++ b/src/Converter/PhpClassConverterInterface.php @@ -0,0 +1,12 @@ + 1, 'object' => 1, 'callable' => 1, @@ -22,30 +23,34 @@ final class SchemaGenerator implements SchemaGeneratorInterface 'mixed' => 1 ]; - /** - * @var string - */ - private $outputDirectory; + private string $outputDirectory; /** * @var ClassRegistryInterface */ - private $classRegistry; + private ClassRegistryInterface $classRegistry; - public function __construct(ClassRegistryInterface $classRegistry, string $outputDirectory = '/tmp') + public function __construct(string $outputDirectory = '/tmp') { - $this->classRegistry = $classRegistry; $this->outputDirectory = $outputDirectory; } /** - * @return ClassRegistryInterface + * @return ClassRegistryInterface|null */ - public function getClassRegistry(): ClassRegistryInterface + public function getClassRegistry(): ?ClassRegistryInterface { return $this->classRegistry; } + /** + * @return ClassRegistryInterface + */ + public function setClassRegistry(ClassRegistryInterface $classRegistry): void + { + $this->classRegistry = $classRegistry; + } + /** * @return string */ @@ -54,6 +59,14 @@ public function getOutputDirectory(): string return $this->outputDirectory; } + /** + * @return string + */ + public function setOutputDirectory(string $outputDirectory): void + { + $this->outputDirectory = $outputDirectory; + } + /** * @return array */ @@ -61,6 +74,10 @@ public function generate(): array { $schemas = []; + if (null === $this->getClassRegistry()) { + throw new RuntimeException('Please set a ClassRegistry for the generator'); + } + /** @var PhpClassInterface $class */ foreach ($this->getClassRegistry()->getClasses() as $class) { $schema = []; @@ -76,13 +93,18 @@ public function generate(): array } $field = ['name' => $property->getPropertyName()]; - if ('array' === $property->getPropertyType()) { - $field['type'] = [ - 'type' => $property->getPropertyType(), - 'items' => $this->convertNamespace($property->getPropertyArrayType() ?? 'string') - ]; - } else { - $field['type'] = $this->convertNamespace($property->getPropertyType()); + $field['type'] = $property->getPropertyType(); + + if (PhpClassPropertyInterface::NO_DEFAULT !== $property->getPropertyDefault()) { + $field['default'] = $property->getPropertyDefault(); + } + + if (null !== $property->getPropertyDoc() && '' !== $property->getPropertyDoc()) { + $field['doc'] = $property->getPropertyDoc(); + } + + if (null !== $property->getPropertyLogicalType()) { + $field['logicalType'] = $property->getPropertyLogicalType(); } $schema['fields'][] = $field; diff --git a/src/Generator/SchemaGeneratorInterface.php b/src/Generator/SchemaGeneratorInterface.php index 08f59d1..92a0a30 100644 --- a/src/Generator/SchemaGeneratorInterface.php +++ b/src/Generator/SchemaGeneratorInterface.php @@ -9,15 +9,25 @@ interface SchemaGeneratorInterface { /** - * @return ClassRegistryInterface + * @return ClassRegistryInterface|null */ - public function getClassRegistry(): ClassRegistryInterface; + public function getClassRegistry(): ?ClassRegistryInterface; + + /** + * @param ClassRegistryInterface $classRegistry + */ + public function setClassRegistry(ClassRegistryInterface $classRegistry): void; /** * @return string */ public function getOutputDirectory(): string; + /** + * @param string $outputDirectory + */ + public function setOutputDirectory(string $outputDirectory): void; + /** * @return array */ diff --git a/src/Merger/SchemaMerger.php b/src/Merger/SchemaMerger.php index bb83e84..3c33b27 100644 --- a/src/Merger/SchemaMerger.php +++ b/src/Merger/SchemaMerger.php @@ -11,38 +11,40 @@ use PhpKafka\PhpAvroSchemaGenerator\Optimizer\OptimizerInterface; use PhpKafka\PhpAvroSchemaGenerator\Registry\SchemaRegistryInterface; use PhpKafka\PhpAvroSchemaGenerator\Schema\SchemaTemplateInterface; +use RuntimeException; final class SchemaMerger implements SchemaMergerInterface { - /** - * @var string - */ - private $outputDirectory; + private string $outputDirectory; - /** - * @var SchemaRegistryInterface - */ - private $schemaRegistry; + private ?SchemaRegistryInterface $schemaRegistry; /** * @var OptimizerInterface[] */ - private $optimizers = []; + private array $optimizers = []; - public function __construct(SchemaRegistryInterface $schemaRegistry, string $outputDirectory = '/tmp') + public function __construct(string $outputDirectory = '/tmp') { - $this->schemaRegistry = $schemaRegistry; $this->outputDirectory = $outputDirectory; } /** - * @return SchemaRegistryInterface + * @return SchemaRegistryInterface|null */ - public function getSchemaRegistry(): SchemaRegistryInterface + public function getSchemaRegistry(): ?SchemaRegistryInterface { return $this->schemaRegistry; } + /** + * @return SchemaRegistryInterface + */ + public function setSchemaRegistry(SchemaRegistryInterface $schemaRegistry): void + { + $this->schemaRegistry = $schemaRegistry; + } + /** * @return string */ @@ -51,6 +53,14 @@ public function getOutputDirectory(): string return $this->outputDirectory; } + /** + * @param string $outputDirectory + */ + public function setOutputDirectory(string $outputDirectory): void + { + $this->outputDirectory = $outputDirectory; + } + /** * @param SchemaTemplateInterface $rootSchemaTemplate * @return SchemaTemplateInterface @@ -120,6 +130,10 @@ public function merge( $mergedFiles = 0; $registry = $this->getSchemaRegistry(); + if (null === $registry) { + throw new RuntimeException('Please set a SchemaRegistery for the merger'); + } + /** @var SchemaTemplateInterface $rootSchemaTemplate */ foreach ($registry->getRootSchemas() as $rootSchemaTemplate) { try { diff --git a/src/Merger/SchemaMergerInterface.php b/src/Merger/SchemaMergerInterface.php index ea847a3..f986b93 100644 --- a/src/Merger/SchemaMergerInterface.php +++ b/src/Merger/SchemaMergerInterface.php @@ -11,15 +11,25 @@ interface SchemaMergerInterface { /** - * @return SchemaRegistryInterface + * @return SchemaRegistryInterface|null */ - public function getSchemaRegistry(): SchemaRegistryInterface; + public function getSchemaRegistry(): ?SchemaRegistryInterface; + + /** + * @param SchemaRegistryInterface $schemaRegistry + */ + public function setSchemaRegistry(SchemaRegistryInterface $schemaRegistry): void; /** * @return string */ public function getOutputDirectory(): string; + /** + * @param string $outputDirectory + */ + public function setOutputDirectory(string $outputDirectory): void; + /** * @param SchemaTemplateInterface $rootSchemaTemplate * @return SchemaTemplateInterface diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index dcc930c..4007943 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -17,20 +17,18 @@ class ClassParser implements ClassParserInterface { - private ParserFactory $parserFactory; private ClassPropertyParserInterface $propertyParser; private Parser $parser; private string $code; private array $statements; - public function __construct(ParserFactory $parserFactory, ClassPropertyParserInterface $propertyParser) + public function __construct(Parser $parser, ClassPropertyParserInterface $propertyParser) { - $this->parserFactory = $parserFactory; - $this->parser = $parserFactory->create(ParserFactory::PREFER_PHP7); + $this->parser = $parser; $this->propertyParser = $propertyParser; } - public function setCode(string $code) + public function setCode(string $code): void { $this->code = $code; $this->statements = $this->parser->parse($code); diff --git a/src/Parser/ClassParserInterface.php b/src/Parser/ClassParserInterface.php index 879e6d7..2de0477 100644 --- a/src/Parser/ClassParserInterface.php +++ b/src/Parser/ClassParserInterface.php @@ -23,4 +23,6 @@ public function getProperties(): array; public function getUsedClasses(): array; public function getParentClassName(): ?string; + + public function setCode(string $code): void; } \ No newline at end of file diff --git a/src/Registry/ClassRegistry.php b/src/Registry/ClassRegistry.php index c4d9f50..1f1ee4a 100644 --- a/src/Registry/ClassRegistry.php +++ b/src/Registry/ClassRegistry.php @@ -5,6 +5,7 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Registry; use FilesystemIterator; +use PhpKafka\PhpAvroSchemaGenerator\Converter\PhpClassConverterInterface; use PhpKafka\PhpAvroSchemaGenerator\Exception\ClassRegistryException; use PhpKafka\PhpAvroSchemaGenerator\Parser\TokenParser; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClass; @@ -24,6 +25,13 @@ final class ClassRegistry implements ClassRegistryInterface */ protected $classes = []; + private PhpClassConverterInterface $classConverter; + + public function __construct(PhpClassConverterInterface $classConverter) + { + $this->classConverter = $classConverter; + } + /** * @param string $classDirectory * @return ClassRegistryInterface @@ -95,20 +103,10 @@ private function registerClassFile(SplFileInfo $fileInfo): void ) ); } + $convertedClass = $this->classConverter->convert($fileContent); - $parser = new TokenParser($fileContent); - - if (null === $parser->getClassName()) { - return; + if (null !== $convertedClass) { + $this->classes[] = $convertedClass; } - - /** @var class-string $className */ - $properties = $parser->getProperties(); - $this->classes[] = new PhpClass( - $parser->getClassName(), - $parser->getNamespace(), - $fileContent, - $properties - ); } } diff --git a/src/ServiceProvider/CommandServiceProvider.php b/src/ServiceProvider/CommandServiceProvider.php new file mode 100644 index 0000000..affe3a0 --- /dev/null +++ b/src/ServiceProvider/CommandServiceProvider.php @@ -0,0 +1,37 @@ +create(ParserFactory::PREFER_PHP7); + }; + + $container[DocCommentParserInterface::class] = static function (): DocCommentParserInterface { + return new DocCommentParser(); + }; + + $container[ClassPropertyParserInterface::class] = + static function (Container $container): ClassPropertyParserInterface { + return new ClassPropertyParser($container[DocCommentParserInterface::class]); + }; + + $container[ClassParserInterface::class] = static function (Container $container): ClassParserInterface { + return new ClassParser($container[Parser::class], $container[ClassPropertyParserInterface::class]); + }; + } +} \ No newline at end of file diff --git a/src/ServiceProvider/RegistryServiceProvider.php b/src/ServiceProvider/RegistryServiceProvider.php new file mode 100644 index 0000000..e7a507e --- /dev/null +++ b/src/ServiceProvider/RegistryServiceProvider.php @@ -0,0 +1,28 @@ +add(new SchemaGenerateCommand()); -$application->add(new SubSchemaMergeCommand()); +$container = AppContainer::init(); +$application = new Application(); +$application->addCommands($container['console.commands']); $application->run(); From 0ebe058aee0ab47bc2daf90f71ac634807de7844 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Wed, 15 Dec 2021 22:58:59 +0100 Subject: [PATCH 09/27] update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba64bac..ce4f1be 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Avro schema generator for PHP [![Actions Status](https://github.com/php-kafka/php-avro-schema-generator/workflows/CI/badge.svg)](https://github.com/php-kafka/php-avro-schema-generator/workflows/CI/badge.svg) [![Maintainability](https://api.codeclimate.com/v1/badges/41aecf21566d7e9bfb69/maintainability)](https://codeclimate.com/github/php-kafka/php-avro-schema-generator/maintainability) -[![Test Coverage](https://api.codeclimate.com/v1/badges/41aecf21566d7e9bfb69/test_coverage)](https://codeclimate.com/github/php-kafka/php-avro-schema-generator/test_coverage) +[![Test Coverage](https://api.codeclimate.com/v1/badges/41aecf21566d7e9bfb69/test_coverage)](https://codeclimate.com/github/php-kafka/php-avro-schema-generator/test_coverage) +![Supported PHP versions: 7.4 .. 8.x](https://img.shields.io/badge/php-7.4%20..%208.x-blue.svg) [![Latest Stable Version](https://poser.pugx.org/php-kafka/php-avro-schema-generator/v/stable)](https://packagist.org/packages/php-kafka/php-avro-schema-generator) ## Installation From fcb1168f80adf5b94a940cb342eea4a6dba40882 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Wed, 15 Dec 2021 23:44:25 +0100 Subject: [PATCH 10/27] save work --- src/AppContainer.php | 3 +- src/Converter/PhpClassConverter.php | 5 +- src/Converter/PhpClassConverterInterface.php | 2 +- src/Generator/SchemaGenerator.php | 22 ++++++-- src/Merger/SchemaMerger.php | 8 ++- src/Merger/SchemaMergerInterface.php | 4 +- src/Parser/ClassParser.php | 51 +++++++++++++---- src/Parser/ClassParserInterface.php | 2 +- src/Parser/ClassPropertyParser.php | 56 ++++++++++++++++--- src/Parser/ClassPropertyParserInterface.php | 7 ++- src/Parser/DocCommentParser.php | 12 +++- src/Parser/DocCommentParserInterface.php | 6 +- src/PhpClass/PhpClass.php | 31 ++++------ src/PhpClass/PhpClassInterface.php | 4 +- src/Registry/ClassRegistry.php | 5 +- src/Registry/ClassRegistryInterface.php | 4 +- .../CommandServiceProvider.php | 5 +- .../ConverterServiceProvider.php | 5 +- .../GeneratorServiceProvider.php | 7 +-- src/ServiceProvider/MergerServiceProvider.php | 5 +- src/ServiceProvider/ParserServiceProvider.php | 4 +- .../RegistryServiceProvider.php | 5 +- 22 files changed, 169 insertions(+), 84 deletions(-) diff --git a/src/AppContainer.php b/src/AppContainer.php index 313300f..db8ecb4 100644 --- a/src/AppContainer.php +++ b/src/AppContainer.php @@ -15,7 +15,6 @@ class AppContainer { /** - * @param string $env * @return Container */ public static function init(): Container @@ -33,4 +32,4 @@ public static function init(): Container return $container; } -} \ No newline at end of file +} diff --git a/src/Converter/PhpClassConverter.php b/src/Converter/PhpClassConverter.php index 4d94014..9cb627c 100644 --- a/src/Converter/PhpClassConverter.php +++ b/src/Converter/PhpClassConverter.php @@ -16,7 +16,6 @@ class PhpClassConverter implements PhpClassConverterInterface /** * @param ClassParserInterface $parser - * @param string $phpClass */ public function __construct(ClassParserInterface $parser) { @@ -44,7 +43,7 @@ public function convert(string $phpClass): ?PhpClassInterface /** * @param PhpClassPropertyInterface[] $properties - * @return array + * @return PhpClassPropertyInterface[] */ private function getConvertedProperties(array $properties): array { @@ -68,4 +67,4 @@ private function getConvertedType(string $type): string return $type; } -} \ No newline at end of file +} diff --git a/src/Converter/PhpClassConverterInterface.php b/src/Converter/PhpClassConverterInterface.php index ee37b40..8f8730d 100644 --- a/src/Converter/PhpClassConverterInterface.php +++ b/src/Converter/PhpClassConverterInterface.php @@ -9,4 +9,4 @@ interface PhpClassConverterInterface { public function convert(string $phpClass): ?PhpClassInterface; -} \ No newline at end of file +} diff --git a/src/Generator/SchemaGenerator.php b/src/Generator/SchemaGenerator.php index 2b84368..e7f75cc 100644 --- a/src/Generator/SchemaGenerator.php +++ b/src/Generator/SchemaGenerator.php @@ -44,7 +44,7 @@ public function getClassRegistry(): ?ClassRegistryInterface } /** - * @return ClassRegistryInterface + * @param ClassRegistryInterface $classRegistry */ public function setClassRegistry(ClassRegistryInterface $classRegistry): void { @@ -60,7 +60,7 @@ public function getOutputDirectory(): string } /** - * @return string + * @param string $outputDirectory */ public function setOutputDirectory(string $outputDirectory): void { @@ -110,7 +110,13 @@ public function generate(): array $schema['fields'][] = $field; } - $schemas[$schema['namespace'] . '.' . $schema['name']] = json_encode($schema); + $namespace = $schema['namespace'] . '.' . $schema['name']; + + if (null === $schema['namespace']) { + $namespace = $schema['name']; + } + + $schemas[$namespace] = json_encode($schema); } return $schemas; @@ -143,11 +149,15 @@ private function getSchemaFilename(string $schemaName): string } /** - * @param string $namespace - * @return string + * @param string|null $namespace + * @return string|null */ - private function convertNamespace(string $namespace): string + private function convertNamespace(?string $namespace): ?string { + if (null === $namespace) { + return null; + } + return str_replace('\\', '.', $namespace); } } diff --git a/src/Merger/SchemaMerger.php b/src/Merger/SchemaMerger.php index 3c33b27..9661280 100644 --- a/src/Merger/SchemaMerger.php +++ b/src/Merger/SchemaMerger.php @@ -38,7 +38,7 @@ public function getSchemaRegistry(): ?SchemaRegistryInterface } /** - * @return SchemaRegistryInterface + * @param SchemaRegistryInterface $schemaRegistry */ public function setSchemaRegistry(SchemaRegistryInterface $schemaRegistry): void { @@ -69,6 +69,10 @@ public function setOutputDirectory(string $outputDirectory): void */ public function getResolvedSchemaTemplate(SchemaTemplateInterface $rootSchemaTemplate): SchemaTemplateInterface { + if (null === $this->getSchemaRegistry()) { + throw new RuntimeException('Please set a SchemaRegistery for the merger'); + } + $rootDefinition = $rootSchemaTemplate->getSchemaDefinition(); do { @@ -82,7 +86,7 @@ public function getResolvedSchemaTemplate(SchemaTemplateInterface $rootSchemaTem } $exceptionThrown = true; $schemaId = $this->getSchemaIdFromExceptionMessage($e->getMessage()); - $embeddedTemplate = $this->schemaRegistry->getSchemaById($schemaId); + $embeddedTemplate = $this->getSchemaRegistry()->getSchemaById($schemaId); if (null === $embeddedTemplate) { throw new SchemaMergerException( sprintf(SchemaMergerException::UNKNOWN_SCHEMA_TYPE_EXCEPTION_MESSAGE, $schemaId) diff --git a/src/Merger/SchemaMergerInterface.php b/src/Merger/SchemaMergerInterface.php index f986b93..33f7e5a 100644 --- a/src/Merger/SchemaMergerInterface.php +++ b/src/Merger/SchemaMergerInterface.php @@ -37,9 +37,11 @@ public function setOutputDirectory(string $outputDirectory): void; public function getResolvedSchemaTemplate(SchemaTemplateInterface $rootSchemaTemplate): SchemaTemplateInterface; /** + * @param bool $prefixWithNamespace + * @param bool $useTemplateName * @return int */ - public function merge(): int; + public function merge(bool $prefixWithNamespace = false, bool $useTemplateName = false): int; /** * @param SchemaTemplateInterface $rootRootSchemaTemplate diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index 4007943..6167b25 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -5,12 +5,15 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Parser; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassProperty; +use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyInterface; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Use_; use PhpParser\Node\Stmt\UseUse; -use PhpParser\ParserFactory; use PhpParser\Parser; use ReflectionClass; use ReflectionException; @@ -19,10 +22,11 @@ class ClassParser implements ClassParserInterface { private ClassPropertyParserInterface $propertyParser; private Parser $parser; - private string $code; - private array $statements; - public function __construct(Parser $parser, ClassPropertyParserInterface $propertyParser) + /** @var Stmt[]|null */ + private ?array $statements; + + public function __construct(Parser $parser, ClassPropertyParserInterface $propertyParser) { $this->parser = $parser; $this->propertyParser = $propertyParser; @@ -30,7 +34,6 @@ public function __construct(Parser $parser, ClassPropertyParserInterface $prope public function setCode(string $code): void { - $this->code = $code; $this->statements = $this->parser->parse($code); } @@ -47,7 +50,9 @@ public function getClassName(): ?string if ($statement instanceof Namespace_) { foreach ($statement->stmts as $nsStatement) { if ($nsStatement instanceof Class_) { - return $nsStatement->name->name; + if ($nsStatement->name instanceof Identifier) { + return $nsStatement->name->name; + } } } } @@ -84,6 +89,10 @@ public function getUsedClasses(): array { $usedClasses = []; + if (null === $this->statements) { + return $usedClasses; + } + foreach ($this->statements as $statement) { if ($statement instanceof Namespace_) { foreach ($statement->stmts as $nStatement) { @@ -112,7 +121,9 @@ public function getNamespace(): ?string foreach ($this->statements as $statement) { if ($statement instanceof Namespace_) { - return implode('\\', $statement->name->parts); + if ($statement->name instanceof Name) { + return implode('\\', $statement->name->parts); + } } } @@ -120,11 +131,11 @@ public function getNamespace(): ?string } /** - * @return PhpClassProperty[] + * @return PhpClassPropertyInterface[] */ public function getProperties(): array { - $properties = $this->getClassProperties($this->statements); + $properties = $this->getClassProperties($this->statements ?? []); $parentStatements = $this->getParentClassStatements(); @@ -135,6 +146,10 @@ public function getProperties(): array return $properties; } + /** + * @param Stmt[] $statements + * @return PhpClassPropertyInterface[] + */ private function getClassProperties(array $statements): array { $properties = []; @@ -157,15 +172,27 @@ private function getClassProperties(array $statements): array } /** - * @return array + * @return Stmt[]|null * @throws ReflectionException */ private function getParentClassStatements(): ?array { + /** @var class-string[] $usedClasses */ $usedClasses = $this->getUsedClasses(); + $rc = new ReflectionClass($usedClasses[$this->getParentClassName()]); $filename = $rc->getFileName(); - return $this->parser->parse(file_get_contents($filename)); + if (false === $filename) { + return []; + } + + $parentClass = file_get_contents($filename); + + if (false === $parentClass) { + return []; + } + + return $this->parser->parse($parentClass); } -} \ No newline at end of file +} diff --git a/src/Parser/ClassParserInterface.php b/src/Parser/ClassParserInterface.php index 2de0477..1d685d4 100644 --- a/src/Parser/ClassParserInterface.php +++ b/src/Parser/ClassParserInterface.php @@ -25,4 +25,4 @@ public function getUsedClasses(): array; public function getParentClassName(): ?string; public function setCode(string $code): void; -} \ No newline at end of file +} diff --git a/src/Parser/ClassPropertyParser.php b/src/Parser/ClassPropertyParser.php index 75b85f5..ebf1f6e 100644 --- a/src/Parser/ClassPropertyParser.php +++ b/src/Parser/ClassPropertyParser.php @@ -36,11 +36,18 @@ class ClassPropertyParser implements ClassPropertyParserInterface 'Collection' => 'array', ); + /** + * @param DocCommentParserInterface $docParser + */ public function __construct(DocCommentParserInterface $docParser) { $this->docParser = $docParser; } + /** + * @param Property|mixed $property + * @return PhpClassPropertyInterface + */ public function parseProperty($property): PhpClassPropertyInterface { if (false === $property instanceof Property) { @@ -58,7 +65,10 @@ public function parseProperty($property): PhpClassPropertyInterface ); } - + /** + * @param Property $property + * @return array + */ private function getPropertyAttributes(Property $property): array { $attributes = $this->getEmptyAttributesArray(); @@ -77,10 +87,16 @@ private function getPropertyAttributes(Property $property): array return $attributes; } - private function getPropertyName(Property $property) { + private function getPropertyName(Property $property): string + { return $property->props[0]->name->name; } + /** + * @param Property $property + * @param array $docComments + * @return string + */ private function getPropertyType(Property $property, array $docComments): string { if ($property->type instanceof Identifier) { @@ -90,7 +106,8 @@ private function getPropertyType(Property $property, array $docComments): string $separator = ''; /** @var Identifier $type */ foreach ($property->type->types as $type) { - $types .= $separator . $this->mappedTypes[$type->name] ?? $type->name; + $type = $this->mappedTypes[$type->name] ?? $type->name; + $types .= $separator . $type; $separator = ','; } @@ -100,31 +117,55 @@ private function getPropertyType(Property $property, array $docComments): string return $this->getDocCommentByType($docComments, 'var') ?? 'string'; } + /** + * @param array $docComments + * @return mixed + */ private function getDocCommentByType(array $docComments, string $type) { return $docComments[$type] ?? null; } + /** + * @param array $docComments + * @return string|null + */ private function getTypeFromDocComment(array $docComments): ?string { return $docComments['avro-type'] ?? null; } + /** + * @param array $docComments + * @return string + */ private function getDefaultFromDocComment(array $docComments): string { return $docComments['avro-default'] ?? PhpClassPropertyInterface::NO_DEFAULT; } + /** + * @param array $docComments + * @return string|null + */ private function getLogicalTypeFromDocComment(array $docComments): ?string { return $docComments['avro-logical-type'] ?? null; } + /** + * @param array $docComments + * @return string|null + */ private function getDocFromDocComment(array $docComments): ?string { return $docComments['avro-doc'] ?? $docComments[DocCommentParserInterface::DOC_DESCRIPTION] ?? null; } + /** + * @param Property $property + * @return array + */ private function getAllPropertyDocComments(Property $property): array { $docComments = []; @@ -133,15 +174,17 @@ private function getAllPropertyDocComments(Property $property): array if ('comments' === $attributeName) { /** @var Doc $comment */ foreach ($attributeValue as $comment) { - $docComments += $this->docParser->parseDoc($comment->getText()); + $docComments = array_merge($docComments, $this->docParser->parseDoc($comment->getText())); } } - } return $docComments; } + /** + * @return array + */ private function getEmptyAttributesArray(): array { return [ @@ -152,5 +195,4 @@ private function getEmptyAttributesArray(): array 'doc' => null ]; } - -} \ No newline at end of file +} diff --git a/src/Parser/ClassPropertyParserInterface.php b/src/Parser/ClassPropertyParserInterface.php index 646510c..f180109 100644 --- a/src/Parser/ClassPropertyParserInterface.php +++ b/src/Parser/ClassPropertyParserInterface.php @@ -5,8 +5,13 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Parser; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyInterface; +use PhpParser\Node\Stmt\Property; interface ClassPropertyParserInterface { + /** + * @param Property|mixed $property + * @return PhpClassPropertyInterface + */ public function parseProperty($property): PhpClassPropertyInterface; -} \ No newline at end of file +} diff --git a/src/Parser/DocCommentParser.php b/src/Parser/DocCommentParser.php index e3abc91..5652d02 100644 --- a/src/Parser/DocCommentParser.php +++ b/src/Parser/DocCommentParser.php @@ -6,6 +6,10 @@ class DocCommentParser implements DocCommentParserInterface { + /** + * @param string $docComment + * @return array + */ public function parseDoc(string $docComment): array { $doc = []; @@ -17,7 +21,7 @@ public function parseDoc(string $docComment): array if (true === str_starts_with($line, '@')) { $foundFirstAt = true; $nextSpace = strpos($line, ' '); - $doc[substr($line, 1, $nextSpace -1 )] = substr($line, $nextSpace + 1); + $doc[substr($line, 1, $nextSpace - 1)] = substr($line, $nextSpace + 1); unset($cleanLines[$idx]); } elseif (true === $foundFirstAt) { //ignore other stuff for now @@ -31,6 +35,10 @@ public function parseDoc(string $docComment): array return $doc; } + /** + * @param string[] $docLines + * @return string[] + */ private function cleanDocLines(array $docLines): array { foreach ($docLines as $idx => $docLine) { @@ -65,4 +73,4 @@ private function cleanDocLine(string $docLine): string return ltrim(rtrim($trimmedString)); } -} \ No newline at end of file +} diff --git a/src/Parser/DocCommentParserInterface.php b/src/Parser/DocCommentParserInterface.php index 8313a2b..ccbba2d 100644 --- a/src/Parser/DocCommentParserInterface.php +++ b/src/Parser/DocCommentParserInterface.php @@ -8,5 +8,9 @@ interface DocCommentParserInterface { public const DOC_DESCRIPTION = 'function-description'; + /** + * @param string $docComment + * @return array + */ public function parseDoc(string $docComment): array; -} \ No newline at end of file +} diff --git a/src/PhpClass/PhpClass.php b/src/PhpClass/PhpClass.php index d7f49cf..a629d40 100644 --- a/src/PhpClass/PhpClass.php +++ b/src/PhpClass/PhpClass.php @@ -6,33 +6,22 @@ class PhpClass implements PhpClassInterface { - /** - * @var string - */ - private $classBody; - - /** - * @var string - */ - private $className; - - /** - * @var string - */ - private $classNamespace; + private string $classBody; + private string $className; + private ?string $classNamespace; /** - * @var PhpClassProperty[] + * @var PhpClassPropertyInterface[] */ - private $classProperties; + private array $classProperties; /** * @param string $className - * @param string $classNamespace + * @param ?string $classNamespace * @param string $classBody - * @param PhpClassProperty[] $classProperties + * @param PhpClassPropertyInterface[] $classProperties */ - public function __construct(string $className, string $classNamespace, string $classBody, array $classProperties) + public function __construct(string $className, ?string $classNamespace, string $classBody, array $classProperties) { $this->className = $className; $this->classNamespace = $classNamespace; @@ -43,7 +32,7 @@ public function __construct(string $className, string $classNamespace, string $c /** * @return string */ - public function getClassNamespace(): string + public function getClassNamespace(): ?string { return $this->classNamespace; } @@ -65,7 +54,7 @@ public function getClassBody(): string } /** - * @return PhpClassProperty[] + * @return PhpClassPropertyInterface[] */ public function getClassProperties(): array { diff --git a/src/PhpClass/PhpClassInterface.php b/src/PhpClass/PhpClassInterface.php index 6289c22..126d3b9 100644 --- a/src/PhpClass/PhpClassInterface.php +++ b/src/PhpClass/PhpClassInterface.php @@ -6,14 +6,14 @@ interface PhpClassInterface { - public function getClassNamespace(): string; + public function getClassNamespace(): ?string; public function getClassName(): string; public function getClassBody(): string; /** - * @return PhpClassProperty[] + * @return PhpClassPropertyInterface[] */ public function getClassProperties(): array; } diff --git a/src/Registry/ClassRegistry.php b/src/Registry/ClassRegistry.php index 1f1ee4a..c3bcdc2 100644 --- a/src/Registry/ClassRegistry.php +++ b/src/Registry/ClassRegistry.php @@ -9,6 +9,7 @@ use PhpKafka\PhpAvroSchemaGenerator\Exception\ClassRegistryException; use PhpKafka\PhpAvroSchemaGenerator\Parser\TokenParser; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClass; +use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassInterface; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use SplFileInfo; @@ -21,7 +22,7 @@ final class ClassRegistry implements ClassRegistryInterface protected $classDirectories = []; /** - * @var PhpClass[] + * @var PhpClassInterface[] */ protected $classes = []; @@ -77,7 +78,7 @@ public function load(): ClassRegistryInterface } /** - * @return PhpClass[] + * @return PhpClassInterface[] */ public function getClasses(): array { diff --git a/src/Registry/ClassRegistryInterface.php b/src/Registry/ClassRegistryInterface.php index 1559ec1..3bf1020 100644 --- a/src/Registry/ClassRegistryInterface.php +++ b/src/Registry/ClassRegistryInterface.php @@ -4,7 +4,7 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Registry; -use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClass; +use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassInterface; interface ClassRegistryInterface { @@ -25,7 +25,7 @@ public function getClassDirectories(): array; public function load(): ClassRegistryInterface; /** - * @return PhpClass[] + * @return PhpClassInterface[] */ public function getClasses(): array; } diff --git a/src/ServiceProvider/CommandServiceProvider.php b/src/ServiceProvider/CommandServiceProvider.php index affe3a0..b5f5560 100644 --- a/src/ServiceProvider/CommandServiceProvider.php +++ b/src/ServiceProvider/CommandServiceProvider.php @@ -15,8 +15,7 @@ class CommandServiceProvider implements ServiceProviderInterface { - - public function register(Container $container) + public function register(Container $container): void { $container['console.commands'] = function () use ($container): array { $commands = []; @@ -34,4 +33,4 @@ public function register(Container $container) return $commands; }; } -} \ No newline at end of file +} diff --git a/src/ServiceProvider/ConverterServiceProvider.php b/src/ServiceProvider/ConverterServiceProvider.php index 9dceec7..349867f 100644 --- a/src/ServiceProvider/ConverterServiceProvider.php +++ b/src/ServiceProvider/ConverterServiceProvider.php @@ -12,12 +12,11 @@ class ConverterServiceProvider implements ServiceProviderInterface { - - public function register(Container $container) + public function register(Container $container): void { $container[PhpClassConverterInterface::class] = static function (Container $container): PhpClassConverterInterface { return new PhpClassConverter($container[ClassParserInterface::class]); }; } -} \ No newline at end of file +} diff --git a/src/ServiceProvider/GeneratorServiceProvider.php b/src/ServiceProvider/GeneratorServiceProvider.php index 94d51c9..c64f6a8 100644 --- a/src/ServiceProvider/GeneratorServiceProvider.php +++ b/src/ServiceProvider/GeneratorServiceProvider.php @@ -11,11 +11,10 @@ class GeneratorServiceProvider implements ServiceProviderInterface { - - public function register(Container $container) + public function register(Container $container): void { - $container[SchemaGeneratorInterface::class] = static function (Container $container): SchemaGeneratorInterface { + $container[SchemaGeneratorInterface::class] = static function (): SchemaGeneratorInterface { return new SchemaGenerator(); }; } -} \ No newline at end of file +} diff --git a/src/ServiceProvider/MergerServiceProvider.php b/src/ServiceProvider/MergerServiceProvider.php index 888966d..d41c6ef 100644 --- a/src/ServiceProvider/MergerServiceProvider.php +++ b/src/ServiceProvider/MergerServiceProvider.php @@ -11,11 +11,10 @@ class MergerServiceProvider implements ServiceProviderInterface { - - public function register(Container $container) + public function register(Container $container): void { $container[SchemaMergerInterface::class] = static function (): SchemaMergerInterface { return new SchemaMerger(); }; } -} \ No newline at end of file +} diff --git a/src/ServiceProvider/ParserServiceProvider.php b/src/ServiceProvider/ParserServiceProvider.php index aebc1fa..70530e5 100644 --- a/src/ServiceProvider/ParserServiceProvider.php +++ b/src/ServiceProvider/ParserServiceProvider.php @@ -17,7 +17,7 @@ class ParserServiceProvider implements ServiceProviderInterface { - public function register(Container $container) + public function register(Container $container): void { $container[ParserFactory::class] = static function (): ParserFactory { return new ParserFactory(); @@ -40,4 +40,4 @@ static function (Container $container): ClassPropertyParserInterface { return new ClassParser($container[Parser::class], $container[ClassPropertyParserInterface::class]); }; } -} \ No newline at end of file +} diff --git a/src/ServiceProvider/RegistryServiceProvider.php b/src/ServiceProvider/RegistryServiceProvider.php index e7a507e..d66a941 100644 --- a/src/ServiceProvider/RegistryServiceProvider.php +++ b/src/ServiceProvider/RegistryServiceProvider.php @@ -14,8 +14,7 @@ class RegistryServiceProvider implements ServiceProviderInterface { - - public function register(Container $container) + public function register(Container $container): void { $container[ClassRegistryInterface::class] = static function (Container $container): ClassRegistryInterface { return new ClassRegistry($container[PhpClassConverterInterface::class]); @@ -25,4 +24,4 @@ public function register(Container $container) return new SchemaRegistry(); }; } -} \ No newline at end of file +} From ef22ccde358653864e0b265faea5e89abf33bf3f Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Thu, 16 Dec 2021 01:04:53 +0100 Subject: [PATCH 11/27] save work --- src/Avro/Avro.php | 20 +++++ src/Converter/PhpClassConverter.php | 129 +++++++++++++++++++++++++++- src/Generator/SchemaGenerator.php | 15 ---- src/Parser/ClassParser.php | 12 ++- src/Parser/ClassPropertyParser.php | 25 +----- 5 files changed, 161 insertions(+), 40 deletions(-) diff --git a/src/Avro/Avro.php b/src/Avro/Avro.php index db3a021..ebd28be 100644 --- a/src/Avro/Avro.php +++ b/src/Avro/Avro.php @@ -20,4 +20,24 @@ class Avro 'map' => self::LONELIEST_NUMBER, 'fixed' => self::LONELIEST_NUMBER, ]; + + /** + * @var string[] + */ + public const MAPPED_TYPES = array( + 'null' => 'null', + 'bool' => 'boolean', + 'boolean' => 'boolean', + 'string' => 'string', + 'int' => 'int', + 'integer' => 'int', + 'float' => 'float', + 'double' => 'double', + 'array' => 'array', + 'object' => 'object', + 'callable' => 'callable', + 'resource' => 'resource', + 'mixed' => 'mixed', + 'Collection' => 'array', + ); } diff --git a/src/Converter/PhpClassConverter.php b/src/Converter/PhpClassConverter.php index 9cb627c..86b20f0 100644 --- a/src/Converter/PhpClassConverter.php +++ b/src/Converter/PhpClassConverter.php @@ -4,6 +4,7 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Converter; +use PhpKafka\PhpAvroSchemaGenerator\Avro\Avro; use PhpKafka\PhpAvroSchemaGenerator\Parser\ClassParserInterface; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClass; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassInterface; @@ -14,6 +15,17 @@ class PhpClassConverter implements PhpClassConverterInterface { private ClassParserInterface $parser; + /** + * @var array + */ + private array $typesToSkip = [ + 'null' => 1, + 'object' => 1, + 'callable' => 1, + 'resource' => 1, + 'mixed' => 1 + ]; + /** * @param ClassParserInterface $parser */ @@ -50,6 +62,11 @@ private function getConvertedProperties(array $properties): array $convertedProperties = []; foreach ($properties as $property) { $convertedType = $this->getConvertedType($property->getPropertyType()); + + if (null === $convertedType) { + continue; + } + $convertedProperties[] = new PhpClassProperty( $property->getPropertyName(), $convertedType, @@ -62,9 +79,119 @@ private function getConvertedProperties(array $properties): array return $convertedProperties; } - private function getConvertedType(string $type): string + private function getConvertedType(string $type): ?string + { + $types = explode('|', $type); + + if (1 === count($types)) { + return $this->getFullTypeName($type); + } + + return $this->getConvertedUnionType($types); + } + + private function getFullTypeName(string $type): ?string { + if (true === isset(Avro::MAPPED_TYPES[$type])) { + $type = Avro::MAPPED_TYPES[$type]; + } + + if (true === isset($this->typesToSkip[$type])) { + return null; + } + + if (true === isset(Avro::BASIC_TYPES[$type])) { + return $type; + } + + $usedClasses = $this->parser->getUsedClasses(); + + if (true === isset($usedClasses[$type])) { + return $usedClasses[$type]; + } + + if (null !== $this->parser->getNamespace()) { + return $this->parser->getNamespace() . '\\' . $type; + } return $type; } + + private function getConvertedUnionType(array $types): string + { + $convertedUnionType = '['; + + $separator = ''; + + foreach ($types as $type) { + if (true === isset($this->typesToSkip[$type])) { + continue; + } + + if (false === $this->isArrayType($type)) { + $convertedUnionType .= $separator . $type; + $separator = ','; + } + } + + $arrayType = $this->getArrayType($types); + + if ('' !== $arrayType) { + $convertedUnionType .= $separator . $arrayType; + } + + $convertedUnionType .= ']'; + + return $convertedUnionType; + } + + public function getArrayType(array $types): string + { + $arrayTypes = []; + $itemPrefix = '['; + $itemSuffix = ']'; + + foreach ($types as $type) { + if (true === $this->isArrayType($type)) { + $arrayTypes[] = $type; + } + } + + if (0 === count($arrayTypes)) { + return ''; + } + + foreach ($arrayTypes as $idx => $arrayType) { + if ('array' === $arrayType) { + unset($arrayTypes[$idx]); + continue; + } + + $cleanedType = str_replace('[]', '', $arrayType); + $arrayTypes[$idx] = $this->getFullTypeName($cleanedType); + } + + if (0 === count($arrayTypes)) { + $arrayTypes[] = 'string'; + } + + if (1 === count($arrayTypes)) { + $itemPrefix = ''; + $itemSuffix = ''; + } + + return json_encode([ + 'type' => 'array', + 'items' => $itemPrefix . implode(',', $arrayTypes) . $itemSuffix + ]); + } + + private function isArrayType(string $type): bool + { + if ('array' === $type || str_contains($type, '[]')) { + return true; + } + + return false; + } } diff --git a/src/Generator/SchemaGenerator.php b/src/Generator/SchemaGenerator.php index e7f75cc..a050a95 100644 --- a/src/Generator/SchemaGenerator.php +++ b/src/Generator/SchemaGenerator.php @@ -12,17 +12,6 @@ final class SchemaGenerator implements SchemaGeneratorInterface { - /** - * @var int[] - */ - private array $typesToSkip = [ - 'null' => 1, - 'object' => 1, - 'callable' => 1, - 'resource' => 1, - 'mixed' => 1 - ]; - private string $outputDirectory; /** @@ -88,10 +77,6 @@ public function generate(): array /** @var PhpClassPropertyInterface $property */ foreach ($class->getClassProperties() as $property) { - if (true === isset($this->typesToSkip[$property->getPropertyType()])) { - continue; - } - $field = ['name' => $property->getPropertyName()]; $field['type'] = $property->getPropertyType(); diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index 6167b25..c9b3da5 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -4,7 +4,6 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Parser; -use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassProperty; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyInterface; use PhpParser\Node\Identifier; use PhpParser\Node\Name; @@ -179,8 +178,17 @@ private function getParentClassStatements(): ?array { /** @var class-string[] $usedClasses */ $usedClasses = $this->getUsedClasses(); + $parentClass = $this->getParentClassName(); - $rc = new ReflectionClass($usedClasses[$this->getParentClassName()]); + if (null !== $usedClasses[$this->getParentClassName()]) { + $parentClass = $usedClasses[$this->getParentClassName()]; + } + + if (null === $parentClass) { + return []; + } + + $rc = new ReflectionClass($parentClass); $filename = $rc->getFileName(); if (false === $filename) { diff --git a/src/Parser/ClassPropertyParser.php b/src/Parser/ClassPropertyParser.php index ebf1f6e..870632f 100644 --- a/src/Parser/ClassPropertyParser.php +++ b/src/Parser/ClassPropertyParser.php @@ -4,6 +4,7 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Parser; +use PhpKafka\PhpAvroSchemaGenerator\Avro\Avro; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassProperty; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyInterface; use PhpParser\Comment\Doc; @@ -16,26 +17,6 @@ class ClassPropertyParser implements ClassPropertyParserInterface { private DocCommentParserInterface $docParser; - /** - * @var string[] - */ - private $mappedTypes = array( - 'null' => 'null', - 'bool' => 'boolean', - 'boolean' => 'boolean', - 'string' => 'string', - 'int' => 'int', - 'integer' => 'int', - 'float' => 'float', - 'double' => 'double', - 'array' => 'array', - 'object' => 'object', - 'callable' => 'callable', - 'resource' => 'resource', - 'mixed' => 'mixed', - 'Collection' => 'array', - ); - /** * @param DocCommentParserInterface $docParser */ @@ -100,13 +81,13 @@ private function getPropertyName(Property $property): string private function getPropertyType(Property $property, array $docComments): string { if ($property->type instanceof Identifier) { - return $this->mappedTypes[$property->type->name] ?? $property->type->name; + return Avro::MAPPED_TYPES[$property->type->name] ?? $property->type->name; } elseif ($property->type instanceof UnionType) { $types = ''; $separator = ''; /** @var Identifier $type */ foreach ($property->type->types as $type) { - $type = $this->mappedTypes[$type->name] ?? $type->name; + $type = Avro::MAPPED_TYPES[$type->name] ?? $type->name; $types .= $separator . $type; $separator = ','; } From 3d45eaa10da19106f4b5aa9db5848a029cc81ef0 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Thu, 16 Dec 2021 01:22:29 +0100 Subject: [PATCH 12/27] save work --- src/Converter/PhpClassConverter.php | 31 +++++++++++----------- src/PhpClass/PhpClassProperty.php | 13 ++++++--- src/PhpClass/PhpClassPropertyInterface.php | 5 +++- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/Converter/PhpClassConverter.php b/src/Converter/PhpClassConverter.php index 86b20f0..27d85d4 100644 --- a/src/Converter/PhpClassConverter.php +++ b/src/Converter/PhpClassConverter.php @@ -79,7 +79,7 @@ private function getConvertedProperties(array $properties): array return $convertedProperties; } - private function getConvertedType(string $type): ?string + private function getConvertedType(string $type) { $types = explode('|', $type); @@ -117,11 +117,9 @@ private function getFullTypeName(string $type): ?string return $type; } - private function getConvertedUnionType(array $types): string + private function getConvertedUnionType(array $types): array { - $convertedUnionType = '['; - - $separator = ''; + $convertedUnionType = []; foreach ($types as $type) { if (true === isset($this->typesToSkip[$type])) { @@ -129,23 +127,26 @@ private function getConvertedUnionType(array $types): string } if (false === $this->isArrayType($type)) { - $convertedUnionType .= $separator . $type; - $separator = ','; + $convertedUnionType[] = $type; } } $arrayType = $this->getArrayType($types); - if ('' !== $arrayType) { - $convertedUnionType .= $separator . $arrayType; + if (0 !== count($convertedUnionType) && [] !== $arrayType) { + $convertedUnionType[] = $arrayType; + } else { + return $arrayType; } - $convertedUnionType .= ']'; - return $convertedUnionType; } - public function getArrayType(array $types): string + /** + * @param string[] $types + * @return string[] + */ + private function getArrayType(array $types): array { $arrayTypes = []; $itemPrefix = '['; @@ -158,7 +159,7 @@ public function getArrayType(array $types): string } if (0 === count($arrayTypes)) { - return ''; + return []; } foreach ($arrayTypes as $idx => $arrayType) { @@ -180,10 +181,10 @@ public function getArrayType(array $types): string $itemSuffix = ''; } - return json_encode([ + return [ 'type' => 'array', 'items' => $itemPrefix . implode(',', $arrayTypes) . $itemSuffix - ]); + ]; } private function isArrayType(string $type): bool diff --git a/src/PhpClass/PhpClassProperty.php b/src/PhpClass/PhpClassProperty.php index 89e0807..16e06a9 100644 --- a/src/PhpClass/PhpClassProperty.php +++ b/src/PhpClass/PhpClassProperty.php @@ -13,18 +13,20 @@ class PhpClassProperty implements PhpClassPropertyInterface private ?string $propertyDoc; private ?string $propertyLogicalType; private string $propertyName; - private string $propertyType; + + /** @var string|string[] */ + private $propertyType; /** * @param string $propertyName - * @param string $propertyType + * @param string[]|string $propertyType * @param null|mixed $propertyDefault * @param null|string $propertyDoc * @param null|string $propertyLogicalType */ public function __construct( string $propertyName, - string $propertyType, + $propertyType, $propertyDefault = self::NO_DEFAULT, ?string $propertyDoc = null, ?string $propertyLogicalType = null @@ -59,7 +61,10 @@ public function getPropertyName(): string return $this->propertyName; } - public function getPropertyType(): string + /** + * @return string[]|string + */ + public function getPropertyType() { return $this->propertyType; } diff --git a/src/PhpClass/PhpClassPropertyInterface.php b/src/PhpClass/PhpClassPropertyInterface.php index 98d06d2..b8c6fce 100644 --- a/src/PhpClass/PhpClassPropertyInterface.php +++ b/src/PhpClass/PhpClassPropertyInterface.php @@ -19,5 +19,8 @@ public function getPropertyLogicalType(): ?string; public function getPropertyName(): string; - public function getPropertyType(): string; + /** + * @return string[]|string + */ + public function getPropertyType(); } From 7604e644063a5e3afbaecda7fb3cf6ba846a8b8e Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Thu, 16 Dec 2021 01:32:51 +0100 Subject: [PATCH 13/27] save work --- src/Converter/PhpClassConverter.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Converter/PhpClassConverter.php b/src/Converter/PhpClassConverter.php index 27d85d4..0321eee 100644 --- a/src/Converter/PhpClassConverter.php +++ b/src/Converter/PhpClassConverter.php @@ -107,11 +107,11 @@ private function getFullTypeName(string $type): ?string $usedClasses = $this->parser->getUsedClasses(); if (true === isset($usedClasses[$type])) { - return $usedClasses[$type]; + return $this->convertNamespace($usedClasses[$type]); } if (null !== $this->parser->getNamespace()) { - return $this->parser->getNamespace() . '\\' . $type; + return $this->convertNamespace($this->parser->getNamespace() . '\\' . $type); } return $type; @@ -195,4 +195,9 @@ private function isArrayType(string $type): bool return false; } + + private function convertNamespace(string $namespace): string + { + return str_replace('\\', '.', $namespace); + } } From caf4ec809a4e94d74389a9701912e9c5cb1dda34 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Thu, 16 Dec 2021 01:33:29 +0100 Subject: [PATCH 14/27] remove old token parser --- src/Parser/TokenParser.php | 464 ------------------------------------- 1 file changed, 464 deletions(-) delete mode 100644 src/Parser/TokenParser.php diff --git a/src/Parser/TokenParser.php b/src/Parser/TokenParser.php deleted file mode 100644 index bf0435e..0000000 --- a/src/Parser/TokenParser.php +++ /dev/null @@ -1,464 +0,0 @@ - - * @author Christian Kaps - */ -class TokenParser -{ - /** - * The token list. - * - * @var array - */ - private $tokens; - - /** - * The number of tokens. - * - * @var int - */ - private $numTokens; - - /** - * @var string - */ - private string $className; - - /** - * @var string - */ - private $namespace = ''; - - /** - * The current array pointer. - * - * @var int - */ - private $pointer = 0; - - /** - * @var string[] - */ - private $ignoredTypes = array( - 'null' => 'null', - 'bool' => 'boolean', - 'boolean' => 'boolean', - 'string' => 'string', - 'int' => 'int', - 'integer' => 'int', - 'float' => 'float', - 'double' => 'double', - 'array' => 'array', - 'object' => 'object', - 'callable' => 'callable', - 'resource' => 'resource', - 'mixed' => 'mixed', - 'Collection' => 'array', - ); - - /** - * @param string $contents - */ - public function __construct($contents) - { - $this->tokens = token_get_all($contents); - - // The PHP parser sets internal compiler globals for certain things. Annoyingly, the last docblock comment it - // saw gets stored in doc_comment. When it comes to compile the next thing to be include()d this stored - // doc_comment becomes owned by the first thing the compiler sees in the file that it considers might have a - // docblock. If the first thing in the file is a class without a doc block this would cause calls to - // getDocBlock() on said class to return our long lost doc_comment. Argh. - // To workaround, cause the parser to parse an empty docblock. Sure getDocBlock() will return this, but at least - // it's harmless to us. - token_get_all("numTokens = count($this->tokens); - } - - /** - * @return string - */ - public function getClassName(): ?string - { - if (true === isset($this->className)) { - return $this->className; - } - - for ($i = 0; $i < count($this->tokens); $i++) { - if ($this->tokens[$i][0] === T_CLASS) { - $this->className = $this->tokens[$i + 2][1]; - return $this->className; - } - } - - return null; - } - - /** - * @return string - */ - public function getNamespace(): string - { - if ('' !== $this->namespace) { - return $this->namespace; - } - - for ($i = 0; $i < count($this->tokens); $i++) { - if ($this->tokens[$i][0] === T_NAMESPACE) { - $index = 2; - while (true) { - if (true === is_string($this->tokens[$i + $index])) { - break 2; - } - $this->namespace .= $this->tokens[$i + $index][1]; - ++$index; - } - } - } - - return $this->namespace; - } - - /** - * Gets all use statements. - * - * @param string $namespaceName The namespace name of the reflected class. - * - * @return array A list with all found use statements. - * @codeCoverageIgnore - * @infection-ignore-all - */ - public function parseUseStatements($namespaceName) - { - $statements = array(); - while (($token = $this->next())) { - if ($token[0] === T_USE) { - $statements = array_merge($statements, $this->parseUseStatement()); - continue; - } - if ($token[0] !== T_NAMESPACE || $this->parseNamespace() != $namespaceName) { - continue; - } - - // Get fresh array for new namespace. This is to prevent the parser to collect the use statements - // for a previous namespace with the same name. This is the case if a namespace is defined twice - // or if a namespace with the same name is commented out. - $statements = array(); - } - - $this->pointer = 0; - - return $statements; - } - - /** - * @param class-string $classPath - * @return array - * @throws \ReflectionException - */ - public function getProperties(string $classPath): array - { - $properties = []; - - $reflectionClass = new ReflectionClass($classPath); - - foreach ($reflectionClass->getProperties() as $property) { - $simpleType = (string) $this->getPropertyClass($property, false); - $nestedType = (string) $this->getPropertyClass($property, true); - $properties[] = new PhpClassProperty($property->getName(), $simpleType, $nestedType); - } - - return $properties; - } - - /** - * Parse the docblock of the property to get the class of the var annotation. - * - * @param ReflectionProperty $property - * @param boolean $ignorePrimitive - * - * @throws \RuntimeException - * @return string|null Type of the property (content of var annotation) - * @codeCoverageIgnore - * @infection-ignore-all - */ - public function getPropertyClass(ReflectionProperty $property, bool $ignorePrimitive = true) - { - $type = null; - /** @var false|string $phpVersionResult */ - $phpVersionResult = phpversion(); - $phpVersion = false === $phpVersionResult ? '7.0.0' : $phpVersionResult; - // Get is explicit type declaration if possible - if (version_compare($phpVersion, '7.4.0', '>=') && null !== $property->getType()) { - $reflectionType = $property->getType(); - - if ($reflectionType instanceof \ReflectionNamedType) { - $type = $reflectionType->getName(); - } - } - - if (is_null($type)) { // Try get the content of the @var annotation - if (preg_match('/@var\s+([^\s]+)/', (string) $property->getDocComment(), $matches)) { - list(, $type) = $matches; - } else { - return null; - } - } - - $types = explode('|', $this->replaceTypeStrings($type)); - - foreach ($types as $type) { - // Ignore primitive types - if (true === isset($this->ignoredTypes[$type])) { - if (false === $ignorePrimitive) { - return $this->ignoredTypes[$type]; - } - - if (true === $ignorePrimitive && 1 < count($types)) { - continue; - } - - return null; - } - // Ignore types containing special characters ([], <> ...) - if (!preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) { - return null; - } - $class = $property->getDeclaringClass(); - // If the class name is not fully qualified (i.e. doesn't start with a \) - if ($type[0] !== '\\') { - // Try to resolve the FQN using the class context - $resolvedType = $this->tryResolveFqn($type, $class, $property); - - if (!$resolvedType) { - throw new \RuntimeException(sprintf( - 'The @var annotation on %s::%s contains a non existent class "%s". ' - . 'Did you maybe forget to add a "use" statement for this annotation?', - $class->name, - $property->getName(), - $type - )); - } - - $type = $resolvedType; - } - - if (!$this->classExists($type)) { - throw new \RuntimeException(sprintf( - 'The @var annotation on %s::%s contains a non existent class "%s"', - $class->name, - $property->getName(), - $type - )); - } - - // Remove the leading \ (FQN shouldn't contain it) - $type = ltrim($type, '\\'); - } - - return $type; - } - - /** - * Attempts to resolve the FQN of the provided $type based on the $class and $member context. - * - * @param string $type - * @param ReflectionClass $class - * @param Reflector $member - * - * @return string|null Fully qualified name of the type, or null if it could not be resolved - * @codeCoverageIgnore - * @infection-ignore-all - */ - private function tryResolveFqn($type, ReflectionClass $class, Reflector $member) - { - $alias = ($pos = strpos($type, '\\')) === false ? $type : substr($type, 0, $pos); - $loweredAlias = strtolower($alias); - // Retrieve "use" statements - $parser = new TokenParser((string) file_get_contents((string) $class->getFileName())); - $uses = $parser->parseUseStatements($class->getNamespaceName()); - - if (isset($uses[$loweredAlias])) { - // Imported classes - if ($pos !== false) { - return $uses[$loweredAlias] . substr($type, $pos); - } else { - return $uses[$loweredAlias]; - } - } elseif ($this->classExists($class->getNamespaceName() . '\\' . $type)) { - return $class->getNamespaceName() . '\\' . $type; - } elseif (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) { - // Class namespace - return $uses['__NAMESPACE__'] . '\\' . $type; - } elseif ($this->classExists($type)) { - // No namespace - return $type; - } - if (version_compare((string) phpversion(), '5.4.0', '<')) { - return null; - } else { - // If all fail, try resolving through related traits - return $this->tryResolveFqnInTraits($type, $class, $member); - } - } - - /** - * Attempts to resolve the FQN of the provided $type based on the $class and $member context, specifically searching - * through the traits that are used by the provided $class. - * - * @param string $type - * @param ReflectionClass $class - * @param Reflector $member - * - * @return string|null Fully qualified name of the type, or null if it could not be resolved - * @codeCoverageIgnore - * @infection-ignore-all - */ - private function tryResolveFqnInTraits($type, ReflectionClass $class, Reflector $member) - { - /** @var ReflectionClass[] $traits */ - $traits = array(); - // Get traits for the class and its parents - while ($class) { - $traits = array_merge($traits, $class->getTraits()); - $class = $class->getParentClass(); - } - - foreach ($traits as $trait) { - // Eliminate traits that don't have the property/method/parameter - if ($member instanceof ReflectionProperty && !$trait->hasProperty($member->name)) { - continue; - } elseif ($member instanceof ReflectionMethod && !$trait->hasMethod($member->name)) { - continue; - } elseif ( - $member instanceof ReflectionParameter - && !$trait->hasMethod($member->getDeclaringFunction()->name) - ) { - continue; - } - // Run the resolver again with the ReflectionClass instance for the trait - $resolvedType = $this->tryResolveFqn($type, $trait, $member); - - if ($resolvedType) { - return $resolvedType; - } - } - return null; - } - - /** - * Gets the next non whitespace and non comment token. - * - * @param boolean $docCommentIsComment If TRUE then a doc comment is considered a comment and skipped. - * If FALSE then only whitespace and normal comments are skipped. - * - * @return array|null The token if exists, null otherwise. - */ - private function next($docCommentIsComment = true) - { - for ($i = $this->pointer; $i < $this->numTokens; $i++) { - $this->pointer++; - if ( - $this->tokens[$i][0] === T_WHITESPACE - || $this->tokens[$i][0] === T_COMMENT - || ($docCommentIsComment - && $this->tokens[$i][0] === T_DOC_COMMENT) - ) { - continue; - } - - return $this->tokens[$i]; - } - - return null; - } - - /** - * Parses a single use statement. - * - * @return array A list with all found class names for a use statement. - * @codeCoverageIgnore - * @infection-ignore-all - */ - private function parseUseStatement() - { - $class = ''; - $alias = ''; - $statements = array(); - $explicitAlias = false; - while (($token = $this->next())) { - $isNameToken = $token[0] === T_STRING || $token[0] === T_NS_SEPARATOR; - if (!$explicitAlias && $isNameToken) { - $class .= $token[1]; - $alias = $token[1]; - } elseif ($explicitAlias && $isNameToken) { - $alias .= $token[1]; - } elseif ($token[0] === T_AS) { - $explicitAlias = true; - $alias = ''; - } elseif ($token === ',') { - $statements[strtolower($alias)] = $class; - $class = ''; - $alias = ''; - $explicitAlias = false; - } elseif ($token === ';') { - $statements[strtolower($alias)] = $class; - break; - } else { - break; - } - } - - return $statements; - } - - /** - * Gets the namespace. - * - * @return string The found namespace. - */ - private function parseNamespace() - { - $name = ''; - while (($token = $this->next()) && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR)) { - $name .= $token[1]; - } - - return $name; - } - - /** - * @param string $class - * @return bool - */ - private function classExists($class) - { - return class_exists($class) || interface_exists($class); - } - - /** - * @param string $type - * @return string - */ - private function replaceTypeStrings(string $type): string - { - return str_replace('[]', '', $type); - } -} From 4bb0e661695fb1dfc042e12c05c2a5d2fb272ca1 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Thu, 16 Dec 2021 01:36:53 +0100 Subject: [PATCH 15/27] update cc --- .codeclimate.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 92095bb..baa6b94 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -4,7 +4,7 @@ checks: argument-count: enabled: true config: - threshold: 4 + threshold: 5 complex-logic: enabled: true config: @@ -32,7 +32,7 @@ checks: return-statements: enabled: true config: - threshold: 4 + threshold: 5 similar-code: enabled: true config: @@ -42,7 +42,6 @@ checks: config: threshold: #language-specific defaults. overrides affect all languages. exclude_patterns: - - "src/Parser/TokenParser.php" - "src/Command" - "tests/" - "**/vendor/" From 160a811730a76c58fb7ee36192aa4cd25be42b60 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Thu, 16 Dec 2021 01:54:47 +0100 Subject: [PATCH 16/27] save work --- phpstan.neon | 6 +-- src/Converter/PhpClassConverter.php | 71 +++++++++++++++++++++++------ src/Parser/ClassParser.php | 8 ++-- 3 files changed, 61 insertions(+), 24 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index a39b4ad..d24029b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,8 +1,4 @@ parameters: level: 8 paths: [ src ] - checkGenericClassInNonGenericObjectType: false - ignoreErrors: - - "#Call to function token_get_all\\(\\) on a separate line has no effect.#" - - "#Strict comparison using === between non-empty-array and ',' will always evaluate to false.#" - - "#Strict comparison using === between non-empty-array and ';' will always evaluate to false.#" \ No newline at end of file + checkGenericClassInNonGenericObjectType: false \ No newline at end of file diff --git a/src/Converter/PhpClassConverter.php b/src/Converter/PhpClassConverter.php index 0321eee..460c4f9 100644 --- a/src/Converter/PhpClassConverter.php +++ b/src/Converter/PhpClassConverter.php @@ -61,6 +61,10 @@ private function getConvertedProperties(array $properties): array { $convertedProperties = []; foreach ($properties as $property) { + if (false === is_string($property->getPropertyType())) { + continue; + } + $convertedType = $this->getConvertedType($property->getPropertyType()); if (null === $convertedType) { @@ -79,6 +83,10 @@ private function getConvertedProperties(array $properties): array return $convertedProperties; } + /** + * @param string $type + * @return string|string[]|null + */ private function getConvertedType(string $type) { $types = explode('|', $type); @@ -117,6 +125,10 @@ private function getFullTypeName(string $type): ?string return $type; } + /** + * @param string[] $types + * @return array + */ private function getConvertedUnionType(array $types): array { $convertedUnionType = []; @@ -148,29 +160,16 @@ private function getConvertedUnionType(array $types): array */ private function getArrayType(array $types): array { - $arrayTypes = []; $itemPrefix = '['; $itemSuffix = ']'; - foreach ($types as $type) { - if (true === $this->isArrayType($type)) { - $arrayTypes[] = $type; - } - } + $arrayTypes = $this->getArrayTypes($types); if (0 === count($arrayTypes)) { return []; } - foreach ($arrayTypes as $idx => $arrayType) { - if ('array' === $arrayType) { - unset($arrayTypes[$idx]); - continue; - } - - $cleanedType = str_replace('[]', '', $arrayType); - $arrayTypes[$idx] = $this->getFullTypeName($cleanedType); - } + $arrayTypes = $this->getCleanedArrayTypes($arrayTypes); if (0 === count($arrayTypes)) { $arrayTypes[] = 'string'; @@ -187,6 +186,48 @@ private function getArrayType(array $types): array ]; } + /** + * @param string[] $types + * @return string[] + */ + private function getArrayTypes(array $types): array + { + $arrayTypes = []; + + foreach ($types as $type) { + if (true === $this->isArrayType($type)) { + $arrayTypes[] = $type; + } + } + + return $arrayTypes; + } + + /** + * @param string[] $arrayTypes + * @return string[] + */ + private function getCleanedArrayTypes(array $arrayTypes): array + { + foreach ($arrayTypes as $idx => $arrayType) { + if ('array' === $arrayType) { + unset($arrayTypes[$idx]); + continue; + } + + $cleanedType = str_replace('[]', '', $arrayType); + + if (null === $this->getFullTypeName($cleanedType)) { + unset($arrayTypes[$idx]); + continue; + } + + $arrayTypes[$idx] = $this->getFullTypeName($cleanedType); + } + + return $arrayTypes; + } + private function isArrayType(string $type): bool { if ('array' === $type || str_contains($type, '[]')) { diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index c9b3da5..f87e136 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -180,14 +180,14 @@ private function getParentClassStatements(): ?array $usedClasses = $this->getUsedClasses(); $parentClass = $this->getParentClassName(); - if (null !== $usedClasses[$this->getParentClassName()]) { - $parentClass = $usedClasses[$this->getParentClassName()]; - } - if (null === $parentClass) { return []; } + if (null !== $usedClasses[$this->getParentClassName()]) { + $parentClass = $usedClasses[$this->getParentClassName()]; + } + $rc = new ReflectionClass($parentClass); $filename = $rc->getFileName(); From 2db8c417f69bb9ad9bd4057a1bb51d04927b733f Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Thu, 16 Dec 2021 02:00:00 +0100 Subject: [PATCH 17/27] save work --- src/Generator/SchemaGenerator.php | 40 +++++++++++++++++++------------ src/Merger/SchemaMerger.php | 2 ++ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/Generator/SchemaGenerator.php b/src/Generator/SchemaGenerator.php index a050a95..c752d13 100644 --- a/src/Generator/SchemaGenerator.php +++ b/src/Generator/SchemaGenerator.php @@ -77,21 +77,7 @@ public function generate(): array /** @var PhpClassPropertyInterface $property */ foreach ($class->getClassProperties() as $property) { - $field = ['name' => $property->getPropertyName()]; - $field['type'] = $property->getPropertyType(); - - if (PhpClassPropertyInterface::NO_DEFAULT !== $property->getPropertyDefault()) { - $field['default'] = $property->getPropertyDefault(); - } - - if (null !== $property->getPropertyDoc() && '' !== $property->getPropertyDoc()) { - $field['doc'] = $property->getPropertyDoc(); - } - - if (null !== $property->getPropertyLogicalType()) { - $field['logicalType'] = $property->getPropertyLogicalType(); - } - + $field = $this->getFieldForProperty($property); $schema['fields'][] = $field; } @@ -107,6 +93,30 @@ public function generate(): array return $schemas; } + /** + * @param PhpClassPropertyInterface $property + * @return array + */ + private function getFieldForProperty(PhpClassPropertyInterface $property): array + { + $field = ['name' => $property->getPropertyName()]; + $field['type'] = $property->getPropertyType(); + + if (PhpClassPropertyInterface::NO_DEFAULT !== $property->getPropertyDefault()) { + $field['default'] = $property->getPropertyDefault(); + } + + if (null !== $property->getPropertyDoc() && '' !== $property->getPropertyDoc()) { + $field['doc'] = $property->getPropertyDoc(); + } + + if (null !== $property->getPropertyLogicalType()) { + $field['logicalType'] = $property->getPropertyLogicalType(); + } + + return $field; + } + /** * @param array $schemas * @return int diff --git a/src/Merger/SchemaMerger.php b/src/Merger/SchemaMerger.php index 6546c56..70a20f7 100644 --- a/src/Merger/SchemaMerger.php +++ b/src/Merger/SchemaMerger.php @@ -84,9 +84,11 @@ public function getResolvedSchemaTemplate(SchemaTemplateInterface $rootSchemaTem if (false === strpos($e->getMessage(), ' is not a schema we know about.')) { throw $e; } + $exceptionThrown = true; $schemaId = $this->getSchemaIdFromExceptionMessage($e->getMessage()); $embeddedTemplate = $this->getSchemaRegistry()->getSchemaById($schemaId); + if (null === $embeddedTemplate) { throw new SchemaMergerException( sprintf(SchemaMergerException::UNKNOWN_SCHEMA_TYPE_EXCEPTION_MESSAGE, $schemaId) From 70dffba5d8289e47359cc5bc5f9bcc35666cb9fc Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Thu, 16 Dec 2021 02:31:42 +0100 Subject: [PATCH 18/27] fix tests --- tests/Integration/Parser/ClassParserTest.php | 61 +++++++++++++++++++ tests/Integration/Parser/TokenParserTest.php | 50 --------------- .../Registry/ClassRegistryTest.php | 25 ++++++-- tests/Unit/Generator/SchemaGeneratorTest.php | 26 ++++---- tests/Unit/Merger/SchemaMergerTest.php | 46 ++++++++------ tests/Unit/PhpClass/PhpClassPropertyTest.php | 6 +- 6 files changed, 129 insertions(+), 85 deletions(-) create mode 100644 tests/Integration/Parser/ClassParserTest.php delete mode 100644 tests/Integration/Parser/TokenParserTest.php diff --git a/tests/Integration/Parser/ClassParserTest.php b/tests/Integration/Parser/ClassParserTest.php new file mode 100644 index 0000000..a863fc2 --- /dev/null +++ b/tests/Integration/Parser/ClassParserTest.php @@ -0,0 +1,61 @@ +create(ParserFactory::PREFER_PHP7), $propertyParser); + $parser->setCode(file_get_contents($filePath)); + self::assertEquals('SomeTestClass', $parser->getClassName()); + self::assertEquals('SomeTestClass', $parser->getClassName()); + } + + public function testGetClassNameForInterface() + { + $filePath = __DIR__ . '/../../../example/classes/SomeTestInterface.php'; + $propertyParser = new ClassPropertyParser(new DocCommentParser()); + $parser = new ClassParser((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $propertyParser); + $parser->setCode(file_get_contents($filePath)); + self::assertNull($parser->getClassName()); + } + + public function testGetNamespace() + { + $filePath = __DIR__ . '/../../../example/classes/SomeTestClass.php'; + $propertyParser = new ClassPropertyParser(new DocCommentParser()); + $parser = new ClassParser((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $propertyParser); + $parser->setCode(file_get_contents($filePath)); + self::assertEquals('PhpKafka\\PhpAvroSchemaGenerator\\Example', $parser->getNamespace()); + self::assertEquals('PhpKafka\\PhpAvroSchemaGenerator\\Example', $parser->getNamespace()); + } + + public function testGetProperties() + { + $filePath = __DIR__ . '/../../../example/classes/SomeTestClass.php'; + $propertyParser = new ClassPropertyParser(new DocCommentParser()); + $parser = new ClassParser((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $propertyParser); + $parser->setCode(file_get_contents($filePath)); + $properties = $parser->getProperties(); + self::assertCount(15, $properties); + + foreach($properties as $property) { + self::assertInstanceOf(PhpClassPropertyInterface::class, $property); + } + } +} diff --git a/tests/Integration/Parser/TokenParserTest.php b/tests/Integration/Parser/TokenParserTest.php deleted file mode 100644 index 2010b84..0000000 --- a/tests/Integration/Parser/TokenParserTest.php +++ /dev/null @@ -1,50 +0,0 @@ -getClassName()); - self::assertEquals('SomeTestClass', $parser->getClassName()); - } - - public function testGetClassNameForInterface() - { - $filePath = __DIR__ . '/../../../example/classes/SomeTestInterface.php'; - $parser = new TokenParser(file_get_contents($filePath)); - self::assertNull($parser->getClassName()); - } - - public function testGetNamespace() - { - $filePath = __DIR__ . '/../../../example/classes/SomeTestClass.php'; - $parser = new TokenParser(file_get_contents($filePath)); - self::assertEquals('PhpKafka\\PhpAvroSchemaGenerator\\Example', $parser->getNamespace()); - self::assertEquals('PhpKafka\\PhpAvroSchemaGenerator\\Example', $parser->getNamespace()); - } - - public function testGetProperties() - { - $filePath = __DIR__ . '/../../../example/classes/SomeTestClass.php'; - $parser = new TokenParser(file_get_contents($filePath)); - $properties = $parser->getProperties($parser->getNamespace() . '\\' . $parser->getClassName()); - self::assertCount(15, $properties); - - foreach($properties as $property) { - self::assertInstanceOf(PhpClassPropertyInterface::class, $property); - } - } -} diff --git a/tests/Integration/Registry/ClassRegistryTest.php b/tests/Integration/Registry/ClassRegistryTest.php index 4d2c33a..7812c9c 100644 --- a/tests/Integration/Registry/ClassRegistryTest.php +++ b/tests/Integration/Registry/ClassRegistryTest.php @@ -4,10 +4,15 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Tests\Integration\Registry; +use PhpKafka\PhpAvroSchemaGenerator\Converter\PhpClassConverter; use PhpKafka\PhpAvroSchemaGenerator\Exception\ClassRegistryException; +use PhpKafka\PhpAvroSchemaGenerator\Parser\ClassParser; +use PhpKafka\PhpAvroSchemaGenerator\Parser\ClassPropertyParser; +use PhpKafka\PhpAvroSchemaGenerator\Parser\DocCommentParser; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassInterface; use PhpKafka\PhpAvroSchemaGenerator\Registry\ClassRegistry; use PhpKafka\PhpAvroSchemaGenerator\Registry\ClassRegistryInterface; +use PhpParser\ParserFactory; use PHPUnit\Framework\TestCase; use ReflectionClass; use SplFileInfo; @@ -19,7 +24,10 @@ class ClassRegistryTest extends TestCase { public function testClassDirectory() { - $registry = new ClassRegistry(); + $propertyParser = new ClassPropertyParser(new DocCommentParser()); + $parser = new ClassParser((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $propertyParser); + $converter = new PhpClassConverter($parser); + $registry = new ClassRegistry($converter); $result = $registry->addClassDirectory('/tmp'); self::assertInstanceOf(ClassRegistryInterface::class, $result); @@ -30,7 +38,10 @@ public function testLoad() { $classDir = __DIR__ . '/../../../example/classes'; - $registry = (new ClassRegistry())->addClassDirectory($classDir)->load(); + $propertyParser = new ClassPropertyParser(new DocCommentParser()); + $parser = new ClassParser((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $propertyParser); + $converter = new PhpClassConverter($parser); + $registry = (new ClassRegistry($converter))->addClassDirectory($classDir)->load(); self::assertInstanceOf(ClassRegistryInterface::class, $registry); @@ -46,7 +57,10 @@ public function testLoad() public function testRegisterSchemaFileThatDoesntExist() { $fileInfo = new SplFileInfo('somenonexistingfile'); - $registry = new ClassRegistry(); + $propertyParser = new ClassPropertyParser(new DocCommentParser()); + $parser = new ClassParser((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $propertyParser); + $converter = new PhpClassConverter($parser); + $registry = new ClassRegistry($converter); self::expectException(ClassRegistryException::class); self::expectExceptionMessage(ClassRegistryException::FILE_PATH_EXCEPTION_MESSAGE); @@ -64,7 +78,10 @@ public function testRegisterSchemaFileThatIsNotReadable() $fileInfo = new SplFileInfo('testfile'); - $registry = new ClassRegistry(); + $propertyParser = new ClassPropertyParser(new DocCommentParser()); + $parser = new ClassParser((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $propertyParser); + $converter = new PhpClassConverter($parser); + $registry = new ClassRegistry($converter); self::expectException(ClassRegistryException::class); self::expectExceptionMessage( diff --git a/tests/Unit/Generator/SchemaGeneratorTest.php b/tests/Unit/Generator/SchemaGeneratorTest.php index da58fdd..77b43d8 100644 --- a/tests/Unit/Generator/SchemaGeneratorTest.php +++ b/tests/Unit/Generator/SchemaGeneratorTest.php @@ -17,7 +17,8 @@ public function testDefaultOutputDirectory() { $registry = $this->getMockForAbstractClass(ClassRegistryInterface::class); - $generator = new SchemaGenerator($registry); + $generator = new SchemaGenerator(); + $generator->setClassRegistry($registry); self::assertEquals($registry, $generator->getClassRegistry()); self::assertEquals('/tmp', $generator->getOutputDirectory()); @@ -28,7 +29,9 @@ public function testGetters() $registry = $this->getMockForAbstractClass(ClassRegistryInterface::class); $directory = '/tmp/foo'; - $generator = new SchemaGenerator($registry, $directory); + $generator = new SchemaGenerator(); + $generator->setClassRegistry($registry); + $generator->setOutputDirectory($directory); self::assertEquals($registry, $generator->getClassRegistry()); self::assertEquals($directory, $generator->getOutputDirectory()); @@ -69,22 +72,19 @@ public function testGenerate() ]; $property1 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); - $property1->expects(self::exactly(3))->method('getPropertyType')->willReturn('array'); + $property1->expects(self::exactly(1))->method('getPropertyType')->willReturn(["type" => "array","items" => "test.foo"]); $property1->expects(self::exactly(1))->method('getPropertyName')->willReturn('items'); - $property1->expects(self::exactly(1))->method('getPropertyArrayType')->willReturn('test\\foo'); + $property1->expects(self::exactly(1))->method('getPropertyDefault')->willReturn(PhpClassPropertyInterface::NO_DEFAULT); $property2 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); - $property2->expects(self::exactly(6))->method('getPropertyType')->willReturn('string'); + $property2->expects(self::exactly(2))->method('getPropertyType')->willReturn('string'); $property2->expects(self::exactly(2))->method('getPropertyName')->willReturn('name'); - - $property3 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); - $property3->expects(self::once())->method('getPropertyType')->willReturn('mixed'); - $property3->expects(self::never())->method('getPropertyName'); + $property2->expects(self::exactly(2))->method('getPropertyDefault')->willReturn(PhpClassPropertyInterface::NO_DEFAULT); $class1 = $this->getMockForAbstractClass(PhpClassInterface::class); $class1->expects(self::once())->method('getClassName')->willReturn('TestClass'); $class1->expects(self::once())->method('getClassNamespace')->willReturn('name\\space'); - $class1->expects(self::once())->method('getClassProperties')->willReturn([$property1, $property2, $property3]); + $class1->expects(self::once())->method('getClassProperties')->willReturn([$property1, $property2]); $class2 = $this->getMockForAbstractClass(PhpClassInterface::class); $class2->expects(self::once())->method('getClassName')->willReturn('Test2Class'); @@ -94,7 +94,8 @@ public function testGenerate() $registry = $this->getMockForAbstractClass(ClassRegistryInterface::class); $registry->expects(self::once())->method('getClasses')->willReturn([$class1, $class2]); - $generator = new SchemaGenerator($registry); + $generator = new SchemaGenerator(); + $generator->setClassRegistry($registry); $result = $generator->generate(); self::assertEquals($expectedResult, $result); self::assertCount(2, $result); @@ -107,7 +108,8 @@ public function testExportSchemas() ]; $registry = $this->getMockForAbstractClass(ClassRegistryInterface::class); - $generator = new SchemaGenerator($registry); + $generator = new SchemaGenerator(); + $generator->setClassRegistry($registry); $fileCount = $generator->exportSchemas($schemas); self::assertFileExists('/tmp/filename.avsc'); diff --git a/tests/Unit/Merger/SchemaMergerTest.php b/tests/Unit/Merger/SchemaMergerTest.php index 486cf1b..263d3a3 100644 --- a/tests/Unit/Merger/SchemaMergerTest.php +++ b/tests/Unit/Merger/SchemaMergerTest.php @@ -21,22 +21,21 @@ class SchemaMergerTest extends TestCase public function testGetSchemaRegistry() { $schemaRegistry = $this->getMockForAbstractClass(SchemaRegistryInterface::class); - $merger = new SchemaMerger($schemaRegistry); + $merger = new SchemaMerger(); + $merger->setSchemaRegistry($schemaRegistry); self::assertEquals($schemaRegistry, $merger->getSchemaRegistry()); } public function testGetOutputDirectoryDefault() { - $schemaRegistry = $this->getMockForAbstractClass(SchemaRegistryInterface::class); - $merger = new SchemaMerger($schemaRegistry); + $merger = new SchemaMerger(); self::assertEquals('/tmp', $merger->getOutputDirectory()); } public function testGetOutputDirectory() { - $schemaRegistry = $this->getMockForAbstractClass(SchemaRegistryInterface::class); $outputDirectory = '/root'; - $merger = new SchemaMerger($schemaRegistry, $outputDirectory); + $merger = new SchemaMerger($outputDirectory); self::assertEquals($outputDirectory, $merger->getOutputDirectory()); } @@ -47,7 +46,8 @@ public function testGetResolvedSchemaTemplateThrowsException() $schemaRegistry = $this->getMockForAbstractClass(SchemaRegistryInterface::class); $schemaTemplate = $this->getMockForAbstractClass(SchemaTemplateInterface::class); $schemaTemplate->expects(self::once())->method('getSchemaDefinition')->willReturn('{"type": 1}'); - $merger = new SchemaMerger($schemaRegistry); + $merger = new SchemaMerger(); + $merger->setSchemaRegistry($schemaRegistry); self::assertEquals([], $merger->getResolvedSchemaTemplate($schemaTemplate)); } @@ -71,7 +71,8 @@ public function testGetResolvedSchemaTemplateResolveEmbeddedException() ->expects(self::once()) ->method('getSchemaDefinition') ->willReturn($definitionWithType); - $merger = new SchemaMerger($schemaRegistry); + $merger = new SchemaMerger(); + $merger->setSchemaRegistry($schemaRegistry); self::assertEquals([], $merger->getResolvedSchemaTemplate($schemaTemplate)); } @@ -119,7 +120,8 @@ public function testGetResolvedSchemaTemplate() ->with($expectedResult) ->willReturn($rootSchemaTemplate); - $merger = new SchemaMerger($schemaRegistry); + $merger = new SchemaMerger(); + $merger->setSchemaRegistry($schemaRegistry); $merger->getResolvedSchemaTemplate($rootSchemaTemplate); } @@ -289,7 +291,8 @@ public function testGetResolvedSchemaTemplateWithMultiEmbedd() ->with($expectedResult) ->willReturn($rootSchemaTemplate); - $merger = new SchemaMerger($schemaRegistry); + $merger = new SchemaMerger(); + $merger->setSchemaRegistry($schemaRegistry); $merger->getResolvedSchemaTemplate($rootSchemaTemplate); } @@ -339,7 +342,8 @@ public function testGetResolvedSchemaTemplateWithDifferentNamespaceForEmbeddedSc ->with($expectedResult) ->willReturn($rootSchemaTemplate); - $merger = new SchemaMerger($schemaRegistry); + $merger = new SchemaMerger(); + $merger->setSchemaRegistry($schemaRegistry); $merger->getResolvedSchemaTemplate($rootSchemaTemplate); } @@ -368,7 +372,9 @@ public function testMergeException() ->expects(self::once()) ->method('getRootSchemas') ->willReturn([$schemaTemplate]); - $merger = new SchemaMerger($schemaRegistry); + $merger = new SchemaMerger(); + $merger->setSchemaRegistry($schemaRegistry); + $merger->merge(); } @@ -401,7 +407,8 @@ public function testMerge() ->willReturn([$schemaTemplate]); $optimizer = $this->getMockForAbstractClass(OptimizerInterface::class); $optimizer->expects(self::once())->method('optimize')->with($schemaTemplate)->willReturn($schemaTemplate); - $merger = new SchemaMerger($schemaRegistry, '/tmp/foobar'); + $merger = new SchemaMerger('/tmp/foobar'); + $merger->setSchemaRegistry($schemaRegistry); $merger->addOptimizer($optimizer); $mergedFiles = $merger->merge(true); @@ -437,7 +444,8 @@ public function testMergePrimitive() ->expects(self::once()) ->method('getRootSchemas') ->willReturn([$schemaTemplate]); - $merger = new SchemaMerger($schemaRegistry, '/tmp/foobar'); + $merger = new SchemaMerger('/tmp/foobar'); + $merger->setSchemaRegistry($schemaRegistry); $merger->merge(false, true); self::assertFileExists('/tmp/foobar/primitive-type.avsc'); @@ -477,7 +485,8 @@ public function testMergePrimitiveWithOptimizerEnabled() ->willReturn([$schemaTemplate]); $optimizer = $this->getMockBuilder(PrimitiveSchemaOptimizer::class)->getMock(); $optimizer->expects(self::once())->method('optimize')->with($schemaTemplate)->willReturn($schemaTemplate); - $merger = new SchemaMerger($schemaRegistry, '/tmp/foobar'); + $merger = new SchemaMerger('/tmp/foobar'); + $merger->setSchemaRegistry($schemaRegistry); $merger->addOptimizer($optimizer); $merger->merge(true); @@ -517,7 +526,8 @@ public function testMergeWithFilenameOption() ->expects(self::once()) ->method('getRootSchemas') ->willReturn([$schemaTemplate]); - $merger = new SchemaMerger($schemaRegistry, '/tmp/foobar'); + $merger = new SchemaMerger('/tmp/foobar'); + $merger->setSchemaRegistry($schemaRegistry); $merger->merge(true, true); self::assertFileExists('/tmp/foobar/bla.avsc'); @@ -534,7 +544,8 @@ public function testExportSchema() ->willReturn('{"name": "test"}'); $schemaRegistry = $this->getMockForAbstractClass(SchemaRegistryInterface::class); - $merger = new SchemaMerger($schemaRegistry); + $merger = new SchemaMerger(); + $merger->setSchemaRegistry($schemaRegistry); $merger->exportSchema($schemaTemplate); self::assertFileExists('/tmp/test.avsc'); @@ -559,7 +570,8 @@ public function testExportSchemaPrimitiveWithWrongOptions() ->willReturn('test.avsc'); $schemaRegistry = $this->getMockForAbstractClass(SchemaRegistryInterface::class); - $merger = new SchemaMerger($schemaRegistry); + $merger = new SchemaMerger(); + $merger->setSchemaRegistry($schemaRegistry); $merger->exportSchema($schemaTemplate, true); self::assertFileExists('/tmp/test.avsc'); diff --git a/tests/Unit/PhpClass/PhpClassPropertyTest.php b/tests/Unit/PhpClass/PhpClassPropertyTest.php index 1efe2fa..4a59531 100644 --- a/tests/Unit/PhpClass/PhpClassPropertyTest.php +++ b/tests/Unit/PhpClass/PhpClassPropertyTest.php @@ -14,10 +14,12 @@ class PhpClassPropertyTest extends TestCase { public function testGetters() { - $property = new PhpClassProperty('propertyName', 'array', 'string'); + $property = new PhpClassProperty('propertyName', 'array', 'default', 'doc', 'logicalType'); self::assertEquals('propertyName', $property->getPropertyName()); self::assertEquals('array', $property->getPropertyType()); - self::assertEquals('string', $property->getPropertyArrayType()); + self::assertEquals('default', $property->getPropertyDefault()); + self::assertEquals('doc', $property->getPropertyDoc()); + self::assertEquals('logicalType', $property->getPropertyLogicalType()); } } From f1885827b69f65606d343d803b6b34b8e29d9942 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Thu, 16 Dec 2021 02:43:33 +0100 Subject: [PATCH 19/27] save work --- phpunit.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit.xml b/phpunit.xml index cff2887..40ef934 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -6,6 +6,7 @@ src/Command + src/AppContainer.php From a6b09a8fc1e9861f8834ea0c9eda2ff161d76638 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Sat, 18 Dec 2021 01:52:30 +0100 Subject: [PATCH 20/27] add tests --- .../CommandServiceProviderTest.php | 33 ++++++++++++++++ .../ConverterServiceProviderTest.php | 28 ++++++++++++++ .../GeneratorServiceProviderTest.php | 26 +++++++++++++ .../MergerServiceProviderTest.php | 26 +++++++++++++ .../ParserServiceProviderTest.php | 38 +++++++++++++++++++ .../RegistryServiceProviderTest.php | 31 +++++++++++++++ 6 files changed, 182 insertions(+) create mode 100644 tests/Unit/ServiceProvider/CommandServiceProviderTest.php create mode 100644 tests/Unit/ServiceProvider/ConverterServiceProviderTest.php create mode 100644 tests/Unit/ServiceProvider/GeneratorServiceProviderTest.php create mode 100644 tests/Unit/ServiceProvider/MergerServiceProviderTest.php create mode 100644 tests/Unit/ServiceProvider/ParserServiceProviderTest.php create mode 100644 tests/Unit/ServiceProvider/RegistryServiceProviderTest.php diff --git a/tests/Unit/ServiceProvider/CommandServiceProviderTest.php b/tests/Unit/ServiceProvider/CommandServiceProviderTest.php new file mode 100644 index 0000000..f135972 --- /dev/null +++ b/tests/Unit/ServiceProvider/CommandServiceProviderTest.php @@ -0,0 +1,33 @@ +getMockForAbstractClass(ClassRegistryInterface::class); + $container[SchemaGeneratorInterface::class] = $this->getMockForAbstractClass(SchemaGeneratorInterface::class); + $container[SchemaMergerInterface::class] = $this->getMockForAbstractClass(SchemaMergerInterface::class); + $container[SchemaRegistryInterface::class] = $this->getMockForAbstractClass(SchemaRegistryInterface::class); + + (new CommandServiceProvider())->register($container); + + self::assertTrue(isset($container['console.commands'])); + self::assertEquals(2, count($container['console.commands'])); + } +} \ No newline at end of file diff --git a/tests/Unit/ServiceProvider/ConverterServiceProviderTest.php b/tests/Unit/ServiceProvider/ConverterServiceProviderTest.php new file mode 100644 index 0000000..e02505e --- /dev/null +++ b/tests/Unit/ServiceProvider/ConverterServiceProviderTest.php @@ -0,0 +1,28 @@ +getMockForAbstractClass(ClassParserInterface::class); + + (new ConverterServiceProvider())->register($container); + + self::assertTrue(isset($container[PhpClassConverterInterface::class])); + self::assertInstanceOf(PhpClassConverterInterface::class, $container[PhpClassConverterInterface::class]); + } +} \ No newline at end of file diff --git a/tests/Unit/ServiceProvider/GeneratorServiceProviderTest.php b/tests/Unit/ServiceProvider/GeneratorServiceProviderTest.php new file mode 100644 index 0000000..cca783c --- /dev/null +++ b/tests/Unit/ServiceProvider/GeneratorServiceProviderTest.php @@ -0,0 +1,26 @@ +register($container); + + self::assertTrue(isset($container[SchemaGeneratorInterface::class])); + self::assertInstanceOf(SchemaGeneratorInterface::class, $container[SchemaGeneratorInterface::class]); + } +} \ No newline at end of file diff --git a/tests/Unit/ServiceProvider/MergerServiceProviderTest.php b/tests/Unit/ServiceProvider/MergerServiceProviderTest.php new file mode 100644 index 0000000..419b13b --- /dev/null +++ b/tests/Unit/ServiceProvider/MergerServiceProviderTest.php @@ -0,0 +1,26 @@ +register($container); + + self::assertTrue(isset($container[SchemaMergerInterface::class])); + self::assertInstanceOf(SchemaMergerInterface::class, $container[SchemaMergerInterface::class]); + } +} \ No newline at end of file diff --git a/tests/Unit/ServiceProvider/ParserServiceProviderTest.php b/tests/Unit/ServiceProvider/ParserServiceProviderTest.php new file mode 100644 index 0000000..89ba158 --- /dev/null +++ b/tests/Unit/ServiceProvider/ParserServiceProviderTest.php @@ -0,0 +1,38 @@ +getMockBuilder(ParserFactory::class)->getMock(); + $container[Parser::class] = $this->getMockForAbstractClass(Parser::class); + + + (new ParserServiceProvider())->register($container); + + self::assertTrue(isset($container[DocCommentParserInterface::class])); + self::assertInstanceOf(DocCommentParserInterface::class, $container[DocCommentParserInterface::class]); + self::assertTrue(isset($container[ClassPropertyParserInterface::class])); + self::assertInstanceOf(ClassPropertyParserInterface::class, $container[ClassPropertyParserInterface::class]); + self::assertTrue(isset($container[ClassParserInterface::class])); + self::assertInstanceOf(ClassParserInterface::class, $container[ClassParserInterface::class]); + + } +} \ No newline at end of file diff --git a/tests/Unit/ServiceProvider/RegistryServiceProviderTest.php b/tests/Unit/ServiceProvider/RegistryServiceProviderTest.php new file mode 100644 index 0000000..d8344a8 --- /dev/null +++ b/tests/Unit/ServiceProvider/RegistryServiceProviderTest.php @@ -0,0 +1,31 @@ +getMockForAbstractClass(PhpClassConverterInterface::class); + + (new RegistryServiceProvider())->register($container); + + self::assertTrue(isset($container[ClassRegistryInterface::class])); + self::assertInstanceOf(ClassRegistryInterface::class, $container[ClassRegistryInterface::class]); + self::assertTrue(isset($container[SchemaRegistryInterface::class])); + self::assertInstanceOf(SchemaRegistryInterface::class, $container[SchemaRegistryInterface::class]); + } +} \ No newline at end of file From 33284807a466597c441fa4ba0a7b2eb0acec3f5d Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Sat, 18 Dec 2021 03:34:03 +0100 Subject: [PATCH 21/27] up cov --- src/Converter/PhpClassConverter.php | 2 +- src/Generator/SchemaGenerator.php | 8 ++- tests/Unit/Generator/SchemaGeneratorTest.php | 35 ++++++++-- tests/Unit/Merger/SchemaMergerTest.php | 33 ++++++++++ tests/Unit/PhpClassConverterTest.php | 68 ++++++++++++++++++++ 5 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 tests/Unit/PhpClassConverterTest.php diff --git a/src/Converter/PhpClassConverter.php b/src/Converter/PhpClassConverter.php index 460c4f9..9fab08e 100644 --- a/src/Converter/PhpClassConverter.php +++ b/src/Converter/PhpClassConverter.php @@ -139,7 +139,7 @@ private function getConvertedUnionType(array $types): array } if (false === $this->isArrayType($type)) { - $convertedUnionType[] = $type; + $convertedUnionType[] = $this->getFullTypeName($type); } } diff --git a/src/Generator/SchemaGenerator.php b/src/Generator/SchemaGenerator.php index c752d13..7e4ec1c 100644 --- a/src/Generator/SchemaGenerator.php +++ b/src/Generator/SchemaGenerator.php @@ -15,9 +15,9 @@ final class SchemaGenerator implements SchemaGeneratorInterface private string $outputDirectory; /** - * @var ClassRegistryInterface + * @var ?ClassRegistryInterface */ - private ClassRegistryInterface $classRegistry; + private ?ClassRegistryInterface $classRegistry; public function __construct(string $outputDirectory = '/tmp') { @@ -72,7 +72,9 @@ public function generate(): array $schema = []; $schema['type'] = 'record'; $schema['name'] = $class->getClassName(); - $schema['namespace'] = $this->convertNamespace($class->getClassNamespace()); + if (null !== $this->convertNamespace($class->getClassNamespace())) { + $schema['namespace'] = $this->convertNamespace($class->getClassNamespace()); + } $schema['fields'] = []; /** @var PhpClassPropertyInterface $property */ diff --git a/tests/Unit/Generator/SchemaGeneratorTest.php b/tests/Unit/Generator/SchemaGeneratorTest.php index 77b43d8..0a798e6 100644 --- a/tests/Unit/Generator/SchemaGeneratorTest.php +++ b/tests/Unit/Generator/SchemaGeneratorTest.php @@ -54,18 +54,23 @@ public function testGenerate() ], [ 'name' => 'name', - 'type' => 'string' + 'type' => 'string', + 'default' => 'test', + 'doc' => 'test', + 'logicalType' => 'test' ] ] ]), - 'name.space.Test2Class' => json_encode([ + 'Test2Class' => json_encode([ 'type' => 'record', 'name' => 'Test2Class', - 'namespace' => 'name.space', 'fields' => [ [ 'name' => 'name', - 'type' => 'string' + 'type' => 'string', + 'default' => 'test', + 'doc' => 'test', + 'logicalType' => 'test' ] ] ]) @@ -79,16 +84,19 @@ public function testGenerate() $property2 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); $property2->expects(self::exactly(2))->method('getPropertyType')->willReturn('string'); $property2->expects(self::exactly(2))->method('getPropertyName')->willReturn('name'); - $property2->expects(self::exactly(2))->method('getPropertyDefault')->willReturn(PhpClassPropertyInterface::NO_DEFAULT); + $property2->expects(self::exactly(4))->method('getPropertyDefault')->willReturn('test'); + $property2->expects(self::exactly(6))->method('getPropertyDoc')->willReturn('test'); + $property2->expects(self::exactly(4))->method('getPropertyLogicalType')->willReturn('test'); + $class1 = $this->getMockForAbstractClass(PhpClassInterface::class); $class1->expects(self::once())->method('getClassName')->willReturn('TestClass'); - $class1->expects(self::once())->method('getClassNamespace')->willReturn('name\\space'); + $class1->expects(self::exactly(2))->method('getClassNamespace')->willReturn('name\\space'); $class1->expects(self::once())->method('getClassProperties')->willReturn([$property1, $property2]); $class2 = $this->getMockForAbstractClass(PhpClassInterface::class); $class2->expects(self::once())->method('getClassName')->willReturn('Test2Class'); - $class2->expects(self::once())->method('getClassNamespace')->willReturn('name\\space'); + $class2->expects(self::once())->method('getClassNamespace')->willReturn(null); $class2->expects(self::once())->method('getClassProperties')->willReturn([$property2]); $registry = $this->getMockForAbstractClass(ClassRegistryInterface::class); @@ -118,4 +126,17 @@ public function testExportSchemas() unlink('/tmp/filename.avsc'); } + + public function testGenerateWithoutRegistry() + { + self::expectException(\RuntimeException::class); + self::expectExceptionMessage('Please set a ClassRegistry for the generator'); + + $generator = new SchemaGenerator(); + $refObject = new \ReflectionObject($generator); + $refProperty = $refObject->getProperty('classRegistry'); + $refProperty->setAccessible( true ); + $refProperty->setValue($generator, null); + $generator->generate(); + } } diff --git a/tests/Unit/Merger/SchemaMergerTest.php b/tests/Unit/Merger/SchemaMergerTest.php index 263d3a3..1558c21 100644 --- a/tests/Unit/Merger/SchemaMergerTest.php +++ b/tests/Unit/Merger/SchemaMergerTest.php @@ -39,6 +39,14 @@ public function testGetOutputDirectory() self::assertEquals($outputDirectory, $merger->getOutputDirectory()); } + public function testSetOutputDirectory() + { + $outputDirectory = '/root'; + $merger = new SchemaMerger(); + $merger->setOutputDirectory($outputDirectory); + self::assertEquals($outputDirectory, $merger->getOutputDirectory()); + } + public function testGetResolvedSchemaTemplateThrowsException() { self::expectException(\AvroSchemaParseException::class); @@ -578,6 +586,31 @@ public function testExportSchemaPrimitiveWithWrongOptions() unlink('/tmp/test.avsc'); } + public function testMergeWithoutRegistry() + { + self::expectException(\RuntimeException::class); + self::expectExceptionMessage('Please set a SchemaRegistery for the merger'); + $merger = new SchemaMerger(); + $refObject = new \ReflectionObject($merger); + $refProperty = $refObject->getProperty('schemaRegistry'); + $refProperty->setAccessible( true ); + $refProperty->setValue($merger, null); + + $merger->merge(); + } + + public function testGetResolvedSchemaTemplateWithoutRegistry() + { + self::expectException(\RuntimeException::class); + self::expectExceptionMessage('Please set a SchemaRegistery for the merger'); + $merger = new SchemaMerger(); + $refObject = new \ReflectionObject($merger); + $refProperty = $refObject->getProperty('schemaRegistry'); + $refProperty->setAccessible( true ); + $refProperty->setValue($merger, null); + $merger->getResolvedSchemaTemplate($this->getMockForAbstractClass(SchemaTemplateInterface::class)); + } + private function reformatJsonString(string $jsonString): string { return json_encode(json_decode($jsonString, false, JSON_THROW_ON_ERROR), JSON_THROW_ON_ERROR); diff --git a/tests/Unit/PhpClassConverterTest.php b/tests/Unit/PhpClassConverterTest.php new file mode 100644 index 0000000..154971f --- /dev/null +++ b/tests/Unit/PhpClassConverterTest.php @@ -0,0 +1,68 @@ +getMockForAbstractClass(PhpClassPropertyInterface::class); + $property1->expects(self::once())->method('getPropertyType')->willReturn(1); + $property2 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); + $property2->expects(self::exactly(2))->method('getPropertyType')->willReturn('string|array|int[]|mixed[]'); + $property3 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); + $property3->expects(self::exactly(2))->method('getPropertyType')->willReturn('string'); + $property4 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); + $property4->expects(self::exactly(2))->method('getPropertyType')->willReturn('object|XYZ|UC'); + $property5 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); + $property5->expects(self::exactly(2))->method('getPropertyType')->willReturn('mixed'); + $property6 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); + $property6->expects(self::exactly(2))->method('getPropertyType')->willReturn('array|mixed[]'); + + + $parser = $this->getMockForAbstractClass(ClassParserInterface::class); + $parser->expects(self::once())->method('setCode')->with('some class stuff'); + $parser->expects(self::exactly(2))->method('getClassName')->willReturn('foo'); + $parser->expects(self::once())->method('getProperties')->willReturn( + [$property1, $property2, $property3, $property4, $property5, $property6] + ); + $parser->expects(self::exactly(2))->method('getUsedClasses')->willReturn(['XYZ' => 'a\\b\\ZYX']); + $parser->expects(self::exactly(3))->method('getNamespace')->willReturn('x\\y'); + + $converter = new PhpClassConverter($parser); + self::assertInstanceOf(PhpClassInterface::class, $converter->convert('some class stuff')); + } + + public function testConvertWithNoNamesace(): void + { + $property1 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); + $property1->expects(self::exactly(2))->method('getPropertyType')->willReturn('ABC'); + + + $parser = $this->getMockForAbstractClass(ClassParserInterface::class); + $parser->expects(self::once())->method('setCode')->with('some class stuff'); + $parser->expects(self::exactly(2))->method('getClassName')->willReturn('foo'); + $parser->expects(self::once())->method('getProperties')->willReturn([$property1]); + $parser->expects(self::exactly(1))->method('getUsedClasses')->willReturn([]); + $parser->expects(self::exactly(2))->method('getNamespace')->willReturn(null); + + $converter = new PhpClassConverter($parser); + self::assertInstanceOf(PhpClassInterface::class, $converter->convert('some class stuff')); + } + + public function testConvertOfNonClass(): void + { + $parser = $this->getMockForAbstractClass(ClassParserInterface::class); + $parser->expects(self::once())->method('getClassName')->willReturn(null); + $converter = new PhpClassConverter($parser); + self::assertNull($converter->convert('some class stuff')); + } +} \ No newline at end of file From 3718670ae351d2f5a526877eb078d1c564071de6 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Sat, 18 Dec 2021 03:59:06 +0100 Subject: [PATCH 22/27] save work --- src/Parser/ClassParser.php | 6 ++++ tests/Integration/Parser/ClassParserTest.php | 34 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index f87e136..ec5e937 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -78,6 +78,12 @@ public function getParentClassName(): ?string } } } + } else { + if ($statement instanceof Class_) { + if (null !== $statement->extends) { + return implode('\\', $statement->extends->parts); + } + } } } diff --git a/tests/Integration/Parser/ClassParserTest.php b/tests/Integration/Parser/ClassParserTest.php index a863fc2..7ef9793 100644 --- a/tests/Integration/Parser/ClassParserTest.php +++ b/tests/Integration/Parser/ClassParserTest.php @@ -58,4 +58,38 @@ public function testGetProperties() self::assertInstanceOf(PhpClassPropertyInterface::class, $property); } } + + public function testClassAndNamespaceAreNullWithNoCode(): void + { + $propertyParser = new ClassPropertyParser(new DocCommentParser()); + $parser = new ClassParser((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $propertyParser); + $refObject = new \ReflectionObject($parser); + $refProperty = $refObject->getProperty('statements'); + $refProperty->setAccessible( true ); + $refProperty->setValue($parser, null); + self::assertNull($parser->getClassName()); + self::assertNull($parser->getNamespace()); + self::assertNull($parser->getParentClassName()); + self::assertEquals([], $parser->getUsedClasses()); + } + + public function testClassWithNoParent(): void + { + $propertyParser = new ClassPropertyParser(new DocCommentParser()); + $parser = new ClassParser((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $propertyParser); + $parser->setCode('getNamespace()); + self::assertNull($parser->getParentClassName()); + self::assertEquals([], $parser->getProperties()); + + } + + public function testClassWithNoParentFile(): void + { + $propertyParser = new ClassPropertyParser(new DocCommentParser()); + $parser = new ClassParser((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $propertyParser); + $parser->setCode('getProperties()); + + } } From d040555c5a6c566d2f3937b14c0b8e4e25c7231f Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Sat, 18 Dec 2021 04:04:10 +0100 Subject: [PATCH 23/27] save work --- .../Parser/DocCommentParserTest.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/Integration/Parser/DocCommentParserTest.php diff --git a/tests/Integration/Parser/DocCommentParserTest.php b/tests/Integration/Parser/DocCommentParserTest.php new file mode 100644 index 0000000..a15e6cb --- /dev/null +++ b/tests/Integration/Parser/DocCommentParserTest.php @@ -0,0 +1,31 @@ +parseDoc('/** + * @var string + asdf some text + */'); + + self::assertEquals( + [ + 'var' => 'string', + 'function-description' =>'' + ], + $result + ); + } +} \ No newline at end of file From 14992afdf0d607d285b0912f39f13121e7c53081 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Sat, 18 Dec 2021 04:34:03 +0100 Subject: [PATCH 24/27] save work --- src/Parser/ClassParser.php | 2 + tests/Unit/Parser/ClassPropertyParserTest.php | 59 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/Unit/Parser/ClassPropertyParserTest.php diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index ec5e937..e23a069 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -204,7 +204,9 @@ private function getParentClassStatements(): ?array $parentClass = file_get_contents($filename); if (false === $parentClass) { + // @codeCoverageIgnoreStart return []; + // @codeCoverageIgnoreEnd } return $this->parser->parse($parentClass); diff --git a/tests/Unit/Parser/ClassPropertyParserTest.php b/tests/Unit/Parser/ClassPropertyParserTest.php new file mode 100644 index 0000000..5566342 --- /dev/null +++ b/tests/Unit/Parser/ClassPropertyParserTest.php @@ -0,0 +1,59 @@ +getMockBuilder(Doc::class)->disableOriginalConstructor()->getMock(); + $varId = $this->getMockBuilder(VarLikeIdentifier::class)->disableOriginalConstructor()->getMock(); + $varId->name = 'bla'; + $identifier = $this->getMockBuilder(Identifier::class)->disableOriginalConstructor()->getMock(); + $identifier->name = 'int'; + $ut = $this->getMockBuilder(UnionType::class)->disableOriginalConstructor()->getMock(); + $ut->types = [$identifier]; + $propertyProperty = $this->getMockBuilder(PropertyProperty::class)->disableOriginalConstructor()->getMock(); + $propertyProperty->name = $varId; + $doc->expects(self::once())->method('getText')->willReturn('bla'); + $docParser = $this->getMockForAbstractClass(DocCommentParserInterface::class); + $property1 = $this->getMockBuilder(Property::class)->disableOriginalConstructor()->getMock(); + $property1->expects(self::once())->method('getAttributes')->willReturn(['comments' => [$doc]]); + $property1->props = [$propertyProperty]; + $property1->type = $identifier; + $property2 = $this->getMockBuilder(Property::class)->disableOriginalConstructor()->getMock(); + $property2->type = 'string'; + $property2->props = [$propertyProperty]; + $property3 = $this->getMockBuilder(Property::class)->disableOriginalConstructor()->getMock(); + $property3->type = $ut; + $property3->props = [$propertyProperty]; + $cpp = new ClassPropertyParser($docParser); + + self::assertInstanceOf(PhpClassPropertyInterface::class, $cpp->parseProperty($property1)); + self::assertInstanceOf(PhpClassPropertyInterface::class, $cpp->parseProperty($property2)); + self::assertInstanceOf(PhpClassPropertyInterface::class, $cpp->parseProperty($property3)); + } + + public function testParsePropertyExceptionOnNonProperty(): void + { + self::expectException(\RuntimeException::class); + self::expectExceptionMessage('Property must be of type: PhpParser\Node\Stmt\Property'); + $docParser = $this->getMockForAbstractClass(DocCommentParserInterface::class); + $cpp = new ClassPropertyParser($docParser); + + $cpp->parseProperty(1); + } +} \ No newline at end of file From 4e6175b2fa3f3e2770fec3fd84074eac25bc384a Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Sat, 18 Dec 2021 04:37:29 +0100 Subject: [PATCH 25/27] stan --- src/Generator/SchemaGenerator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Generator/SchemaGenerator.php b/src/Generator/SchemaGenerator.php index 7e4ec1c..9e99bc6 100644 --- a/src/Generator/SchemaGenerator.php +++ b/src/Generator/SchemaGenerator.php @@ -83,10 +83,10 @@ public function generate(): array $schema['fields'][] = $field; } - $namespace = $schema['namespace'] . '.' . $schema['name']; - - if (null === $schema['namespace']) { + if (false === isset($schema['namespace'])) { $namespace = $schema['name']; + } else { + $namespace = $schema['namespace'] . '.' . $schema['name']; } $schemas[$namespace] = json_encode($schema); From 9ea4fab37395a902e4d24d724d48f4b20655f2b1 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Sat, 18 Dec 2021 04:42:58 +0100 Subject: [PATCH 26/27] exclude new parser --- .codeclimate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.codeclimate.yml b/.codeclimate.yml index baa6b94..c3ec879 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -43,6 +43,7 @@ checks: threshold: #language-specific defaults. overrides affect all languages. exclude_patterns: - "src/Command" + - "src/Parser/ClassParser.php" - "tests/" - "**/vendor/" - "example/" From 35fd28761a0b86d98533d948f5165d6e806f1c28 Mon Sep 17 00:00:00 2001 From: Nick Chiu Date: Sat, 18 Dec 2021 04:44:46 +0100 Subject: [PATCH 27/27] increase func max --- .codeclimate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index c3ec879..e9dc751 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -24,7 +24,7 @@ checks: method-lines: enabled: true config: - threshold: 25 + threshold: 30 nested-control-flow: enabled: true config: