diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b7b7302a8..4b55228f3 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -17,4 +17,4 @@ jobs: name: "PHPUnit" uses: "doctrine/.github/.github/workflows/continuous-integration.yml@1.1.1" with: - php-versions: '["7.1", "7.2", "7.3", "7.4", "8.0", "8.1"]' + php-versions: '["7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2"]' diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 3eebb2540..748a07b21 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - phpVersion: 80100 + phpVersion: 80200 level: 3 paths: - src diff --git a/src/Proxy/ProxyGenerator.php b/src/Proxy/ProxyGenerator.php index 3ef767670..5d5b67e38 100644 --- a/src/Proxy/ProxyGenerator.php +++ b/src/Proxy/ProxyGenerator.php @@ -74,7 +74,13 @@ class ProxyGenerator * Used to match very simple id methods that don't need * to be decorated since the identifier is known. */ - public const PATTERN_MATCH_ID_METHOD = '((public\s+)?(function\s+%s\s*\(\)\s*)\s*(?::\s*\??\s*\\\\?[a-z_\x7f-\xff][\w\x7f-\xff]*(?:\\\\[a-z_\x7f-\xff][\w\x7f-\xff]*)*\s*)?{\s*return\s*\$this->%s;\s*})i'; + public const PATTERN_MATCH_ID_METHOD = <<<'EOT' +((?(DEFINE) + (?\\?[a-z_\x7f-\xff][\w\x7f-\xff]*(?:\\[a-z_\x7f-\xff][\w\x7f-\xff]*)*) + (?(?&type)\s*&\s*(?&type)) + (?(?:(?:\(\s*(?&intersection_type)\s*\))|(?&type))(?:\s*\|\s*(?:(?:\(\s*(?&intersection_type)\s*\))|(?&type)))+) +)(?:public\s+)?(?:function\s+%s\s*\(\)\s*)\s*(?::\s*(?:(?&union_type)|(?&intersection_type)|(?:\??(?&type)))\s*)?{\s*return\s*\$this->%s;\s*})i +EOT; /** * The namespace that contains all proxy classes. @@ -1218,6 +1224,10 @@ private function formatType( if ($type instanceof ReflectionUnionType) { return implode('|', array_map( function (ReflectionType $unionedType) use ($method, $parameter) { + if ($unionedType instanceof ReflectionIntersectionType) { + return '(' . $this->formatType($unionedType, $method, $parameter) . ')'; + } + return $this->formatType($unionedType, $method, $parameter); }, $type->getTypes() diff --git a/tests/Common/Proxy/LazyLoadableObjectWithPHP81IntersectionType.php b/tests/Common/Proxy/LazyLoadableObjectWithPHP81IntersectionType.php new file mode 100644 index 000000000..55475c8ae --- /dev/null +++ b/tests/Common/Proxy/LazyLoadableObjectWithPHP81IntersectionType.php @@ -0,0 +1,13 @@ +identifierFieldIntersectionType; + } +} diff --git a/tests/Common/Proxy/LazyLoadableObjectWithPHP81IntersectionTypeClassMetadata.php b/tests/Common/Proxy/LazyLoadableObjectWithPHP81IntersectionTypeClassMetadata.php new file mode 100644 index 000000000..54ab55f04 --- /dev/null +++ b/tests/Common/Proxy/LazyLoadableObjectWithPHP81IntersectionTypeClassMetadata.php @@ -0,0 +1,156 @@ + */ + protected $identifier = [ + 'identifierFieldIntersectionType' => true, + ]; + + /** @var array */ + protected $fields = [ + 'identifierFieldIntersectionType' => true, + ]; + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->getReflectionClass()->getName(); + } + + /** + * {@inheritDoc} + */ + public function getIdentifier() + { + return array_keys($this->identifier); + } + + /** + * {@inheritDoc} + */ + public function getReflectionClass() + { + if ($this->reflectionClass === null) { + $this->reflectionClass = new ReflectionClass(__NAMESPACE__ . '\LazyLoadableObjectWithPHP81IntersectionType'); + } + + return $this->reflectionClass; + } + + /** + * {@inheritDoc} + */ + public function isIdentifier($fieldName) + { + return isset($this->identifier[$fieldName]); + } + + /** + * {@inheritDoc} + */ + public function hasField($fieldName) + { + return isset($this->fields[$fieldName]); + } + + /** + * {@inheritDoc} + */ + public function hasAssociation($fieldName) + { + return false; + } + + /** + * {@inheritDoc} + */ + public function isSingleValuedAssociation($fieldName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function isCollectionValuedAssociation($fieldName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function getFieldNames() + { + return array_keys($this->fields); + } + + /** + * {@inheritDoc} + */ + public function getIdentifierFieldNames() + { + return $this->getIdentifier(); + } + + /** + * {@inheritDoc} + */ + public function getAssociationNames() + { + return []; + } + + /** + * {@inheritDoc} + */ + public function getTypeOfField($fieldName) + { + return 'string'; + } + + /** + * {@inheritDoc} + */ + public function getAssociationTargetClass($assocName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function isAssociationInverseSide($assocName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function getAssociationMappedByTargetField($assocName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function getIdentifierValues($object) + { + throw new BadMethodCallException('not implemented'); + } +} diff --git a/tests/Common/Proxy/LazyLoadableObjectWithPHP82UnionAndIntersectionType.php b/tests/Common/Proxy/LazyLoadableObjectWithPHP82UnionAndIntersectionType.php new file mode 100644 index 000000000..7333fe77c --- /dev/null +++ b/tests/Common/Proxy/LazyLoadableObjectWithPHP82UnionAndIntersectionType.php @@ -0,0 +1,13 @@ +identifierFieldUnionAndIntersectionType; + } +} diff --git a/tests/Common/Proxy/LazyLoadableObjectWithPHP82UnionAndIntersectionTypeClassMetadata.php b/tests/Common/Proxy/LazyLoadableObjectWithPHP82UnionAndIntersectionTypeClassMetadata.php new file mode 100644 index 000000000..6f9b4e4b1 --- /dev/null +++ b/tests/Common/Proxy/LazyLoadableObjectWithPHP82UnionAndIntersectionTypeClassMetadata.php @@ -0,0 +1,156 @@ + */ + protected $identifier = [ + 'identifierFieldUnionAndIntersectionType' => true, + ]; + + /** @var array */ + protected $fields = [ + 'identifierFieldUnionAndIntersectionType' => true, + ]; + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->getReflectionClass()->getName(); + } + + /** + * {@inheritDoc} + */ + public function getIdentifier() + { + return array_keys($this->identifier); + } + + /** + * {@inheritDoc} + */ + public function getReflectionClass() + { + if ($this->reflectionClass === null) { + $this->reflectionClass = new ReflectionClass(__NAMESPACE__ . '\LazyLoadableObjectWithPHP82UnionAndIntersectionType'); + } + + return $this->reflectionClass; + } + + /** + * {@inheritDoc} + */ + public function isIdentifier($fieldName) + { + return isset($this->identifier[$fieldName]); + } + + /** + * {@inheritDoc} + */ + public function hasField($fieldName) + { + return isset($this->fields[$fieldName]); + } + + /** + * {@inheritDoc} + */ + public function hasAssociation($fieldName) + { + return false; + } + + /** + * {@inheritDoc} + */ + public function isSingleValuedAssociation($fieldName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function isCollectionValuedAssociation($fieldName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function getFieldNames() + { + return array_keys($this->fields); + } + + /** + * {@inheritDoc} + */ + public function getIdentifierFieldNames() + { + return $this->getIdentifier(); + } + + /** + * {@inheritDoc} + */ + public function getAssociationNames() + { + return []; + } + + /** + * {@inheritDoc} + */ + public function getTypeOfField($fieldName) + { + return 'string'; + } + + /** + * {@inheritDoc} + */ + public function getAssociationTargetClass($assocName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function isAssociationInverseSide($assocName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function getAssociationMappedByTargetField($assocName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function getIdentifierValues($object) + { + throw new BadMethodCallException('not implemented'); + } +} diff --git a/tests/Common/Proxy/LazyLoadableObjectWithPHP8UnionType.php b/tests/Common/Proxy/LazyLoadableObjectWithPHP8UnionType.php new file mode 100644 index 000000000..9cddd8c66 --- /dev/null +++ b/tests/Common/Proxy/LazyLoadableObjectWithPHP8UnionType.php @@ -0,0 +1,13 @@ +identifierFieldUnionType; + } +} diff --git a/tests/Common/Proxy/LazyLoadableObjectWithPHP8UnionTypeClassMetadata.php b/tests/Common/Proxy/LazyLoadableObjectWithPHP8UnionTypeClassMetadata.php new file mode 100644 index 000000000..5e1b8a14a --- /dev/null +++ b/tests/Common/Proxy/LazyLoadableObjectWithPHP8UnionTypeClassMetadata.php @@ -0,0 +1,156 @@ + */ + protected $identifier = [ + 'identifierFieldUnionType' => true, + ]; + + /** @var array */ + protected $fields = [ + 'identifierFieldUnionType' => true, + ]; + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->getReflectionClass()->getName(); + } + + /** + * {@inheritDoc} + */ + public function getIdentifier() + { + return array_keys($this->identifier); + } + + /** + * {@inheritDoc} + */ + public function getReflectionClass() + { + if ($this->reflectionClass === null) { + $this->reflectionClass = new ReflectionClass(__NAMESPACE__ . '\LazyLoadableObjectWithPHP8UnionType'); + } + + return $this->reflectionClass; + } + + /** + * {@inheritDoc} + */ + public function isIdentifier($fieldName) + { + return isset($this->identifier[$fieldName]); + } + + /** + * {@inheritDoc} + */ + public function hasField($fieldName) + { + return isset($this->fields[$fieldName]); + } + + /** + * {@inheritDoc} + */ + public function hasAssociation($fieldName) + { + return false; + } + + /** + * {@inheritDoc} + */ + public function isSingleValuedAssociation($fieldName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function isCollectionValuedAssociation($fieldName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function getFieldNames() + { + return array_keys($this->fields); + } + + /** + * {@inheritDoc} + */ + public function getIdentifierFieldNames() + { + return $this->getIdentifier(); + } + + /** + * {@inheritDoc} + */ + public function getAssociationNames() + { + return []; + } + + /** + * {@inheritDoc} + */ + public function getTypeOfField($fieldName) + { + return 'string'; + } + + /** + * {@inheritDoc} + */ + public function getAssociationTargetClass($assocName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function isAssociationInverseSide($assocName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function getAssociationMappedByTargetField($assocName) + { + throw new BadMethodCallException('not implemented'); + } + + /** + * {@inheritDoc} + */ + public function getIdentifierValues($object) + { + throw new BadMethodCallException('not implemented'); + } +} diff --git a/tests/Common/Proxy/ProxyLogicIdentifierGetterTest.php b/tests/Common/Proxy/ProxyLogicIdentifierGetterTest.php index e84363a09..40ac68711 100644 --- a/tests/Common/Proxy/ProxyLogicIdentifierGetterTest.php +++ b/tests/Common/Proxy/ProxyLogicIdentifierGetterTest.php @@ -8,6 +8,8 @@ use stdClass; use function class_exists; +use const PHP_VERSION_ID; + /** * Test that identifier getter does not cause lazy loading. * These tests make assumptions about the structure of LazyLoadableObjectWithTypehints @@ -58,7 +60,7 @@ static function () { */ public function methodsForWhichLazyLoadingShouldBeDisabled() { - return [ + $data = [ [new LazyLoadableObjectClassMetadata(), 'protectedIdentifierField', 'foo'], [new LazyLoadableObjectWithTypehintsClassMetadata(), 'identifierFieldNoReturnTypehint', 'noTypeHint'], [new LazyLoadableObjectWithTypehintsClassMetadata(), 'identifierFieldReturnTypehintScalar', 'scalarValue'], @@ -71,5 +73,30 @@ public function methodsForWhichLazyLoadingShouldBeDisabled() [new LazyLoadableObjectWithNullableTypehintsClassMetadata(), 'identifierFieldReturnClassOneLetterNullable', new stdClass()], [new LazyLoadableObjectWithNullableTypehintsClassMetadata(), 'identifierFieldReturnClassOneLetterNullableWithSpace', new stdClass()], ]; + + if (PHP_VERSION_ID >= 80000) { + $data[] = [new LazyLoadableObjectWithPHP8UnionTypeClassMetadata(), 'identifierFieldUnionType', 123]; + $data[] = [new LazyLoadableObjectWithPHP8UnionTypeClassMetadata(), 'identifierFieldUnionType', 'string']; + } + + if (PHP_VERSION_ID >= 80100) { + $data[] = [new LazyLoadableObjectWithPHP81IntersectionTypeClassMetadata(), 'identifierFieldIntersectionType', new class extends \stdClass implements \Stringable { + public function __toString(): string + { + return ''; + } + }]; + } + + if (PHP_VERSION_ID >= 80200) { + $data[] = [new LazyLoadableObjectWithPHP82UnionAndIntersectionTypeClassMetadata(), 'identifierFieldUnionAndIntersectionType', new class extends \stdClass implements \Stringable { + public function __toString(): string + { + return ''; + } + }]; + } + + return $data; } } diff --git a/tests/Common/Proxy/ProxyLogicTest.php b/tests/Common/Proxy/ProxyLogicTest.php index e1952d4c3..05a5df23e 100644 --- a/tests/Common/Proxy/ProxyLogicTest.php +++ b/tests/Common/Proxy/ProxyLogicTest.php @@ -219,6 +219,10 @@ public function testFalseWhenCheckingNonExistentProperty() public function testNoErrorWhenSettingNonExistentProperty() { + if (PHP_VERSION_ID >= 80200) { + $this->markTestSkipped('access to a dynamic property trigger a deprecation notice on PHP 8.2+'); + } + $this->configureInitializerMock(0); $this->lazyObject->non_existing_property = 'now has a value'; diff --git a/tests/Common/Proxy/ProxyLogicTypedPropertiesTest.php b/tests/Common/Proxy/ProxyLogicTypedPropertiesTest.php index 59c6ef149..3eb2be713 100644 --- a/tests/Common/Proxy/ProxyLogicTypedPropertiesTest.php +++ b/tests/Common/Proxy/ProxyLogicTypedPropertiesTest.php @@ -226,6 +226,10 @@ public function testFalseWhenCheckingNonExistentProperty() public function testNoErrorWhenSettingNonExistentProperty() { + if (PHP_VERSION_ID >= 80200) { + $this->markTestSkipped('access to a dynamic property trigger a deprecation notice on PHP 8.2+'); + } + $this->configureInitializerMock(0); $this->lazyObject->non_existing_property = 'now has a value'; diff --git a/tests/Common/Proxy/ProxyMagicMethodsTest.php b/tests/Common/Proxy/ProxyMagicMethodsTest.php index 8bb3293b2..79f14ac4a 100644 --- a/tests/Common/Proxy/ProxyMagicMethodsTest.php +++ b/tests/Common/Proxy/ProxyMagicMethodsTest.php @@ -135,6 +135,10 @@ static function (Proxy $proxy, $method, $params) use (&$counter) { public function testInheritedMagicGetWithVoid() { + if (PHP_VERSION_ID >= 80200) { + $this->markTestSkipped('access to a dynamic property trigger a deprecation notice on PHP 8.2+'); + } + $proxyClassName = $this->generateProxyClass(MagicGetClassWithVoid::class); $proxy = new $proxyClassName(static function (Proxy $proxy, $method, $params) use (&$counter) { if (in_array($params[0], ['publicField', 'test'])) {