From 16a15097f42957585c677b8829f1214eda6727ee Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 11 Oct 2020 19:57:29 +0200 Subject: [PATCH 1/5] Add compatibility with named parameters and constructor property promotion With PHP 8 the combination of named parameters and constructor property promotion will be very cool for the new Attributes feature, but also for Doctrine's Annotation feature. Especially if classes are to be used for both Attributes and Annotations, then the DocParser needs a compatible support for named parameters in annotation class constructors. For backwards compatibility this is done with a marker interface that uses a different instantiation logic than the current one. Currently all annotations properties are passed as a single array argument $values. This patch uses reflection to match annotation values to class constructor parameters. For PHP 8 the integration of named arguments with spread operator is used. A PHP 8 class that could be read using Doctrine Annotations would become: /** @Annotation */ class Table implements NamedArgumentConstructorAnnotation { public function __construct( public string $name, public array $indexes = [] ) {} } /** @Table(name="users") */ /** @Table(name="users", indexes={"foo"}) */ --- lib/Doctrine/Common/Annotations/DocParser.php | 42 ++++++++++++++++ .../NamedArgumentConstructorAnnotation.php | 11 +++++ .../Common/Annotations/DocParserTest.php | 48 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 lib/Doctrine/Common/Annotations/NamedArgumentConstructorAnnotation.php diff --git a/lib/Doctrine/Common/Annotations/DocParser.php b/lib/Doctrine/Common/Annotations/DocParser.php index 031ef55ba..a199d817d 100644 --- a/lib/Doctrine/Common/Annotations/DocParser.php +++ b/lib/Doctrine/Common/Annotations/DocParser.php @@ -502,6 +502,7 @@ class_exists(Attributes::class); $metadata = [ 'default_property' => null, 'has_constructor' => $constructor !== null && $constructor->getNumberOfParameters() > 0, + 'constructor_args' => [], 'properties' => [], 'property_types' => [], 'attribute_types' => [], @@ -510,6 +511,15 @@ class_exists(Attributes::class); 'is_annotation' => strpos($docComment, '@Annotation') !== false, ]; + if (PHP_VERSION_ID < 80000 && $class->implementsInterface(NamedArgumentConstructorAnnotation::class)) { + foreach ($constructor->getParameters() as $parameter) { + $metadata['constructor_args'][$parameter->getName()] = [ + 'position' => $parameter->getPosition(), + 'default' => $parameter->isOptional() ? $parameter->getDefaultValue() : null, + ]; + } + } + // verify that the class is really meant to be an annotation if ($metadata['is_annotation']) { self::$metadataParser->setTarget(Target::TARGET_CLASS); @@ -897,6 +907,38 @@ private function Annotation() } } + if (is_subclass_of($name, NamedArgumentConstructorAnnotation::class)) { + if (PHP_VERSION_ID >= 80000) { + return new $name(...$values); + } + + $positionalValues = []; + foreach (self::$annotationMetadata[$name]['constructor_args'] as $property => $parameter) { + $positionalValues[$parameter['position']] = $parameter['default']; + } + + 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'])) + )); + } + + $positionalValues[self::$annotationMetadata[$name]['constructor_args'][$property]['position']] = $value; + } + + return new $name(...$positionalValues); + } + // check if the annotation expects values via the constructor, // or directly injected into public properties if (self::$annotationMetadata[$name]['has_constructor'] === true) { diff --git a/lib/Doctrine/Common/Annotations/NamedArgumentConstructorAnnotation.php b/lib/Doctrine/Common/Annotations/NamedArgumentConstructorAnnotation.php new file mode 100644 index 000000000..f87a2c5d7 --- /dev/null +++ b/lib/Doctrine/Common/Annotations/NamedArgumentConstructorAnnotation.php @@ -0,0 +1,11 @@ +createTestParser() + ->parse('/** @NamedAnnotation(bar=2222, foo="baz") */'); + + self::assertCount(1, $result); + self::assertInstanceOf(NamedAnnotation::class, $result[0]); + self::assertSame("baz", $result[0]->getFoo()); + self::assertSame(2222, $result[0]->getBar()); + } + + public function testNamedArgumentsConstructorAnnotationWithDefaultValue(): void + { + $result = $this + ->createTestParser() + ->parse('/** @NamedAnnotation(foo="baz") */'); + + self::assertCount(1, $result); + self::assertInstanceOf(NamedAnnotation::class, $result[0]); + self::assertSame("baz", $result[0]->getFoo()); + self::assertSame(1234, $result[0]->getBar()); + } +} + +/** @Annotation */ +class NamedAnnotation implements NamedArgumentConstructorAnnotation +{ + private $foo; + private $bar; + + public function __construct(string $foo, int $bar = 1234) + { + $this->foo = $foo; + $this->bar = $bar; + } + + public function getFoo() : string + { + return $this->foo; + } + + public function getBar() : int + { + return $this->bar; + } } /** @Annotation */ From c2af93af5a82beb3d16872b0bac27571a9416100 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 11 Oct 2020 20:12:04 +0200 Subject: [PATCH 2/5] Housekeeping: CS --- lib/Doctrine/Common/Annotations/DocParser.php | 3 +++ .../Tests/Common/Annotations/DocParserTest.php | 10 ++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/Doctrine/Common/Annotations/DocParser.php b/lib/Doctrine/Common/Annotations/DocParser.php index a199d817d..ed037d803 100644 --- a/lib/Doctrine/Common/Annotations/DocParser.php +++ b/lib/Doctrine/Common/Annotations/DocParser.php @@ -24,6 +24,7 @@ use function interface_exists; use function is_array; use function is_object; +use function is_subclass_of; use function json_encode; use function ltrim; use function preg_match; @@ -38,6 +39,8 @@ use function substr; use function trim; +use const PHP_VERSION_ID; + /** * A parser for docblock annotations. * diff --git a/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php b/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php index 9f8bd2e7f..0209d0849 100644 --- a/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php +++ b/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php @@ -1547,7 +1547,7 @@ public function testNamedArgumentsConstructorAnnotation(): void self::assertCount(1, $result); self::assertInstanceOf(NamedAnnotation::class, $result[0]); - self::assertSame("baz", $result[0]->getFoo()); + self::assertSame('baz', $result[0]->getFoo()); self::assertSame(2222, $result[0]->getBar()); } @@ -1559,7 +1559,7 @@ public function testNamedArgumentsConstructorAnnotationWithDefaultValue(): void self::assertCount(1, $result); self::assertInstanceOf(NamedAnnotation::class, $result[0]); - self::assertSame("baz", $result[0]->getFoo()); + self::assertSame('baz', $result[0]->getFoo()); self::assertSame(1234, $result[0]->getBar()); } } @@ -1567,7 +1567,9 @@ public function testNamedArgumentsConstructorAnnotationWithDefaultValue(): void /** @Annotation */ class NamedAnnotation implements NamedArgumentConstructorAnnotation { + /** @var string */ private $foo; + /** @var int */ private $bar; public function __construct(string $foo, int $bar = 1234) @@ -1576,12 +1578,12 @@ public function __construct(string $foo, int $bar = 1234) $this->bar = $bar; } - public function getFoo() : string + public function getFoo(): string { return $this->foo; } - public function getBar() : int + public function getBar(): int { return $this->bar; } From 701954e2ed7949651902a1dcb4fce5f059bbd544 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 15 Oct 2020 19:54:29 +0200 Subject: [PATCH 3/5] Test that shows ordered vs reordered calling leads to same result. --- .../Tests/Common/Annotations/DocParserTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php b/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php index 0209d0849..b7685275d 100644 --- a/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php +++ b/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php @@ -1540,6 +1540,18 @@ public function testWillParseAnnotationSucceededByANonImmediateDash(): void } public function testNamedArgumentsConstructorAnnotation(): void + { + $result = $this + ->createTestParser() + ->parse('/** @NamedAnnotation(foo="baz", bar=2222) */'); + + self::assertCount(1, $result); + self::assertInstanceOf(NamedAnnotation::class, $result[0]); + self::assertSame('baz', $result[0]->getFoo()); + self::assertSame(2222, $result[0]->getBar()); + } + + public function testNamedReorderedArgumentsConstructorAnnotation(): void { $result = $this ->createTestParser() From 42f64d3538a949673f98a8a4adbb66a59869e537 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 15 Oct 2020 19:54:35 +0200 Subject: [PATCH 4/5] Add documentation --- docs/en/custom.rst | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/docs/en/custom.rst b/docs/en/custom.rst index e589a5432..197f71417 100644 --- a/docs/en/custom.rst +++ b/docs/en/custom.rst @@ -53,6 +53,52 @@ values into public properties directly: public $bar; } +Optional: Constructors with Named Parameters +-------------------------------------------- + +Starting with Annotations v1.11 a new annotation instantiation strategy +is available that aims at compatibility of Annotation classes with the PHP 8 +attribute feature. + +You can implement the +``Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation`` interface +and then declare a constructor with regular parameter names that are matched +from the named arguments in the annotation syntax. + +.. code-block:: php + + namespace MyCompany\Annotations; + + use Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation; + + /** @Annotation */ + class Bar implements NamedArgumentConstructorAnnotation + { + private $foo; + + public function __construct(string $foo) + { + $this->foo = $foo; + } + } + + /** Useable with @Bar(foo="baz") */ + +In combination with PHP 8s constructor property promotion feature +you can simplify this to: + +.. code-block:: php + + namespace MyCompany\Annotations; + + use Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation; + + /** @Annotation */ + class Bar implements NamedArgumentConstructorAnnotation + { + public function __construct(private string $foo) {} + } + Annotation Target ----------------- From 2d9bb04a4ce45a249dc85e4a2055a804071c573e Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 15 Oct 2020 20:07:31 +0200 Subject: [PATCH 5/5] punctuation --- docs/en/custom.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/custom.rst b/docs/en/custom.rst index 197f71417..8ea5f4790 100644 --- a/docs/en/custom.rst +++ b/docs/en/custom.rst @@ -84,7 +84,7 @@ from the named arguments in the annotation syntax. /** Useable with @Bar(foo="baz") */ -In combination with PHP 8s constructor property promotion feature +In combination with PHP 8's constructor property promotion feature you can simplify this to: .. code-block:: php