diff --git a/example/classes_minimal/EpamRole.php b/example/classes_minimal/EpamRole.php new file mode 100644 index 0000000..c261000 --- /dev/null +++ b/example/classes_minimal/EpamRole.php @@ -0,0 +1,226 @@ +code; +} + +/** +* Set code - code +* @param string|null $code +* @return \PhpKafka\PhpAvroSchemaGenerator\Example\Minimal\EpamRole +*/ +public function setCode(?string $code) +{ + $this->code = $code; + + return $this; +} + +/** +* Get name - name +* @return string|null +*/ +public function getName(): ?string +{ + return $this->name; +} + +/** +* Set name - name +* @param string|null $name +* @return \PhpKafka\PhpAvroSchemaGenerator\Example\Minimal\EpamRole +*/ +public function setName(?string $name) +{ + $this->name = $name; + + return $this; +} + +/** +* Get fullName - fullName +* @return string|null +*/ +public function getFullName(): ?string +{ + return $this->fullName; +} + +/** +* Set fullName - fullName +* @param string|null $fullName +* @return \PhpKafka\PhpAvroSchemaGenerator\Example\Minimal\EpamRole +*/ +public function setFullName(?string $fullName) +{ + $this->fullName = $fullName; + + return $this; +} + +/** +* Get masterSystem - masterSystem +* @return string[]|null +*/ +public function getMasterSystem(): ?array +{ + $data = $this->masterSystem; + return $data; +} + +/** +* Set masterSystem - masterSystem +* @param string[]|null $masterSystem +* @return \PhpKafka\PhpAvroSchemaGenerator\Example\Minimal\EpamRole +*/ +public function setMasterSystem(?array $masterSystem) +{ + $this->masterSystem = $masterSystem; + return $this; +} + +/** +* Get roleSubject - subject +* @return string|null +*/ +public function getRoleSubject(): ?string +{ + $data = $this->roleSubject; + return $data; +} + +/** +* Set roleSubject - subject +* @param string|null $roleSubject +* @return \PhpKafka\PhpAvroSchemaGenerator\Example\Minimal\EpamRole +*/ +public function setRoleSubject(?string $roleSubject) +{ + $this->roleSubject = $roleSubject; + return $this; +} + +/** +* Get roleObject - roleObject +* @return string[]|null +*/ +public function getRoleObject(): ?array +{ + $data = $this->roleObject; + return $data; +} + +/** +* Set roleObject - roleObject +* @param string[]|null $roleObject +* @return \PhpKafka\PhpAvroSchemaGenerator\Example\Minimal\EpamRole +*/ +public function setRoleObject(?array $roleObject) +{ + $this->roleObject = $roleObject; + return $this; +} + +/** +* Get topic - topic +* @return string[]|null +*/ +public function getTopic(): ?array +{ + $data = $this->topic; + return $data; +} + +/** +* Set topic - topic +* @param string[]|null $topic +* @return \PhpKafka\PhpAvroSchemaGenerator\Example\Minimal\EpamRole +*/ +public function setTopic(?array $topic) +{ + $this->topic = $topic; + return $this; +} + +/** +* Get description - description +* @return string|null +*/ +public function getDescription(): ?string +{ + $data = $this->description; + return $data; +} + +/** +* Set description - description +* @param string|null $description +* @return \PhpKafka\PhpAvroSchemaGenerator\Example\Minimal\EpamRole +*/ +public function setDescription(?string $description) +{ + $this->description = $description; + return $this; +} + +} diff --git a/src/Converter/PhpClassConverter.php b/src/Converter/PhpClassConverter.php index 542d1f5..16a0bb9 100644 --- a/src/Converter/PhpClassConverter.php +++ b/src/Converter/PhpClassConverter.php @@ -67,16 +67,15 @@ public function convert(string $phpClass): ?PhpClassInterface * @param PhpClassPropertyInterface[] $properties * @return PhpClassPropertyInterface[] */ - private function getConvertedProperties(array $properties): array + protected function getConvertedProperties(array $properties): array { $convertedProperties = []; foreach ($properties as $property) { - if (false === is_string($property->getPropertyType())) { - continue; + $convertedType = $property->getPropertyType(); + if (is_string($convertedType)) { + $convertedType = $this->getConvertedType($property->getPropertyType()); } - $convertedType = $this->getConvertedType($property->getPropertyType()); - if (null === $convertedType) { continue; } @@ -97,7 +96,7 @@ private function getConvertedProperties(array $properties): array * @param string $type * @return string|string[]|null */ - private function getConvertedType(string $type) + protected function getConvertedType(string $type) { $types = explode('|', $type); @@ -143,7 +142,7 @@ private function getFullTypeName(string $type, bool $isUnionType = false): ?stri * @param string[] $types * @return array */ - private function getConvertedUnionType(array $types): array + protected function getConvertedUnionType(array $types): array { $convertedUnionType = []; @@ -168,7 +167,7 @@ private function getConvertedUnionType(array $types): array * @param string[] $types * @return string[] */ - private function getArrayType(array $types): array + protected function getArrayType(array $types): array { $itemPrefix = '['; $itemSuffix = ']'; @@ -200,7 +199,7 @@ private function getArrayType(array $types): array * @param string[] $types * @return string[] */ - private function getArrayTypes(array $types): array + protected function getArrayTypes(array $types): array { $arrayTypes = []; @@ -217,7 +216,7 @@ private function getArrayTypes(array $types): array * @param string[] $arrayTypes * @return string[] */ - private function getCleanedArrayTypes(array $arrayTypes): array + protected function getCleanedArrayTypes(array $arrayTypes): array { foreach ($arrayTypes as $idx => $arrayType) { if ('array' === $arrayType) { @@ -238,7 +237,7 @@ private function getCleanedArrayTypes(array $arrayTypes): array return $arrayTypes; } - private function isArrayType(string $type): bool + protected function isArrayType(string $type): bool { if ('array' === $type || str_contains($type, '[]')) { return true; @@ -247,7 +246,7 @@ private function isArrayType(string $type): bool return false; } - private function convertNamespace(string $namespace): string + protected function convertNamespace(string $namespace): string { return str_replace('\\', '.', $namespace); } diff --git a/src/Exception/SchemaGeneratorException.php b/src/Exception/SchemaGeneratorException.php new file mode 100644 index 0000000..19923a3 --- /dev/null +++ b/src/Exception/SchemaGeneratorException.php @@ -0,0 +1,7 @@ + + * @throws SchemaGeneratorException */ private function getFieldForProperty(PhpClassPropertyInterface $property): array { @@ -106,6 +108,9 @@ private function getFieldForProperty(PhpClassPropertyInterface $property): array if (PhpClassPropertyInterface::NO_DEFAULT !== $property->getPropertyDefault()) { $field['default'] = $property->getPropertyDefault(); + if (null === $field['default'] and !$field['type']->isNullable()){ + throw new SchemaGeneratorException('Provided default value "null", but that type does not present as first possible in union (see https://avro.apache.org/docs/current/spec.html#Unions)!'); + } } if (null !== $property->getPropertyDoc() && '' !== $property->getPropertyDoc()) { diff --git a/src/Parser/AvroClassPropertyParser.php b/src/Parser/AvroClassPropertyParser.php new file mode 100644 index 0000000..b8835be --- /dev/null +++ b/src/Parser/AvroClassPropertyParser.php @@ -0,0 +1,39 @@ +getPropertyName(), 'o_') or str_starts_with($prop->getPropertyName(), '_') + or in_array($prop->getPropertyName(), ['omitMandatoryCheck', 'allLazyKeysMarkedAsLoaded'])) { + throw new SkipPropertyException(); + } +// return $prop; + $prop_ = new PhpClassProperty( + $prop->getPropertyName(), + // make type nullable. Can't now in array. See https://github.com/php-kafka/php-avro-schema-generator/issues/33#issuecomment-1007490595 + $prop->getPropertyType()->isNullable() ? $prop->getPropertyType() : new PhpClassPropertyType(new PhpClassPropertyTypeItem('null'), ...$prop->getPropertyType()->getTypeItems()), +// 'null|' . $prop->getPropertyType(), // make type nullable // See https://github.com/php-kafka/php-avro-schema-generator/issues/33#issuecomment-1007551821 + // and only in string work. See https://github.com/php-kafka/php-avro-schema-generator/issues/33#issuecomment-1007490595 + ($prop->getPropertyDefault() != PhpClassPropertyInterface::NO_DEFAULT ?: null), + $prop->getPropertyDoc(), + $prop->getPropertyLogicalType() + ); + return $prop_; + } +} diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index c35f8f5..1162eff 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -4,6 +4,7 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Parser; +use PhpKafka\PhpAvroSchemaGenerator\Exception\SkipPropertyException; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyInterface; use PhpParser\Node\Identifier; use PhpParser\Node\Name; @@ -183,7 +184,10 @@ private function getAllClassProperties(Class_ $class, array $properties): array { foreach ($class->stmts as $pStatement) { if ($pStatement instanceof Property) { - $properties[] = $this->propertyParser->parseProperty($pStatement); + try { + $properties[] = $this->propertyParser->parseProperty($pStatement); + } + catch(SkipPropertyException $skip){ } } } @@ -196,8 +200,10 @@ private function getAllClassProperties(Class_ $class, array $properties): array */ private function getParentClassStatements(): ?array { - /** @var class-string[] $usedClasses */ - $usedClasses = $this->getUsedClasses(); + $parentClass = $this->getParentClassName(); // Especially for speedup and tests! Do not use Reflection when parser show no any parents. + if (null === $parentClass) { + return []; + } try { $pc = (new ReflectionClass($this->getNamespace() . '\\' . $this->getClassName()))->getParentClass(); @@ -225,4 +231,40 @@ private function getParentClassStatements(): ?array return $this->parser->parse($parentClass); } + +// /** +// * @return Stmt[]|null +// * @throws ReflectionException +// */ +// private function getParentClassStatements(): ?array +// { +// /** @var class-string[] $usedClasses */ +// $usedClasses = $this->getUsedClasses(); +// $parentClass = $this->getParentClassName(); +// +// if (null === $parentClass) { +// return []; +// } +// +// if (null !== $usedClasses[$this->getParentClassName()]) { +// $parentClass = $usedClasses[$this->getParentClassName()]; +// } +// +// $rc = new ReflectionClass($parentClass); +// $filename = $rc->getFileName(); +// +// if (false === $filename) { +// return []; +// } +// +// $parentClass = file_get_contents($filename); +// +// if (false === $parentClass) { +// // @codeCoverageIgnoreStart +// return []; +// // @codeCoverageIgnoreEnd +// } +// +// return $this->parser->parse($parentClass); +// } } diff --git a/src/Parser/ClassPropertyParser.php b/src/Parser/ClassPropertyParser.php index 5c813e1..1639f29 100644 --- a/src/Parser/ClassPropertyParser.php +++ b/src/Parser/ClassPropertyParser.php @@ -4,9 +4,13 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Parser; +use JetBrains\PhpStorm\Pure; use PhpKafka\PhpAvroSchemaGenerator\Avro\Avro; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassProperty; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyInterface; +use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyType; +use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyTypeInterface; +use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyTypeItem; use PhpParser\Comment\Doc; use PhpParser\Node\Identifier; use PhpParser\Node\NullableType; @@ -14,7 +18,7 @@ use PhpParser\Node\UnionType; use RuntimeException; -final class ClassPropertyParser implements ClassPropertyParserInterface +class ClassPropertyParser implements ClassPropertyParserInterface { private DocCommentParserInterface $docParser; @@ -77,49 +81,73 @@ private function getPropertyName(Property $property): string /** * @param Property $property * @param array $docComments - * @return string + * @return PhpClassPropertyTypeInterface */ - private function getPropertyType(Property $property, array $docComments): string + private function getPropertyType(Property $property, array $docComments): PhpClassPropertyTypeInterface { if ($property->type instanceof NullableType) { if ($property->type->type instanceof Identifier) { - $type = Avro::MAPPED_TYPES[$property->type->type->name] ?? $property->type->type->name; - return 'null|' . $type; + return new PhpClassPropertyType(new PhpClassPropertyTypeItem('null'), $this->mapPropertyTypeItem($property->type->type->name)); } } elseif ($property->type instanceof Identifier) { - return Avro::MAPPED_TYPES[$property->type->name] ?? $property->type->name; + return new PhpClassPropertyType($this->mapPropertyTypeItem($property->type->name)); } elseif ($property->type instanceof UnionType) { - $types = ''; - $separator = ''; - /** @var Identifier $type */ - foreach ($property->type->types as $type) { - $type = Avro::MAPPED_TYPES[$type->name] ?? $type->name; - $types .= $separator . $type; - $separator = '|'; - } - - return $types; + return new PhpClassPropertyType( + ...array_map( + function($type){ + return new PhpClassPropertyTypeItem($type->name); + }, + $property->type->types) + ); } - return $this->getDocCommentByType($docComments, 'var') ?? 'string'; + return $this->getDocCommentByType($docComments, 'var'); + } + + /** + * @param string $typeName values like 'string', or 'string|int', or 'string|int[]' + * @return PhpClassPropertyType + */ + protected function mapPropertyType(string $typeName): PhpClassPropertyType + { + return new PhpClassPropertyType( + ...array_map([$this, 'mapPropertyTypeItem'], explode('|', $typeName)) + ); + } + + /** + * @param string $typeName Handle single type item like: 'string', 'string[]' + * @return PhpClassPropertyTypeItem + */ + protected function mapPropertyTypeItem(string $typeName): PhpClassPropertyTypeItem + { + $arr = explode('[]', $typeName); + $itemTypeName = $arr[0]; + + return new PhpClassPropertyTypeItem(Avro::MAPPED_TYPES[$itemTypeName] ?? $itemTypeName, count($arr) > 1); } /** * @param array $docComments - * @return mixed + * @param string $type + * @return PhpClassPropertyType */ - private function getDocCommentByType(array $docComments, string $type) + private function getDocCommentByType(array $docComments, string $type): PhpClassPropertyType { - return $docComments[$type] ?? null; + return isset($docComments[$type]) + ? $this->mapPropertyType($docComments[$type]) + : new PhpClassPropertyType(); } /** * @param array $docComments - * @return string|null + * @return null|PhpClassPropertyType */ - private function getTypeFromDocComment(array $docComments): ?string + private function getTypeFromDocComment(array $docComments): ?PhpClassPropertyType { - return $docComments['avro-type'] ?? null; + if (!isset($docComments['avro-type'])) return null; + + return $this->mapPropertyType($docComments['avro-type']); } /** diff --git a/src/Parser/ClassPropertyParserInterface.php b/src/Parser/ClassPropertyParserInterface.php index f180109..79c5025 100644 --- a/src/Parser/ClassPropertyParserInterface.php +++ b/src/Parser/ClassPropertyParserInterface.php @@ -4,6 +4,7 @@ namespace PhpKafka\PhpAvroSchemaGenerator\Parser; +use PhpKafka\PhpAvroSchemaGenerator\Exception\SkipPropertyException; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyInterface; use PhpParser\Node\Stmt\Property; @@ -12,6 +13,7 @@ interface ClassPropertyParserInterface /** * @param Property|mixed $property * @return PhpClassPropertyInterface + * @throws SkipPropertyException Such property will then just skipped */ public function parseProperty($property): PhpClassPropertyInterface; } diff --git a/src/PhpClass/PhpClassProperty.php b/src/PhpClass/PhpClassProperty.php index 099d2a9..751aac7 100644 --- a/src/PhpClass/PhpClassProperty.php +++ b/src/PhpClass/PhpClassProperty.php @@ -4,8 +4,6 @@ namespace PhpKafka\PhpAvroSchemaGenerator\PhpClass; -use PhpKafka\PhpAvroSchemaGenerator\Parser\PropertyAttributesInterface; - final class PhpClassProperty implements PhpClassPropertyInterface { /** @var mixed */ @@ -13,20 +11,18 @@ final class PhpClassProperty implements PhpClassPropertyInterface private ?string $propertyDoc; private ?string $propertyLogicalType; private string $propertyName; - - /** @var string|string[] */ - private $propertyType; + private PhpClassPropertyTypeInterface $propertyType; /** * @param string $propertyName - * @param string[]|string $propertyType + * @param PhpClassPropertyTypeInterface $propertyType * @param null|mixed $propertyDefault * @param null|string $propertyDoc * @param null|string $propertyLogicalType */ public function __construct( string $propertyName, - $propertyType, + PhpClassPropertyTypeInterface $propertyType, $propertyDefault = self::NO_DEFAULT, ?string $propertyDoc = null, ?string $propertyLogicalType = null @@ -61,10 +57,7 @@ public function getPropertyName(): string return $this->propertyName; } - /** - * @return string[]|string - */ - public function getPropertyType() + public function getPropertyType(): PhpClassPropertyTypeInterface { return $this->propertyType; } diff --git a/src/PhpClass/PhpClassPropertyInterface.php b/src/PhpClass/PhpClassPropertyInterface.php index 5a4d716..c9c9c51 100644 --- a/src/PhpClass/PhpClassPropertyInterface.php +++ b/src/PhpClass/PhpClassPropertyInterface.php @@ -21,8 +21,5 @@ public function getPropertyLogicalType(): ?string; public function getPropertyName(): string; - /** - * @return string[]|string - */ - public function getPropertyType(); + public function getPropertyType(): PhpClassPropertyTypeInterface; } diff --git a/src/PhpClass/PhpClassPropertyType.php b/src/PhpClass/PhpClassPropertyType.php new file mode 100644 index 0000000..1dea4f9 --- /dev/null +++ b/src/PhpClass/PhpClassPropertyType.php @@ -0,0 +1,43 @@ +types = $types; + } + + /** + * @inheritdoc + */ + public function getTypeItems(): array + { + return $this->types; + } + + public function isNullable(): bool + { + return (bool)current(array_filter($this->types, function ($type) { return !$type->isArray() && 'null' == $type->getItemType(); })); + } + + public function jsonSerialize(): mixed + { + if (0 == count($this->types)){ + return []; + } + if (1 == count($this->types)){ + return $this->types[0]; + } + return $this->types; + } +} diff --git a/src/PhpClass/PhpClassPropertyTypeInterface.php b/src/PhpClass/PhpClassPropertyTypeInterface.php new file mode 100644 index 0000000..dbbcd95 --- /dev/null +++ b/src/PhpClass/PhpClassPropertyTypeInterface.php @@ -0,0 +1,18 @@ +isArray = $isArray; + $this->itemType = $itemType; + } + + public function isArray(): bool + { + return $this->isArray; + } + + public function getItemType(): string + { + return $this->itemType; + } + + public function jsonSerialize() + { + if ($this->isArray()){ + return [ + 'type' => 'array', + 'items' => $this->getItemType() + ]; + } + else { + return $this->getItemType(); + } + } +} diff --git a/src/PhpClass/PhpClassPropertyTypeItemInterface.php b/src/PhpClass/PhpClassPropertyTypeItemInterface.php new file mode 100644 index 0000000..3726736 --- /dev/null +++ b/src/PhpClass/PhpClassPropertyTypeItemInterface.php @@ -0,0 +1,15 @@ +getProperties(); self::assertEquals(1, count($properties)); - self::assertEquals('null|string', $properties[0]->getPropertyType()); + self::assertEquals( + new PhpClassPropertyType(new PhpClassPropertyTypeItem('null'), new PhpClassPropertyTypeItem('string')), + $properties[0]->getPropertyType() + ); } public function testClassWithUnionType(): void @@ -111,7 +116,10 @@ class foo { '); $properties = $parser->getProperties(); self::assertEquals(1, count($properties)); - self::assertEquals('int|string', $properties[0]->getPropertyType()); + self::assertEquals( + new PhpClassPropertyType(new PhpClassPropertyTypeItem('int'), new PhpClassPropertyTypeItem('string')), + $properties[0]->getPropertyType() + ); } public function testClassWithDocUnionType(): void @@ -129,7 +137,10 @@ class foo { '); $properties = $parser->getProperties(); self::assertEquals(1, count($properties)); - self::assertEquals('int|string', $properties[0]->getPropertyType()); + self::assertEquals( + new PhpClassPropertyType(new PhpClassPropertyTypeItem('int'), new PhpClassPropertyTypeItem('string')), + $properties[0]->getPropertyType() + ); } public function testClassWithAnnotations(): void @@ -150,7 +161,7 @@ class foo { '); $properties = $parser->getProperties(); self::assertEquals(1, count($properties)); - self::assertEquals('string', $properties[0]->getPropertyType()); + self::assertEquals(new PhpClassPropertyType(new PhpClassPropertyTypeItem('string')), $properties[0]->getPropertyType()); self::assertEquals('abc def', $properties[0]->getPropertyDefault()); self::assertEquals('some doc bla bla', $properties[0]->getPropertyDoc()); diff --git a/tests/Integration/Registry/ClassRegistryTest.php b/tests/Integration/Registry/ClassRegistryTest.php index 7812c9c..d451aae 100644 --- a/tests/Integration/Registry/ClassRegistryTest.php +++ b/tests/Integration/Registry/ClassRegistryTest.php @@ -6,6 +6,8 @@ use PhpKafka\PhpAvroSchemaGenerator\Converter\PhpClassConverter; use PhpKafka\PhpAvroSchemaGenerator\Exception\ClassRegistryException; +use PhpKafka\PhpAvroSchemaGenerator\Generator\SchemaGenerator; +use PhpKafka\PhpAvroSchemaGenerator\Parser\AvroClassPropertyParser; use PhpKafka\PhpAvroSchemaGenerator\Parser\ClassParser; use PhpKafka\PhpAvroSchemaGenerator\Parser\ClassPropertyParser; use PhpKafka\PhpAvroSchemaGenerator\Parser\DocCommentParser; @@ -54,6 +56,31 @@ public function testLoad() } } + public function testLoadMinimal() { + $classDir = __DIR__ . '/../../../example/classes_minimal'; + +// $propertyParser = new ClassPropertyParser(new DocCommentParser()); + $propertyParser = new AvroClassPropertyParser(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); + + $classes = $registry->getClasses(); + + self::assertCount(1, $classes); + + foreach ($classes as $class) { + self::assertInstanceOf(PhpClassInterface::class, $class); + } + + $generator = new SchemaGenerator(''); + $generator->setClassRegistry($registry); + $schemas = $generator->generate(); + self::assertCount(1, $schemas); + } + public function testRegisterSchemaFileThatDoesntExist() { $fileInfo = new SplFileInfo('somenonexistingfile'); diff --git a/tests/Unit/Generator/SchemaGeneratorTest.php b/tests/Unit/Generator/SchemaGeneratorTest.php index 0a798e6..5f9b50a 100644 --- a/tests/Unit/Generator/SchemaGeneratorTest.php +++ b/tests/Unit/Generator/SchemaGeneratorTest.php @@ -5,6 +5,8 @@ use PhpKafka\PhpAvroSchemaGenerator\Generator\SchemaGenerator; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassInterface; use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyInterface; +use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyType; +use PhpKafka\PhpAvroSchemaGenerator\PhpClass\PhpClassPropertyTypeItem; use PhpKafka\PhpAvroSchemaGenerator\Registry\ClassRegistryInterface; use PHPUnit\Framework\TestCase; @@ -77,12 +79,12 @@ public function testGenerate() ]; $property1 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); - $property1->expects(self::exactly(1))->method('getPropertyType')->willReturn(["type" => "array","items" => "test.foo"]); + $property1->expects(self::exactly(1))->method('getPropertyType')->willReturn(new PhpClassPropertyType(new PhpClassPropertyTypeItem('test.foo', true))); $property1->expects(self::exactly(1))->method('getPropertyName')->willReturn('items'); $property1->expects(self::exactly(1))->method('getPropertyDefault')->willReturn(PhpClassPropertyInterface::NO_DEFAULT); $property2 = $this->getMockForAbstractClass(PhpClassPropertyInterface::class); - $property2->expects(self::exactly(2))->method('getPropertyType')->willReturn('string'); + $property2->expects(self::exactly(2))->method('getPropertyType')->willReturn(new PhpClassPropertyType(new PhpClassPropertyTypeItem('string', ))); $property2->expects(self::exactly(2))->method('getPropertyName')->willReturn('name'); $property2->expects(self::exactly(4))->method('getPropertyDefault')->willReturn('test'); $property2->expects(self::exactly(6))->method('getPropertyDoc')->willReturn('test');