diff --git a/src/Definition/ClassDefinition.php b/src/Definition/ClassDefinition.php index c9e8c46c8..563c6a7fb 100644 --- a/src/Definition/ClassDefinition.php +++ b/src/Definition/ClassDefinition.php @@ -15,6 +15,7 @@ public function __construct( public ObjectType $type, public Attributes $attributes, public Properties $properties, + public Properties $magicProperties, public Methods $methods, public bool $isFinal, public bool $isAbstract, diff --git a/src/Definition/Repository/Cache/Compiler/ClassDefinitionCompiler.php b/src/Definition/Repository/Cache/Compiler/ClassDefinitionCompiler.php index cff9bb1eb..095f0bf66 100644 --- a/src/Definition/Repository/Cache/Compiler/ClassDefinitionCompiler.php +++ b/src/Definition/Repository/Cache/Compiler/ClassDefinitionCompiler.php @@ -43,6 +43,13 @@ public function compile(ClassDefinition $value): string $properties = implode(', ', $properties); + $magicProperties = array_map( + $this->propertyCompiler->compile(...), + iterator_to_array($value->magicProperties) + ); + + $magicProperties = implode(', ', $magicProperties); + $methods = array_map( $this->methodCompiler->compile(...), iterator_to_array($value->methods) @@ -60,6 +67,7 @@ public function compile(ClassDefinition $value): string $type, $attributes, new \CuyZ\Valinor\Definition\Properties($properties), + new \CuyZ\Valinor\Definition\Properties($magicProperties), new \CuyZ\Valinor\Definition\Methods($methods), $isFinal, $isAbstract, diff --git a/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php b/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php index 540fbb9d7..f7289d721 100644 --- a/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php +++ b/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php @@ -26,6 +26,7 @@ use CuyZ\Valinor\Type\Types\InterfaceType; use CuyZ\Valinor\Type\Types\NativeClassType; use CuyZ\Valinor\Type\Types\UnresolvableType; +use CuyZ\Valinor\Utility\Reflection\Annotations; use CuyZ\Valinor\Utility\Reflection\Reflection; use ReflectionMethod; use ReflectionProperty; @@ -89,7 +90,8 @@ public function for(ObjectType $type): ClassDefinition $reflection->name, $type, new Attributes(...$this->attributesRepository->for($reflection)), - new Properties(...$this->properties($type, $typeResolver)), + new Properties(...$this->properties($type, $typeResolver, false)), + new Properties(...$this->properties($type, $typeResolver, true)), new Methods(...$this->methods($type, $typeResolver)), $reflection->isFinal(), $reflection->isAbstract(), @@ -131,10 +133,34 @@ private function vacantTypes(ObjectType $type): array /** * @return list */ - private function properties(ObjectType $type, ReflectionTypeResolver $typeResolver): array + private function properties(ObjectType $type, ReflectionTypeResolver $typeResolver, bool $magic): array { $reflection = Reflection::class($type->className()); + if ($magic) { + $result = []; + $className = $type->className(); + foreach (Annotations::magicProperties($reflection) as $name => $propertyType) { + $signature = "$className::\$$name"; + $type = $typeResolver->resolveType(null, $propertyType); + $hasDefaultValue = false; + $defaultValue = null; + $isPublic = true; + $attributes = new Attributes(); + + $result [] = new PropertyDefinition( + $name, + $signature, + $type, + $type, + $hasDefaultValue, + $defaultValue, + $isPublic, + $attributes + ); + } + return $result; + } $properties = []; foreach ($reflection->getProperties() as $property) { diff --git a/src/Utility/Reflection/Annotations.php b/src/Utility/Reflection/Annotations.php index 4e24f5348..191b59bc4 100644 --- a/src/Utility/Reflection/Annotations.php +++ b/src/Utility/Reflection/Annotations.php @@ -19,6 +19,7 @@ use function current; use function end; use function in_array; +use function preg_match_all; use function preg_replace; use function str_starts_with; use function trim; @@ -35,7 +36,7 @@ private function __construct(string|false $docBlock) return; } - $docBlock = $this->sanitizeDocComment($docBlock); + $docBlock = self::sanitizeDocComment($docBlock); $tokens = (new TokensExtractor($docBlock))->all(); @@ -182,13 +183,33 @@ private function filteredByPriority(string ...$allowed): array return $result; } - private function sanitizeDocComment(string $value): string + private static function sanitizeDocComment(string $value): string { $value = preg_replace('#^\s*/\*\*([^/]+)\*/\s*$#', '$1', $value); return preg_replace('/^\s*\*\s*(\S*)/mU', '$1', $value); // @phpstan-ignore-line / We know the regex is correct } + /** + * @param ReflectionClass $reflection + * @return array + */ + public static function magicProperties(ReflectionClass $reflection): array + { + $types = []; + $docComment = self::sanitizeDocComment($reflection->__toString()); + + $expression = '/@property\s+(?.+)\s+\$([a-zA-Z_0-9]+)/u'; + + preg_match_all($expression, $docComment, $matches); + + foreach ($matches[2] as $key => $name) { + $types[$name] = $matches[1][$key]; + } + + return $types; + } + /** * @param list $array * @return list diff --git a/tests/Fake/Definition/FakeClassDefinition.php b/tests/Fake/Definition/FakeClassDefinition.php index ceb24a177..f8088dc4f 100644 --- a/tests/Fake/Definition/FakeClassDefinition.php +++ b/tests/Fake/Definition/FakeClassDefinition.php @@ -28,6 +28,7 @@ public static function new(string $name = stdClass::class): ClassDefinition new NativeClassType($name), new Attributes(), new Properties(), + new Properties(), new Methods(), true, false, @@ -54,6 +55,7 @@ public static function fromReflection(ReflectionClass $reflection): ClassDefinit new NativeClassType($reflection->name), new Attributes(), new Properties(...$properties), + new Properties(), new Methods(...$methods), $reflection->isFinal(), $reflection->isAbstract(), diff --git a/tests/Integration/Normalizer/ExpectedCache/class-with-property-transformer-with-callable.php b/tests/Integration/Normalizer/ExpectedCache/class-with-property-transformer-with-callable.php index 436949a61..6c7a10fe1 100644 --- a/tests/Integration/Normalizer/ExpectedCache/class-with-property-transformer-with-callable.php +++ b/tests/Integration/Normalizer/ExpectedCache/class-with-property-transformer-with-callable.php @@ -13,7 +13,7 @@ public function __construct(array $transformers, CuyZ\Valinor\Normalizer\Transfo public function transform(mixed $value): mixed { $references = new WeakMap(); - return $this->transform_cuyz_valinor_tests_integration_normalizer_temporaryphp85_classwithtransformerwithcallable_0c11da6260a6db3553df282aa070e27a0303112f($value, $references); + return $this->transform_cuyz_valinor_tests_integration_normalizer_temporaryphp85_classwithtransformerwithcallable_4249691f5c85877eea732e0938d14fd26ec54786($value, $references); } private function transform_object_cuyz_valinor_tests_integration_normalizer_temporaryphp85_classwithtransformerwithcallable_ca2b7327(CuyZ\Valinor\Tests\Integration\Normalizer\TemporaryPHP85\ClassWithTransformerWithCallable $value, WeakMap $references): array @@ -29,7 +29,7 @@ private function transform_object_cuyz_valinor_tests_integration_normalizer_temp return $values; } - private function transform_cuyz_valinor_tests_integration_normalizer_temporaryphp85_classwithtransformerwithcallable_0c11da6260a6db3553df282aa070e27a0303112f(mixed $value, WeakMap $references): mixed + private function transform_cuyz_valinor_tests_integration_normalizer_temporaryphp85_classwithtransformerwithcallable_4249691f5c85877eea732e0938d14fd26ec54786(mixed $value, WeakMap $references): mixed { $next = fn () => $this->transform_object_cuyz_valinor_tests_integration_normalizer_temporaryphp85_classwithtransformerwithcallable_ca2b7327($value, $references); $next = fn () => ((new ReflectionClass(CuyZ\Valinor\Tests\Integration\Normalizer\TemporaryPHP85\ClassWithTransformerWithCallable::class))->getAttributes()[0]->newInstance())->normalize($value, $next); diff --git a/tests/Integration/Normalizer/ExpectedCache/class-with-transformer-with-callable.php b/tests/Integration/Normalizer/ExpectedCache/class-with-transformer-with-callable.php index 19f82fe7f..8b6d59e3b 100644 --- a/tests/Integration/Normalizer/ExpectedCache/class-with-transformer-with-callable.php +++ b/tests/Integration/Normalizer/ExpectedCache/class-with-transformer-with-callable.php @@ -27,11 +27,11 @@ private function transform_object_cuyz_valinor_tests_integration_normalizer_temp 'value' => $value->value, ]; $transformed = []; - $transformed['value'] = $this->transform_string_ee734ad1df9498f24acd0cad1cd2fed3e95f69b3($values['value'], $references); + $transformed['value'] = $this->transform_string_996b61d05a213b4a77de5f27b57df2564765d558($values['value'], $references); return $transformed; } - private function transform_string_ee734ad1df9498f24acd0cad1cd2fed3e95f69b3(mixed $value, WeakMap $references): mixed + private function transform_string_996b61d05a213b4a77de5f27b57df2564765d558(mixed $value, WeakMap $references): mixed { $next = fn () => $value; $next = fn () => ((new ReflectionProperty(CuyZ\Valinor\Tests\Integration\Normalizer\TemporaryPHP85\ClassWithPropertyTransformerWithCallable::class, 'value'))->getAttributes()[0]->newInstance())->normalize($value, $next); diff --git a/tests/Integration/Normalizer/ExpectedCache/class-with-unresolvable-type-and-mixed-native-type.php b/tests/Integration/Normalizer/ExpectedCache/class-with-unresolvable-type-and-mixed-native-type.php index 33a5f6520..5d21d81ca 100644 --- a/tests/Integration/Normalizer/ExpectedCache/class-with-unresolvable-type-and-mixed-native-type.php +++ b/tests/Integration/Normalizer/ExpectedCache/class-with-unresolvable-type-and-mixed-native-type.php @@ -27,7 +27,7 @@ private function transform_object_cuyz_valinor_tests_integration_normalizer_clas 'value' => $value->value, ]; $transformed = []; - $transformed['value'] = $this->transform_mixed_81119e8ac7b14bb88fa09fcfa886afe87aa69fa1($values['value'], $references); + $transformed['value'] = $this->transform_mixed_470fe1dfefcac95513283699f5d61a2abba0c1f8($values['value'], $references); return $transformed; } @@ -75,7 +75,7 @@ private function transform_iterable_mixed_bf66259c(iterable $value, WeakMap $ref })(); } - private function transform_mixed_81119e8ac7b14bb88fa09fcfa886afe87aa69fa1(mixed $value, WeakMap $references): mixed + private function transform_mixed_470fe1dfefcac95513283699f5d61a2abba0c1f8(mixed $value, WeakMap $references): mixed { $next = fn () => $this->transform_mixed($value, $references); if (\is_string($value)) { diff --git a/tests/Integration/Normalizer/ExpectedCache/class-with-unresolvable-type-and-string-native-type.php b/tests/Integration/Normalizer/ExpectedCache/class-with-unresolvable-type-and-string-native-type.php index 84f541aa3..7633c555f 100644 --- a/tests/Integration/Normalizer/ExpectedCache/class-with-unresolvable-type-and-string-native-type.php +++ b/tests/Integration/Normalizer/ExpectedCache/class-with-unresolvable-type-and-string-native-type.php @@ -27,11 +27,11 @@ private function transform_object_cuyz_valinor_tests_integration_normalizer_clas 'value' => $value->value, ]; $transformed = []; - $transformed['value'] = $this->transform_string_c80acf3f0352152a3298abfbddc6f6c62c5da4b6($values['value'], $references); + $transformed['value'] = $this->transform_string_570c90bf0c9037b1771b87d70b8c18cd95dd7982($values['value'], $references); return $transformed; } - private function transform_string_c80acf3f0352152a3298abfbddc6f6c62c5da4b6(mixed $value, WeakMap $references): mixed + private function transform_string_570c90bf0c9037b1771b87d70b8c18cd95dd7982(mixed $value, WeakMap $references): mixed { $next = fn () => $value; $next = fn () => (new CuyZ\Valinor\Tests\Integration\Normalizer\PrependToStringAttribute())->normalize($value, $next); diff --git a/tests/Unit/Definition/ClassDefinitionTest.php b/tests/Unit/Definition/ClassDefinitionTest.php index cd71af414..3fc9cbb8a 100644 --- a/tests/Unit/Definition/ClassDefinitionTest.php +++ b/tests/Unit/Definition/ClassDefinitionTest.php @@ -24,7 +24,7 @@ public function test_class_data_can_be_retrieved(): void $properties = new Properties(FakePropertyDefinition::new()); $methods = new Methods(FakeMethodDefinition::new()); - $class = new ClassDefinition(stdClass::class, $type, $attributes, $properties, $methods, true, false); + $class = new ClassDefinition(stdClass::class, $type, $attributes, $properties, new Properties(), $methods, true, false); self::assertSame(stdClass::class, $class->name); self::assertSame($type, $class->type); diff --git a/tests/Unit/Definition/Repository/Cache/Compiler/ClassDefinitionCompilerTest.php b/tests/Unit/Definition/Repository/Cache/Compiler/ClassDefinitionCompilerTest.php index a7eda14d9..2afa9ad3f 100644 --- a/tests/Unit/Definition/Repository/Cache/Compiler/ClassDefinitionCompilerTest.php +++ b/tests/Unit/Definition/Repository/Cache/Compiler/ClassDefinitionCompilerTest.php @@ -12,6 +12,7 @@ use CuyZ\Valinor\Tests\Fixture\Object\StringableObject; use CuyZ\Valinor\Tests\Unit\UnitTestCase; use CuyZ\Valinor\Type\Types\NativeClassType; +use CuyZ\Valinor\Type\Types\NativeIntegerType; use CuyZ\Valinor\Type\Types\NativeStringType; use Error; use ReflectionClass; @@ -23,21 +24,27 @@ final class ClassDefinitionCompilerTest extends UnitTestCase public function test_class_definition_is_compiled_correctly(): void { $object = - new class () { - public string $property = 'Some property default value'; - - #[Constructor] - public static function method(string $parameter = 'Some parameter default value', string ...$variadic): string - { - return $parameter . implode(' / ', $variadic); - } - - #[Constructor] - public static function methodWithDefaultObjectValue(StringableObject $object = new StringableObject('bar')): StringableObject - { - return $object; + new + /** + * @property string $v + * @property int $vInt + */ + class () { + public string $property = 'Some property default value'; + + #[Constructor] + public static function method(string $parameter = 'Some parameter default value', string ...$variadic): string + { + return $parameter . implode(' / ', $variadic); + } + + #[Constructor] + public static function methodWithDefaultObjectValue(StringableObject $object = new StringableObject('bar')): StringableObject + { + return $object; + } } - }; + ; $className = $object::class; @@ -66,6 +73,30 @@ public static function methodWithDefaultObjectValue(StringableObject $object = n self::assertSame('Some property default value', $property->defaultValue); self::assertTrue($property->isPublic); + $magicProperties = $class->magicProperties; + + self::assertTrue($magicProperties->has('v')); + + $mp = $magicProperties->get('v'); + + self::assertSame('v', $mp->name); + self::assertSame($className . '::$v', $mp->signature); + self::assertSame(NativeStringType::get(), $mp->type); + self::assertFalse($mp->hasDefaultValue); + self::assertSame(null, $mp->defaultValue); + self::assertTrue($mp->isPublic); + + self::assertTrue($magicProperties->has('vInt')); + + $mp = $magicProperties->get('vInt'); + + self::assertSame('vInt', $mp->name); + self::assertSame($className . '::$vInt', $mp->signature); + self::assertSame(NativeIntegerType::get(), $mp->type); + self::assertFalse($mp->hasDefaultValue); + self::assertSame(null, $mp->defaultValue); + self::assertTrue($mp->isPublic); + $method = $class->methods->get('method'); self::assertSame('method', $method->name);