Skip to content

Commit

Permalink
Support dynamic objects and class attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Dec 3, 2023
1 parent 5e0aa1a commit 00d1fa2
Show file tree
Hide file tree
Showing 17 changed files with 366 additions and 7 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
"src/Iso/compose.php",
"src/Iso/object_data.php",
"src/Reflect/instantiate.php",
"src/Reflect/object_attributes.php",
"src/Reflect/object_has_attribute.php",
"src/Reflect/object_is_dynamic.php",
"src/Reflect/properties_get.php",
"src/Reflect/properties_set.php",
"src/Reflect/property_get.php",
Expand Down
52 changes: 52 additions & 0 deletions docs/reflect.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,58 @@ try {
}
```

#### object_attributes

Detects all attributes at the class level of the given object that match the optionally provided argument type (or super-type).
If the object is not reflectable or there is an error instantiating any argument, an `UnreflectableException` exception is triggered!
The result of this function is of type: `list<object>`. However, if you provide an argument name: psalm will know the type of the attribute.

```php
use function VeeWee\Reflecta\Reflect\object_attributes;

try {
$allAttributes = object_attributes($yourObject);
$allAttributesOfType = object_attributes($yourObject, \YourAttributeType::class);
$allAttributesOfType = object_attributes($yourObject, \YourAbstractBaseType::class);
} catch (UnreflectableException) {
// Deal with it
}
```

#### object_has_attribute

Checks if the object contains an attribute of given type (or super-type).
If the object is not reflectable, an `UnreflectableException` exception is triggered!

```php
use function VeeWee\Reflecta\Reflect\object_has_attribute;

try {
$hasAttribute = object_has_attribute($yourObject, \YourAttributeType::class);
$hasAttributeThatImplementsBaseType = object_has_attribute($yourObject, \YourAbstractBaseType::class);
} catch (UnreflectableException) {
// Deal with it
}
```

#### object_is_dynamic

Checks if the provided object is considered a safe dynamic object that implements `AllowDynamicProperties`.
Since this property was only added in PHP 8.1, all older versions will always return `true` and allow adding dynamic properties to your object.
If the object is not reflectable, an `UnreflectableException` exception is triggered!

```php
use function VeeWee\Reflecta\Reflect\object_is_dynamic;

try {
$isDynamic = object_is_dynamic(new stdClass());
$isDynamic = object_is_dynamic(new #[\AllowDynamicProperties] class() {});
$isNotDynamic = object_is_dynamic(new class() {});
} catch (UnreflectableException) {
// Deal with it
}
```

#### properties_get

Detects all values of all properties for a given object.
Expand Down
34 changes: 34 additions & 0 deletions src/Reflect/object_attributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\Reflect;

use ReflectionAttribute;
use Throwable;
use VeeWee\Reflecta\Reflect\Exception\UnreflectableException;
use function Psl\Result\wrap;
use function Psl\Vec\map;

/**
* @template T extends object
*
* @param class-string<T>|null $attributeClassName
* @return (T is null ? list<object> : list<T>)
*
* @throws UnreflectableException
*/
function object_attributes(object $object, ?string $attributeClassName = null): array
{
$propertyInfo = reflect_object($object);

return map(
$propertyInfo->getAttributes($attributeClassName, ReflectionAttribute::IS_INSTANCEOF),
static fn (ReflectionAttribute $attribute): object => wrap(static fn () => $attribute->newInstance())
->catch(
static fn (Throwable $error) => throw UnreflectableException::nonInstantiatable(
$attribute->getName(),
$error
)
)
->getResult()
);
}
18 changes: 18 additions & 0 deletions src/Reflect/object_has_attribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\Reflect;

use ReflectionAttribute;
use VeeWee\Reflecta\Reflect\Exception\UnreflectableException;

/**
* @throws UnreflectableException
*
* @param class-string $attributeClassName
*/
function object_has_attribute(object $object, string $attributeClassName): bool
{
$propertyInfo = reflect_object($object);

return (bool) $propertyInfo->getAttributes($attributeClassName, ReflectionAttribute::IS_INSTANCEOF);
}
21 changes: 21 additions & 0 deletions src/Reflect/object_is_dynamic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\Reflect;

use AllowDynamicProperties;
use VeeWee\Reflecta\Reflect\Exception\UnreflectableException;
use const PHP_VERSION_ID;

/**
* @throws UnreflectableException
*/
function object_is_dynamic(object $object): bool
{
// Dynamic props is a 80200 feature.
// IN previous versions, all objects are dynamic (without any warning).
if (PHP_VERSION_ID < 80200) {
return true;
}

return object_has_attribute($object, AllowDynamicProperties::class);
}
16 changes: 13 additions & 3 deletions src/Reflect/property_set.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,25 @@
*/
function property_set(object $object, string $name, mixed $value): object
{

$propertyInfo = reflect_property($object, $name);

try {
$new = clone $object;
} catch (Throwable $previous) {
throw CloneException::impossibleToClone($object, $previous);
}

try {
$propertyInfo = reflect_property($object, $name);
} catch (UnreflectableException $e) {
// In case the property is unknown, try to set a dynamic property.
if (object_is_dynamic($new)) {
$new->{$name} = $value;

return $new;
}

throw $e;
}

try {
$propertyInfo->setValue($new, $value);
} catch (Throwable $previous) {
Expand Down
9 changes: 9 additions & 0 deletions tests/fixtures/AbstractAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\TestFixtures;

#[Attribute(Attribute::TARGET_ALL)]
abstract class AbstractAttribute
{
}
11 changes: 11 additions & 0 deletions tests/fixtures/CustomAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\TestFixtures;

use Attribute;

#[Attribute(Attribute::TARGET_ALL)]
final class CustomAttribute
{
}
12 changes: 12 additions & 0 deletions tests/fixtures/Dynamic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\TestFixtures;

use AllowDynamicProperties;

#[AllowDynamicProperties]
final class Dynamic
{
public string $x;
}
11 changes: 11 additions & 0 deletions tests/fixtures/InheritedCustomAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\TestFixtures;

use Attribute;

#[Attribute(Attribute::TARGET_ALL)]
final class InheritedCustomAttribute extends AbstractAttribute
{
}
27 changes: 27 additions & 0 deletions tests/static-analyzer/Reflect/object_attributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\SaTests\Reflect;

use AllowDynamicProperties;
use stdClass;
use function VeeWee\Reflecta\Reflect\object_attributes;

/**
* @return list<object>
*/
function test_get_all(): array
{
$std = new stdClass();

return object_attributes($std);
}

/**
* @return list<AllowDynamicProperties>
*/
function test_get_specific(): array
{
$std = new stdClass();

return object_attributes($std, AllowDynamicProperties::class);
}
4 changes: 2 additions & 2 deletions tests/unit/Lens/PropertyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

final class PropertyTest extends TestCase
{

public function test_it_can_work_with_property(): void
{
$lens = property('hello');
Expand All @@ -21,6 +21,6 @@ public function test_it_can_work_with_property(): void
static::assertEquals((object) ['hello' => 'earth'], $lens->set($data, 'earth'));
static::assertNotSame($data, $lens->set($data, 'earth'));
static::assertEquals((object) ['hello' => 'earth'], $lens->trySet($data, 'earth')->getResult());
static::assertTrue($lens->trySet((object)[], 'earth')->isFailed());
static::assertEquals((object) ['hello' => 'earth'], $lens->trySet((object)[], 'earth')->getResult());
}
}
56 changes: 56 additions & 0 deletions tests/unit/Reflect/ObjectAttributesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\UnitTests\Reflect;

use PHPUnit\Framework\TestCase;
use ThisIsAnUnknownAttribute;
use VeeWee\Reflecta\Reflect\Exception\UnreflectableException;
use VeeWee\Reflecta\TestFixtures\AbstractAttribute;
use VeeWee\Reflecta\TestFixtures\CustomAttribute;
use VeeWee\Reflecta\TestFixtures\InheritedCustomAttribute;
use function VeeWee\Reflecta\Reflect\object_attributes;

final class ObjectAttributesTest extends TestCase
{
public function test_it_can_get_attributes(): void
{
$x = new #[InheritedCustomAttribute, CustomAttribute] class {};

$actual = object_attributes($x);

static::assertCount(2, $actual);
static::assertInstanceOf(InheritedCustomAttribute::class, $actual[0]);
static::assertInstanceOf(CustomAttribute::class, $actual[1]);
}

public function test_it_can_get_attributes_of_type(): void
{
$x = new #[InheritedCustomAttribute, CustomAttribute] class {};

$actual = object_attributes($x, InheritedCustomAttribute::class);

static::assertCount(1, $actual);
static::assertInstanceOf(InheritedCustomAttribute::class, $actual[0]);
}

public function test_it_can_get_attributes_of_subtype(): void
{
$x = new #[InheritedCustomAttribute] class {};

$actual = object_attributes($x, AbstractAttribute::class);

static::assertCount(1, $actual);
static::assertInstanceOf(InheritedCustomAttribute::class, $actual[0]);
}

public function test_it_can_fail_on_attribute_instantiation(): void
{
$x = new #[ThisIsAnUnknownAttribute] class {};

$this->expectException(UnreflectableException::class);
$this->expectExceptionMessage('Unable to instantiate class ThisIsAnUnknownAttribute.');

object_attributes($x);
}
}
29 changes: 29 additions & 0 deletions tests/unit/Reflect/ObjectHasAttributeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\UnitTests\Reflect;

use PHPUnit\Framework\TestCase;
use VeeWee\Reflecta\TestFixtures\AbstractAttribute;
use VeeWee\Reflecta\TestFixtures\CustomAttribute;
use VeeWee\Reflecta\TestFixtures\InheritedCustomAttribute;
use function VeeWee\Reflecta\Reflect\object_has_attribute;

final class ObjectHasAttributeTest extends TestCase
{
public function test_it_can_check_for_attribute(): void
{
$x = new #[CustomAttribute] class {};

static::assertTrue(object_has_attribute($x, CustomAttribute::class));
static::assertFalse(object_has_attribute($x, InheritedCustomAttribute::class));
}

public function test_it_can_check_for_attributes_of_subtype(): void
{
$x = new #[InheritedCustomAttribute] class {};

static::assertTrue(object_has_attribute($x, AbstractAttribute::class));
static::assertTrue(object_has_attribute($x, InheritedCustomAttribute::class));
}
}
43 changes: 43 additions & 0 deletions tests/unit/Reflect/ObjectIsDynamicTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\UnitTests\Reflect;

use AllowDynamicProperties;
use PHPUnit\Framework\TestCase;
use stdClass;
use function VeeWee\Reflecta\Reflect\object_is_dynamic;
use const PHP_VERSION_ID;

final class ObjectIsDynamicTest extends TestCase
{
public function test_it_can_check_for_dynamic_objects(): void
{
if (PHP_VERSION_ID < 80200) {
static::markTestSkipped('On PHP 8.2, all classes are safely dynamic');
}

$x = new #[AllowDynamicProperties] class {};
$y = new class {};
$s = new stdClass();

static::assertTrue(object_is_dynamic($x));
static::assertFalse(object_is_dynamic($y));
static::assertTrue(object_is_dynamic($s));
}

public function test_it_can_check_for_dynamic_objects_in_php_81(): void
{
if (PHP_VERSION_ID >= 80200) {
static::markTestSkipped('On PHP 8.2, all classes are safely dynamic');
}

$x = new #[AllowDynamicProperties] class {};
$y = new class {};
$s = new stdClass();

static::assertTrue(object_is_dynamic($x));
static::assertTrue(object_is_dynamic($y));
static::assertTrue(object_is_dynamic($s));
}
}
Loading

0 comments on commit 00d1fa2

Please sign in to comment.