Skip to content

Commit

Permalink
Merge pull request #3328 from robertlemke/feature/3026-php-dnf-types
Browse files Browse the repository at this point in the history
FEATURE: Introduce PHP 8.2 DNF type support
  • Loading branch information
kitsunet authored Mar 12, 2024
2 parents cc194e8 + cf1c5bf commit be6bd67
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 101 deletions.
110 changes: 57 additions & 53 deletions Neos.Flow/Classes/Reflection/ReflectionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -443,9 +443,9 @@ public function isClassAnnotatedWith(string $className, string $annotationClassN
/**
* Returns the specified class annotations or an empty array
*
* @param null|string $annotationClassName
* @return array<object>
*
* @param null|string $annotationClassName
*/
public function getClassAnnotations(string $className, string|null $annotationClassName = null): array
{
Expand Down Expand Up @@ -797,9 +797,9 @@ public function getMethodParameters(string $className, string $methodName): arra
/**
* Returns the declared return type of a method (for PHP < 7.0 this will always return null)
*
* @param class-string $className
* @return ?string The declared return type of the method or null if none was declared
*
* @param class-string $className
*/
public function getMethodDeclaredReturnType(string $className, string $methodName): ?string
{
Expand Down Expand Up @@ -1118,15 +1118,14 @@ protected function reflectClass(string $className): void
$this->annotatedClasses[$annotationClassName][$className] = true;
$this->classReflectionData[$className][self::DATA_CLASS_ANNOTATIONS][] = $annotation;
}
if (PHP_MAJOR_VERSION >= 8) {
foreach ($class->getAttributes() as $attribute) {
$annotationClassName = $attribute->getName();
if ($this->isAttributeIgnored($annotationClassName)) {
continue;
}
$this->annotatedClasses[$annotationClassName][$className] = true;
$this->classReflectionData[$className][self::DATA_CLASS_ANNOTATIONS][] = $attribute->newInstance();

foreach ($class->getAttributes() as $attribute) {
$annotationClassName = $attribute->getName();
if ($this->isAttributeIgnored($annotationClassName)) {
continue;
}
$this->annotatedClasses[$annotationClassName][$className] = true;
$this->classReflectionData[$className][self::DATA_CLASS_ANNOTATIONS][] = $attribute->newInstance();
}

foreach ($class->getProperties() as $property) {
Expand Down Expand Up @@ -1169,18 +1168,17 @@ public function reflectClassProperty(string $className, PropertyReflection $prop
foreach ($this->annotationReader->getPropertyAnnotations($property) as $annotation) {
$this->classReflectionData[$className][self::DATA_CLASS_PROPERTIES][$propertyName][self::DATA_PROPERTY_ANNOTATIONS][get_class($annotation)][] = $annotation;
}
if (PHP_MAJOR_VERSION >= 8) {
foreach ($property->getAttributes() as $attribute) {
if ($this->isAttributeIgnored($attribute->getName())) {
continue;
}
try {
$attributeInstance = $attribute->newInstance();
} catch (\Error $error) {
throw new \RuntimeException(sprintf('Attribute "%s" used in class "%s" was not found.', $attribute->getName(), $className), 1695635128, $error);
}
$this->classReflectionData[$className][self::DATA_CLASS_PROPERTIES][$propertyName][self::DATA_PROPERTY_ANNOTATIONS][$attribute->getName()][] = $attributeInstance;

foreach ($property->getAttributes() as $attribute) {
if ($this->isAttributeIgnored($attribute->getName())) {
continue;
}
try {
$attributeInstance = $attribute->newInstance();
} catch (\Error $error) {
throw new \RuntimeException(sprintf('Attribute "%s" used in class "%s" was not found.', $attribute->getName(), $className), 1695635128, $error);
}
$this->classReflectionData[$className][self::DATA_CLASS_PROPERTIES][$propertyName][self::DATA_PROPERTY_ANNOTATIONS][$attribute->getName()][] = $attributeInstance;
}

return $visibility;
Expand Down Expand Up @@ -1239,7 +1237,9 @@ protected function addImplementedInterface(string $className, ClassReflection $i
}

/**
* @throws FilesException
* @throws ReflectionException
* @throws \Neos\Flow\Utility\Exception
*/
protected function reflectClassMethod(string $className, MethodReflection $method): void
{
Expand All @@ -1261,8 +1261,8 @@ protected function reflectClassMethod(string $className, MethodReflection $metho
$this->classesByMethodAnnotations[$annotationClassName][$className][] = $methodName;
}

$returnType= $method->getDeclaredReturnType();
$applyLeadingSlashIfNeeded = function (string $type): string {
$returnType = $method->getDeclaredReturnType();
$applyLeadingSlashIfNeeded = static function (string $type): string {
if (!in_array($type, ['self', 'parent', 'static', 'null', 'callable', 'void', 'never', 'iterable', 'object', 'resource', 'mixed'])
&& !TypeHandling::isSimpleType($type)
) {
Expand Down Expand Up @@ -1295,7 +1295,7 @@ protected function reflectClassMethod(string $className, MethodReflection $metho
* @param ParameterReflection $parameter
* @return void
*/
protected function reflectClassMethodParameter($className, MethodReflection $method, ParameterReflection $parameter): void
protected function reflectClassMethodParameter(string $className, MethodReflection $method, ParameterReflection $parameter): void
{
$methodName = $method->getName();
$paramAnnotations = $method->isTaggedWith('param') ? $method->getTagValues('param') : [];
Expand Down Expand Up @@ -1370,9 +1370,9 @@ protected function expandType(ClassReflection $class, string $type): string

// ... and try to expand them
$typeParts = explode('\\', $typeWithoutNull, 2);
$lowercasedFirstTypePart = strtolower($typeParts[0]);
if (isset($useStatementsForClass[$lowercasedFirstTypePart])) {
$typeParts[0] = $useStatementsForClass[$lowercasedFirstTypePart];
$lowerCasedFirstTypePart = strtolower($typeParts[0]);
if (isset($useStatementsForClass[$lowerCasedFirstTypePart])) {
$typeParts[0] = $useStatementsForClass[$lowerCasedFirstTypePart];

return implode('\\', $typeParts) . ($isNullable ? '|null' : '');
}
Expand All @@ -1399,9 +1399,10 @@ protected function getParentClasses(ClassReflection $class, array $parentClasses
/**
* Builds class schemata from classes annotated as entities or value objects
*
* @throws ClassLoadingForReflectionFailedException
* @throws ClassSchemaConstraintViolationException
* @throws Exception
* @throws InvalidPropertyTypeException
* @throws InvalidClassException
* @throws InvalidValueObjectException
*/
protected function buildClassSchemata(array $classNames): void
Expand All @@ -1416,8 +1417,10 @@ protected function buildClassSchemata(array $classNames): void

/**
* @param class-string $className
* @throws InvalidValueObjectException
* @return ClassSchema
* @throws ClassSchemaConstraintViolationException
* @throws InvalidPropertyTypeException
* @throws InvalidValueObjectException
*/
protected function buildClassSchema(string $className): ClassSchema
{
Expand Down Expand Up @@ -1681,30 +1684,7 @@ protected function convertParameterReflectionToArray(ParameterReflection $parame
$parameterInformation[self::DATA_PARAMETER_ALLOWS_NULL] = true;
}

/** @var \ReflectionNamedType|\ReflectionUnionType|\ReflectionIntersectionType|null $parameterType */
$parameterType = $parameter->getType();
if ($parameterType !== null) {
if ($parameterType instanceof \ReflectionUnionType) {
// ReflectionUnionType as of PHP 8
$parameterType = implode('|', array_map(
static function (\ReflectionNamedType $type) {
return $type->getName();
},
$parameterType->getTypes()
));
} elseif ($parameterType instanceof \ReflectionIntersectionType) {
// ReflectionIntersectionType as of PHP 8.1
$parameterType = implode('&', array_map(
static function (\ReflectionNamedType $type) {
return $type->getName();
},
$parameterType->getTypes()
));
} else {
// ReflectionNamedType as of PHP 7.1
$parameterType = $parameterType->getName();
}
}
$parameterType = $this->renderParameterType($parameter->getType());
if ($parameterType !== null && !TypeHandling::isSimpleType($parameterType)) {
// We use parameter type here to make class_alias usage work and return the hinted class name instead of the alias
$parameterInformation[self::DATA_PARAMETER_CLASS] = $parameterType;
Expand Down Expand Up @@ -2143,4 +2123,28 @@ protected function hasFrozenCacheInProduction(): bool
&& $this->reflectionDataRuntimeCache->getBackend() instanceof FreezableBackendInterface
&& $this->reflectionDataRuntimeCache->getBackend()->isFrozen();
}

private function renderParameterType(?\ReflectionType $parameterType): ?string
{
$that = $this;
return match (true) {
$parameterType instanceof \ReflectionUnionType => implode('|', array_map(
static function (\ReflectionNamedType | \ReflectionIntersectionType $type) use ($that) {
if ($type instanceof \ReflectionNamedType) {
return $type->getName();
}
return '(' . $that->renderParameterType($type) . ')';
},
$parameterType->getTypes()
)),
$parameterType instanceof \ReflectionIntersectionType => implode('&', array_map(
static function (\ReflectionNamedType $type) use ($that) {
return $that->renderParameterType($type);
},
$parameterType->getTypes()
)),
$parameterType instanceof \ReflectionNamedType => $parameterType->getName(),
default => null,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,20 @@

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\ClassToBeSerialized;
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\PrototypeClassA;
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\PrototypeClassB;
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\PrototypeClassC;
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\SingletonClassA;

/**
* A class with PHP 8 type hints with union types
* @Flow\Scope("prototype")
*/
class ClassWithUnionTypes
{
/* Make sure that this class is proxied, so we can test the proxy compiler */
#[Flow\Inject]
protected SingletonClassA $classA;

protected ?string $propertyA;

/* This should be fully equal to $propertyA */
Expand All @@ -32,6 +39,8 @@ class ClassWithUnionTypes

protected int|float|string|null $propertyE;

protected PrototypeClassA|(PrototypeClassB&PrototypeClassC)|null $propertyF;

public function getPropertyA(): ?string
{
return $this->propertyA;
Expand Down Expand Up @@ -81,4 +90,14 @@ public function setPropertyE(float|int|string|null $propertyE): void
{
$this->propertyE = $propertyE;
}

public function setPropertyF(PrototypeClassA | (PrototypeClassB & PrototypeClassC) | null $propertyF): void
{
$this->propertyF = $propertyF;
}

public function classA(): SingletonClassA
{
return $this->classA;
}
}
54 changes: 37 additions & 17 deletions Neos.Flow/Tests/Functional/ObjectManagement/ProxyCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\ClassExtendingClassWithPrivateConstructor;
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\ClassImplementingInterfaceWithConstructor;
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\ClassWithPrivateConstructor;
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\PHP8\ClassWithUnionTypes;
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\PHP81\BackedEnumWithMethod;
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\PrototypeClassA;
use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\PrototypeClassK;
Expand All @@ -33,6 +34,18 @@
*/
class ProxyCompilerTest extends FunctionalTestCase
{
/**
* Make sure that we are actually testing proxy classes and not the
* original PHP class.
*
* @test
*/
public function classWithUnionTypesIsProxied(): void
{
$object = new ClassWithUnionTypes();
self::assertInstanceOf(ProxyInterface::class, $object);
}

/**
* @test
*/
Expand Down Expand Up @@ -140,10 +153,6 @@ public function classesAnnotatedWithProxyDisableAreNotProxied(): void
*/
public function enumsAreNotProxied(): void
{
if (PHP_VERSION_ID <= 80100) {
$this->markTestSkipped('Only for PHP.1 8 with Enums');
}

# PHP < 8.1 would fail compiling this test case if we used the syntax BackedEnumWithMethod::ESPRESSO->label()
$this->assertSame('Espresso', BackedEnumWithMethod::getLabel(BackedEnumWithMethod::ESPRESSO));
}
Expand Down Expand Up @@ -215,9 +224,6 @@ public function classKeywordIsIgnoredInsideClassBody(): void
*/
public function attributesArePreserved(): void
{
if (PHP_MAJOR_VERSION < 8) {
$this->markTestSkipped('Only for PHP 8 with Attributes');
}
$reflectionClass = new ClassReflection(Fixtures\ClassWithPhpAttributes::class);
$attributes = $reflectionClass->getAttributes();
self::assertCount(2, $attributes);
Expand All @@ -244,14 +250,16 @@ public function proxyingClassImplementingInterfacesWithParametrizedConstructorsL
*/
public function complexPropertyTypesArePreserved(): void
{
if (PHP_MAJOR_VERSION < 8) {
$this->markTestSkipped('Only for PHP 8 with UnionTypes');
}
$reflectionClass = new ClassReflection(Fixtures\PHP8\ClassWithUnionTypes::class);

foreach ($reflectionClass->getProperties() as $property) {
assert($property instanceof PropertyReflection);
if ($property->getName() !== 'propertyA' && $property->getName() !== 'propertyB' && !str_starts_with($property->getName(), 'Flow_')) {
if (
$property->getName() !== 'classA' &&
$property->getName() !== 'propertyA' &&
$property->getName() !== 'propertyB' &&
!str_starts_with($property->getName(), 'Flow_')
) {
self::assertInstanceOf(\ReflectionUnionType::class, $property->getType(), sprintf('Property "%s" is of type "%s"', $property->getName(), $property->getType()));
}
}
Expand All @@ -267,9 +275,6 @@ public function complexPropertyTypesArePreserved(): void
*/
public function complexMethodReturnTypesArePreserved(): void
{
if (PHP_MAJOR_VERSION < 8) {
$this->markTestSkipped('Only for PHP 8 with UnionTypes');
}
$reflectionClass = new ClassReflection(Fixtures\PHP8\ClassWithUnionTypes::class);
foreach ($reflectionClass->getMethods() as $method) {
if (str_starts_with($method->getName(), 'get') &&
Expand All @@ -285,14 +290,29 @@ public function complexMethodReturnTypesArePreserved(): void
);
}

/**
* @test
* @throws
*/
public function complexMethodParametersArePreserved(): void
{
$proxyClassReflection = new ClassReflection(Fixtures\PHP8\ClassWithUnionTypes::class);
$originalClassReflection = new ClassReflection(get_parent_class(Fixtures\PHP8\ClassWithUnionTypes::class));

$proxyMethodReflection = $proxyClassReflection->getMethod('setPropertyF');
$originalMethodReflection = $originalClassReflection->getMethod('setPropertyF');

self::assertEquals(
$proxyMethodReflection->getParameters()[0]->getType()->getTypes(),
$originalMethodReflection->getParameters()[0]->getType()->getTypes(),
);
}

/**
* @test
*/
public function constructorPropertiesArePreserved(): void
{
if (PHP_MAJOR_VERSION < 8) {
$this->markTestSkipped('Only for PHP 8 with Constructor properties');
}
$reflectionClass = new ClassReflection(Fixtures\PHP8\ClassWithConstructorProperties::class);
/** @var PropertyReflection $property */
self::assertTrue($reflectionClass->hasProperty('propertyA'));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
namespace Neos\Flow\Tests\Functional\Reflection\Fixtures\PHP8;

/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Tests\Functional\Reflection\Fixtures\DummyClassWithProperties;
use Neos\Flow\Tests\Functional\Reflection\Fixtures\DummyClassWithTypeHints;
use Neos\Flow\Tests\Functional\Reflection\Fixtures\DummyReadonlyClass;

/**
* A class with PHP 8.2 disjunctive normal form types
*
* @see https://wiki.php.net/rfc/dnf_types
*/
class DummyClassWithDisjunctiveNormalFormTypes
{
public function dnfTypesA(DummyReadonlyClass | (DummyClassWithTypeHints & DummyClassWithUnionTypeHints) | null $theParameter): void
{
}

public function dnfTypesB(DummyReadonlyClass | (DummyClassWithTypeHints & DummyClassWithUnionTypeHints) | (DummyClassWithTypeHints & DummyClassWithProperties) | null $theParameter): void
{
}
}
Loading

0 comments on commit be6bd67

Please sign in to comment.