├── .github ├── FUNDING.yml ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── Support_Question.md │ ├── Feature_Request.md │ └── Bug.md └── workflows │ ├── code-style.yaml │ ├── analyzers.yaml │ └── tests.yaml ├── .gitignore ├── docs ├── assets │ ├── iso.png │ ├── lens.png │ └── optics.rp ├── array-access.md ├── isomorphisms.md ├── lens.md └── reflect.md ├── tests ├── fixtures │ ├── X.php │ ├── AbstractAttribute.php │ ├── CustomAttribute.php │ ├── Dynamic.php │ ├── MultipleProperties.php │ ├── ReadonlyX.php │ ├── RepeatedAttribute.php │ ├── AbstractProperties.php │ ├── InheritedCustomAttribute.php │ ├── Unconstructable.php │ └── Unclonable.php ├── static-analyzer │ ├── Reflect │ │ ├── object_attributes.php │ │ ├── property_get.php │ │ ├── property_set.php │ │ ├── properties_set.php │ │ └── properties_get.php │ ├── Iso │ │ └── compose.php │ └── Lens │ │ └── compose.php └── unit │ ├── Reflect │ ├── Type │ │ ├── VisibilityTest.php │ │ ├── ReflectedAttributeTest.php │ │ ├── ReflectedPropertyTest.php │ │ └── ReflectedClassTest.php │ ├── InstantiateTest.php │ ├── PropertiesGetTest.php │ ├── PropertyGetTest.php │ ├── ObjectHasAttributeTest.php │ ├── ClassHasAttributeTest.php │ ├── ObjectIsDynamicTest.php │ ├── ClassIsDynamicTest.php │ ├── Predicate │ │ └── PropertyVisibilityTest.php │ ├── ObjectAttributesTest.php │ ├── ClassAttributesTest.php │ ├── PropertiesSetTest.php │ ├── PropertySetTest.php │ └── Exception │ │ └── UnreflectableExceptionTest.php │ ├── Lens │ ├── ReadonlyTest.php │ ├── ComposeTest.php │ ├── IndexTest.php │ ├── PropertyTest.php │ ├── OptionalTest.php │ ├── PropertiesTest.php │ └── LensTest.php │ ├── Exception │ ├── ReadonlyExceptionTest.php │ └── CloneExceptionTest.php │ ├── ArrayAccess │ ├── Exception │ │ └── ArrayAccessExceptionTest.php │ ├── IndexGetTest.php │ └── IndexSetTest.php │ └── Iso │ ├── ComposeTest.php │ ├── ObjectDataTest.php │ └── IsoTest.php ├── src ├── Exception │ ├── RuntimeException.php │ ├── ReadonlyException.php │ └── CloneException.php ├── Reflect │ ├── object_is_dynamic.php │ ├── class_is_dynamic.php │ ├── Predicate │ │ └── property_visibility.php │ ├── object_has_attribute.php │ ├── instantiate.php │ ├── class_has_attribute.php │ ├── Type │ │ ├── Visibility.php │ │ ├── ReflectedAttribute.php │ │ ├── ReflectedProperty.php │ │ └── ReflectedClass.php │ ├── object_attributes.php │ ├── class_attributes.php │ ├── properties_get.php │ ├── property_get.php │ ├── properties_set.php │ ├── property_set.php │ └── Exception │ │ └── UnreflectableException.php ├── ArrayAccess │ ├── index_set.php │ ├── Exception │ │ └── ArrayAccessException.php │ └── index_get.php ├── Lens │ ├── read_only.php │ ├── index.php │ ├── compose.php │ ├── property.php │ ├── properties.php │ ├── optional.php │ ├── LensInterface.php │ └── Lens.php ├── Psalm │ ├── Reflect │ │ ├── Infer │ │ │ ├── PropertyNameType.php │ │ │ ├── ObjectType.php │ │ │ ├── PropertyValueType.php │ │ │ └── PropertiesValuesType.php │ │ └── Provider │ │ │ ├── PropertiesGetProvider.php │ │ │ ├── PropertiesSetProvider.php │ │ │ ├── PropertyGetProvider.php │ │ │ └── PropertySetProvider.php │ ├── Plugin.php │ ├── Iso │ │ └── Provider │ │ │ └── ComposeProvider.php │ └── Lens │ │ └── Provider │ │ └── ComposeProvider.php └── Iso │ ├── compose.php │ ├── object_data.php │ ├── IsoInterface.php │ └── Iso.php ├── psalm.xml ├── infection.json.dist ├── LICENSE ├── phpunit.xml ├── .php-cs-fixer.dist.php ├── README.md └── composer.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [veewee] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | .phpunit.cache 4 | .php-cs-fixer.cache 5 | -------------------------------------------------------------------------------- /docs/assets/iso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veewee/reflecta/HEAD/docs/assets/iso.png -------------------------------------------------------------------------------- /docs/assets/lens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veewee/reflecta/HEAD/docs/assets/lens.png -------------------------------------------------------------------------------- /docs/assets/optics.rp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veewee/reflecta/HEAD/docs/assets/optics.rp -------------------------------------------------------------------------------- /tests/fixtures/X.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | | Q | A 4 | |------------- | ----------- 5 | | Type | bug/feature/improvement 6 | | BC Break | yes/no 7 | | Fixed issues | 8 | 9 | #### Summary 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Support_Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ❓ Support Question 3 | about: Have a problem that you can't figure out? 🤔 4 | --- 5 | 6 | 7 | 8 | | Q | A 9 | |------------ | ----- 10 | | Version | x.y.z 11 | 12 | 13 | ### Support Question 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Reflect/object_is_dynamic.php: -------------------------------------------------------------------------------- 1 | isDynamic(); 14 | } 15 | -------------------------------------------------------------------------------- /src/Reflect/class_is_dynamic.php: -------------------------------------------------------------------------------- 1 | isDynamic(); 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_Request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🎉 Feature Request 3 | about: You have a neat idea that should be implemented? 🎩 4 | --- 5 | 6 | ### Feature Request 7 | 8 | 9 | 10 | | Q | A 11 | |------------ | ------ 12 | | New Feature | yes 13 | | RFC | yes/no 14 | | BC Break | yes/no 15 | 16 | #### Summary 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Reflect/Predicate/property_visibility.php: -------------------------------------------------------------------------------- 1 | $property->visibility() === $visibility; 15 | } 16 | -------------------------------------------------------------------------------- /src/ArrayAccess/index_set.php: -------------------------------------------------------------------------------- 1 | $array 12 | * @param Tk $index 13 | * @param Tv $value 14 | * 15 | * @return array 16 | */ 17 | function index_set(array $array, string|int $index, $value): array 18 | { 19 | $new = array_merge($array, []); 20 | $new[$index] = $value; 21 | 22 | return $new; 23 | } 24 | -------------------------------------------------------------------------------- /src/Lens/read_only.php: -------------------------------------------------------------------------------- 1 | $that 10 | * 11 | * @return Lens 12 | * 13 | * @psalm-pure 14 | */ 15 | function read_only(LensInterface $that): Lens 16 | { 17 | return Lens::readonly( 18 | /** 19 | * @param S $subject 20 | * @return A 21 | */ 22 | static fn ($subject) => $that->get($subject) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/Reflect/object_has_attribute.php: -------------------------------------------------------------------------------- 1 | hasAttributeOfType($attributeClassName); 16 | } 17 | -------------------------------------------------------------------------------- /src/Reflect/instantiate.php: -------------------------------------------------------------------------------- 1 | $className 11 | * @return T 12 | * 13 | * @throws UnreflectableException 14 | */ 15 | function instantiate(string $className): mixed 16 | { 17 | /** @var T */ 18 | return ReflectedClass::fromFullyQualifiedClassName($className)->instantiate(); 19 | } 20 | -------------------------------------------------------------------------------- /src/ArrayAccess/Exception/ArrayAccessException.php: -------------------------------------------------------------------------------- 1 | hasAttributeOfType($attributeClassName); 17 | } 18 | -------------------------------------------------------------------------------- /src/Lens/index.php: -------------------------------------------------------------------------------- 1 | 11 | * @psalm-pure 12 | */ 13 | function index($index): Lens 14 | { 15 | /** @return Lens */ 16 | return new Lens( 17 | static fn (array $subject): mixed => index_get($subject, $index), 18 | static fn (array $subject, mixed $value): array => index_set($subject, $index, $value), 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/Reflect/Type/Visibility.php: -------------------------------------------------------------------------------- 1 | isPrivate() => self::Private, 20 | $property->isProtected() => self::Protected, 21 | $property->isPublic() => self::Public, 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Reflect/object_attributes.php: -------------------------------------------------------------------------------- 1 | |null $attributeClassName 12 | * @return (T is null ? list : list) 13 | * 14 | * @throws UnreflectableException 15 | */ 16 | function object_attributes(object $object, ?string $attributeClassName = null): array 17 | { 18 | return ReflectedClass::fromObject($object)->attributes($attributeClassName); 19 | } 20 | -------------------------------------------------------------------------------- /tests/static-analyzer/Reflect/object_attributes.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | function test_get_all(): array 13 | { 14 | $std = new stdClass(); 15 | 16 | return object_attributes($std); 17 | } 18 | 19 | /** 20 | * @return list 21 | */ 22 | function test_get_specific(): array 23 | { 24 | $std = new stdClass(); 25 | 26 | return object_attributes($std, AllowDynamicProperties::class); 27 | } 28 | -------------------------------------------------------------------------------- /src/Psalm/Reflect/Infer/PropertyNameType.php: -------------------------------------------------------------------------------- 1 | infer($arg); 15 | if (!$propertyNameTypeUnion->isSingleStringLiteral()) { 16 | return null; 17 | } 18 | 19 | return $propertyNameTypeUnion->getSingleStringLiteral(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ArrayAccess/index_get.php: -------------------------------------------------------------------------------- 1 | $array 15 | * @param Tk $index 16 | * @return Tv 17 | * 18 | * @throws ArrayAccessException 19 | */ 20 | function index_get(array $array, string|int $index): mixed 21 | { 22 | if (!array_key_exists($index, $array)) { 23 | throw ArrayAccessException::cannotAccessIndex($index); 24 | } 25 | 26 | return $array[$index]; 27 | } 28 | -------------------------------------------------------------------------------- /tests/unit/Reflect/Type/VisibilityTest.php: -------------------------------------------------------------------------------- 1 | property('z'); 16 | $visibility = $prop->visibility(); 17 | 18 | static::assertSame(Visibility::Public, $visibility); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Reflect/class_attributes.php: -------------------------------------------------------------------------------- 1 | |null $attributeClassName 13 | * @return (T is null ? list : list) 14 | * 15 | * @throws UnreflectableException 16 | */ 17 | function class_attributes(string $className, ?string $attributeClassName = null): array 18 | { 19 | return ReflectedClass::fromFullyQualifiedClassName($className)->attributes($attributeClassName); 20 | } 21 | -------------------------------------------------------------------------------- /src/Iso/compose.php: -------------------------------------------------------------------------------- 1 | > $isos 14 | * 15 | * @return IsoInterface 16 | * 17 | * @psalm-pure 18 | * @psalm-suppress ImpureFunctionCall 19 | */ 20 | function compose(IsoInterface ... $isos): IsoInterface 21 | { 22 | /** @var IsoInterface */ 23 | return reduce( 24 | $isos, 25 | static fn (IsoInterface $current, IsoInterface $next) => $current->compose($next), 26 | Iso::identity() 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/Lens/compose.php: -------------------------------------------------------------------------------- 1 | > $lenses 14 | * 15 | * @return LensInterface 16 | * 17 | * @psalm-pure 18 | * @psalm-suppress ImpureFunctionCall 19 | */ 20 | function compose(LensInterface ... $lenses): LensInterface 21 | { 22 | /** @var LensInterface */ 23 | return reduce( 24 | $lenses, 25 | static fn (LensInterface $current, LensInterface $next) => $current->compose($next), 26 | Lens::identity() 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /tests/unit/Lens/ReadonlyTest.php: -------------------------------------------------------------------------------- 1 | 'bar']; 18 | 19 | static::assertSame('bar', $lens->get($data)); 20 | 21 | $this->expectExceptionObject(ReadonlyException::couldNotWrite()); 22 | $lens->set('hello', $data); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Lens/property.php: -------------------------------------------------------------------------------- 1 | 11 | * @psalm-pure 12 | */ 13 | function property(string $propertyName): Lens 14 | { 15 | return new Lens( 16 | /** 17 | * @param S $subject 18 | */ 19 | static fn (object $subject): mixed => property_get($subject, $propertyName), 20 | /** 21 | * @param S $subject 22 | * @return S 23 | */ 24 | static fn (object $subject, mixed $value): object => property_set($subject, $propertyName, $value), 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /tests/unit/Exception/ReadonlyExceptionTest.php: -------------------------------------------------------------------------------- 1 | expectExceptionObject($exception); 18 | $this->expectException(RuntimeException::class); 19 | $this->expectExceptionMessage('Could not write to the provided lens: it is readonly.'); 20 | 21 | throw $exception; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/unit/Lens/ComposeTest.php: -------------------------------------------------------------------------------- 1 | ['message' => 'hello']]; 19 | 20 | static::assertSame('hello', $composed->get($data)); 21 | static::assertSame(['greet' => ['message' => 'goodbye']], $composed->set($data, 'goodbye')); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Reflect/properties_get.php: -------------------------------------------------------------------------------- 1 | 15 | * @throws UnreflectableException 16 | */ 17 | function properties_get(object $object, Closure|null $predicate = null): array 18 | { 19 | return map( 20 | ReflectedClass::fromObject($object)->properties($predicate), 21 | static fn (ReflectedProperty $property): mixed => property_get($object, $property->name()), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /tests/unit/ArrayAccess/Exception/ArrayAccessExceptionTest.php: -------------------------------------------------------------------------------- 1 | expectExceptionObject($exception); 17 | $this->expectException(RuntimeException::class); 18 | $this->expectExceptionMessage('Impossible to access array at index x.'); 19 | 20 | throw $exception; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Reflect/property_get.php: -------------------------------------------------------------------------------- 1 | property($name); 17 | 18 | try { 19 | return $property->apply( 20 | static fn (ReflectionProperty $reflectionProperty): mixed => $reflectionProperty->getValue($object) 21 | ); 22 | } catch (Throwable $previous) { 23 | throw UnreflectableException::unreadableProperty($class->fullName(), $name, $previous); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Psalm/Reflect/Infer/ObjectType.php: -------------------------------------------------------------------------------- 1 | infer($arg); 16 | if (!$objectTypeUnion->isSingle()) { 17 | return null; 18 | } 19 | 20 | /** @var TNamedObject | TTemplateParam | null $objectType */ 21 | $objectType = $objectTypeUnion->getSingleAtomic(); 22 | if (!$objectType->isNamedObjectType()) { 23 | return null; 24 | } 25 | 26 | return $objectType; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/unit/Lens/IndexTest.php: -------------------------------------------------------------------------------- 1 | 'world']; 16 | 17 | static::assertSame('world', $lens->get($data)); 18 | static::assertSame('world', $lens->tryGet($data)->getResult()); 19 | static::assertTrue($lens->tryGet([])->isFailed()); 20 | 21 | static::assertSame(['hello' => 'earth'], $lens->set($data, 'earth')); 22 | static::assertSame(['hello' => 'earth'], $lens->trySet($data, 'earth')->getResult()); 23 | static::assertSame(['hello' => 'earth'], $lens->trySet([], 'earth')->getResult()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/static-analyzer/Reflect/property_get.php: -------------------------------------------------------------------------------- 1 | z = 123; 14 | 15 | return property_get($x, $z); 16 | } 17 | 18 | function test_get_mixed_return_type_on_templated_object(): mixed 19 | { 20 | $curried = static fn (string $path): Closure => static fn (object $object): mixed => property_get($object, $path); 21 | $z = 'z'; 22 | $x = new X(); 23 | 24 | return $curried($z)($x); 25 | } 26 | 27 | /** 28 | * @psalm-suppress UndefinedPropertyFetch 29 | */ 30 | function test_getting_unknown_property(): mixed 31 | { 32 | $unknown = 'unknown'; 33 | $x = new X(); 34 | 35 | return property_get($x, $unknown); 36 | } 37 | -------------------------------------------------------------------------------- /tests/unit/Exception/CloneExceptionTest.php: -------------------------------------------------------------------------------- 1 | getPrevious()); 21 | static::assertSame(0, $exception->getCode()); 22 | $this->expectExceptionObject($exception); 23 | $this->expectException(RuntimeException::class); 24 | $this->expectExceptionMessage('Impossible to clone type'); 25 | 26 | throw $exception; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug Report 3 | about: Something is broken? 🔨 4 | --- 5 | 6 | ### Bug Report 7 | 8 | 9 | 10 | | Q | A 11 | |------------ | ------ 12 | | BC Break | yes/no 13 | | Version | x.y.z 14 | 15 | #### Summary 16 | 17 | 18 | 19 | #### Current behaviour 20 | 21 | 22 | 23 | #### How to reproduce 24 | 25 | 30 | 31 | #### Expected behaviour 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ], 6 | "excludes": [ 7 | "Psalm" 8 | ] 9 | }, 10 | "minMsi": 100, 11 | "minCoveredMsi": 100, 12 | "logs": { 13 | "text": ".phpunit.cache/infection.log", 14 | "html": ".phpunit.cache/infection" 15 | }, 16 | "mutators": { 17 | "@default": true, 18 | "CastInt": { 19 | "ignore": [ 20 | "VeeWee\\Reflecta\\*Exception::__construct" 21 | ] 22 | }, 23 | "LessThan": { 24 | "ignore": [ 25 | "VeeWee\\Reflecta\\Reflect\\Type\\ReflectedClass::isDynamic" 26 | ] 27 | }, 28 | "TrueValue": { 29 | "ignore": [ 30 | "VeeWee\\Reflecta\\Reflect\\Type\\ReflectedClass::isDynamic", 31 | "VeeWee\\Reflecta\\Reflect\\Type\\Visibility::forProperty" 32 | ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/unit/Reflect/InstantiateTest.php: -------------------------------------------------------------------------------- 1 | expectException(UnreflectableException::class); 19 | instantiate(Generator::class); 20 | } 21 | 22 | public function it_returns_instance() 23 | { 24 | $x = instantiate(X::class); 25 | 26 | static::assertInstanceOf(X::class, $x); 27 | } 28 | 29 | public function it_skips_constructor() 30 | { 31 | $boom = instantiate(Unconstructable::class); 32 | 33 | static::assertInstanceOf(Unconstructable::class, $boom); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/unit/Reflect/PropertiesGetTest.php: -------------------------------------------------------------------------------- 1 | z = 123; 19 | 20 | $actual = properties_get($x); 21 | 22 | static::assertSame(['z' => 123], $actual); 23 | } 24 | 25 | public function test_it_can_get_with_predicate_filter(): void 26 | { 27 | $x = new Dynamic(); 28 | $x->x = '123'; 29 | $x->y = '123'; 30 | $actual = properties_get($x, static fn (ReflectedProperty $property): bool => $property->isDefault()); 31 | 32 | static::assertSame(['x' => '123'], $actual); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/Lens/PropertyTest.php: -------------------------------------------------------------------------------- 1 | 'world']; 16 | 17 | static::assertSame('world', $lens->get($data)); 18 | static::assertSame('world', $lens->tryGet($data)->getResult()); 19 | static::assertTrue($lens->tryGet((object)[])->isFailed()); 20 | 21 | static::assertEquals((object) ['hello' => 'earth'], $lens->set($data, 'earth')); 22 | static::assertNotSame($data, $lens->set($data, 'earth')); 23 | static::assertEquals((object) ['hello' => 'earth'], $lens->trySet($data, 'earth')->getResult()); 24 | static::assertEquals((object) ['hello' => 'earth'], $lens->trySet((object)[], 'earth')->getResult()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/static-analyzer/Iso/compose.php: -------------------------------------------------------------------------------- 1 | $iso1 15 | * @param Iso $iso2 16 | * @param Iso $iso3 17 | * @return Iso 18 | */ 19 | function it_knows_composed_result(Iso $iso1, Iso $iso2, Iso $iso3): Iso 20 | { 21 | return compose($iso1, $iso2, $iso3); 22 | } 23 | 24 | /** 25 | * @template A 26 | * @template B 27 | * @template C 28 | * @template D 29 | * 30 | * @param Iso $iso1 31 | * @param Iso $iso2 32 | * @param Iso $iso3 33 | * @return Iso 34 | * 35 | * @ psalm-suppress InvalidArgument - 36 | * TODO : Invalid compose iso-param detection does not work any since contravariant templates are introduced. 37 | */ 38 | function it_knows_broken_composition(Iso $iso1, Iso $iso2, Iso $iso3): Iso 39 | { 40 | return compose($iso1, $iso2, $iso3); 41 | } 42 | -------------------------------------------------------------------------------- /tests/unit/Iso/ComposeTest.php: -------------------------------------------------------------------------------- 1 | join(',', $keywords), 22 | static fn (string $keywords): array => explode(',', $keywords) 23 | ); 24 | 25 | $commaSeparatedBase64 = compose($commaSeparated, $base64); 26 | 27 | $data = ['hello' ,'world']; 28 | $joined = $commaSeparatedBase64->to($data); 29 | $exploded = $commaSeparatedBase64->from($joined); 30 | 31 | static::assertSame(base64_encode('hello,world'), $joined); 32 | static::assertSame($data, $exploded); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/Reflect/PropertyGetTest.php: -------------------------------------------------------------------------------- 1 | z = 123; 18 | 19 | $actual = property_get($x, 'z'); 20 | 21 | static::assertSame(123, $actual); 22 | } 23 | 24 | public function test_it_can_fail_getting_property_value(): void 25 | { 26 | $x = new class() { 27 | /** 28 | * This property is not initialize yet... 29 | */ 30 | private string $x; 31 | }; 32 | 33 | $this->expectException(UnreflectableException::class); 34 | $this->expectExceptionMessage('Unable to read property '.$x::class.'::$x.'); 35 | property_get($x, 'x'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/static-analyzer/Lens/compose.php: -------------------------------------------------------------------------------- 1 | $lens1 15 | * @param Lens $lens2 16 | * @param Lens $lens3 17 | * @return Lens 18 | */ 19 | function it_knows_composed_result(Lens $lens1, Lens $lens2, Lens $lens3): Lens 20 | { 21 | return compose($lens1, $lens2, $lens3); 22 | } 23 | 24 | /** 25 | * @template A 26 | * @template B 27 | * @template C 28 | * @template D 29 | * 30 | * @param Lens $lens1 31 | * @param Lens $lens2 32 | * @param Lens $lens3 33 | * @return Lens 34 | * 35 | * @ psalm-suppress InvalidArgument - 36 | * TODO : Invalid compose lens-param detection does not work any since contravariant templates are introduced. 37 | */ 38 | function it_knows_broken_composition(Lens $lens1, Lens $lens2, Lens $lens3): Lens 39 | { 40 | return compose($lens1, $lens2, $lens3); 41 | } 42 | -------------------------------------------------------------------------------- /src/Iso/object_data.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @param class-string $className 14 | * @param null|Lens $accessor 15 | * 16 | * @return Iso 17 | * 18 | * @psalm-pure 19 | */ 20 | function object_data(string $className, ?Lens $accessor = null): Iso 21 | { 22 | /** @var Lens $typedAccessor */ 23 | $typedAccessor = $accessor ?? properties(); 24 | 25 | return new Iso( 26 | /** 27 | * @param S $object 28 | * @return A 29 | */ 30 | static fn (object $object): array => $typedAccessor->get($object), 31 | /** 32 | * @param A $properties 33 | * @return S 34 | */ 35 | static fn (array $properties): object => $typedAccessor->set( 36 | instantiate($className), 37 | $properties 38 | ), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /tests/unit/Reflect/ObjectHasAttributeTest.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * @param null|Closure(ReflectedProperty): bool $predicate 15 | * 16 | * @return Lens 17 | * @psalm-pure 18 | */ 19 | function properties(Closure|null $predicate = null): Lens 20 | { 21 | /** @var Lens */ 22 | return new Lens( 23 | /** 24 | * @param S $subject 25 | * @return A 26 | * 27 | * @psalm-suppress InvalidReturnType, InvalidReturnStatement 28 | */ 29 | static fn (object $subject): array => properties_get($subject, $predicate), 30 | /** 31 | * @param S $subject 32 | * @param A $value 33 | * @return S 34 | */ 35 | static fn (object $subject, array $value): object => properties_set($subject, $value, $predicate), 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/code-style.yaml: -------------------------------------------------------------------------------- 1 | name: CodeStyle 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | PHP_CS_FIXER_IGNORE_ENV: 1 7 | 8 | jobs: 9 | run: 10 | runs-on: ${{ matrix.operating-system }} 11 | strategy: 12 | matrix: 13 | operating-system: [ubuntu-latest] 14 | php-versions: ['8.3', '8.4', '8.5'] 15 | composer-options: ['--ignore-platform-req=php+'] 16 | fail-fast: false 17 | name: PHP ${{ matrix.php-versions }} @ ${{ matrix.operating-system }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@master 21 | - name: Install PHP 22 | uses: shivammathur/setup-php@master 23 | with: 24 | php-version: ${{ matrix.php-versions }} 25 | tools: 'composer:v2' 26 | extensions: pcov, mbstring, posix 27 | - name: Install dependencies 28 | run: composer --ignore-platform-req=php+ update --prefer-dist --no-progress --no-suggest ${{ matrix.composer-options }} 29 | - name: Run the tests 30 | run: composer run cs 31 | -------------------------------------------------------------------------------- /src/Psalm/Reflect/Infer/PropertyValueType.php: -------------------------------------------------------------------------------- 1 | value)->property($propertyNameType->value); 29 | 30 | return Reflection::getPsalmTypeFromReflectionType($prop->apply( 31 | static fn (ReflectionProperty $reflected) => $reflected->getType() 32 | )); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/array-access.md: -------------------------------------------------------------------------------- 1 | # 🚀 Array Alchemy 2 | 3 | Elevate your array manipulation game. 4 | Unleash the power of arrays with tools that turn the mundane into the extraordinary. 5 | 6 | ## Functions 7 | 8 | This package provides following functions for dealing with arrays. 9 | 10 | #### index_get 11 | 12 | Detects the value of an array at a given index. 13 | The index could either be a number or a string. 14 | If the index is not available inside the array, an `ArrayAccessException` exception is triggered! 15 | 16 | ```php 17 | use VeeWee\Reflecta\ArrayAccess\Exception\ArrayAccessException; 18 | use function VeeWee\Reflecta\ArrayAccess\index_get; 19 | 20 | try { 21 | $value = index_get($yourArray, $indexNumberOrName); 22 | } catch (ArrayAccessException $e) { 23 | // Deal with it 24 | } 25 | ``` 26 | 27 | #### index_set 28 | 29 | Immutably saves a new value at a given index inside an array. 30 | The index could either be a number or a string. 31 | If the index is not available inside the array, a new index will be added. 32 | 33 | ```php 34 | use function VeeWee\Reflecta\ArrayAccess\index_set; 35 | 36 | $yourNewArray = index_set($yourOldArray, $indexNumberOrName, $newValueAtIndex); 37 | ``` 38 | -------------------------------------------------------------------------------- /tests/static-analyzer/Reflect/property_set.php: -------------------------------------------------------------------------------- 1 | z = 123; 14 | 15 | return property_set($x, $z, 456); 16 | } 17 | 18 | /** 19 | * @psalm-suppress InvalidScalarArgument 20 | */ 21 | function test_set_invalid_prop_value_type(): X 22 | { 23 | $z = 'z'; 24 | $x = new X(); 25 | $x->z = 123; 26 | 27 | return property_set($x, $z, 'nope'); 28 | } 29 | 30 | /** 31 | * @psalm-suppress UndefinedPropertyAssignment 32 | */ 33 | function test_assigning_unknown_property(): X 34 | { 35 | $unknown = 'unknown'; 36 | $x = new X(); 37 | 38 | return property_set($x, $unknown, 'nope'); 39 | } 40 | 41 | function test_return_type_on_templated_object(): object 42 | { 43 | $curried = static fn (string $path): Closure => static fn (object $object, mixed $value): mixed => property_set($object, $path, $value); 44 | $z = 'z'; 45 | $x = new X(); 46 | 47 | return $curried($z)($x, 456); 48 | } 49 | -------------------------------------------------------------------------------- /tests/unit/ArrayAccess/IndexGetTest.php: -------------------------------------------------------------------------------- 1 | [ 16 | [1, 2, 3], 17 | 0, 18 | 1 19 | ]; 20 | yield 'dict' => [ 21 | ['hello' => 'world'], 22 | 'hello', 23 | 'world' 24 | ]; 25 | } 26 | 27 | #[DataProvider('provideGetCases')] 28 | public function test_it_can_get_from_array( 29 | array $data, 30 | string|int $index, 31 | mixed $expected 32 | ): void { 33 | $actual = index_get($data, $index); 34 | static::assertSame($expected, $actual); 35 | } 36 | 37 | 38 | public function test_it_can_not_get_from_array(): void 39 | { 40 | $this->expectException(ArrayAccessException::class); 41 | index_get([], 'invalid'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Psalm/Plugin.php: -------------------------------------------------------------------------------- 1 | getHooks() as $hook) { 15 | /** @psalm-suppress UnresolvableInclude */ 16 | require_once __DIR__ . '/' . str_replace([__NAMESPACE__, '\\'], ['', '/'], $hook) . '.php'; 17 | 18 | $registration->registerHooksFromClass($hook); 19 | } 20 | } 21 | 22 | /** 23 | * @template T 24 | * 25 | * @return iterable 26 | */ 27 | private function getHooks(): iterable 28 | { 29 | yield Iso\Provider\ComposeProvider::class; 30 | yield Lens\Provider\ComposeProvider::class; 31 | yield Reflect\Provider\PropertiesSetProvider::class; 32 | yield Reflect\Provider\PropertiesGetProvider::class; 33 | yield Reflect\Provider\PropertyGetProvider::class; 34 | yield Reflect\Provider\PropertySetProvider::class; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/unit/Reflect/ClassHasAttributeTest.php: -------------------------------------------------------------------------------- 1 | [ 15 | [1, 2, 3], 16 | 0, 17 | 100, 18 | [100, 2, 3] 19 | ]; 20 | yield 'dict' => [ 21 | ['hello' => 'world'], 22 | 'hello', 23 | 'earth', 24 | ['hello' => 'earth'] 25 | ]; 26 | yield 'unkown' => [ 27 | ['hello' => 'world'], 28 | 'unkown', 29 | 'unkown', 30 | ['hello' => 'world', 'unkown' => 'unkown'] 31 | ]; 32 | } 33 | 34 | #[DataProvider('provideSetCases')] 35 | public function test_it_can_set_in_array( 36 | array $data, 37 | string|int $index, 38 | mixed $value, 39 | array $expected 40 | ): void { 41 | $actual = index_set($data, $index, $value); 42 | static::assertSame($expected, $actual); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Iso/IsoInterface.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | public function tryTo($s): ResultInterface; 27 | 28 | /** 29 | * @param A $a 30 | * @return S 31 | */ 32 | public function from($a); 33 | 34 | /** 35 | * @param A $a 36 | * @return ResultInterface 37 | */ 38 | public function tryFrom($a): ResultInterface; 39 | 40 | /** 41 | * @return LensInterface 42 | */ 43 | public function asLens(): LensInterface; 44 | 45 | /** 46 | * @return IsoInterface 47 | */ 48 | public function inverse(): self; 49 | 50 | /** 51 | * @template S2 52 | * @template A2 53 | * @param IsoInterface $that 54 | * @return IsoInterface 55 | */ 56 | public function compose(self $that): self; 57 | } 58 | -------------------------------------------------------------------------------- /src/Lens/optional.php: -------------------------------------------------------------------------------- 1 | $that 10 | * 11 | * @return Lens 12 | * 13 | * @psalm-pure 14 | */ 15 | function optional(LensInterface $that): Lens 16 | { 17 | return new Lens( 18 | /** 19 | * @param S|null $subject 20 | * @return A|null 21 | */ 22 | static fn ($subject) => $that->tryGet($subject)->proceed( 23 | /** 24 | * @param A $a 25 | * @return A 26 | */ 27 | static fn ($a) => $a, 28 | /** 29 | * @return null 30 | */ 31 | static fn () => null 32 | ), 33 | /** 34 | * @param S|null $subject 35 | * @param A $value 36 | * @return S|null 37 | */ 38 | static fn ($subject, $value) => $that->trySet($subject, $value)->proceed( 39 | /** 40 | * @param S $s 41 | * @return S 42 | */ 43 | static fn ($s) => $s, 44 | /** 45 | * @return null 46 | */ 47 | static fn () => null 48 | ), 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/analyzers.yaml: -------------------------------------------------------------------------------- 1 | name: Analyzers 2 | 3 | on: 4 | pull_request: ~ 5 | push: ~ 6 | schedule: 7 | - cron: '0 9 * * 5' 8 | 9 | jobs: 10 | run: 11 | runs-on: ${{ matrix.operating-system }} 12 | strategy: 13 | matrix: 14 | operating-system: [ubuntu-latest] 15 | php-versions: ['8.3', '8.4', '8.5'] 16 | composer-options: ['--ignore-platform-req=php+'] 17 | fail-fast: false 18 | name: PHP ${{ matrix.php-versions }} @ ${{ matrix.operating-system }} 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@master 22 | - name: Install PHP 23 | uses: shivammathur/setup-php@master 24 | with: 25 | php-version: ${{ matrix.php-versions }} 26 | tools: 'composer:v2' 27 | extensions: pcov, mbstring, posix 28 | - name: Install dependencies 29 | run: composer update --ignore-platform-req=php+ --prefer-dist --no-progress --no-suggest ${{ matrix.composer-options }} 30 | - name: Run the tests 31 | run: composer run psalm 32 | continue-on-error: ${{ matrix.php-versions == '8.5' }} # Infection dependency is causing issues. 33 | -------------------------------------------------------------------------------- /src/Reflect/Type/ReflectedAttribute.php: -------------------------------------------------------------------------------- 1 | attribute->getName(); 23 | } 24 | 25 | public function isRepeated() : bool 26 | { 27 | return $this->attribute->isRepeated(); 28 | } 29 | 30 | public function instantiate(): object 31 | { 32 | try { 33 | return $this->attribute->newInstance(); 34 | } catch (Throwable $error) { 35 | throw UnreflectableException::nonInstantiatable( 36 | $this->attribute->getName(), 37 | $error 38 | ); 39 | } 40 | } 41 | 42 | /** 43 | * @template T 44 | * @param Closure(ReflectionAttribute): T $closure 45 | */ 46 | public function apply(\Closure $closure): mixed 47 | { 48 | return $closure($this->attribute); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests/unit 15 | 16 | 17 | 18 | 19 | 20 | src 21 | 22 | 23 | src/Psalm 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/unit/Lens/OptionalTest.php: -------------------------------------------------------------------------------- 1 | $value]; 30 | }, 31 | )); 32 | 33 | $validData = ['hello' => 'world']; 34 | $invalidData = []; 35 | 36 | static::assertSame('world', $lens->get($validData)); 37 | static::assertSame(null, $lens->get($invalidData)); 38 | 39 | static::assertSame(['hello' => 'earth'], $lens->set($validData, 'earth')); 40 | static::assertSame(null, $lens->set($invalidData, 'earth')); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: ~ 5 | push: ~ 6 | schedule: 7 | - cron: '0 9 * * 5' 8 | 9 | jobs: 10 | run: 11 | runs-on: ${{ matrix.operating-system }} 12 | strategy: 13 | matrix: 14 | operating-system: [ubuntu-latest] 15 | php-versions: ['8.3', '8.4', '8.5'] 16 | composer-options: ['--ignore-platform-req=php+'] 17 | experimental: [false] 18 | fail-fast: false 19 | name: PHP ${{ matrix.php-versions }} @ ${{ matrix.operating-system }} 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@master 23 | - name: Install PHP 24 | uses: shivammathur/setup-php@master 25 | with: 26 | php-version: ${{ matrix.php-versions }} 27 | tools: 'composer:v2' 28 | ini-values: error_reporting=E_ALL 29 | extensions: pcov, mbstring, posix 30 | - name: Install dependencies 31 | run: composer update --prefer-dist --no-progress --no-suggest ${{ matrix.composer-options }} 32 | - name: Run the tests 33 | run: composer run tests 34 | - name: Check tests quality 35 | run: composer run testquality 36 | # continue-on-error: ${{ matrix.php-versions == '8.5' }} # Infection is not ready yet. 37 | -------------------------------------------------------------------------------- /tests/unit/Reflect/ObjectIsDynamicTest.php: -------------------------------------------------------------------------------- 1 | = 80200) { 32 | static::markTestSkipped('On PHP 8.2, all classes are safely dynamic'); 33 | } 34 | 35 | $x = new #[AllowDynamicProperties] class {}; 36 | $y = new class {}; 37 | $s = new stdClass(); 38 | 39 | static::assertTrue(object_is_dynamic($x)); 40 | static::assertTrue(object_is_dynamic($y)); 41 | static::assertTrue(object_is_dynamic($s)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/unit/Reflect/ClassIsDynamicTest.php: -------------------------------------------------------------------------------- 1 | = 80200) { 33 | static::markTestSkipped('On PHP 8.2, all classes are safely dynamic'); 34 | } 35 | 36 | $x = new #[AllowDynamicProperties] class {}; 37 | $y = new class {}; 38 | $s = new stdClass(); 39 | 40 | static::assertTrue(class_is_dynamic(get_class($x))); 41 | static::assertTrue(class_is_dynamic(get_class($y))); 42 | static::assertTrue(class_is_dynamic(get_class($s))); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Reflect/properties_set.php: -------------------------------------------------------------------------------- 1 | $values 18 | * @param null|Closure(ReflectedProperty): bool $predicate 19 | * 20 | * @return T 21 | * @throws UnreflectableException 22 | * 23 | * @template T of object 24 | */ 25 | function properties_set(object $object, array $values, Closure|null $predicate = null): object 26 | { 27 | $class = ReflectedClass::fromObject($object); 28 | 29 | if ($predicate) { 30 | $allProperties = $class->properties(); 31 | $filteredProperties = filter($allProperties, $predicate); 32 | $unknownValues = $class->isDynamic() ? diff_by_key($values, $allProperties) : []; 33 | 34 | $values = merge($unknownValues, intersect_by_key($values, $filteredProperties)); 35 | } 36 | 37 | return reduce_with_keys( 38 | $values, 39 | /** 40 | * @param T $object 41 | * @return T 42 | */ 43 | static fn (object $object, string $name, mixed $value): object => property_set($object, $name, $value), 44 | $object 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/Lens/LensInterface.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public function tryGet($s): ResultInterface; 26 | 27 | /** 28 | * @param S $s 29 | * @param A $a 30 | * @return S 31 | */ 32 | public function set($s, $a); 33 | 34 | /** 35 | * @param S $s 36 | * @param A $a 37 | * @return ResultInterface 38 | */ 39 | public function trySet($s, $a): ResultInterface; 40 | 41 | /** 42 | * @param S $s 43 | * @param callable(A): A $f 44 | * @return S 45 | */ 46 | public function update($s, callable $f); 47 | 48 | /** 49 | * @param S $s 50 | * @param callable(A): A $f 51 | * @return ResultInterface 52 | */ 53 | public function tryUpdate($s, callable $f): ResultInterface; 54 | 55 | /** 56 | * @return LensInterface 57 | */ 58 | public function optional(): LensInterface; 59 | 60 | /** 61 | * @template S2 62 | * @template A2 63 | * @param LensInterface $that 64 | * @return LensInterface 65 | */ 66 | public function compose(LensInterface $that): LensInterface; 67 | } 68 | -------------------------------------------------------------------------------- /src/Reflect/property_set.php: -------------------------------------------------------------------------------- 1 | property($name); 31 | } catch (UnreflectableException $e) { 32 | // In case the property is unknown, try to set a dynamic property. 33 | if (object_is_dynamic($new)) { 34 | $new->{$name} = $value; 35 | 36 | return $new; 37 | } 38 | 39 | throw $e; 40 | } 41 | 42 | try { 43 | $property->apply( 44 | static fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->setValue($new, $value) 45 | ); 46 | } catch (Throwable $previous) { 47 | throw UnreflectableException::unwritableProperty(get_debug_type($object), $name, $value, $previous); 48 | } 49 | 50 | return $new; 51 | } 52 | -------------------------------------------------------------------------------- /src/Psalm/Reflect/Infer/PropertiesValuesType.php: -------------------------------------------------------------------------------- 1 | value); 32 | $properties = $class->properties(); 33 | 34 | return new Union([ 35 | new TKeyedArray( 36 | map( 37 | $properties, 38 | static fn (ReflectedProperty $prop) => Reflection::getPsalmTypeFromReflectionType($prop->apply( 39 | static fn (ReflectionProperty $reflected) => $reflected->getType() 40 | ))->setPossiblyUndefined($partial), 41 | ), 42 | fallback_params: $class->isDynamic() ? [Type::getArrayKey(), Type::getMixed()] : null, 43 | ) 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/static-analyzer/Reflect/properties_set.php: -------------------------------------------------------------------------------- 1 | z = 123; 16 | 17 | return properties_set($x, ['z' => 456]); 18 | } 19 | 20 | function test_set_valid_prop_value_type_with_predicate(): X 21 | { 22 | $x = new X(); 23 | $x->z = 123; 24 | 25 | return properties_set($x, ['z' => 456], property_visibility(Visibility::Private)); 26 | } 27 | 28 | function test_set_partial_props(): MultipleProperties 29 | { 30 | $x = new MultipleProperties(); 31 | $x->a = ''; 32 | $x->b = ''; 33 | 34 | return properties_set($x, ['c' => 'foo']); 35 | } 36 | 37 | /** 38 | * @psalm-suppress InvalidScalarArgument 39 | */ 40 | function test_set_invalid_prop_value_type(): X 41 | { 42 | $x = new X(); 43 | $x->z = 123; 44 | 45 | return properties_set($x, ['z' => 'nope']); 46 | } 47 | 48 | /** 49 | * @psalm-suppress InvalidArgument 50 | */ 51 | function test_assigning_unknown_property(): X 52 | { 53 | $x = new X(); 54 | 55 | return properties_set($x, ['unknown' => 'nope']); 56 | } 57 | 58 | function test_set_new_prop_on_dynamic_class(): Dynamic 59 | { 60 | $x = new Dynamic(); 61 | $x->x = 'string'; 62 | 63 | return properties_set($x, ['foo' => 'bar']); 64 | } 65 | -------------------------------------------------------------------------------- /src/Reflect/Exception/UnreflectableException.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public static function getFunctionIds(): array 22 | { 23 | return ['veewee\reflecta\reflect\properties_get']; 24 | } 25 | 26 | public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage 27 | { 28 | $args = $event->getArgs(); 29 | $inferrer = $event->getArgTypeInferer(); 30 | 31 | $objectType = ObjectType::infer($inferrer, $args[0]); 32 | $hasPredicate = array_key_exists(1, $args); 33 | $predicateType = $hasPredicate ? $inferrer->infer($args[1]) : Type::getNull(); 34 | $valuesType = PropertiesValuesType::infer($objectType, partial: $hasPredicate); 35 | 36 | if (!$objectType || !$valuesType) { 37 | return null; 38 | } 39 | 40 | $storage = new DynamicFunctionStorage(); 41 | $storage->params = [ 42 | new FunctionLikeParameter('object', false, new Union([$objectType])), 43 | new FunctionLikeParameter('predicate', false, $predicateType), 44 | ]; 45 | $storage->return_type = $valuesType; 46 | 47 | return $storage; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Psalm/Reflect/Provider/PropertiesSetProvider.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public static function getFunctionIds(): array 22 | { 23 | return ['veewee\reflecta\reflect\properties_set']; 24 | } 25 | 26 | public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage 27 | { 28 | $args = $event->getArgs(); 29 | $inferrer = $event->getArgTypeInferer(); 30 | 31 | $objectType = ObjectType::infer($inferrer, $args[0]); 32 | $valuesType = PropertiesValuesType::infer($objectType, partial: true); 33 | $predicateType = array_key_exists(2, $args) ? $inferrer->infer($args[2]) : Type::getNull(); 34 | 35 | if (!$objectType || !$valuesType) { 36 | return null; 37 | } 38 | 39 | $storage = new DynamicFunctionStorage(); 40 | $storage->params = [ 41 | new FunctionLikeParameter('object', false, new Union([$objectType])), 42 | new FunctionLikeParameter('values', false, $valuesType), 43 | new FunctionLikeParameter('predicate', false, $predicateType), 44 | ]; 45 | $storage->return_type = new Union([$objectType]); 46 | 47 | return $storage; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/unit/Reflect/Predicate/PropertyVisibilityTest.php: -------------------------------------------------------------------------------- 1 | property('z'); 19 | 20 | static::assertTrue($prop->check(property_visibility(Visibility::Public))); 21 | static::assertFalse($prop->check(property_visibility(Visibility::Protected))); 22 | static::assertFalse($prop->check(property_visibility(Visibility::Private))); 23 | } 24 | 25 | public function test_it_knows_protected_visibility_from_property(): void 26 | { 27 | $x = new class() { 28 | protected int $z = 0; 29 | }; 30 | $prop = ReflectedClass::fromObject($x)->property('z'); 31 | 32 | static::assertTrue($prop->check(property_visibility(Visibility::Protected))); 33 | static::assertFalse($prop->check(property_visibility(Visibility::Public))); 34 | static::assertFalse($prop->check(property_visibility(Visibility::Private))); 35 | } 36 | 37 | public function test_it_knows_private_visibility_from_property(): void 38 | { 39 | $x = new class() { 40 | private int $z = 0; 41 | }; 42 | $prop = ReflectedClass::fromObject($x)->property('z'); 43 | 44 | static::assertTrue($prop->check(property_visibility(Visibility::Private))); 45 | static::assertFalse($prop->check(property_visibility(Visibility::Public))); 46 | static::assertFalse($prop->check(property_visibility(Visibility::Protected))); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setFinder( 5 | \Symfony\Component\Finder\Finder::create() 6 | ->in([ 7 | __DIR__ . '/src', 8 | __DIR__ . '/tests', 9 | ]) 10 | ->name('*.php') 11 | ) 12 | ->setRiskyAllowed(true) 13 | ->setRules([ 14 | '@PSR2' => true, 15 | 'align_multiline_comment' => true, 16 | 'array_indentation' => true, 17 | 'declare_strict_types' => true, 18 | 'final_class' => true, 19 | 'global_namespace_import' => [ 20 | 'import_classes' => true, 21 | 'import_constants' => true, 22 | 'import_functions' => true, 23 | ], 24 | 'list_syntax' => [ 25 | 'syntax' => 'short', 26 | ], 27 | 'constant_case' => [ 28 | 'case' => 'lower', 29 | ], 30 | 'multiline_comment_opening_closing' => true, 31 | 'native_function_casing' => true, 32 | 'no_empty_phpdoc' => true, 33 | 'no_leading_import_slash' => true, 34 | 'no_superfluous_phpdoc_tags' => [ 35 | 'allow_mixed' => true, 36 | ], 37 | 'no_unused_imports' => true, 38 | 'no_useless_else' => true, 39 | 'no_useless_return' => true, 40 | 'ordered_imports' => [ 41 | 'imports_order' => ['class', 'function', 'const'], 42 | ], 43 | 'ordered_interfaces' => true, 44 | 'php_unit_test_annotation' => true, 45 | 'php_unit_test_case_static_method_calls' => [ 46 | 'call_type' => 'static', 47 | ], 48 | 'php_unit_method_casing' => [ 49 | 'case' => 'snake_case', 50 | ], 51 | 'single_import_per_statement' => true, 52 | 'single_trait_insert_per_statement' => true, 53 | 'static_lambda' => true, 54 | 'strict_comparison' => true, 55 | 'strict_param' => true, 56 | 'native_function_invocation' => true, 57 | 'nullable_type_declaration_for_default_null_value' => true, 58 | ]) 59 | ; 60 | -------------------------------------------------------------------------------- /tests/unit/Reflect/ObjectAttributesTest.php: -------------------------------------------------------------------------------- 1 | expectException(UnreflectableException::class); 52 | $this->expectExceptionMessage('Unable to instantiate class ThisIsAnUnknownAttribute.'); 53 | 54 | object_attributes($x); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/static-analyzer/Reflect/properties_get.php: -------------------------------------------------------------------------------- 1 | z = 123; 20 | 21 | return properties_get($x); 22 | } 23 | 24 | /** 25 | * @return array{z ?: int|null} 26 | */ 27 | function test_get_optional_prop_return_type(): array 28 | { 29 | $x = new X(); 30 | $x->z = 123; 31 | 32 | return properties_get($x, property_visibility(Visibility::Private)); 33 | } 34 | 35 | /** 36 | * @return array{a: string, b: string, c: string} 37 | */ 38 | function test_get_multi_props_return_type(): array 39 | { 40 | $x = new MultipleProperties(); 41 | 42 | return properties_get($x); 43 | } 44 | 45 | /** 46 | * @return array{a ?: string, b ?: string, c ?: string} 47 | */ 48 | function test_get_optional_multi_props_return_type(): array 49 | { 50 | $x = new MultipleProperties(); 51 | 52 | return properties_get($x, property_visibility(Visibility::Private)); 53 | } 54 | 55 | /** 56 | * @return array{x: string, ...} 57 | */ 58 | function test_get_dynamic_props_return_type(): array 59 | { 60 | $x = new Dynamic(); 61 | 62 | return properties_get($x); 63 | } 64 | 65 | /** 66 | * @return array{x ?: string, ...} 67 | */ 68 | function test_get_optional_dynamic_props_return_type(): array 69 | { 70 | $x = new Dynamic(); 71 | 72 | return properties_get($x, property_visibility(Visibility::Private)); 73 | } 74 | 75 | function test_get_mixed_return_type_on_templated_object(): array 76 | { 77 | $curried = static fn (): Closure => static fn (object $object): array => properties_get($object); 78 | $x = new X(); 79 | 80 | return $curried()($x); 81 | } 82 | -------------------------------------------------------------------------------- /tests/unit/Reflect/ClassAttributesTest.php: -------------------------------------------------------------------------------- 1 | expectException(UnreflectableException::class); 53 | $this->expectExceptionMessage('Unable to instantiate class ThisIsAnUnknownAttribute.'); 54 | 55 | class_attributes(get_class($x)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/unit/Lens/PropertiesTest.php: -------------------------------------------------------------------------------- 1 | 'world']; 17 | $data = (object)$expected; 18 | 19 | static::assertSame($expected, $lens->get($data)); 20 | static::assertSame($expected, $lens->tryGet($data)->getResult()); 21 | static::assertTrue($lens->tryGet(null)->isFailed()); 22 | 23 | static::assertEquals((object)['hello' => 'earth'], $lens->set($data, ['hello' => 'earth'])); 24 | static::assertNotSame($data, $lens->set($data, ['hello' => 'earth'])); 25 | static::assertEquals((object)['hello' => 'earth'], $lens->trySet($data, ['hello' => 'earth'])->getResult()); 26 | static::assertTrue($lens->trySet(null, 'earth')->isFailed()); 27 | } 28 | 29 | public function test_it_can_apply_a_predicate(): void 30 | { 31 | $lens = properties(static fn (ReflectedProperty $property) => $property->name() === 'hello'); 32 | $expected = ['hello' => 'world']; 33 | $moreInfo = [ 34 | ...$expected, 35 | 'goodbye' => 'earth', 36 | ]; 37 | $data = (object)$moreInfo; 38 | 39 | static::assertSame($expected, $lens->get($data)); 40 | static::assertSame($expected, $lens->tryGet($data)->getResult()); 41 | static::assertTrue($lens->tryGet(null)->isFailed()); 42 | 43 | static::assertEquals((object)[...$moreInfo, 'hello' => 'earth'], $lens->set($data, ['hello' => 'earth', 'goodbye' => 'Toon'])); 44 | static::assertNotSame($data, $lens->set($data, ['hello' => 'earth'])); 45 | static::assertEquals((object)[...$moreInfo, 'hello' => 'earth'], $lens->trySet($data, ['hello' => 'earth', 'goodbye' => 'Toon'])->getResult()); 46 | static::assertTrue($lens->trySet(null, 'earth')->isFailed()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ## 🪞 REFLECTA 🪞 4 | ### Unleash the Power of Optics in your code 5 | 6 |
7 | 8 | Welcome to Reflecta! 9 | 10 | Transform your data like never before with this optics implementation – a set of sleek, intuitive tools designed to empower your code with the finesse of reflection, the precision of lenses, and the magic of Isomorphisms. 11 | Say goodbye to the complexities, and hello to a world where building, accessing and manipulating your data structures becomes as effortless as a brushstroke on a canvas. 12 | 13 | Dive into this Optics Toolkit, where coding meets artistry, and simplicity meets power. 14 | It's not just about data – it's about orchestrating a symphony of possibilities. 15 | Your data has never looked so good! 16 | 17 | ## Installation 18 | 19 | Reflecta is installed via Composer. 20 | 21 | ``` 22 | composer require veewee/reflecta 23 | ``` 24 | > Requires PHP 8.1+ 25 | 26 | **Warning.** This package is primarily published to receive early feedback and for contributors, during this development phase we cannot guarantee the stability of the APIs, consider each release to contain breaking changes. 27 | 28 | ### Psalm support 29 | 30 | This package is created with type-safety and error awareness in mind. 31 | 32 | In order to have better type inference, this package comes shipped with a psalm plugin. 33 | You can enable it by: 34 | 35 | ``` 36 | ./vendor/bin/psalm-plugin enable 'VeeWee\Reflecta\Psalm\Plugin' 37 | ``` 38 | > Requires vimeo/psalm 5+ 39 | 40 | ## Components 41 | 42 | This package provides following components: 43 | 44 | * [ArrayAccess](/docs/array-access.md): Helps you read from and write to arrays. 45 | * [Iso](/docs/isomorphisms.md): Provides bidirectional transformations on your data. 46 | * [Lens](/docs/lens.md): Separate your data from it's structure 47 | * [Reflect](/docs/reflect.md): Helps you read from and write to objects in a runtime-safe context. 48 | 49 | 50 | ## Inspiration 51 | 52 | This library was inspired by the following projects: 53 | 54 | * [marcosh/lamphpda-optics](https://github.com/marcosh/lamphpda-optics) 55 | * [fp-ts/optic](https://github.com/fp-ts/optic) 56 | * [haskell lens](https://hackage.haskell.org/package/lens) 57 | -------------------------------------------------------------------------------- /tests/unit/Reflect/PropertiesSetTest.php: -------------------------------------------------------------------------------- 1 | 123]); 20 | 21 | static::assertNotSame($x, $actual); 22 | static::assertInstanceOf(X::class, $actual); 23 | static::assertSame($actual->z, 123); 24 | } 25 | 26 | public function test_it_can_set_with_predicate_filter(): void 27 | { 28 | $x = new Dynamic(); 29 | $x->x = '123'; 30 | $x->y = '123'; 31 | $actual = properties_set($x, ['x' => '456', 'y' => '456'], static fn (ReflectedProperty $property): bool => $property->isDefault()); 32 | 33 | static::assertNotSame($x, $actual); 34 | static::assertInstanceOf(Dynamic::class, $actual); 35 | static::assertSame($actual->x, '456'); 36 | static::assertSame($actual->y, '123'); 37 | } 38 | 39 | public function test_it_can_hydrate_new_props_on_std_class(): void 40 | { 41 | $x = new stdClass(); 42 | $x->foo = 'foo'; 43 | 44 | $actual = properties_set($x, ['bar' => 'bar']); 45 | 46 | static::assertNotSame($x, $actual); 47 | static::assertInstanceOf(stdClass::class, $actual); 48 | static::assertSame($actual->foo, 'foo'); 49 | static::assertSame($actual->bar, 'bar'); 50 | } 51 | 52 | public function test_it_can_hydrate_new_props_on_std_class_with_predicate(): void 53 | { 54 | $x = new stdClass(); 55 | $x->foo = 'foo'; 56 | 57 | $actual = properties_set( 58 | $x, 59 | [ 60 | 'foo' => 'baz', 61 | 'bar' => 'bar', 62 | ], 63 | static fn (ReflectedProperty $property): bool => false 64 | ); 65 | 66 | static::assertNotSame($x, $actual); 67 | static::assertInstanceOf(stdClass::class, $actual); 68 | static::assertSame($actual->foo, 'foo'); 69 | static::assertSame($actual->bar, 'bar'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Psalm/Reflect/Provider/PropertyGetProvider.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public static function getFunctionIds(): array 24 | { 25 | return ['veewee\reflecta\reflect\property_get']; 26 | } 27 | 28 | public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage 29 | { 30 | $args = $event->getArgs(); 31 | $inferrer = $event->getArgTypeInferer(); 32 | 33 | $objectType = ObjectType::infer($inferrer, $args[0]); 34 | $propertyNameType = PropertyNameType::infer($inferrer, $args[1]); 35 | 36 | try { 37 | $inferredReturnType = PropertyValueType::infer($objectType, $propertyNameType); 38 | } catch (UnreflectableException $e) { 39 | IssueBuffer::maybeAdd( 40 | new UndefinedPropertyFetch( 41 | $e->getMessage(), 42 | $event->getCodeLocation(), 43 | $objectType->value . '::' . $propertyNameType->value, 44 | ), 45 | $event->getStatementSource()->getSuppressedIssues() 46 | ); 47 | 48 | return null; 49 | } 50 | 51 | if (!$objectType || !$propertyNameType || !$inferredReturnType) { 52 | return null; 53 | } 54 | 55 | $storage = new DynamicFunctionStorage(); 56 | $storage->params = [ 57 | new FunctionLikeParameter('object', false, new Union([$objectType])), 58 | new FunctionLikeParameter('name', false, new Union([$propertyNameType])), 59 | ]; 60 | $storage->return_type = $inferredReturnType; 61 | 62 | return $storage; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Psalm/Reflect/Provider/PropertySetProvider.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public static function getFunctionIds(): array 24 | { 25 | return ['veewee\reflecta\reflect\property_set']; 26 | } 27 | 28 | public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage 29 | { 30 | $args = $event->getArgs(); 31 | $inferrer = $event->getArgTypeInferer(); 32 | 33 | $objectType = ObjectType::infer($inferrer, $args[0]); 34 | $propertyNameType = PropertyNameType::infer($inferrer, $args[1]); 35 | 36 | try { 37 | $inferredValueType = PropertyValueType::infer($objectType, $propertyNameType); 38 | } catch (UnreflectableException $e) { 39 | IssueBuffer::maybeAdd( 40 | new UndefinedPropertyAssignment( 41 | $e->getMessage(), 42 | $event->getCodeLocation(), 43 | $objectType->value . '::' . $propertyNameType->value, 44 | ), 45 | $event->getStatementSource()->getSuppressedIssues() 46 | ); 47 | 48 | return null; 49 | } 50 | 51 | if (!$objectType || !$propertyNameType || !$inferredValueType) { 52 | return null; 53 | } 54 | 55 | $storage = new DynamicFunctionStorage(); 56 | $storage->params = [ 57 | new FunctionLikeParameter('object', false, new Union([$objectType])), 58 | new FunctionLikeParameter('name', false, new Union([$propertyNameType])), 59 | new FunctionLikeParameter('value', false, $inferredValueType), 60 | ]; 61 | $storage->return_type = new Union([$objectType]); 62 | 63 | return $storage; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/unit/Reflect/PropertySetTest.php: -------------------------------------------------------------------------------- 1 | z = 123; 23 | 24 | $actual = property_set($x, 'z', 345); 25 | 26 | static::assertNotSame($x, $actual); 27 | static::assertInstanceOf(X::class, $actual); 28 | static::assertSame(345, $actual->z); 29 | } 30 | 31 | 32 | public function test_it_errors_on_unclonable(): void 33 | { 34 | $this->expectException(CloneException::class); 35 | 36 | $x = new Unclonable(); 37 | property_set($x, 'z', 345); 38 | } 39 | 40 | public function test_it_errors_on_unknown_prop(): void 41 | { 42 | if (PHP_VERSION_ID < 80200) { 43 | static::markTestSkipped('On PHP 8.2, all classes are safely dynamic'); 44 | } 45 | 46 | $x = new X(); 47 | $x->z = 123; 48 | 49 | $this->expectException(UnreflectableException::class); 50 | property_set($x, 'unkown', 123); 51 | } 52 | 53 | public function test_it_can_set_unkown_prop_on_dynamic_object(): void 54 | { 55 | $x = new Dynamic(); 56 | $actual = property_set($x, 'unkown', 123); 57 | 58 | static::assertNotSame($x, $actual); 59 | static::assertInstanceOf(Dynamic::class, $actual); 60 | static::assertSame(123, $actual->unkown); 61 | } 62 | 63 | public function test_it_errors_on_initialized_readonly(): void 64 | { 65 | $this->expectException(UnreflectableException::class); 66 | 67 | $x = new ReadonlyX(123); 68 | property_set($x, 'z', 345); 69 | } 70 | 71 | 72 | public function test_it_can_deal_with_uninitialized_readonly(): void 73 | { 74 | $x = instantiate(ReadonlyX::class); 75 | $actual = property_set($x, 'z', 345); 76 | 77 | static::assertNotSame($x, $actual); 78 | static::assertInstanceOf(ReadonlyX::class, $actual); 79 | static::assertSame(345, $actual->z); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/unit/Iso/ObjectDataTest.php: -------------------------------------------------------------------------------- 1 | 100]; 22 | $expectedInstance = new X(); 23 | $expectedInstance->z = 100; 24 | 25 | $instance = $iso->from($expectedData); 26 | $actualData = $iso->to($instance); 27 | 28 | static::assertEquals($expectedInstance, $instance); 29 | static::assertSame($expectedData, $actualData); 30 | } 31 | 32 | public function test_it_can_use_alternate_lens(): void 33 | { 34 | $x = new class { 35 | public int $z = 100; 36 | private int $y = 200; 37 | }; 38 | 39 | $iso = object_data($x::class, properties(property_visibility(Visibility::Public))); 40 | 41 | $expectedData = ['z' => 100]; 42 | $expectedInstance = clone $x; 43 | 44 | $instance = $iso->from($expectedData); 45 | $actualData = $iso->to($instance); 46 | 47 | static::assertEquals($expectedInstance, $instance); 48 | static::assertSame($expectedData, $actualData); 49 | 50 | $actualSkippedPrivate = $iso->from(['z' => 300, 'y' => 5000]); 51 | $expectedInstance->z = 300; 52 | static::assertEquals($expectedInstance, $actualSkippedPrivate); 53 | } 54 | 55 | public function test_it_can_hydrate_inherited_private_props(): void 56 | { 57 | $x = new class extends AbstractProperties { 58 | private string $d = 'd'; 59 | protected string $e = 'e'; 60 | public string $f = 'f'; 61 | }; 62 | $iso = object_data($x::class); 63 | 64 | $expectedData = [ 65 | 'a' => 'a', 66 | 'b' => 'b', 67 | 'c' => 'c', 68 | 'd' => 'd', 69 | 'e' => 'e', 70 | 'f' => 'f', 71 | ]; 72 | $expectedInstance = $x; 73 | 74 | $instance = $iso->from($expectedData); 75 | $actualData = $iso->to($instance); 76 | 77 | static::assertEquals($expectedInstance, $instance); 78 | static::assertSame($expectedData, $actualData); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/unit/Reflect/Type/ReflectedAttributeTest.php: -------------------------------------------------------------------------------- 1 | getAttributes()[0]; 21 | $reflected = new ReflectedAttribute($attribute); 22 | 23 | static::assertSame(CustomAttribute::class, $reflected->fullName()); 24 | } 25 | 26 | public function test_it_knows_if_it_is_not_repeated(): void 27 | { 28 | $x = new #[CustomAttribute] class {}; 29 | $attribute = (new ReflectionClass($x))->getAttributes()[0]; 30 | $reflected = new ReflectedAttribute($attribute); 31 | 32 | static::assertFalse($reflected->isRepeated()); 33 | } 34 | 35 | public function test_it_knows_if_it_is_repeated(): void 36 | { 37 | $x = new #[RepeatedAttribute] #[RepeatedAttribute] class {}; 38 | $attribute = (new ReflectionClass($x))->getAttributes()[0]; 39 | $reflected = new ReflectedAttribute($attribute); 40 | 41 | static::assertTrue($reflected->isRepeated()); 42 | } 43 | 44 | public function test_it_can_be_instantiated(): void 45 | { 46 | $x = new #[CustomAttribute] class {}; 47 | $attribute = (new ReflectionClass($x))->getAttributes()[0]; 48 | $reflected = new ReflectedAttribute($attribute); 49 | 50 | static::assertEquals(new CustomAttribute(), $reflected->instantiate()); 51 | } 52 | 53 | public function test_it_can_not_instantiate(): void 54 | { 55 | $x = new #[ThisIsAnUnknownAttribute] class {}; 56 | $attribute = (new ReflectionClass($x))->getAttributes()[0]; 57 | $reflected = new ReflectedAttribute($attribute); 58 | 59 | $this->expectException(UnreflectableException::class); 60 | $this->expectExceptionMessage('Unable to instantiate class ThisIsAnUnknownAttribute.'); 61 | 62 | $reflected->instantiate(); 63 | } 64 | 65 | public function test_it_can_apply(): void 66 | { 67 | $x = new #[CustomAttribute] class {}; 68 | $attribute = (new ReflectionClass($x))->getAttributes()[0]; 69 | $reflected = new ReflectedAttribute($attribute); 70 | 71 | $result = $reflected->apply( 72 | static fn (ReflectionAttribute $attr): string => $attr->getName() 73 | ); 74 | 75 | static::assertSame(CustomAttribute::class, $result); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "veewee/reflecta", 3 | "description": "Unleash the Power of Optics in your code!", 4 | "keywords": ["reflection", "lenses", "optics", "array-access", "isomorphisms"], 5 | "type": "library", 6 | "license": "MIT", 7 | "autoload": { 8 | "psr-4": { 9 | "VeeWee\\Reflecta\\": "src/" 10 | }, 11 | "files": [ 12 | "src/ArrayAccess/index_get.php", 13 | "src/ArrayAccess/index_set.php", 14 | "src/Lens/optional.php", 15 | "src/Lens/compose.php", 16 | "src/Lens/index.php", 17 | "src/Lens/properties.php", 18 | "src/Lens/property.php", 19 | "src/Lens/read_only.php", 20 | "src/Iso/compose.php", 21 | "src/Iso/object_data.php", 22 | "src/Reflect/class_attributes.php", 23 | "src/Reflect/class_has_attribute.php", 24 | "src/Reflect/class_is_dynamic.php", 25 | "src/Reflect/instantiate.php", 26 | "src/Reflect/object_attributes.php", 27 | "src/Reflect/object_has_attribute.php", 28 | "src/Reflect/object_is_dynamic.php", 29 | "src/Reflect/properties_get.php", 30 | "src/Reflect/properties_set.php", 31 | "src/Reflect/property_get.php", 32 | "src/Reflect/property_set.php", 33 | "src/Reflect/Predicate/property_visibility.php" 34 | ] 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "VeeWee\\Reflecta\\TestFixtures\\": "tests/fixtures/", 39 | "VeeWee\\Reflecta\\SaTests\\": "tests/static-analyzer/", 40 | "VeeWee\\Reflecta\\UnitTests\\": "tests/unit/" 41 | } 42 | }, 43 | "authors": [ 44 | { 45 | "name": "Toon Verwerft", 46 | "email": "toonverwerft@gmail.com" 47 | } 48 | ], 49 | "require": { 50 | "php": "~8.3.0 || ~8.4.0 || ~8.5.0", 51 | "azjezz/psl": "~3.0 || ~4.0" 52 | }, 53 | "require-dev": { 54 | "vimeo/psalm": "~6.13", 55 | "phpunit/phpunit": "~12.3", 56 | "php-cs-fixer/shim": "~3.88", 57 | "veewee/composer-run-parallel": "^1.3", 58 | "infection/infection": "^0.31.7" 59 | }, 60 | "scripts": { 61 | "cs": "PHP_CS_FIXER_IGNORE_ENV=1 php ./vendor/bin/php-cs-fixer fix --dry-run", 62 | "cs:fix": "PHP_CS_FIXER_IGNORE_ENV=1 php ./vendor/bin/php-cs-fixer fix", 63 | "psalm": "./vendor/bin/psalm --no-cache --stats", 64 | "tests": "./vendor/bin/phpunit --coverage-text --color", 65 | "testquality": "@parallel infection", 66 | "infection": [ 67 | "Composer\\Config::disableProcessTimeout", 68 | "./vendor/bin/infection --show-mutations -v" 69 | ], 70 | "ci": [ 71 | "@parallel cs psalm tests", 72 | "@parallel infection" 73 | ] 74 | }, 75 | "config": { 76 | "allow-plugins": { 77 | "veewee/composer-run-parallel": true, 78 | "infection/extension-installer": true 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/unit/Reflect/Exception/UnreflectableExceptionTest.php: -------------------------------------------------------------------------------- 1 | getPrevious()); 20 | static::assertSame(0, $exception->getCode()); 21 | $this->expectExceptionObject($exception); 22 | $this->expectException(RuntimeException::class); 23 | $this->expectExceptionMessage('Unable to locate property class::$prop.'); 24 | 25 | throw $exception; 26 | } 27 | 28 | 29 | public function test_it_can_throw_non_instantiatable_class(): void 30 | { 31 | $previous = new Exception('hey'); 32 | $exception = UnreflectableException::nonInstantiatable('className', $previous); 33 | 34 | static::assertSame($previous, $exception->getPrevious()); 35 | static::assertSame(0, $exception->getCode()); 36 | $this->expectExceptionObject($exception); 37 | $this->expectException(RuntimeException::class); 38 | $this->expectExceptionMessage('Unable to instantiate class className.'); 39 | 40 | throw $exception; 41 | } 42 | 43 | 44 | public function test_it_can_throw_unkown_class(): void 45 | { 46 | $previous = new Exception('hey'); 47 | $exception = UnreflectableException::unknownClass('className', $previous); 48 | 49 | static::assertSame($previous, $exception->getPrevious()); 50 | static::assertSame(0, $exception->getCode()); 51 | $this->expectExceptionObject($exception); 52 | $this->expectException(RuntimeException::class); 53 | $this->expectExceptionMessage('Unable to locate class className.'); 54 | 55 | throw $exception; 56 | } 57 | 58 | 59 | public function test_it_can_throw_unwritable_property(): void 60 | { 61 | $previous = new Exception('hey'); 62 | $exception = UnreflectableException::unwritableProperty('class', 'prop', 'string', $previous); 63 | 64 | static::assertSame($previous, $exception->getPrevious()); 65 | static::assertSame(0, $exception->getCode()); 66 | $this->expectExceptionObject($exception); 67 | $this->expectException(RuntimeException::class); 68 | $this->expectExceptionMessage('Unable to write type string to property class::$prop.'); 69 | 70 | throw $exception; 71 | } 72 | 73 | public function test_it_can_throw_unreadable_property(): void 74 | { 75 | $previous = new Exception('hey'); 76 | $exception = UnreflectableException::unreadableProperty('class', 'prop', $previous); 77 | 78 | static::assertSame($previous, $exception->getPrevious()); 79 | static::assertSame(0, $exception->getCode()); 80 | $this->expectExceptionObject($exception); 81 | $this->expectException(RuntimeException::class); 82 | $this->expectExceptionMessage('Unable to read property class::$prop.'); 83 | 84 | throw $exception; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Iso/Iso.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class Iso implements IsoInterface 20 | { 21 | /** @var callable(S): A */ 22 | private $to; 23 | 24 | /** @var callable(A): S */ 25 | private $from; 26 | 27 | /** 28 | * @param callable(S): A $to 29 | * @param callable(A): S $from 30 | */ 31 | public function __construct(callable $to, callable $from) 32 | { 33 | $this->to = $to; 34 | $this->from = $from; 35 | } 36 | 37 | /** 38 | * @pure 39 | * @template I 40 | * @return Iso 41 | */ 42 | public static function identity(): self 43 | { 44 | return new self( 45 | /** 46 | * @param I $s 47 | * @returns I 48 | */ 49 | static fn ($s) => $s, 50 | /** 51 | * @param I $s 52 | * @returns I 53 | */ 54 | static fn ($s) => $s 55 | ); 56 | } 57 | 58 | /** 59 | * @param S $s 60 | * @return A 61 | */ 62 | public function to($s) 63 | { 64 | return ($this->to)($s); 65 | } 66 | 67 | /** 68 | * @param S $s 69 | * @return ResultInterface 70 | */ 71 | public function tryTo($s): ResultInterface 72 | { 73 | return wrap(fn () => ($this->to)($s)); 74 | } 75 | 76 | /** 77 | * @param A $a 78 | * @return S 79 | */ 80 | public function from($a) 81 | { 82 | return ($this->from)($a); 83 | } 84 | 85 | /** 86 | * @param A $a 87 | * @return ResultInterface 88 | */ 89 | public function tryFrom($a): ResultInterface 90 | { 91 | return wrap(fn () => ($this->from)($a)); 92 | } 93 | 94 | /** 95 | * @return Lens 96 | */ 97 | public function asLens(): LensInterface 98 | { 99 | return new Lens( 100 | $this->to, 101 | /** 102 | * @param S $_ 103 | * @param A $a 104 | * @return S 105 | */ 106 | fn ($_, $a) => $this->from($a) 107 | ); 108 | } 109 | 110 | /** 111 | * @return Iso 112 | */ 113 | public function inverse(): self 114 | { 115 | return new self($this->from, $this->to); 116 | } 117 | 118 | /** 119 | * @template S2 120 | * @template A2 121 | * @param IsoInterface $that 122 | * @return Iso 123 | */ 124 | public function compose(IsoInterface $that): IsoInterface 125 | { 126 | /** @psalm-suppress InvalidArgument */ 127 | return new self( 128 | /** 129 | * @param S $s 130 | * @return A2 131 | */ 132 | fn ($s) => $that->to($this->to($s)), 133 | /** 134 | * @param A2 $a2 135 | * @return S 136 | */ 137 | fn ($a2) => $this->from($that->from($a2)) 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Psalm/Iso/Provider/ComposeProvider.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public static function getFunctionIds(): array 26 | { 27 | return ['veewee\reflecta\iso\compose']; 28 | } 29 | 30 | public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage 31 | { 32 | $templateProvider = $event->getTemplateProvider(); 33 | $argsCount = count($event->getArgs()); 34 | 35 | // Create S->A iso pairs 36 | $composedIsos = array_map( 37 | static fn (int $callable_offset) => self::createABIso( 38 | self::createTemplateFromOffset($templateProvider, $callable_offset), 39 | self::createTemplateFromOffset($templateProvider, $callable_offset + 1), 40 | ), 41 | range(1, $argsCount) 42 | ); 43 | 44 | $composeStorage = new DynamicFunctionStorage(); 45 | $composeStorage->params = [ 46 | ...array_map( 47 | static fn (TGenericObject $iso, int $offset) => self::createParam( 48 | "iso_{$offset}", 49 | new Union([$iso]), 50 | ), 51 | $composedIsos, 52 | array_keys($composedIsos) 53 | ) 54 | ]; 55 | 56 | // Add compose template list for each intermediate Iso 57 | $composeStorage->templates = array_map( 58 | static fn ($offset) => self::createTemplateFromOffset($templateProvider, $offset), 59 | range(1, $argsCount + 1), 60 | ); 61 | 62 | // Compose return type from templates T1 -> TLast (Where TLast could also be T1 when no arguments are provided.) 63 | $composeStorage->return_type = new Union([ 64 | self::createABIso( 65 | current($composeStorage->templates), 66 | end($composeStorage->templates) 67 | ) 68 | ]); 69 | 70 | return $composeStorage; 71 | } 72 | 73 | private static function createTemplateFromOffset( 74 | DynamicTemplateProvider $template_provider, 75 | int $offset 76 | ): TTemplateParam { 77 | return $template_provider->createTemplate("T{$offset}"); 78 | } 79 | 80 | private static function createABIso( 81 | TTemplateParam $aType, 82 | TTemplateParam $bType 83 | ): TGenericObject { 84 | return new TGenericObject( 85 | Iso::class, 86 | [ 87 | new Union([$aType]), 88 | new Union([$bType]), 89 | ] 90 | ); 91 | } 92 | 93 | private static function createParam(string $name, Union $type): FunctionLikeParameter 94 | { 95 | return new FunctionLikeParameter($name, false, $type); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Psalm/Lens/Provider/ComposeProvider.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public static function getFunctionIds(): array 26 | { 27 | return ['veewee\reflecta\lens\compose']; 28 | } 29 | 30 | public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage 31 | { 32 | $templateProvider = $event->getTemplateProvider(); 33 | $argsCount = count($event->getArgs()); 34 | 35 | // Create S->A lens pairs 36 | $composedLenses = array_map( 37 | static fn (int $callable_offset) => self::createABLens( 38 | self::createTemplateFromOffset($templateProvider, $callable_offset), 39 | self::createTemplateFromOffset($templateProvider, $callable_offset + 1), 40 | ), 41 | range(1, $argsCount) 42 | ); 43 | 44 | $composeStorage = new DynamicFunctionStorage(); 45 | $composeStorage->params = [ 46 | ...array_map( 47 | static fn (TGenericObject $lens, int $offset) => self::createParam( 48 | "lens_{$offset}", 49 | new Union([$lens]), 50 | ), 51 | $composedLenses, 52 | array_keys($composedLenses) 53 | ) 54 | ]; 55 | 56 | // Add compose template list for each intermediate Lens 57 | $composeStorage->templates = array_map( 58 | static fn ($offset) => self::createTemplateFromOffset($templateProvider, $offset), 59 | range(1, $argsCount + 1), 60 | ); 61 | 62 | // Compose return type from templates T1 -> TLast (Where TLast could also be T1 when no arguments are provided.) 63 | $composeStorage->return_type = new Union([ 64 | self::createABLens( 65 | current($composeStorage->templates), 66 | end($composeStorage->templates) 67 | ) 68 | ]); 69 | 70 | return $composeStorage; 71 | } 72 | 73 | private static function createTemplateFromOffset( 74 | DynamicTemplateProvider $template_provider, 75 | int $offset 76 | ): TTemplateParam { 77 | return $template_provider->createTemplate("T{$offset}"); 78 | } 79 | 80 | private static function createABLens( 81 | TTemplateParam $aType, 82 | TTemplateParam $bType 83 | ): TGenericObject { 84 | return new TGenericObject( 85 | Lens::class, 86 | [ 87 | new Union([$aType]), 88 | new Union([$bType]), 89 | ] 90 | ); 91 | } 92 | 93 | private static function createParam(string $name, Union $type): FunctionLikeParameter 94 | { 95 | return new FunctionLikeParameter($name, false, $type); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Reflect/Type/ReflectedProperty.php: -------------------------------------------------------------------------------- 1 | property->getName(); 22 | } 23 | 24 | public function declaringClass(): ReflectedClass 25 | { 26 | return new ReflectedClass($this->property->getDeclaringClass()); 27 | } 28 | 29 | public function visibility(): Visibility 30 | { 31 | return Visibility::forProperty($this->property); 32 | } 33 | 34 | /** 35 | * @param Closure(ReflectedProperty): bool $predicate 36 | */ 37 | public function check(Closure $predicate): bool 38 | { 39 | return $predicate($this); 40 | } 41 | 42 | public function isPublic(): bool 43 | { 44 | return $this->property->isPublic(); 45 | } 46 | 47 | public function isProtected(): bool 48 | { 49 | return $this->property->isProtected(); 50 | } 51 | 52 | public function isPrivate(): bool 53 | { 54 | return $this->property->isPrivate(); 55 | } 56 | 57 | public function isPromoted(): bool 58 | { 59 | return $this->property->isPromoted(); 60 | } 61 | 62 | public function isStatic(): bool 63 | { 64 | return $this->property->isStatic(); 65 | } 66 | 67 | public function isReadOnly(): bool 68 | { 69 | return $this->property->isReadOnly(); 70 | } 71 | 72 | public function isDefault(): bool 73 | { 74 | return $this->property->isDefault(); 75 | } 76 | 77 | public function isDynamic(): bool 78 | { 79 | return !$this->isDefault(); 80 | } 81 | 82 | public function docComment(): string 83 | { 84 | return $this->property->getDocComment(); 85 | } 86 | 87 | public function defaultValue(): mixed 88 | { 89 | return $this->property->getDefaultValue(); 90 | } 91 | 92 | public function hasDefaultValue(): bool 93 | { 94 | return $this->property->hasDefaultValue(); 95 | } 96 | 97 | /** 98 | * @template T extends object 99 | * 100 | * @param class-string|null $attributeClassName 101 | * 102 | * @return (T is null ? list : list) 103 | * @throws UnreflectableException 104 | */ 105 | public function attributes(?string $attributeClassName = null): array 106 | { 107 | return map( 108 | $this->property->getAttributes($attributeClassName, ReflectionAttribute::IS_INSTANCEOF), 109 | static fn (ReflectionAttribute $attribute): object => (new ReflectedAttribute($attribute))->instantiate() 110 | ); 111 | } 112 | 113 | /** 114 | * @param class-string $attributeClassName 115 | */ 116 | public function hasAttributeOfType(string $attributeClassName): bool 117 | { 118 | return (bool) $this->property->getAttributes($attributeClassName, ReflectionAttribute::IS_INSTANCEOF); 119 | } 120 | 121 | /** 122 | * @template T 123 | * @param Closure(ReflectionProperty): T $closure 124 | */ 125 | public function apply(Closure $closure): mixed 126 | { 127 | return $closure($this->property); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Lens/Lens.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class Lens implements LensInterface 19 | { 20 | /** 21 | * @var callable(S): A 22 | */ 23 | private $get; 24 | 25 | /** 26 | * @var callable(S, A): S 27 | */ 28 | private $set; 29 | 30 | /** 31 | * @param callable(S): A $get 32 | * @param callable(S, A): S $set 33 | */ 34 | public function __construct(callable $get, callable $set) 35 | { 36 | $this->get = $get; 37 | $this->set = $set; 38 | } 39 | 40 | /** 41 | * @pure 42 | * @template RS 43 | * @template RA 44 | * @param callable(RS): RA $get 45 | * @return Lens 46 | */ 47 | public static function readonly(callable $get): self 48 | { 49 | return new self($get, static fn ($s, $a) => throw ReadonlyException::couldNotWrite()); 50 | } 51 | 52 | /** 53 | * @pure 54 | * @template I 55 | * @return Lens 56 | */ 57 | public static function identity(): self 58 | { 59 | return new self( 60 | /** 61 | * @param I $s 62 | * @returns I 63 | */ 64 | static fn ($s) => $s, 65 | /** 66 | * @param I $_ 67 | * @param I $a 68 | * @returns I 69 | */ 70 | static fn ($_, $a) => $a 71 | ); 72 | } 73 | 74 | /** 75 | * @param S $s 76 | * @return A 77 | */ 78 | public function get($s) 79 | { 80 | return ($this->get)($s); 81 | } 82 | 83 | /** 84 | * @param S $s 85 | * @return ResultInterface 86 | */ 87 | public function tryGet($s): ResultInterface 88 | { 89 | return wrap(fn () => ($this->get)($s)); 90 | } 91 | 92 | /** 93 | * @param S $s 94 | * @param A $a 95 | * @return S 96 | */ 97 | public function set($s, $a) 98 | { 99 | return ($this->set)($s, $a); 100 | } 101 | 102 | /** 103 | * @param S $s 104 | * @param A $a 105 | * @return ResultInterface 106 | */ 107 | public function trySet($s, $a): ResultInterface 108 | { 109 | return wrap(fn () => ($this->set)($s, $a)); 110 | } 111 | 112 | /** 113 | * @param S $s 114 | * @param callable(A): A $f 115 | * @return S 116 | */ 117 | public function update($s, callable $f) 118 | { 119 | return $this->set($s, $f(($this->get)($s))); 120 | } 121 | 122 | /** 123 | * @param S $s 124 | * @param callable(A): A $f 125 | * @return ResultInterface 126 | */ 127 | public function tryUpdate($s, callable $f): ResultInterface 128 | { 129 | return wrap(fn () => $this->set($s, $f(($this->get)($s)))); 130 | } 131 | 132 | /** 133 | * @return LensInterface 134 | */ 135 | public function optional(): LensInterface 136 | { 137 | return optional($this); 138 | } 139 | 140 | /** 141 | * @template S2 142 | * @template A2 143 | * @param LensInterface $that 144 | * @return LensInterface 145 | */ 146 | public function compose(LensInterface $that): LensInterface 147 | { 148 | /** @psalm-suppress InvalidArgument */ 149 | return new self( 150 | /** 151 | * @param S $s 152 | * @return A2 153 | */ 154 | fn ($s) => $that->get(($this->get)($s)), 155 | /** 156 | * @param S $s 157 | * @param A2 $a2 158 | * @return S 159 | */ 160 | fn ($s, $a2) => $this->set($s, $that->set($this->get($s), $a2)) 161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /tests/unit/Lens/LensTest.php: -------------------------------------------------------------------------------- 1 | $data['hello'], 21 | static fn (array $data, string $value) => [...$data, 'hello' => $value], 22 | ); 23 | 24 | $data = ['hello' => 'world']; 25 | static::assertSame('world', $lens->get($data)); 26 | static::assertSame('world', $lens->tryGet($data)->getResult()); 27 | } 28 | 29 | 30 | public function test_it_can_set_data(): void 31 | { 32 | $lens = new Lens( 33 | static fn (array $data) => $data['hello'], 34 | static fn (array $data, string $value) => [...$data, 'hello' => $value], 35 | ); 36 | 37 | $data = ['hello' => 'world']; 38 | static::assertSame(['hello' => 'earth'], $lens->set($data, 'earth')); 39 | static::assertSame(['hello' => 'earth'], $lens->trySet($data, 'earth')->getResult()); 40 | } 41 | 42 | 43 | public function test_it_can_update_data(): void 44 | { 45 | $lens = new Lens( 46 | static fn (array $data) => $data['hello'], 47 | static fn (array $data, string $value) => [...$data, 'hello' => $value], 48 | ); 49 | 50 | $data = ['hello' => 'w']; 51 | static::assertSame( 52 | ['hello' => 'world'], 53 | $lens->update($data, static fn (string $message) => $message.'orld') 54 | ); 55 | static::assertSame( 56 | ['hello' => 'world'], 57 | $lens->tryUpdate($data, static fn (string $message) => $message.'orld')->getResult() 58 | ); 59 | } 60 | 61 | 62 | public function test_it_can_have_identity(): void 63 | { 64 | $lens = Lens::identity(); 65 | 66 | static::assertSame('hello', $lens->get('hello')); 67 | static::assertSame('hello', $lens->set('ignored', 'hello')); 68 | } 69 | 70 | 71 | public function test_it_can_be_optional(): void 72 | { 73 | $lens = (new Lens( 74 | static function (array $data): mixed { 75 | if (!array_key_exists('hello', $data)) { 76 | throw new RuntimeException('nope'); 77 | } 78 | return $data['hello']; 79 | }, 80 | static function (array $data, mixed $value): mixed { 81 | if (!array_key_exists('hello', $data)) { 82 | throw new RuntimeException('nope'); 83 | } 84 | 85 | return [...$data, 'hello' => $value]; 86 | }, 87 | ))->optional(); 88 | 89 | $validData = ['hello' => 'world']; 90 | $invalidData = []; 91 | 92 | static::assertSame('world', $lens->get($validData)); 93 | static::assertSame(null, $lens->get($invalidData)); 94 | 95 | static::assertSame(['hello' => 'earth'], $lens->set($validData, 'earth')); 96 | static::assertSame(null, $lens->set($invalidData, 'earth')); 97 | } 98 | 99 | 100 | public function test_it_can_compose_lenses(): void 101 | { 102 | $greetLens = index('greet'); 103 | $messageLens = index('message'); 104 | $composed = $greetLens->compose($messageLens); 105 | 106 | $data = ['greet' => ['message' => 'hello']]; 107 | 108 | static::assertSame('hello', $composed->get($data)); 109 | static::assertSame(['greet' => ['message' => 'goodbye']], $composed->set($data, 'goodbye')); 110 | } 111 | 112 | public function test_it_can_read_from_readonly_lens(): void 113 | { 114 | $lens = Lens::readonly(identity()); 115 | 116 | static::assertSame('result', $lens->get('result')); 117 | } 118 | 119 | public function test_it_can_not_write_to_readonly_lens(): void 120 | { 121 | $lens = Lens::readonly(identity()); 122 | 123 | $this->expectExceptionObject(ReadonlyException::couldNotWrite()); 124 | $lens->set('result', 'impossible'); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/unit/Iso/IsoTest.php: -------------------------------------------------------------------------------- 1 | join(',', $keywords), 17 | static fn (string $keywords): array => explode(',', $keywords) 18 | ); 19 | 20 | $data = ['hello' ,'world']; 21 | $joined = $commaSeparated->to($data); 22 | $exploded = $commaSeparated->from($joined); 23 | 24 | static::assertSame('hello,world', $joined); 25 | static::assertSame($data, $exploded); 26 | } 27 | 28 | 29 | public function test_it_can_succeed_trying_to_be_isomorphic(): void 30 | { 31 | $commaSeparated = new Iso( 32 | static fn (array $keywords): string => join(',', $keywords), 33 | static fn (string $keywords): array => explode(',', $keywords) 34 | ); 35 | 36 | $data = ['hello' ,'world']; 37 | $joined = $commaSeparated->tryTo($data); 38 | $exploded = $commaSeparated->tryFrom('hello,world'); 39 | 40 | static::assertSame('hello,world', $joined->getResult()); 41 | static::assertSame($data, $exploded->getResult()); 42 | } 43 | 44 | 45 | public function test_it_can_fail_trying_to_be_isomorphic(): void 46 | { 47 | $exception = new RuntimeException('fail'); 48 | $commaSeparated = new Iso( 49 | static fn (array $keywords): string => throw $exception, 50 | static fn (string $keywords): array => throw $exception 51 | ); 52 | 53 | $data = ['hello' ,'world']; 54 | $joined = $commaSeparated->tryTo($data); 55 | $exploded = $commaSeparated->tryFrom('hello,world'); 56 | 57 | static::assertSame($exception, $joined->getThrowable()); 58 | static::assertSame($exception, $exploded->getThrowable()); 59 | } 60 | 61 | 62 | public function test_it_has_identity(): void 63 | { 64 | $identity = Iso::identity(); 65 | $initial = "hello"; 66 | $to = $identity->to($initial); 67 | $from = $identity->from($initial); 68 | 69 | static::assertSame($initial, $to); 70 | static::assertSame($initial, $from); 71 | } 72 | 73 | 74 | public function test_it_can_be_inversed(): void 75 | { 76 | $commaSeparated = (new Iso( 77 | static fn (array $keywords): string => join(',', $keywords), 78 | static fn (string $keywords): array => explode(',', $keywords) 79 | ))->inverse(); 80 | 81 | $data = ['hello' ,'world']; 82 | $joined = $commaSeparated->from($data); 83 | $exploded = $commaSeparated->to($joined); 84 | 85 | static::assertSame('hello,world', $joined); 86 | static::assertSame($data, $exploded); 87 | } 88 | 89 | 90 | public function test_it_can_be_transformed_in_a_lens(): void 91 | { 92 | $commaSeparated = new Iso( 93 | static fn (array $keywords): string => join(',', $keywords), 94 | static fn (string $keywords): array => explode(',', $keywords) 95 | ); 96 | $lens = $commaSeparated->asLens(); 97 | 98 | $data = ['hello' ,'world']; 99 | $joined = $lens->get($data); 100 | $exploded = $lens->set(null, $joined); 101 | 102 | static::assertSame('hello,world', $joined); 103 | static::assertSame($data, $exploded); 104 | } 105 | 106 | 107 | public function test_it_can_be_composed(): void 108 | { 109 | $base64 = new Iso( 110 | base64_encode(...), 111 | base64_decode(...), 112 | ); 113 | 114 | $commaSeparated = new Iso( 115 | static fn (array $keywords): string => join(',', $keywords), 116 | static fn (string $keywords): array => explode(',', $keywords) 117 | ); 118 | 119 | $commaSeparatedBase64 = $commaSeparated->compose($base64); 120 | 121 | $data = ['hello' ,'world']; 122 | $joined = $commaSeparatedBase64->to($data); 123 | $exploded = $commaSeparatedBase64->from($joined); 124 | 125 | static::assertSame(base64_encode('hello,world'), $joined); 126 | static::assertSame($data, $exploded); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /docs/isomorphisms.md: -------------------------------------------------------------------------------- 1 | # 🔄 Isomorphic Magic 2 | 3 | Seamlessly navigate between different data representations with our Isomorphisms. 4 | Experience the enchantment of transforming your data effortlessly, as if conducting a symphony of bits and bytes. 5 | 6 |
7 | Isos visualized 8 |
9 | 10 | An isomorphism, in the world of optics, is like a universal translator for your data. 11 | It's the secret sauce that allows you to seamlessly switch between different representations of information without losing any meaning. 12 | Imagine effortlessly translating a complex concept from one language to another, each version capturing the essence perfectly. 13 | Isomorphisms do just that for your data, providing a smooth and reversible way to navigate between various structures, 14 | ensuring that your information remains intact and coherent, regardless of its form. 15 | 16 | Let's dive into a small example: 17 | 18 | # Iso 19 | 20 | ```php 21 | use VeeWee\Reflecta\Iso\Iso; 22 | 23 | $base64 = new Iso( 24 | to: base64_encode(...), 25 | from: base64_decode(...), 26 | ); 27 | 28 | $data = 'hello world'; 29 | $encoded = $base64->to($data); 30 | // > "aGVsbG8gd29ybGQ=" 31 | $actual = $base64->from($encoded); 32 | // > "hello world" 33 | 34 | assert($actual === $data); 35 | ``` 36 | 37 | This example provides an isomorphism for encoding a value to base64 or decoding from a base64 value. 38 | The result of the `to()` method will always be revertable by using the `from()` method. 39 | 40 | ## Composability 41 | 42 | You can compose many ISOs into one bigger ISO: 43 | 44 | ```php 45 | use VeeWee\Reflecta\Iso\Iso; 46 | 47 | $base64 = new Iso( 48 | base64_encode(...), 49 | base64_decode(...), 50 | ); 51 | 52 | $commaSeparated = new Iso( 53 | static fn (array $keywords): string => join(',', $keywords), 54 | static fn (string $keywords): array => explode(',', $keywords) 55 | ); 56 | 57 | $commaSeparatedBase64 = $commaSeparated->compose($base64); 58 | 59 | $data = ['hello' ,'world']; 60 | $encoded = $commaSeparatedBase64->to($data); 61 | // > ["hello", "world"] -> "hello,world" -> aGVsbG8sd29ybGQ= 62 | $actual = $commaSeparatedBase64->from($encoded); 63 | // > aGVsbG8sd29ybGQ= -> "hello,world", ["hello", "world"] 64 | 65 | assert($actual === $data); 66 | ``` 67 | 68 | 69 | ## Functions 70 | 71 | #### compose 72 | 73 | This function is able to compose multiple isomorphisms into a new one. 74 | Check the chapter [composability](#composability) for more information. 75 | A psalm plugin is available that validates if the types of the ISOs are composable. 76 | 77 | ```php 78 | use function VeeWee\Reflecta\Iso\compose; 79 | 80 | $commaSeparatedBase64 = compose( 81 | $commaSeparated, 82 | $base64, 83 | // ...others 84 | ); 85 | ``` 86 | 87 | #### object_data 88 | 89 | This function will create an Iso that can access object data. 90 | 91 | * **To** will transform an `array` and use this array to fill a new instance of the provided named object. 92 | * **From** will transform an instance of the provided named object into a `array` that contains a full list of all properties with their values. 93 | 94 | ```php 95 | use function VeeWee\Reflecta\Iso\object_data; 96 | 97 | class Item { 98 | public string $value; 99 | } 100 | 101 | $itemData = object_data(Item::class); 102 | 103 | $data = [ 104 | 'value' => 'hello' 105 | ]; 106 | $itemInstance = $itemData->from($data); 107 | $actualData = $itemData->to($item); 108 | 109 | assert($data === $actualData); 110 | ``` 111 | 112 | Additionally, you can provide a custom Lens to the `object_data` function which will be used to read and write the data. 113 | This makes it possible to check property visibility or alternatively use getters and setters to access the data. 114 | 115 | ```php 116 | use VeeWee\Reflecta\Reflect\Type\Visibility; 117 | use function VeeWee\Reflecta\Iso\object_data; 118 | use function VeeWee\Reflecta\Lens\properties; 119 | use function VeeWee\Reflecta\Reflect\Predicate\property_visibility; 120 | 121 | class Item { 122 | public string $value = 'value'; 123 | private string $hidden = 'hidden'; 124 | } 125 | 126 | $itemData = object_data(Item::class, properties(property_visibility(Visibility::Public))); 127 | 128 | $data = [ 129 | 'value' => 'hello', 130 | 'hidden' => 'ignored', 131 | ]; 132 | 133 | $itemInstance = $itemData->from($data); 134 | // > Item { value: "hello", hidden: "hidden" } 135 | $actualData = $itemData->to($item); 136 | // > ['value' => 'hello'] 137 | 138 | assert($data === $actualData); 139 | ``` 140 | -------------------------------------------------------------------------------- /src/Reflect/Type/ReflectedClass.php: -------------------------------------------------------------------------------- 1 | class->getName(); 61 | } 62 | 63 | public function shortName(): string 64 | { 65 | return $this->class->getShortName(); 66 | } 67 | 68 | public function namespaceName(): string 69 | { 70 | return $this->class->getNamespaceName(); 71 | } 72 | 73 | /** 74 | * @return Option 75 | */ 76 | public function parent(): Option 77 | { 78 | $parent = $this->class->getParentClass(); 79 | 80 | return $parent? Option::some(new self($parent)) : Option::none(); 81 | } 82 | 83 | /** 84 | * @param Closure(ReflectedClass): bool $predicate 85 | */ 86 | public function check(Closure $predicate): bool 87 | { 88 | return $predicate($this); 89 | } 90 | 91 | public function isFinal(): bool 92 | { 93 | return $this->class->isFinal(); 94 | } 95 | 96 | public function isDynamic(): bool 97 | { 98 | // Dynamic props is a 80200 feature. 99 | // IN previous versions, all objects are dynamic (without any warning). 100 | if (PHP_VERSION_ID < 80200) { 101 | return true; 102 | } 103 | 104 | return $this->hasAttributeOfType(AllowDynamicProperties::class); 105 | } 106 | 107 | public function isAbstract(): bool 108 | { 109 | return $this->class->isAbstract(); 110 | } 111 | 112 | public function isInstantiable(): bool 113 | { 114 | return $this->class->isInstantiable(); 115 | } 116 | 117 | public function isCloneable(): bool 118 | { 119 | return $this->class->isCloneable(); 120 | } 121 | 122 | public function docComment(): string 123 | { 124 | return $this->class->getDocComment(); 125 | } 126 | 127 | public function property(string $property): ReflectedProperty 128 | { 129 | if ($this->class->hasProperty($property)) { 130 | return new ReflectedProperty($this->class->getProperty($property)); 131 | } 132 | 133 | if ($parent = $this->parent()->unwrapOr(null)) { 134 | return $parent->property($property); 135 | } 136 | 137 | throw UnreflectableException::unknownProperty($this->fullName(), $property); 138 | } 139 | 140 | /** 141 | * @param null|Closure(ReflectedProperty): bool $predicate 142 | * 143 | * @return array 144 | * 145 | * @throws UnreflectableException 146 | */ 147 | public function properties(Closure|null $predicate = null): array 148 | { 149 | $properties = reindex( 150 | [ 151 | // Collects private properties from parents as well: 152 | ...$this->parent()->map( 153 | static fn (ReflectedClass $parent): array => $parent->properties() 154 | )->unwrapOr([]), 155 | // Collects properties from the current class: 156 | ...map( 157 | $this->class->getProperties(), 158 | static fn (ReflectionProperty $property): ReflectedProperty => new ReflectedProperty($property) 159 | ) 160 | ], 161 | static fn (ReflectedProperty $property): string => $property->name(), 162 | ); 163 | 164 | if ($predicate !== null) { 165 | return filter($properties, $predicate); 166 | } 167 | 168 | return $properties; 169 | } 170 | 171 | /** 172 | * @template Ta extends object 173 | * 174 | * @param class-string|null $attributeClassName 175 | * 176 | * @return (Ta is null ? list : list) 177 | * @throws UnreflectableException 178 | */ 179 | public function attributes(?string $attributeClassName = null): array 180 | { 181 | return map( 182 | $this->class->getAttributes($attributeClassName, ReflectionAttribute::IS_INSTANCEOF), 183 | static fn (ReflectionAttribute $attribute): object => (new ReflectedAttribute($attribute))->instantiate() 184 | ); 185 | } 186 | 187 | /** 188 | * @param class-string $attributeClassName 189 | */ 190 | public function hasAttributeOfType(string $attributeClassName): bool 191 | { 192 | return (bool) $this->class->getAttributes($attributeClassName, ReflectionAttribute::IS_INSTANCEOF); 193 | } 194 | 195 | /** 196 | * @throws UnreflectableException 197 | */ 198 | public function instantiate(): object 199 | { 200 | try { 201 | return $this->class->newInstanceWithoutConstructor(); 202 | } catch (Throwable $previous) { 203 | throw UnreflectableException::nonInstantiatable($this->fullName(), $previous); 204 | } 205 | } 206 | 207 | /** 208 | * @template T 209 | * @param Closure(ReflectionClass): T $closure 210 | */ 211 | public function apply(Closure $closure): mixed 212 | { 213 | return $closure($this->class); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /tests/unit/Reflect/Type/ReflectedPropertyTest.php: -------------------------------------------------------------------------------- 1 | 'value' 21 | ]; 22 | 23 | $property = ReflectedClass::fromObject($x)->property('name'); 24 | static::assertSame('name', $property->name()); 25 | } 26 | 27 | public function test_it_can_get_declaring_class(): void 28 | { 29 | $class = ReflectedClass::fromFullyQualifiedClassName(X::class); 30 | $property = $class->property('z'); 31 | 32 | static::assertEquals($class, $property->declaringClass()); 33 | } 34 | 35 | public function test_it_knows_public_visibility_from_property(): void 36 | { 37 | $x = new class() { 38 | public int $z = 0; 39 | }; 40 | $prop = ReflectedClass::fromObject($x)->property('z'); 41 | $visibility = $prop->visibility(); 42 | 43 | static::assertSame(Visibility::Public, $visibility); 44 | static::assertTrue($prop->isPublic()); 45 | static::assertFalse($prop->isProtected()); 46 | static::assertFalse($prop->isPrivate()); 47 | static::assertTrue($prop->check(static fn (ReflectedProperty $property) => $property->isPublic())); 48 | } 49 | 50 | public function test_it_knows_protected_visibility_from_property(): void 51 | { 52 | $x = new class() { 53 | protected int $z = 0; 54 | }; 55 | $prop = ReflectedClass::fromObject($x)->property('z'); 56 | $visibility = $prop->visibility(); 57 | 58 | static::assertSame(Visibility::Protected, $visibility); 59 | static::assertFalse($prop->isPublic()); 60 | static::assertTrue($prop->isProtected()); 61 | static::assertFalse($prop->isPrivate()); 62 | static::assertTrue($prop->check(static fn (ReflectedProperty $property) => $property->isProtected())); 63 | } 64 | 65 | public function test_it_knows_private_visibility_from_property(): void 66 | { 67 | $x = new class() { 68 | private int $z = 0; 69 | }; 70 | $prop = ReflectedClass::fromObject($x)->property('z'); 71 | $visibility = $prop->visibility(); 72 | 73 | static::assertSame(Visibility::Private, $visibility); 74 | static::assertFalse($prop->isPublic()); 75 | static::assertFalse($prop->isProtected()); 76 | static::assertTrue($prop->isPrivate()); 77 | static::assertTrue($prop->check(static fn (ReflectedProperty $property) => $property->isPrivate())); 78 | } 79 | 80 | public function test_it_knows_promoted_property(): void 81 | { 82 | $x = new class(1) { 83 | private int $foo = 0; 84 | 85 | public function __construct( 86 | private int $bar 87 | ) { 88 | } 89 | }; 90 | $class = ReflectedClass::fromObject($x); 91 | 92 | static::assertFalse($class->property('foo')->isPromoted()); 93 | static::assertTrue($class->property('bar')->isPromoted()); 94 | } 95 | 96 | public function test_it_knows_static_property(): void 97 | { 98 | $x = new class(1) { 99 | private int $foo = 0; 100 | private static int $bar = 0; 101 | }; 102 | $class = ReflectedClass::fromObject($x); 103 | 104 | static::assertFalse($class->property('foo')->isStatic()); 105 | static::assertTrue($class->property('bar')->isStatic()); 106 | } 107 | 108 | public function test_it_knows_readonly_property(): void 109 | { 110 | $x = new class(1) { 111 | private int $foo = 0; 112 | private readonly int $bar; 113 | }; 114 | $class = ReflectedClass::fromObject($x); 115 | 116 | static::assertFalse($class->property('foo')->isReadOnly()); 117 | static::assertTrue($class->property('bar')->isReadOnly()); 118 | } 119 | 120 | public function test_it_knows_is_default_property_on_dynamic_class(): void 121 | { 122 | if (PHP_VERSION_ID < 80200) { 123 | static::markTestSkipped('On PHP 8.2, all classes are safely dynamic'); 124 | } 125 | 126 | $x = new #[AllowDynamicProperties] class() { 127 | private int $foo = 0; 128 | }; 129 | $x->bar = 0; 130 | $class = ReflectedClass::fromObject($x); 131 | 132 | static::assertTrue($class->property('foo')->isDefault()); 133 | static::assertFalse($class->property('foo')->isDynamic()); 134 | static::assertFalse($class->property('bar')->isDefault()); 135 | static::assertTrue($class->property('bar')->isDynamic()); 136 | } 137 | 138 | public function test_it_knows_is_default_property(): void 139 | { 140 | if (PHP_VERSION_ID >= 80200) { 141 | static::markTestSkipped('On PHP 8.2, dynamic classes should be marked with #[AllowDynamicProperties] attribute'); 142 | } 143 | 144 | $x = new class() { 145 | private int $foo = 0; 146 | }; 147 | $x->bar = 0; 148 | $class = ReflectedClass::fromObject($x); 149 | 150 | static::assertTrue($class->property('foo')->isDefault()); 151 | static::assertFalse($class->property('foo')->isDynamic()); 152 | static::assertFalse($class->property('bar')->isDefault()); 153 | static::assertTrue($class->property('bar')->isDynamic()); 154 | } 155 | 156 | public function test_it_knows_default_values(): void 157 | { 158 | $x = new class() { 159 | private int $foo = 0; 160 | private int $bar; 161 | }; 162 | $class = ReflectedClass::fromObject($x); 163 | 164 | static::assertTrue($class->property('foo')->hasDefaultValue()); 165 | static::assertSame(0, $class->property('foo')->defaultValue()); 166 | static::assertFalse($class->property('bar')->hasDefaultValue()); 167 | static::assertNull($class->property('bar')->defaultValue()); 168 | } 169 | 170 | public function test_it_knows_about_docblocks(): void 171 | { 172 | $x = new class() { 173 | /** docblock */ 174 | private int $foo = 0; 175 | }; 176 | $class = ReflectedClass::fromObject($x); 177 | 178 | static::assertSame('/** docblock */', $class->property('foo')->docComment()); 179 | } 180 | 181 | public function test_it_knows_about_attributes(): void 182 | { 183 | $x = new class() { 184 | #[CustomAttribute] 185 | private int $foo = 0; 186 | }; 187 | $prop = ReflectedClass::fromObject($x)->property('foo'); 188 | 189 | $attributes = $prop->attributes(); 190 | $filteredAttributes = $prop->attributes(CustomAttribute::class); 191 | 192 | static::assertTrue($prop->hasAttributeOfType(CustomAttribute::class)); 193 | static::assertEquals($attributes, $filteredAttributes); 194 | static::assertEquals([new CustomAttribute()], $attributes); 195 | } 196 | 197 | public function test_it_can_apply(): void 198 | { 199 | $x = new class() { 200 | private int $foo = 0; 201 | }; 202 | $prop = ReflectedClass::fromObject($x)->property('foo'); 203 | 204 | $result = $prop->apply( 205 | static fn (ReflectionProperty $prop): string => $prop->getName() 206 | ); 207 | 208 | static::assertSame('foo', $result); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /docs/lens.md: -------------------------------------------------------------------------------- 1 | # 🔍 Lenses of Clarity 2 | 3 | Focus on what matters most. 4 | Our lenses provide a crystal-clear view, allowing you to zero in on specific data points without the distraction of unnecessary details. 5 | Precision meets simplicity in every line of code. 6 | 7 |
8 | Lenses visualized 9 |
10 | 11 | Lenses, in the realm of optics, act as your data's precision-guided magnifying glasses. 12 | Think of them as focused spotlights that allow you to zoom in on specific details within your data without being bogged down by the noise around it. 13 | Like a photographer adjusting the lens to capture the perfect shot, lenses enable you to fine-tune your view, isolating and manipulating exactly what you need. 14 | They bring clarity to the intricacies of your data, offering a straightforward and elegant way to extract, modify, or navigate through the heart of your information with surgical precision. 15 | 16 | Let's dive into a small example: 17 | 18 | # Lens 19 | 20 | Given your data object looks like this: 21 | 22 | ```php 23 | class Item { 24 | public string $value; 25 | } 26 | ``` 27 | 28 | You can create a lens that zooms in on the `value` property of an `Item` instance. 29 | When creating a lens, we don't care much about the data by itself, but rather about what the data structure looks like. 30 | A (not so safe) example lens for this example could be this one: 31 | 32 | ```php 33 | use VeeWee\Reflecta\Lens\Lens; 34 | 35 | $objectValueLens = new Lens( 36 | get: fn (Item $item): string => $item->value, 37 | set: function (Item $item, string $value): Item { 38 | $new = clone $item; 39 | $new->value = $value; 40 | 41 | return $new; 42 | }, 43 | ); 44 | ``` 45 | 46 | This lens will allow you to fetch and manipulate data like this: 47 | 48 | ```php 49 | $item = new Item(); 50 | $item->value = 'Hello there!'; 51 | 52 | $theValue = $objectValueLens->get($item); 53 | // > "Hello there!" 54 | $newItem = $objectValueLens->set($item, 'Hello back to you!'); 55 | // > Item { value: "Hello back to you!" } 56 | $newItem = $objectValueLens->update($item, fn (string $oldValue) => $oldValue . ' Have a nice day!'); 57 | // > Item { value: "Hello there! Have a nice day!" } 58 | ``` 59 | 60 | You can pass in any instance of `Item` and it will work as expected. 61 | 62 | 63 | ## Composability 64 | 65 | Things get fun once you start composing lenses. 66 | Think about this about having deeply nested objects. 67 | If you have a data structure that looks like this: 68 | 69 | ```php 70 | class Person { 71 | public Hat $hat; 72 | } 73 | 74 | class Hat { 75 | public string $color; 76 | } 77 | ``` 78 | 79 | If you want to grab the color of the hat, you could compose a lens like this: 80 | 81 | ```php 82 | use function VeeWee\Reflecta\Lens\property; 83 | 84 | $hatLens = property('hat'); 85 | $colorLens = property('color'); 86 | $personsHatColorLens = $hatLens->compose($colorLens); 87 | 88 | $person = new Person(); 89 | $person->hat = new Hat(); 90 | $person->hat->color = 'green'; 91 | 92 | $theColor = $personsHatColorLens->get($person); 93 | // > "green" 94 | $newPerson = $personsHatColorLens->set($person, 'red'); 95 | // > Person { hat: { color: "red" }} 96 | ``` 97 | 98 | ## Functions 99 | 100 | #### compose 101 | 102 | This function is able to compose multiple lenses into a new one. 103 | Check the chapter [composability](#composability) for more information. 104 | A psalm plugin is available that validates if the types of the lenses are composable. 105 | 106 | ```php 107 | use function VeeWee\Reflecta\Lens\compose; 108 | 109 | $personsHatColorLens = compose( 110 | $hatLens, 111 | $colorLens, 112 | // ...others 113 | ); 114 | ``` 115 | 116 | #### index 117 | 118 | This function returns an index Lens that operates on `array`. 119 | You can specify either a numeric index or an index by name. 120 | 121 | * **Get** will fetch the array item by key or throw an `ArrayAccessException` if the key cannot be found. 122 | * **Set** will make a copy from the input array and change the index in that new version of the array. 123 | 124 | 125 | ```php 126 | use function VeeWee\Reflecta\Lens\index; 127 | 128 | $dict = ['a' => 'Apple', 'b' => 'Blueberry']; 129 | $vec = ['a', 'b', 'c']; 130 | 131 | index('b')->get($dict); 132 | // > Blueberry 133 | index(1)->get($vec); 134 | // > b 135 | 136 | index('b')->set($dict, 'Banana'); 137 | // > ['a' => 'Apple', 'b' => 'Banana'] 138 | index(1)->set($vec, 'Banana'); 139 | // > ['a', 'Banana', 'c'] 140 | ``` 141 | 142 | #### optional 143 | 144 | This function returns an optional Lens that decorates another lens. 145 | It's comparable to the null-safe operator `?->` in PHP: 146 | If the property you are trying to zoom in upon does not exist, it allows you to continue code execution with a `null` value. 147 | Internally, this lens will try to access the underlying lens and will return `null` upon errors and null returns. 148 | 149 | * **Get** will try to get() from the decorated lens and return `null` on failure. 150 | * **Set** will try to set() from the decorated lens and return `null` on failure. 151 | 152 | ```php 153 | use function VeeWee\Reflecta\Lens\optional; 154 | use function VeeWee\Reflecta\Lens\property; 155 | 156 | $optionalValueLens = optional(property('value')); 157 | 158 | $optionalValueLens->get(new class { 159 | public string $value = 'hello'; 160 | }); 161 | // > "hello" 162 | $optionalValueLens->get(new class {}); 163 | // > null 164 | 165 | 166 | $optionalValueLens->set(new class { 167 | public string $value = 'hello'; 168 | }, 'world'); 169 | // > Object {value: "world"} 170 | $optionalValueLens->set(new class {}, 'world'); 171 | // > null 172 | ``` 173 | 174 | Since this is a very common lens, a shortcut got added to any `Lens` instance in order to make it optional: 175 | 176 | ```php 177 | $optionalLens = $lens->optional(); 178 | ``` 179 | 180 | #### properties 181 | 182 | This function will create a lens that can zoom in on an object's properties (with any visibility). 183 | 184 | * **Get** will find all properties from the given object and return a `array` that contains the property name as key and the property value as value. 185 | * **Set** will create a copy of the provided object and will use the provided `array` to store back the values onto this new cloned object. 186 | 187 | ```php 188 | use function VeeWee\Reflecta\Lens\properties; 189 | 190 | class Item { 191 | public string $value = 'hello'; 192 | } 193 | 194 | $propertiesLens = properties(); 195 | 196 | $propertiesLens->get(new Item()); 197 | // > ["value": "hello"] 198 | 199 | $propertiesLens->set(new Item(), ['value': 'world']); 200 | // > Item { value: "world" } 201 | ``` 202 | 203 | Additionally, you can pass a predicate to filter the properties you want to zoom in on: 204 | 205 | ```php 206 | use VeeWee\Reflecta\Reflect\Type\Visibility; 207 | use function VeeWee\Reflecta\Lens\properties; 208 | use function VeeWee\Reflecta\Reflect\Predicate\property_visibility; 209 | 210 | class Item { 211 | public string $value = 'hello'; 212 | private string $hidden = 'hidden'; 213 | } 214 | 215 | $propertiesLens = properties(property_visibility(Visibility::Public)); 216 | 217 | $propertiesLens->get(new Item()); 218 | // > ["value": "hello"] 219 | 220 | $propertiesLens->set(new Item(), ['value': 'world', 'hidden' => 'is-ignored']); 221 | // > Item { value: "world", hidden: "hidden" } 222 | ``` 223 | 224 | #### property 225 | 226 | This function will create a lens that can zoom in on one named object's property (with any visibility). 227 | 228 | * **Get** will find the value of the requested property in the provided object. 229 | * **Set** will create a copy of the provided object and will set the provided value of the requested property into this new object. 230 | 231 | ```php 232 | use function VeeWee\Reflecta\Lens\property; 233 | 234 | class Item { 235 | public string $value = 'hello'; 236 | } 237 | 238 | $propertyLens = property('value'); 239 | 240 | $propertyLens->get(new Item()); 241 | // > "hello" 242 | 243 | $propertyLens->set(new Item(), ['value': 'world']); 244 | // > Item { value: "world" } 245 | ``` 246 | 247 | #### read_only 248 | 249 | This function will create a lens that can only be used to get the value of the provided value. 250 | 251 | * **Get** will try to get() from the decorated lens 252 | * **Set** will throw a `ReadonlyException` when trying to set a value. 253 | 254 | ```php 255 | use function VeeWee\Reflecta\Lens\property; 256 | use function VeeWee\Reflecta\Lens\read_only; 257 | 258 | $readonlyValueLens = read_only(property('value')); 259 | 260 | $readonlyValueLens->get(new class { 261 | public string $value = 'hello'; 262 | }); 263 | // > "hello" 264 | 265 | 266 | $optionalValueLens->set(new class { 267 | public string $value = 'hello'; 268 | }, 'world'); 269 | 270 | // > Throws ReadonlyException 271 | ``` 272 | 273 | The main Lens class has a shortcut function as well to create a readonly lens from a getter: 274 | 275 | ```php 276 | use VeeWee\Reflecta\Lens\Lens; 277 | 278 | $getter = fn (mixed $item): mixed => $item; 279 | $lens = Lens::readonly($getter); 280 | ``` 281 | -------------------------------------------------------------------------------- /docs/reflect.md: -------------------------------------------------------------------------------- 1 | # 🧲 Magnetic Reflections: 2 | 3 | Illuminate your code's inner world with our reflection tools. 4 | Peek into the heart of your data structures effortlessly, revealing the hidden gems that propel your applications forward. 5 | 6 | This component provides runtime-safe reflections on objects. 7 | 8 | ## Functions 9 | 10 | This package provides following functions for dealing with objects. 11 | 12 | #### class_attributes 13 | 14 | Detects all attributes at the class level of the given className that match the optionally provided argument type (or super-type). 15 | If the class is not reflectable or there is an error instantiating any argument, an `UnreflectableException` exception is triggered! 16 | The result of this function is of type: `list`. However, if you provide an argument name: psalm will know the type of the attribute. 17 | 18 | ```php 19 | use function VeeWee\Reflecta\Reflect\class_attributes; 20 | 21 | try { 22 | $allAttributes = class_attributes(YourClass::name); 23 | $allAttributesOfType = class_attributes(YourClass::name, \YourAttributeType::class); 24 | $allAttributesOfType = class_attributes(YourClass::name, \YourAbstractBaseType::class); 25 | } catch (UnreflectableException) { 26 | // Deal with it 27 | } 28 | ``` 29 | 30 | #### class_has_attribute 31 | 32 | Checks if the class contains an attribute of given type (or super-type). 33 | If the class is not reflectable, an `UnreflectableException` exception is triggered! 34 | 35 | ```php 36 | use function VeeWee\Reflecta\Reflect\object_has_attribute; 37 | 38 | try { 39 | $hasAttribute = class_has_attribute(YourClass::name, \YourAttributeType::class); 40 | $hasAttributeThatImplementsBaseType = class_has_attribute(YourClass::name, \YourAbstractBaseType::class); 41 | } catch (UnreflectableException) { 42 | // Deal with it 43 | } 44 | ``` 45 | 46 | #### class_is_dynamic 47 | 48 | Checks if the provided class is considered a safe dynamic object that implements `AllowDynamicProperties`. 49 | 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. 50 | If the object is not reflectable, an `UnreflectableException` exception is triggered! 51 | 52 | ```php 53 | use function VeeWee\Reflecta\Reflect\class_is_dynamic; 54 | 55 | try { 56 | $isDynamic = class_is_dynamic(new stdClass()); 57 | $isDynamic = class_is_dynamic(new #[\AllowDynamicProperties] class() {}); 58 | $isNotDynamic = class_is_dynamic(new class() {}); 59 | } catch (UnreflectableException) { 60 | // Deal with it 61 | } 62 | ``` 63 | 64 | #### instantiate 65 | 66 | This function instantiates a new object of the provided type by bypassing the constructor. 67 | 68 | ```php 69 | use VeeWee\Reflecta\ArrayAccess\Exception\ArrayAccessException; 70 | use function VeeWee\Reflecta\Reflect\instantiate; 71 | 72 | try { 73 | $yourObject = instantiate(YourObject::class); 74 | } catch (UnreflectableException) { 75 | // Deal with it 76 | } 77 | ``` 78 | 79 | #### object_attributes 80 | 81 | Detects all attributes at the class level of the given object that match the optionally provided argument type (or super-type). 82 | If the object is not reflectable or there is an error instantiating any argument, an `UnreflectableException` exception is triggered! 83 | The result of this function is of type: `list`. However, if you provide an argument name: psalm will know the type of the attribute. 84 | 85 | ```php 86 | use function VeeWee\Reflecta\Reflect\object_attributes; 87 | 88 | try { 89 | $allAttributes = object_attributes($yourObject); 90 | $allAttributesOfType = object_attributes($yourObject, \YourAttributeType::class); 91 | $allAttributesOfType = object_attributes($yourObject, \YourAbstractBaseType::class); 92 | } catch (UnreflectableException) { 93 | // Deal with it 94 | } 95 | ``` 96 | 97 | #### object_has_attribute 98 | 99 | Checks if the object contains an attribute of given type (or super-type). 100 | If the object is not reflectable, an `UnreflectableException` exception is triggered! 101 | 102 | ```php 103 | use function VeeWee\Reflecta\Reflect\object_has_attribute; 104 | 105 | try { 106 | $hasAttribute = object_has_attribute($yourObject, \YourAttributeType::class); 107 | $hasAttributeThatImplementsBaseType = object_has_attribute($yourObject, \YourAbstractBaseType::class); 108 | } catch (UnreflectableException) { 109 | // Deal with it 110 | } 111 | ``` 112 | 113 | #### object_is_dynamic 114 | 115 | Checks if the provided object is considered a safe dynamic object that implements `AllowDynamicProperties`. 116 | 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. 117 | If the object is not reflectable, an `UnreflectableException` exception is triggered! 118 | 119 | ```php 120 | use function VeeWee\Reflecta\Reflect\object_is_dynamic; 121 | 122 | try { 123 | $isDynamic = object_is_dynamic(new stdClass()); 124 | $isDynamic = object_is_dynamic(new #[\AllowDynamicProperties] class() {}); 125 | $isNotDynamic = object_is_dynamic(new class() {}); 126 | } catch (UnreflectableException) { 127 | // Deal with it 128 | } 129 | ``` 130 | 131 | #### properties_get 132 | 133 | Detects all values of all properties for a given object. 134 | The properties can have any visibility. 135 | If the object is not reflectable, an `UnreflectableException` exception is triggered! 136 | The result of this function is of type: `array`. 137 | 138 | ```php 139 | use VeeWee\Reflecta\ArrayAccess\Exception\ArrayAccessException; 140 | use function VeeWee\Reflecta\Reflect\properties_get; 141 | 142 | try { 143 | $aDictOfProperties = properties_get($yourObject); 144 | } catch (UnreflectableException) { 145 | // Deal with it 146 | } 147 | ``` 148 | 149 | Additionally, you can provide a custom predicate to filter the properties you want to zoom in on: 150 | 151 | ```php 152 | use VeeWee\Reflecta\Reflect\Type\Visibility; 153 | use function VeeWee\Reflecta\Reflect\Predicate\property_visibility; 154 | 155 | try { 156 | $aDictOfProperties = properties_get($yourObject, property_visibility(Visibility::Public)); 157 | } catch (UnreflectableException) { 158 | // Deal with it 159 | } 160 | ``` 161 | 162 | #### properties_set 163 | 164 | Immutably saves the new values at the provided property locations inside a given object. 165 | The values is a `array` that takes the name of the property as key and the new property value as value. 166 | If the property is not available on the object, an `UnreflectableException` will be thrown. 167 | 168 | ```php 169 | use VeeWee\Reflecta\Exception\CloneException; 170 | use function VeeWee\Reflecta\Reflect\properties_set; 171 | 172 | try { 173 | $yourNewObject = properties_set($yourOldObject, $newValuesDict); 174 | } catch (UnreflectableException | CloneException) { 175 | // Deal with it 176 | } 177 | ``` 178 | 179 | Additionally, you can provide a custom predicate to filter the properties you want to set. 180 | This predicate will make sure that only the data that should be stores is set. 181 | 182 | ```php 183 | use VeeWee\Reflecta\Reflect\Type\Visibility; 184 | use function VeeWee\Reflecta\Reflect\Predicate\property_visibility; 185 | use function VeeWee\Reflecta\Reflect\properties_set; 186 | 187 | try { 188 | $aDictOfProperties = properties_set($yourObject, $newValuesDict, property_visibility(Visibility::Public)); 189 | } catch (UnreflectableException) { 190 | // Deal with it 191 | } 192 | ``` 193 | 194 | **Note:** When setting a predicate on a dynamic class, new properties will always be added. 195 | The next time you call `properties_get` on the same object, the predicate knows about the new property and will take it into account whilst filtering. 196 | 197 | #### property_get 198 | 199 | Detects the value of a property for a given object. 200 | The property could have any visibility. 201 | If the property is not available inside the object, an `UnreflectableException` exception is triggered! 202 | If all stars align, the result of this function gets inferred by psalm based on the provided named object and literal property. 203 | 204 | 205 | ```php 206 | use VeeWee\Reflecta\ArrayAccess\Exception\ArrayAccessException; 207 | use function VeeWee\Reflecta\Reflect\property_get; 208 | 209 | try { 210 | $value = property_get($yourObject, $theProperty); 211 | } catch (UnreflectableException) { 212 | // Deal with it 213 | } 214 | ``` 215 | 216 | #### property_set 217 | 218 | Immutably saves a new value at a specific property inside a given object. 219 | If the property is not available on the object, an `UnreflectableException` will be thrown. 220 | 221 | ```php 222 | use VeeWee\Reflecta\Exception\CloneException; 223 | use function VeeWee\Reflecta\Reflect\property_set; 224 | 225 | try { 226 | $yourNewObject = property_set($yourOldObject, $theProperty, $newValueForProp); 227 | } catch (UnreflectableException | CloneException) { 228 | // Deal with it 229 | } 230 | ``` 231 | 232 | ## Types 233 | 234 | This package provides following classes for dealing with reflection in a central location: 235 | 236 | #### ReflectedAttribute 237 | 238 | Provides a tiny wrapper around PHP's `\ReflectionAttribute`; 239 | 240 | ```php 241 | use VeeWee\Reflecta\Reflect\Type\ReflectedAttribute; 242 | 243 | #[MyAttribute] 244 | class X { 245 | } 246 | 247 | $attribute = new ReflectedAttribute( 248 | new \ReflectionClass(X::class)->getAttributes()[0] 249 | ); 250 | ``` 251 | 252 | #### ReflectedClass 253 | 254 | Provides a tiny wrapper around PHP's `\ReflectionClass`; 255 | 256 | ```php 257 | use VeeWee\Reflecta\Reflect\Type\ReflectedClass; 258 | 259 | $class = new ReflectedClass(new \ReflectionClass(Your::class)); 260 | $class = ReflectedClass::fromObject(new Your()); 261 | $class = ReflectedClass::fromClassName(Your::class); 262 | $class = ReflectedClass::from($instanceOrFqcn); 263 | ``` 264 | 265 | #### ReflectedProperty 266 | 267 | Provides a tiny wrapper around PHP's `\ReflectionProperty`; 268 | 269 | ```php 270 | use VeeWee\Reflecta\Reflect\Type\ReflectedClass; 271 | use VeeWee\Reflecta\Reflect\Type\ReflectedProperty; 272 | 273 | $property = new ReflectedProperty(new \ReflectionProperty(Your::class, 'property')); 274 | $property = ReflectedClass::from(new Your())->property('property'); 275 | ``` 276 | -------------------------------------------------------------------------------- /tests/unit/Reflect/Type/ReflectedClassTest.php: -------------------------------------------------------------------------------- 1 | fullName()); 26 | static::assertSame(X::class, ReflectedClass::from(X::class)->fullName()); 27 | } 28 | 29 | public function test_it_can_not_load_from_fqcn(): void 30 | { 31 | $this->expectException(UnreflectableException::class); 32 | $this->expectExceptionMessage('Unable to locate class UnknownClass.'); 33 | 34 | ReflectedClass::fromFullyQualifiedClassName('UnknownClass'); 35 | } 36 | 37 | public function test_it_can_load_class_from_object(): void 38 | { 39 | $object = new X(); 40 | 41 | static::assertSame(X::class, ReflectedClass::fromObject($object)->fullName()); 42 | static::assertSame(X::class, ReflectedClass::from($object)->fullName()); 43 | } 44 | 45 | public function test_it_knows_name_parts(): void 46 | { 47 | $class = ReflectedClass::fromFullyQualifiedClassName(X::class); 48 | 49 | static::assertSame(X::class, $class->fullName()); 50 | static::assertSame('X', $class->shortName()); 51 | static::assertSame('VeeWee\Reflecta\TestFixtures', $class->namespaceName()); 52 | } 53 | 54 | public function test_it_knows_parent(): void 55 | { 56 | $withParent = new class() extends AbstractAttribute {}; 57 | $withParentParent = ReflectedClass::fromObject($withParent)->parent(); 58 | $withoutParent = new class() {}; 59 | $withoutParentParent = ReflectedClass::fromObject($withoutParent)->parent(); 60 | 61 | static::assertTrue($withParentParent->isSome()); 62 | static::assertEquals($withParentParent->unwrap(), ReflectedClass::fromFullyQualifiedClassName(AbstractAttribute::class)); 63 | static::assertTrue($withoutParentParent->isNone()); 64 | } 65 | 66 | public function test_it_knows_it_is_final(): void 67 | { 68 | $x = ReflectedClass::fromFullyQualifiedClassName(X::class); 69 | $y = ReflectedClass::fromObject(new class() {}); 70 | 71 | static::assertTrue($x->isFinal()); 72 | static::assertTrue($x->check(static fn (ReflectedClass $class) => $class->isFinal())); 73 | static::assertFalse($y->isFinal()); 74 | static::assertFalse($y->check(static fn (ReflectedClass $class) => $class->isFinal())); 75 | } 76 | 77 | public function test_it_can_check_for_dynamic_objects(): void 78 | { 79 | if (PHP_VERSION_ID < 80200) { 80 | static::markTestSkipped('On PHP 8.2, all classes are safely dynamic'); 81 | } 82 | 83 | $x = new #[AllowDynamicProperties] class {}; 84 | $y = new class {}; 85 | $s = new stdClass(); 86 | 87 | static::assertTrue(ReflectedClass::fromObject($x)->isDynamic()); 88 | static::assertFalse(ReflectedClass::fromObject($y)->isDynamic()); 89 | static::assertTrue(ReflectedClass::fromObject($s)->isDynamic()); 90 | } 91 | 92 | public function test_it_can_check_for_dynamic_objects_in_php_81(): void 93 | { 94 | if (PHP_VERSION_ID >= 80200) { 95 | static::markTestSkipped('On PHP 8.2, all classes are safely dynamic'); 96 | } 97 | 98 | $x = new #[AllowDynamicProperties] class {}; 99 | $y = new class {}; 100 | $s = new stdClass() 101 | ; 102 | static::assertTrue(ReflectedClass::fromObject($x)->isDynamic()); 103 | static::assertTrue(ReflectedClass::fromObject($y)->isDynamic()); 104 | static::assertTrue(ReflectedClass::fromObject($s)->isDynamic()); 105 | } 106 | 107 | public function test_it_knows_if_class_is_abstract(): void 108 | { 109 | $abstract = ReflectedClass::fromFullyQualifiedClassName(AbstractAttribute::class); 110 | $concrete = ReflectedClass::fromObject(new class() {}); 111 | 112 | static::assertTrue($abstract->isAbstract()); 113 | static::assertTrue($abstract->check(static fn (ReflectedClass $class) => $class->isAbstract())); 114 | static::assertFalse($concrete->isAbstract()); 115 | static::assertFalse($concrete->check(static fn (ReflectedClass $class) => $class->isAbstract())); 116 | } 117 | 118 | public function test_it_knows_it_is_instantiable(): void 119 | { 120 | $abstract = ReflectedClass::fromFullyQualifiedClassName(AbstractAttribute::class); 121 | $concrete = ReflectedClass::fromObject(new class() {}); 122 | 123 | static::assertFalse($abstract->isInstantiable()); 124 | static::assertFalse($abstract->check(static fn (ReflectedClass $class) => $class->isInstantiable())); 125 | static::assertTrue($concrete->isInstantiable()); 126 | static::assertTrue($concrete->check(static fn (ReflectedClass $class) => $class->isInstantiable())); 127 | } 128 | 129 | public function test_it_knows_it_is_cloneable(): void 130 | { 131 | $abstract = ReflectedClass::fromFullyQualifiedClassName(AbstractAttribute::class); 132 | $concrete = ReflectedClass::fromObject(new class() {}); 133 | 134 | static::assertFalse($abstract->isCloneable()); 135 | static::assertFalse($abstract->check(static fn (ReflectedClass $class) => $class->isCloneable())); 136 | static::assertTrue($concrete->isCloneable()); 137 | static::assertTrue($concrete->check(static fn (ReflectedClass $class) => $class->isCloneable())); 138 | } 139 | 140 | public function test_it_knows_about_doc_comment(): void 141 | { 142 | $x = ReflectedClass::fromObject(/** doc */ new class() {}); 143 | 144 | static::assertSame('/** doc */', $x->docComment()); 145 | } 146 | 147 | public function test_it_can_grab_property_by_name(): void 148 | { 149 | $class = ReflectedClass::fromFullyQualifiedClassName(X::class); 150 | $property = $class->property('z'); 151 | 152 | static::assertSame('z', $property->name()); 153 | } 154 | 155 | public function test_it_can_grab_inherited_property_by_name(): void 156 | { 157 | $x = new class extends AbstractProperties {}; 158 | $class = ReflectedClass::fromObject($x); 159 | $property = $class->property('a'); 160 | 161 | static::assertSame('a', $property->name()); 162 | } 163 | 164 | public function test_it_can_list_all_properties(): void 165 | { 166 | $class = ReflectedClass::fromFullyQualifiedClassName(X::class); 167 | $properties = $class->properties(); 168 | 169 | static::assertCount(1, $properties); 170 | static::assertSame('z', $properties['z']->name()); 171 | } 172 | 173 | public function test_it_can_list_dynamic_properties(): void 174 | { 175 | $x = new Dynamic(); 176 | $x->x = 'foo'; 177 | $x->y = 'bar'; 178 | $class = ReflectedClass::fromObject($x); 179 | $properties = $class->properties(); 180 | 181 | static::assertCount(2, $properties); 182 | static::assertSame('x', $properties['x']->name()); 183 | static::assertSame('y', $properties['y']->name()); 184 | } 185 | 186 | public function test_it_can_list_inherited_properties(): void 187 | { 188 | $x = new class extends AbstractProperties { 189 | private string $d = 'd'; 190 | protected string $e = 'e'; 191 | public string $f = 'f'; 192 | }; 193 | 194 | $class = ReflectedClass::fromObject($x); 195 | $properties = $class->properties(); 196 | 197 | static::assertCount(6, $properties); 198 | static::assertSame('a', $properties['a']->name()); 199 | static::assertSame(AbstractProperties::class, $properties['a']->declaringClass()->fullName()); 200 | static::assertSame('b', $properties['b']->name()); 201 | static::assertSame(AbstractProperties::class, $properties['b']->declaringClass()->fullName()); 202 | static::assertSame('c', $properties['c']->name()); 203 | static::assertSame(AbstractProperties::class, $properties['c']->declaringClass()->fullName()); 204 | static::assertSame('d', $properties['d']->name()); 205 | static::assertSame($x::class, $properties['d']->declaringClass()->fullName()); 206 | static::assertSame('e', $properties['e']->name()); 207 | static::assertSame($x::class, $properties['e']->declaringClass()->fullName()); 208 | static::assertSame('f', $properties['f']->name()); 209 | static::assertSame($x::class, $properties['f']->declaringClass()->fullName()); 210 | } 211 | 212 | public function test_it_can_overwrite_inherited_props(): void 213 | { 214 | $x = new class extends AbstractProperties { 215 | private string $a = 'd'; 216 | protected string $b = 'e'; 217 | public string $c = 'f'; 218 | }; 219 | 220 | $class = ReflectedClass::fromObject($x); 221 | $properties = $class->properties(); 222 | 223 | static::assertCount(3, $properties); 224 | static::assertSame('a', $properties['a']->name()); 225 | static::assertSame($x::class, $properties['a']->declaringClass()->fullName()); 226 | static::assertSame('b', $properties['b']->name()); 227 | static::assertSame($x::class, $properties['b']->declaringClass()->fullName()); 228 | static::assertSame('c', $properties['c']->name()); 229 | static::assertSame($x::class, $properties['c']->declaringClass()->fullName()); 230 | } 231 | 232 | public function test_it_can_filter_properties(): void 233 | { 234 | $class = ReflectedClass::fromFullyQualifiedClassName(X::class); 235 | $publicProps = $class->properties(static fn (ReflectedProperty $prop): bool => $prop->isPublic()); 236 | $privateProps = $class->properties(static fn (ReflectedProperty $prop): bool => $prop->isPrivate()); 237 | 238 | static::assertCount(1, $publicProps); 239 | static::assertSame('z', $publicProps['z']->name()); 240 | static::assertCount(0, $privateProps); 241 | } 242 | 243 | public function test_it_can_get_attributes(): void 244 | { 245 | $x = new #[InheritedCustomAttribute, CustomAttribute] class {}; 246 | 247 | $actual = ReflectedClass::fromObject($x)->attributes(); 248 | 249 | static::assertCount(2, $actual); 250 | static::assertInstanceOf(InheritedCustomAttribute::class, $actual[0]); 251 | static::assertInstanceOf(CustomAttribute::class, $actual[1]); 252 | } 253 | 254 | public function test_it_can_get_attributes_of_type(): void 255 | { 256 | $x = new #[InheritedCustomAttribute, CustomAttribute] class {}; 257 | 258 | $class = ReflectedClass::fromObject($x); 259 | $actual = $class->attributes(InheritedCustomAttribute::class); 260 | 261 | static::assertCount(1, $actual); 262 | static::assertTrue($class->hasAttributeOfType(InheritedCustomAttribute::class)); 263 | static::assertInstanceOf(InheritedCustomAttribute::class, $actual[0]); 264 | } 265 | 266 | public function test_it_can_get_attributes_of_subtype(): void 267 | { 268 | $x = new #[InheritedCustomAttribute] class {}; 269 | 270 | $class = ReflectedClass::fromObject($x); 271 | $actual = $class->attributes(AbstractAttribute::class); 272 | 273 | static::assertCount(1, $actual); 274 | static::assertTrue($class->hasAttributeOfType(AbstractAttribute::class)); 275 | static::assertInstanceOf(InheritedCustomAttribute::class, $actual[0]); 276 | } 277 | 278 | public function test_it_can_fail_on_attribute_instantiation(): void 279 | { 280 | $x = new #[ThisIsAnUnknownAttribute] class {}; 281 | $class = ReflectedClass::fromObject($x); 282 | 283 | $this->expectException(UnreflectableException::class); 284 | $this->expectExceptionMessage('Unable to instantiate class ThisIsAnUnknownAttribute.'); 285 | 286 | $class->attributes(); 287 | } 288 | 289 | public function test_it_can_apply(): void 290 | { 291 | $class = ReflectedClass::fromFullyQualifiedClassName(X::class); 292 | 293 | $result = $class->apply( 294 | static fn (ReflectionClass $class): string => $class->getName() 295 | ); 296 | 297 | static::assertSame(X::class, $result); 298 | } 299 | } 300 | --------------------------------------------------------------------------------