diff --git a/src/main/php/lang.base.php b/src/main/php/lang.base.php index 698cebf66c..9428fd47a3 100755 --- a/src/main/php/lang.base.php +++ b/src/main/php/lang.base.php @@ -418,6 +418,17 @@ function __construct($str) { } // }}} +// {{{ PHP 8.1 enums +if (!function_exists('enum_exists')) { + interface UnitEnum { } + interface BackedEnum extends UnitEnum { } + + function enum_exists($name, $load) { + return class_exists($name, $load) && $name instanceof \UnitEnum; + } +} +// }}} + // {{{ main error_reporting(E_ALL); set_error_handler('__error'); @@ -438,6 +449,7 @@ function __construct($str) { defined('T_ATTRIBUTE') || define('T_ATTRIBUTE', -383); defined('T_NAME_FULLY_QUALIFIED') || define('T_NAME_FULLY_QUALIFIED', -312); defined('T_NAME_QUALIFIED') || define('T_NAME_QUALIFIED', -314); +defined('T_ENUM') || define('T_ENUM', -369); xp::$loader= new xp(); diff --git a/src/main/php/lang/AbstractClassLoader.class.php b/src/main/php/lang/AbstractClassLoader.class.php index b42c92d274..4c057029f9 100755 --- a/src/main/php/lang/AbstractClassLoader.class.php +++ b/src/main/php/lang/AbstractClassLoader.class.php @@ -85,7 +85,7 @@ public function loadClass0($class) { // If class was declared, but loading threw an exception it means // a "soft" dependency, one that is only required at runtime, was // not loaded, the class itself has been declared. - if (class_exists($name, false) || interface_exists($name, false) || trait_exists($name, false)) { + if (class_exists($name, false) || interface_exists($name, false) || trait_exists($name, false) || enum_exists($name, false)) { throw new ClassDependencyException($class, [$this], $e); } @@ -101,7 +101,8 @@ public function loadClass0($class) { $e= new ClassNotFoundException($class, [$this]); \xp::gc(__FILE__); throw $e; - } else if (!class_exists($name, false) && !interface_exists($name, false) && !trait_exists($name, false)) { + } else if (!class_exists($name, false) && !interface_exists($name, false) && !trait_exists($name, false) && !enum_exists($name, false)) { + \xp::gc(__FILE__); $bytes= $this->loadClassBytes($class); if (preg_match('/(class|interface|trait)\s+([^ ]+)/', $bytes, $decl)) { preg_match('/namespace\s+([^;]+);/', $bytes, $ns); diff --git a/src/main/php/lang/Enum.class.php b/src/main/php/lang/Enum.class.php index 55e4b21abc..4783a4d81d 100755 --- a/src/main/php/lang/Enum.class.php +++ b/src/main/php/lang/Enum.class.php @@ -45,11 +45,8 @@ public function __construct(int $ordinal= 0, string $name= '') { * @return self * @throws lang.IllegalArgumentException in case the enum member does not exist or when the given class is not an enum */ - public static function valueOf($type, string $name): self { + public static function valueOf($type, string $name) { $class= $type instanceof XPClass ? $type : XPClass::forName($type); - if (!$class->isEnum()) { - throw new IllegalArgumentException('Argument class must be lang.XPClass'); - } if ($class->isSubclassOf(self::class)) { try { @@ -58,13 +55,15 @@ public static function valueOf($type, string $name): self { } catch (\ReflectionException $e) { throw new IllegalArgumentException($e->getMessage()); } - } else { - if ($class->reflect()->hasConstant($name)) { - $t= ClassLoader::defineClass($class->getName().'Enum', self::class, []); - return $t->newInstance($class->reflect()->getConstant($name), $name); - } + + throw new IllegalArgumentException('Not an enum member "'.$name.'" in '.$class->getName()); + } else if ($class->isSubclassOf(\UnitEnum::class)) { + if ($class->hasConstant($name)) return $class->getConstant($name); + + throw new IllegalArgumentException('Not such case "'.$name.'" in '.$class->getName()); } - throw new IllegalArgumentException('No such member "'.$name.'" in '.$class->getName()); + + throw new IllegalArgumentException('Argument class must be an enum'); } /** @@ -76,22 +75,18 @@ public static function valueOf($type, string $name): self { */ public static function valuesOf($type) { $class= $type instanceof XPClass ? $type : XPClass::forName($type); - if (!$class->isEnum()) { - throw new IllegalArgumentException('Argument class must be lang.XPClass'); - } - $r= []; if ($class->isSubclassOf(self::class)) { + $r= []; foreach ($class->reflect()->getStaticProperties() as $prop) { $class->isInstance($prop) && $r[]= $prop; } - } else { - $t= ClassLoader::defineClass($class->getName().'Enum', self::class, []); - foreach ($class->reflect()->getMethod('getValues')->invoke(null) as $name => $ordinal) { - $r[]= $t->newInstance($ordinal, $name); - } + return $r; + } else if ($class->isSubclassOf(\UnitEnum::class)) { + return $class->getMethod('cases')->invoke(null); } - return $r; + + throw new IllegalArgumentException('Argument class must be enum'); } /** diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index 830b47e3ac..19588127aa 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -57,7 +57,7 @@ public function newType0($base, $arguments) { $qname= $base->name.'<'.substr($qc, 1).'>'; // Create class if it doesn't exist yet - if (!class_exists($name, false) && !interface_exists($name, false)) { + if (!class_exists($name, false) && !interface_exists($name, false) && !trait_exists($name, false) && !enum_exists($name, false)) { $meta= \xp::$meta[$base->name]; // Parse placeholders into a lookup map diff --git a/src/main/php/lang/XPClass.class.php b/src/main/php/lang/XPClass.class.php index 04248460db..04e2af62e1 100755 --- a/src/main/php/lang/XPClass.class.php +++ b/src/main/php/lang/XPClass.class.php @@ -471,7 +471,8 @@ public function isTrait(): bool { * @return bool */ public function isEnum(): bool { - return class_exists(Enum::class, false) && $this->reflect()->isSubclassOf(Enum::class); + $r= $this->reflect(); + return $r->isSubclassOf(Enum::class) || $r->isSubclassOf(\UnitEnum::class); } /** @@ -800,7 +801,7 @@ public static function forName($name, IClassLoader $classloader= null): self { $name= strtr($resolved, '\\', '.'); } - if (class_exists($resolved, false) || interface_exists($resolved, false) || trait_exists($resolved, false)) { + if (class_exists($resolved, false) || interface_exists($resolved, false) || trait_exists($resolved, false) || enum_exists($resolved, false)) { return new self($resolved); } else if (null === $classloader) { return ClassLoader::getDefault()->loadClass($name); diff --git a/src/main/php/lang/reflect/ClassParser.class.php b/src/main/php/lang/reflect/ClassParser.class.php index 6058491dbe..7151eccfb8 100755 --- a/src/main/php/lang/reflect/ClassParser.class.php +++ b/src/main/php/lang/reflect/ClassParser.class.php @@ -36,7 +36,7 @@ protected function resolve($type, $context, $imports) { return XPClass::forName($type); } else if (isset($imports[$type])) { return XPClass::forName($imports[$type]); - } else if (class_exists($type, false) || interface_exists($type, false)) { + } else if (class_exists($type, false) || interface_exists($type, false) || trait_exists($type, false) || enum_exists($type, false)) { return new XPClass($type); } else if (false !== ($p= strrpos($context, '.'))) { return XPClass::forName(substr($context, 0, $p + 1).$type); @@ -562,8 +562,7 @@ public function parseDetails($bytes, $context= '') { case T_CLASS: if (isset($details['class'])) break; // Inside class, e.g. $lookup= ['self' => self::class] - case T_INTERFACE: - case T_TRAIT: + case T_INTERFACE: case T_TRAIT: case T_ENUM: if ($parsed) { $annotations= $this->parseAnnotations($parsed, $context, $imports, $tokens[$i][2] ?? -1); $parsed= ''; diff --git a/src/test/php/net/xp_framework/unittest/core/EnumTest.class.php b/src/test/php/net/xp_framework/unittest/core/EnumTest.class.php index 00a4fe1605..d1214aa794 100755 --- a/src/test/php/net/xp_framework/unittest/core/EnumTest.class.php +++ b/src/test/php/net/xp_framework/unittest/core/EnumTest.class.php @@ -1,21 +1,22 @@ assertTrue(XPClass::forName(Coin::class)->isEnum()); } #[Test] - public function operationIsAnEnums() { + public function operation_is_an_enum() { $this->assertTrue(XPClass::forName(Operation::class)->isEnum()); } + #[Test, Action(eval: 'new VerifyThat(fn() => class_exists("ReflectionEnum", false))')] + public function sortorder_is_an_enum() { + $this->assertTrue(XPClass::forName(SortOrder::class)->isEnum()); + } + #[Test] - public function thisIsNotAnEnum() { + public function this_is_not_an_enum() { $this->assertFalse(typeof($this)->isEnum()); } #[Test] - public function enumBaseClassIsAbstract() { + public function enum_base_class_is_abstract() { $this->assertAbstract(XPClass::forName(Enum::class)->getModifiers()); } #[Test] - public function operationEnumIsAbstract() { + public function operation_enum_is_abstract() { $this->assertAbstract(XPClass::forName(Operation::class)->getModifiers()); } #[Test] - public function coinEnumIsNotAbstract() { + public function coin_enum_is_not_abstract() { $this->assertNotAbstract(XPClass::forName(Coin::class)->getModifiers()); } #[Test] - public function coinMemberAreSameClass() { + public function coin_member_is_instance_of_coin() { $this->assertInstanceOf(Coin::class, Coin::$penny); } #[Test] - public function operationMembersAreSubclasses() { + public function operation_member_is_instance_of_operation() { $this->assertInstanceOf(Operation::class, Operation::$plus); } #[Test] - public function enumMembersAreNotAbstract() { + public function enum_members_are_not_abstract() { $this->assertNotAbstract(typeof(Coin::$penny)->getModifiers()); $this->assertNotAbstract(typeof(Operation::$plus)->getModifiers()); } #[Test] - public function coinValues() { + public function coin_values() { $this->assertEquals( [Coin::$penny, Coin::$nickel, Coin::$dime, Coin::$quarter], Coin::values() @@ -100,7 +106,7 @@ public function coinValues() { } #[Test] - public function operationValues() { + public function operation_values() { $this->assertEquals( [Operation::$plus, Operation::$minus, Operation::$times, Operation::$divided_by], Operation::values() @@ -108,37 +114,32 @@ public function operationValues() { } #[Test] - public function pennyCoinClass() { - $this->assertInstanceOf(Coin::class, Coin::$penny); - } - - #[Test] - public function nickelCoinName() { + public function nickel_coin_name() { $this->assertEquals('nickel', Coin::$nickel->name()); } #[Test] - public function nickelCoinValue() { + public function nickel_coin_value() { $this->assertEquals(2, Coin::$nickel->value()); } #[Test] - public function stringRepresentation() { + public function string_representation() { $this->assertEquals('dime', Coin::$dime->toString()); } #[Test] - public function sameCoinsAreEqual() { + public function same_coins_are_equal() { $this->assertEquals(Coin::$quarter, Coin::$quarter); } #[Test] - public function differentCoinsAreNotEqual() { + public function different_coins_are_not_equal() { $this->assertNotEquals(Coin::$penny, Coin::$quarter); } #[Test, Expect(CloneNotSupportedException::class)] - public function enumMembersAreNotCloneable() { + public function enum_members_cannot_be_cloned() { clone Coin::$penny; } @@ -158,18 +159,31 @@ public function valueOf_string() { ); } + #[Test, Action(eval: 'new VerifyThat(fn() => class_exists("ReflectionEnum", false))')] + public function valueOf_sortorder_enum() { + $this->assertEquals( + SortOrder::ASC, + Enum::valueOf(SortOrder::class, 'ASC') + ); + } + + #[Test, Expect(IllegalArgumentException::class), Action(eval: 'new VerifyThat(fn() => class_exists("ReflectionEnum", false))')] + public function valueOf_nonexistant_sortorder_enum() { + Enum::valueOf(SortOrder::class, 'ESC'); + } + #[Test, Expect(IllegalArgumentException::class)] - public function valueOfNonExistant() { + public function valueOf_nonexistant() { Enum::valueOf(XPClass::forName(Coin::class), '@@DOES_NOT_EXIST@@'); } #[Test, Expect(IllegalArgumentException::class)] - public function valueOfNonEnum() { + public function valueOf_non_enum() { Enum::valueOf(self::class, 'irrelevant'); } #[Test] - public function valueOfAbstractEnum() { + public function valueOf_abstract_enum() { $this->assertEquals( Operation::$plus, Enum::valueOf(XPClass::forName(Operation::class), 'plus') @@ -193,40 +207,48 @@ public function valuesOf_string() { } #[Test] - public function valuesOfAbstractEnum() { + public function valuesOf_abstract_enum() { $this->assertEquals( [Operation::$plus, Operation::$minus, Operation::$times, Operation::$divided_by], Enum::valuesOf(XPClass::forName(Operation::class)) ); } + #[Test, Action(eval: 'new VerifyThat(fn() => class_exists("ReflectionEnum", false))')] + public function valuesOf_sortorder_enum() { + $this->assertEquals( + [SortOrder::ASC, SortOrder::DESC], + Enum::valuesOf(XPClass::forName(SortOrder::class)) + ); + } + #[Test, Expect(IllegalArgumentException::class)] - public function valuesOfNonEnum() { + public function valuesOf_non_enum() { Enum::valuesOf(self::class); } #[Test] - public function plusOperation() { + public function plus_operation() { $this->assertEquals(2, Operation::$plus->evaluate(1, 1)); } #[Test] - public function minusOperation() { + public function minus_operation() { $this->assertEquals(0, Operation::$minus->evaluate(1, 1)); } #[Test] - public function timesOperation() { + public function times_operation() { $this->assertEquals(21, Operation::$times->evaluate(7, 3)); } #[Test] - public function dividedByOperation() { + public function dividedBy_operation() { $this->assertEquals(5, Operation::$divided_by->evaluate(10, 2)); } #[Test] - public function staticMemberNotInEnumValuesOf() { + public function static_member_not_in_enum_valuesOf() { $this->assertEquals( [Profiling::$INSTANCE, Profiling::$EXTENSION], Enum::valuesOf(XPClass::forName('net.xp_framework.unittest.core.Profiling')) @@ -234,7 +256,7 @@ public function staticMemberNotInEnumValuesOf() { } #[Test] - public function staticMemberNotInValues() { + public function static_member_not_in_values() { $this->assertEquals( [Profiling::$INSTANCE, Profiling::$EXTENSION], Profiling::values() @@ -242,12 +264,12 @@ public function staticMemberNotInValues() { } #[Test, Expect(IllegalArgumentException::class)] - public function staticMemberNotWithEnumValueOf() { + public function static_member_not_acceptable_in_valueOf() { Enum::valueOf(XPClass::forName('net.xp_framework.unittest.core.Profiling'), 'fixture'); } #[Test] - public function staticEnumMemberNotInEnumValuesOf() { + public function static_member_with_enum_type_not_in_enum_valuesOf() { Profiling::$fixture= Coin::$penny; $this->assertEquals( [Profiling::$INSTANCE, Profiling::$EXTENSION], @@ -257,7 +279,7 @@ public function staticEnumMemberNotInEnumValuesOf() { } #[Test] - public function staticEnumMemberNotInValues() { + public function static_member_with_enum_type_not_in_enum_values() { Profiling::$fixture= Coin::$penny; $this->assertEquals( [Profiling::$INSTANCE, Profiling::$EXTENSION], @@ -267,7 +289,7 @@ public function staticEnumMemberNotInValues() { } #[Test] - public function staticObjectMemberNotInEnumValuesOf() { + public function static_object_member_not_in_enum_valuesOf() { Profiling::$fixture= $this; $this->assertEquals( [Profiling::$INSTANCE, Profiling::$EXTENSION], @@ -277,7 +299,7 @@ public function staticObjectMemberNotInEnumValuesOf() { } #[Test] - public function staticObjectMemberNotInValues() { + public function static_object_member_not_in_enum_values() { Profiling::$fixture= $this; $this->assertEquals( [Profiling::$INSTANCE, Profiling::$EXTENSION], @@ -287,7 +309,7 @@ public function staticObjectMemberNotInValues() { } #[Test] - public function staticPrimitiveMemberNotInEnumValuesOf() { + public function static_primitive_member_not_in_enum_valuesOf() { Profiling::$fixture= [$this, $this->name]; $this->assertEquals( [Profiling::$INSTANCE, Profiling::$EXTENSION], @@ -297,7 +319,7 @@ public function staticPrimitiveMemberNotInEnumValuesOf() { } #[Test] - public function staticPrimitiveMemberNotInValues() { + public function static_primitive_member_not_in_enum_values() { Profiling::$fixture= [$this, $this->name]; $this->assertEquals( [Profiling::$INSTANCE, Profiling::$EXTENSION], @@ -307,7 +329,7 @@ public function staticPrimitiveMemberNotInValues() { } #[Test] - public function enumValuesMethodProvided() { + public function enum_values_method() { $this->assertEquals( [Weekday::$MON, Weekday::$TUE, Weekday::$WED, Weekday::$THU, Weekday::$FRI, Weekday::$SAT, Weekday::$SUN], Weekday::values() @@ -315,7 +337,12 @@ public function enumValuesMethodProvided() { } #[Test] - public function enumValueInitializedToDeclaration() { + public function enum_value_initialized_to_declaration() { $this->assertEquals(1, Weekday::$MON->ordinal()); } + + #[Test, Action(eval: 'new VerifyThat(fn() => class_exists("ReflectionEnum", false))')] + public function annotations_on_sortorder_enum() { + $this->assertEquals(['usedBy' => self::class], XPClass::forName(SortOrder::class)->getAnnotations()); + } } \ No newline at end of file diff --git a/src/test/php/net/xp_framework/unittest/core/SortOrder.class.php b/src/test/php/net/xp_framework/unittest/core/SortOrder.class.php new file mode 100755 index 0000000000..0e16063852 --- /dev/null +++ b/src/test/php/net/xp_framework/unittest/core/SortOrder.class.php @@ -0,0 +1,7 @@ +