Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Introduce PHP 8.2 DNF type support #3328

Merged
merged 3 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
FEATURE: Introduce PHP 8.2 DNF type support
The Reflection Service now supports Disjunctive Normal Form (DNF) types for method arguments.

See: https://www.php.net/releases/8.2/en.php#dnf_types

Resolves #3026
  • Loading branch information
robertlemke committed Mar 12, 2024
commit cf1c5bf907465c57449a965d37b0f833659717a5
49 changes: 25 additions & 24 deletions Neos.Flow/Classes/Reflection/ReflectionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -1684,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 @@ -2146,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
{
}
}
32 changes: 32 additions & 0 deletions Neos.Flow/Tests/Functional/Reflection/ReflectionServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,38 @@ public function unionReturnTypesWorkCorrectly(): void
self::assertEquals('?\Neos\Flow\Tests\Functional\Reflection\Fixtures\PHP8\DummyClassWithUnionTypeHints', $returnTypeC);
}

/**
* @test
*/
public function disjunctiveNormalFormTypesWorkCorrectly(): void
{
$parameters = $this->reflectionService->getMethodParameters(Reflection\Fixtures\PHP8\DummyClassWithDisjunctiveNormalFormTypes::class, 'dnfTypesA');
self::assertEquals(
Reflection\Fixtures\DummyReadonlyClass::class .
'|(' .
Reflection\Fixtures\DummyClassWithTypeHints::class .
'&' .
Reflection\Fixtures\PHP8\DummyClassWithUnionTypeHints::class .
')|null',
$parameters['theParameter']['type']
);

$parameters = $this->reflectionService->getMethodParameters(Reflection\Fixtures\PHP8\DummyClassWithDisjunctiveNormalFormTypes::class, 'dnfTypesB');
self::assertEquals(
Reflection\Fixtures\DummyReadonlyClass::class .
'|(' .
Reflection\Fixtures\DummyClassWithTypeHints::class .
'&' .
Reflection\Fixtures\PHP8\DummyClassWithUnionTypeHints::class .
')|(' .
Reflection\Fixtures\DummyClassWithTypeHints::class .
'&' .
Reflection\Fixtures\DummyClassWithProperties::class .
')|null',
$parameters['theParameter']['type']
);
}

/**
* @test
*/
Expand Down
Loading