Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
11 changes: 8 additions & 3 deletions lib/Doctrine/Persistence/Mapping/RuntimeReflectionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Doctrine\Persistence\Reflection\RuntimePublicReflectionProperty;
use Doctrine\Persistence\Reflection\TypedNoDefaultReflectionProperty;
use Doctrine\Persistence\Reflection\TypedNoDefaultRuntimePublicReflectionProperty;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
Expand Down Expand Up @@ -86,10 +87,14 @@ public function getAccessibleProperty($class, $property)
{
$reflectionProperty = new ReflectionProperty($class, $property);

if ($reflectionProperty->isPublic()) {
if ($this->supportsTypedPropertiesWorkaround && ! array_key_exists($property, $this->getClass($class)->getDefaultProperties())) {
if ($reflectionProperty->isPublic()) {
$reflectionProperty = new TypedNoDefaultRuntimePublicReflectionProperty($class, $property);
} else {
$reflectionProperty = new TypedNoDefaultReflectionProperty($class, $property);
}
} elseif ($reflectionProperty->isPublic()) {
$reflectionProperty = new RuntimePublicReflectionProperty($class, $property);
} elseif ($this->supportsTypedPropertiesWorkaround && ! array_key_exists($property, $this->getClass($class)->getDefaultProperties())) {
$reflectionProperty = new TypedNoDefaultReflectionProperty($class, $property);
}

$reflectionProperty->setAccessible(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,12 @@

namespace Doctrine\Persistence\Reflection;

use Closure;
use ReflectionProperty;
use ReturnTypeWillChange;

use function assert;

/**
* PHP Typed No Default Reflection Property - special override for typed properties without a default value.
*/
class TypedNoDefaultReflectionProperty extends ReflectionProperty
{
/**
* {@inheritDoc}
*
* Checks that a typed property is initialized before accessing its value.
* This is necessary to avoid PHP error "Error: Typed property must not be accessed before initialization".
* Should be used only for reflecting typed properties without a default value.
*
* @param object $object
*/
#[ReturnTypeWillChange]
public function getValue($object = null)
{
return $object !== null && $this->isInitialized($object) ? parent::getValue($object) : null;
}

/**
* {@inheritDoc}
*
* Works around the problem with setting typed no default properties to
* NULL which is not supported, instead unset() to uninitialize.
*
* @link https://github.com/doctrine/orm/issues/7999
*
* @param object $object
*/
#[ReturnTypeWillChange]
public function setValue($object, $value = null)
{
if ($value === null && $this->hasType() && ! $this->getType()->allowsNull()) {
$propertyName = $this->getName();

$unsetter = function () use ($propertyName): void {
unset($this->$propertyName);
};
$unsetter = $unsetter->bindTo($object, $this->getDeclaringClass()->getName());

assert($unsetter instanceof Closure);

$unsetter();

return;
}

parent::setValue($object, $value);
}
use TypedNoDefaultReflectionPropertyBase;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Doctrine\Persistence\Reflection;

use Closure;
use ReturnTypeWillChange;

use function assert;

/**
* PHP Typed No Default Reflection Property Base - special override for typed properties without a default value.
*/
trait TypedNoDefaultReflectionPropertyBase
{
/**
* {@inheritDoc}
*
* Checks that a typed property is initialized before accessing its value.
* This is necessary to avoid PHP error "Error: Typed property must not be accessed before initialization".
* Should be used only for reflecting typed properties without a default value.
*
* @param object $object
*/
#[ReturnTypeWillChange]
public function getValue($object = null)
{
return $object !== null && $this->isInitialized($object) ? parent::getValue($object) : null;
}

/**
* {@inheritDoc}
*
* Works around the problem with setting typed no default properties to
* NULL which is not supported, instead unset() to uninitialize.
*
* @link https://github.com/doctrine/orm/issues/7999
*
* @param object $object
*/
#[ReturnTypeWillChange]
public function setValue($object, $value = null)
{
if ($value === null && $this->hasType() && ! $this->getType()->allowsNull()) {
$propertyName = $this->getName();

$unsetter = function () use ($propertyName): void {
unset($this->$propertyName);
};
$unsetter = $unsetter->bindTo($object, $this->getDeclaringClass()->getName());

assert($unsetter instanceof Closure);

$unsetter();

return;
}

parent::setValue($object, $value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Doctrine\Persistence\Reflection;

/**
* PHP Typed No Default Runtime Public Reflection Property - special override for public typed properties without a default value.
*/
class TypedNoDefaultRuntimePublicReflectionProperty extends RuntimePublicReflectionProperty
{
use TypedNoDefaultReflectionPropertyBase;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Doctrine\Persistence\Mapping\RuntimeReflectionService;
use Doctrine\Persistence\Reflection\RuntimePublicReflectionProperty;
use Doctrine\Persistence\Reflection\TypedNoDefaultReflectionProperty;
use Doctrine\Persistence\Reflection\TypedNoDefaultRuntimePublicReflectionProperty;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;

Expand All @@ -20,8 +21,17 @@ class RuntimeReflectionServiceTest extends TestCase

private string $typedNoDefaultProperty;
private string $typedDefaultProperty = '';
/** @var string */
private $nonTypedNoDefaultProperty; // phpcs:ignore SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedProperty
/** @var string */
private $nonTypedDefaultProperty = ''; // phpcs:ignore SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedProperty

public string $typedNoDefaultPublicProperty;
public string $typedDefaultPublicProperty = '';
/** @var string */
public $nonTypedNoDefaultPublicProperty;
/** @var string */
public $nonTypedDefaultPublicProperty = '';

protected function setUp(): void
{
Expand All @@ -45,6 +55,33 @@ public function testGetTypedPublicNoDefaultPropertyWorksWithGetValue(): void
{
$reflProp = $this->reflectionService->getAccessibleProperty(self::class, 'typedNoDefaultPublicProperty');
self::assertInstanceOf(RuntimePublicReflectionProperty::class, $reflProp);
self::assertInstanceOf(TypedNoDefaultRuntimePublicReflectionProperty::class, $reflProp);
self::assertNull($reflProp->getValue($this));
}

public function testGetNonTypedNoDefaultReflectionProperty(): void
{
$reflProp = $this->reflectionService->getAccessibleProperty(self::class, 'nonTypedNoDefaultProperty');
self::assertInstanceOf(ReflectionProperty::class, $reflProp);
}

public function testGetNonTypedDefaultReflectionProperty(): void
{
$reflProp = $this->reflectionService->getAccessibleProperty(self::class, 'nonTypedDefaultProperty');
self::assertInstanceOf(ReflectionProperty::class, $reflProp);
self::assertNotInstanceOf(TypedNoDefaultReflectionProperty::class, $reflProp);
}

public function testGetTypedPublicDefaultPropertyWorksWithGetValue(): void
{
$reflProp = $this->reflectionService->getAccessibleProperty(self::class, 'typedDefaultPublicProperty');
self::assertInstanceOf(ReflectionProperty::class, $reflProp);
self::assertNotInstanceOf(TypedNoDefaultReflectionProperty::class, $reflProp);
}

public function testGetNonTypedPublicDefaultPropertyWorksWithGetValue(): void
{
$reflProp = $this->reflectionService->getAccessibleProperty(self::class, 'nonTypedDefaultPublicProperty');
self::assertInstanceOf(RuntimePublicReflectionProperty::class, $reflProp);
}
}