Skip to content

Commit

Permalink
Merge pull request #4 from veewee/class-attributes
Browse files Browse the repository at this point in the history
Split up Reflect into object_ and class_ specific functions
  • Loading branch information
veewee authored Jan 30, 2024
2 parents 830314c + 017a8b1 commit 029a631
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 0 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"src/Lens/properties.php",
"src/Iso/compose.php",
"src/Iso/object_data.php",
"src/Reflect/class_attributes.php",
"src/Reflect/class_has_attribute.php",
"src/Reflect/class_is_dynamic.php",
"src/Reflect/instantiate.php",
"src/Reflect/object_attributes.php",
"src/Reflect/object_has_attribute.php",
Expand Down
52 changes: 52 additions & 0 deletions docs/reflect.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,58 @@ This component provides runtime-safe reflections on objects.

This package provides following functions for dealing with objects.

#### class_attributes

Detects all attributes at the class level of the given className that match the optionally provided argument type (or super-type).
If the class 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\class_attributes;

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

#### class_has_attribute

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

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

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

#### class_is_dynamic

Checks if the provided class 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 that class.
If the object is not reflectable, an `UnreflectableException` exception is triggered!

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

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

#### instantiate

This function instantiates a new object of the provided type by bypassing the constructor.
Expand Down
35 changes: 35 additions & 0 deletions src/Reflect/class_attributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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 $className
* @param class-string<T>|null $attributeClassName
* @return (T is null ? list<object> : list<T>)
*
* @throws UnreflectableException
*/
function class_attributes(string $className, ?string $attributeClassName = null): array
{
$propertyInfo = reflect_class($className);

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()
);
}
19 changes: 19 additions & 0 deletions src/Reflect/class_has_attribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\Reflect;

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

/**
* @throws UnreflectableException
*
* @param class-string $className
* @param class-string $attributeClassName
*/
function class_has_attribute(string $className, string $attributeClassName): bool
{
$propertyInfo = reflect_class($className);

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

namespace VeeWee\Reflecta\Reflect;

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

/**
* @throws UnreflectableException
*
* @param class-string $className
*/
function class_is_dynamic(string $className): 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 class_has_attribute($className, AllowDynamicProperties::class);
}
56 changes: 56 additions & 0 deletions tests/unit/Reflect/ClassAttributesTest.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\class_attributes;

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

$actual = class_attributes(get_class($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 = class_attributes(get_class($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 = class_attributes(get_class($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.');

class_attributes(get_class($x));
}
}
31 changes: 31 additions & 0 deletions tests/unit/Reflect/ClassHasAttributeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?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\class_has_attribute;

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

static::assertTrue(class_has_attribute($className, CustomAttribute::class));
static::assertFalse(class_has_attribute($className, InheritedCustomAttribute::class));
}

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

static::assertTrue(class_has_attribute($className, AbstractAttribute::class));
static::assertTrue(class_has_attribute($className, InheritedCustomAttribute::class));
}
}
43 changes: 43 additions & 0 deletions tests/unit/Reflect/ClassIsDynamicTest.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\class_is_dynamic;
use const PHP_VERSION_ID;

final class ClassIsDynamicTest 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(class_is_dynamic(get_class($x)));
static::assertFalse(class_is_dynamic(get_class($y)));
static::assertTrue(class_is_dynamic(get_class(($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(class_is_dynamic(get_class($x)));
static::assertTrue(class_is_dynamic(get_class($y)));
static::assertTrue(class_is_dynamic(get_class($s)));
}
}

0 comments on commit 029a631

Please sign in to comment.