diff --git a/lib/Doctrine/Common/Annotations/DocParser.php b/lib/Doctrine/Common/Annotations/DocParser.php index 4133fe5f4..5ec150d3e 100644 --- a/lib/Doctrine/Common/Annotations/DocParser.php +++ b/lib/Doctrine/Common/Annotations/DocParser.php @@ -613,6 +613,10 @@ class_exists(NamedArgumentConstructor::class); $metadata['default_property'] = reset($metadata['properties']); } elseif ($metadata['has_named_argument_constructor']) { foreach ($constructor->getParameters() as $parameter) { + if ($parameter->isVariadic()) { + break; + } + $metadata['constructor_args'][$parameter->getName()] = [ 'position' => $parameter->getPosition(), 'default' => $parameter->isOptional() ? $parameter->getDefaultValue() : null, @@ -942,6 +946,23 @@ private function Annotation() if (self::$annotationMetadata[$name]['has_named_argument_constructor']) { if (PHP_VERSION_ID >= 80000) { + foreach ($values as $property => $value) { + if (! isset(self::$annotationMetadata[$name]['constructor_args'][$property])) { + throw AnnotationException::creationError(sprintf( + <<<'EXCEPTION' +The annotation @%s declared on %s does not have a property named "%s" +that can be set through its named arguments constructor. +Available named arguments: %s +EXCEPTION + , + $originalName, + $this->context, + $property, + implode(', ', array_keys(self::$annotationMetadata[$name]['constructor_args'])) + )); + } + } + return $this->instantiateAnnotiation($originalName, $this->context, $name, $values); } diff --git a/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php b/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php index 297d7e90c..00400a423 100644 --- a/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php +++ b/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php @@ -1644,6 +1644,18 @@ public function testNamedArgumentsConstructorInterfaceWithDefaultValue(): void self::assertSame(1234, $result[0]->getBar()); } + public function testNamedArgumentsConstructorInterfaceWithExtraArguments(): void + { + $docParser = $this->createTestParser(); + + $this->expectException(AnnotationException::class); + $this->expectExceptionMessageMatches( + '/does not have a property named "invalid"\s.*\sAvailable named arguments: foo, bar/' + ); + + $docParser->parse('/** @NamedAnnotation(foo="baz", invalid="uh oh") */'); + } + public function testNamedArgumentsConstructorAnnotation(): void { $result = $this @@ -1692,6 +1704,18 @@ public function testNamedArgumentsConstructorAnnotationWithDefaultProperty(): vo self::assertSame(1234, $result[0]->getBar()); } + public function testNamedArgumentsConstructorAnnotationWithExtraArguments(): void + { + $docParser = $this->createTestParser(); + + $this->expectException(AnnotationException::class); + $this->expectExceptionMessageMatches( + '/does not have a property named "invalid"\s.*\sAvailable named arguments: foo, bar/' + ); + + $docParser->parse('/** @AnotherNamedAnnotation(foo="baz", invalid="uh oh") */'); + } + public function testNamedArgumentsConstructorAnnotationWithDefaultPropertyAsArray(): void { $result = $this @@ -1744,6 +1768,115 @@ public function testNamedArgumentsConstructorAnnotationWithWrongArgumentType(): } } + public function testAnnotationWithConstructorWithVariadicParamAndExtraNamedArguments(): void + { + $parser = $this->createTestParser(); + $docblock = <<<'DOCBLOCK' +/** + * @SomeAnnotationWithConstructorWithVariadicParam(name = "Some data", foo = "Foo", bar = "Bar") + */ +DOCBLOCK; + + $this->expectException(AnnotationException::class); + $this->expectExceptionMessageMatches( + '/does not have a property named "foo"\s.*\sAvailable named arguments: name/' + ); + + $parser->parse($docblock); + } + + public function testAnnotationWithConstructorWithVariadicParamAndExtraNamedArgumentsShuffled(): void + { + $parser = $this->createTestParser(); + $docblock = <<<'DOCBLOCK' +/** + * @SomeAnnotationWithConstructorWithVariadicParam(foo = "Foo", name = "Some data", bar = "Bar") + */ +DOCBLOCK; + + $this->expectException(AnnotationException::class); + $this->expectExceptionMessageMatches( + '/does not have a property named "foo"\s.*\sAvailable named arguments: name/' + ); + + $parser->parse($docblock); + } + + public function testAnnotationWithConstructorWithVariadicParamAndCombinedNamedAndPositionalArguments(): void + { + $parser = $this->createTestParser(); + $docblock = <<<'DOCBLOCK' +/** + * @SomeAnnotationWithConstructorWithVariadicParam("Some data", "Foo", bar = "Bar") + */ +DOCBLOCK; + + $this->expectException(AnnotationException::class); + $this->expectExceptionMessageMatches( + '/does not have a property named "bar"\s.*\sAvailable named arguments: name/' + ); + + $parser->parse($docblock); + } + + public function testAnnotationWithConstructorWithVariadicParamPassOneNamedArgument(): void + { + $parser = $this->createTestParser(); + $docblock = <<<'DOCBLOCK' +/** + * @SomeAnnotationWithConstructorWithVariadicParam(name = "Some data", data = "Foo") + */ +DOCBLOCK; + + $this->expectException(AnnotationException::class); + $this->expectExceptionMessageMatches( + '/does not have a property named "data"\s.*\sAvailable named arguments: name/' + ); + + $parser->parse($docblock); + } + + public function testAnnotationWithConstructorWithVariadicParamPassPositionalArguments(): void + { + $parser = $this->createTestParser(); + $docblock = <<<'DOCBLOCK' +/** + * @SomeAnnotationWithConstructorWithVariadicParam("Some data", "Foo", "Bar") + */ +DOCBLOCK; + + $result = $parser->parse($docblock); + self::assertCount(1, $result); + $annot = $result[0]; + + self::assertInstanceOf(SomeAnnotationWithConstructorWithVariadicParam::class, $annot); + + self::assertSame('Some data', $annot->name); + // Positional extra arguments will be ignored + self::assertSame([], $annot->data); + } + + public function testAnnotationWithConstructorWithVariadicParamNoArgs(): void + { + $parser = $this->createTestParser(); + + // Without variadic arguments + $docblock = <<<'DOCBLOCK' +/** + * @SomeAnnotationWithConstructorWithVariadicParam("Some data") + */ +DOCBLOCK; + + $result = $parser->parse($docblock); + self::assertCount(1, $result); + $annot = $result[0]; + + self::assertInstanceOf(SomeAnnotationWithConstructorWithVariadicParam::class, $annot); + + self::assertSame('Some data', $annot->name); + self::assertSame([], $annot->data); + } + /** * Override for BC with PHPUnit <8 */ @@ -1850,6 +1983,25 @@ public function getBar(): int } } +/** + * @Annotation + * @NamedArgumentConstructor + */ +class SomeAnnotationWithConstructorWithVariadicParam +{ + public function __construct(string $name, string ...$data) + { + $this->name = $name; + $this->data = $data; + } + + /** @var string[] */ + public $data; + + /** @var string */ + public $name; +} + /** @Annotation */ class SettingsAnnotation {