diff --git a/.travis.yml b/.travis.yml index 5f25a67..a140fcb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,15 +19,18 @@ install: - chmod +x coverage/bin/phpcov script: + - php vendor/bin/psalm - phpdbg -qrr vendor/bin/phpunit --coverage-php coverage/cov/main.cov - php examples/card_type.php - php examples/class_static_construct.php - php examples/day.php - php examples/flag.php + - php examples/option.php - php examples/php-enum_comparision.php - php examples/planet.php - php examples/serialization_php74.php + - php examples/shape.php after_script: - curl -OL https://github.com/php-coveralls/php-coveralls/releases/download/v1.0.0/coveralls.phar diff --git a/README.md b/README.md index f44d94c..c6b4249 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,15 @@ foreach (Action::values() as $name => $action) { } ``` +More usage examples: +* [Static constructor usage](./examples/class_static_construct.php) +* [Option enum](./examples/day.php) similar to Rust enum +* [Shape enum](./examples/shape.php) similar to Rust enum +* [Serialization restriction](./examples/serialization_php74.php) +* [Day enum](./examples/day.php) +* [Flag enum](./examples/day.php) +* [Planet enum](./examples/planet.php) + ## Known issues ### Readonly Properties In the current implementation, static property value can be occasionally replaced. @@ -181,6 +190,14 @@ use Dbalabka\StaticConstructorLoader\StaticConstructorLoader; $composer = require_once(__DIR__ . '/vendor/autoload.php'); $loader = new StaticConstructorLoader($composer); ``` +Also, it would be very helpful to have expression based properties initialization: +```php +class Enum { + // this is not allowed + public static $FOO = new Enum(); + public static $BAR = new Enum(); +} +``` See [examples/class_static_construct.php](examples/class_static_construct.php) for example. ### Serialization @@ -195,7 +212,7 @@ but it gives the possibility to control this in the class which holds the refere with [Serializable Interface](https://www.php.net/manual/en/class.serializable.php) in a similar way. For example, Java [handles](https://stackoverflow.com/questions/15521309/is-custom-enum-serializable-too/15522276#15522276) Enums serialization differently than other classes, but you can serialize it directly thanks to [readResolve()](https://docs.oracle.com/javase/7/docs/api/java/io/Serializable.html). In PHP, we can't serialize Enums directly, but we can handle Enums serialization in class that holds the reference. We can serialize the name of the Enum constant and use `valueOf()` method to obtain the Enum constant value during unserialization. -So this problem somewhat resolved a the cost of a worse developer experience. Hope it will be solved in future RFCs. +So this problem somewhat resolved the cost of a worse developer experience. Hope it will be solved in future RFCs. ```php class SomeClass { @@ -212,7 +229,36 @@ class SomeClass } } ``` -See complete example in [examples/serialization_php74.php](examples/serialization_php74.php). +See complete example in [examples/serialization_php74.php](examples/serialization_php74.php). + +### Callable static properties syntax +Unfortunately, you can not simply use static property as callable. There was a +[syntax change](https://wiki.php.net/rfc/uniform_variable_syntax#backward_incompatible_changes) since PHP 7.0. +```php +// Instead of using syntax +Option::$some('1'); // this line will rise an error "Function name must be a string" +// you should use +(Option::$some)('1'); +``` +It might be that using static variables isn't best option. + +Probably the another option is to use magic calls with `__callStatic` but this variant suffers from missing autosuggestion, +negative performance impact and missing static analysis that might be overcome by additional manually added annotations. +```php +Option::some('1'); +``` + +The best option is native PHP implementation. Unfortunately, it might be complicated task. It might seem that a quick solution it would be +helpful to have late (in runtime) constants initialization or/and expression based class constants initialization: +```php +class Enum { + // this is not allowed + public const FOO = new Enum(); + public const BAR = new Enum(); +} +``` +Still, calling `Enum::FOO()` will try to find a method instead of trying to treat constant's value as a callable. +So we make a conclusion that enum syntax that use constants will not work but static properties will. ## Existing solutions Libraries: diff --git a/composer.json b/composer.json index 10a69fc..b00f6e4 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "require-dev": { "myclabs/php-enum": "^1.0", "phpunit/phpunit": "^7.5", - "phpbench/phpbench": "^0.16.9" + "phpbench/phpbench": "^0.16.9", + "vimeo/psalm": "^3.5" }, "autoload": { "psr-4": { diff --git a/examples/Enum/CardType.php b/examples/Enum/CardType.php index a2e3eee..58f475d 100644 --- a/examples/Enum/CardType.php +++ b/examples/Enum/CardType.php @@ -7,8 +7,11 @@ final class CardType extends Enumeration { + /** @var $this */ public static $amex; + /** @var $this */ public static $visa; + /** @var $this */ public static $masterCard; protected static function initializeValues() : void diff --git a/examples/Enum/Circle.php b/examples/Enum/Circle.php new file mode 100644 index 0000000..026e231 --- /dev/null +++ b/examples/Enum/Circle.php @@ -0,0 +1,20 @@ +point = $point; + $circle->radius = $radius; + return $circle; + } +} diff --git a/examples/Enum/Color.php b/examples/Enum/Color.php index 74c61d2..c91d4e3 100644 --- a/examples/Enum/Color.php +++ b/examples/Enum/Color.php @@ -7,8 +7,11 @@ final class Color extends Enumeration { + /** @var $this */ public static $red; + /** @var $this */ public static $green; + /** @var $this */ public static $blue; private $value; diff --git a/examples/Enum/Flag.php b/examples/Enum/Flag.php index ba842a7..97c41e6 100644 --- a/examples/Enum/Flag.php +++ b/examples/Enum/Flag.php @@ -7,11 +7,16 @@ final class Flag extends Enumeration { + /** @var $this */ public static $noState; + /** @var $this */ public static $ok; + /** @var $this */ public static $notOk; + /** @var $this */ public static $unavailable; + /** @var int */ private $flagValue; protected function __construct() diff --git a/examples/Enum/Option.php b/examples/Enum/Option.php new file mode 100644 index 0000000..789b0b4 --- /dev/null +++ b/examples/Enum/Option.php @@ -0,0 +1,121 @@ + + * @template T + */ +class Option extends Enumeration +{ + /** + * @psalm-var Option + */ + public static $some; + + /** + * @psalm-var Option + */ + public static $none; + + /** + * @psalm-var T + */ + private $value; + + /** + * @psalm-param T $value + */ + protected function __construct($value) + { + $this->value = $value; + } + + protected static function initializeValues(): void + { + self::$some = new class (null) extends Option { }; + self::$none = new class (null) extends Option { }; + } + + /** + * @psalm-return T + */ + public function unwrap() + { + if ($this->isSome()) { + return $this->value; + } + throw new \Exception('Called `Option::unwrap()` on a `Option::$none` value'); + } + + /** + * @psalm-param T $default + * @psalm-return T + */ + public function unwrapOr($default) + { + return $this->isSome() ? $this->value : $default; + } + + /** + * @psalm-param callable():T $func + * @psalm-return T + */ + public function unwrapOrElse(callable $func) + { + if ($this->isSome()) { + return $this->value; + } + return $func(); + } + + public function isSome():bool + { + return $this instanceof Option::$some; + } + + public function isNone():bool + { + return !$this->isSome(); + } + + /** + * @psalm-param T $x + */ + public function contains($x): bool + { + if ($this->isSome()) { + return $this->value === $x; + } + return false; + } + + /** + * @psalm-return T + */ + public function expect(string $message) + { + if ($this->isSome()) { + return $this->value; + } + throw new \Exception($message); + } + + /** + * @template G + * @psalm-param G $value + * @psalm-return Option + */ + public function __invoke($value) + { + if ($this instanceof Option::$none) { + throw new \Exception('Can not instantiate option of none'); + } + return new static($value); + } +} diff --git a/examples/Enum/Planet.php b/examples/Enum/Planet.php index eb9752f..0357dfd 100644 --- a/examples/Enum/Planet.php +++ b/examples/Enum/Planet.php @@ -45,7 +45,8 @@ public function surfaceGravity() : float return self::G * $this->mass / ($this->radius * $this->radius); } - public function surfaceWeight(float $otherMass) { + public function surfaceWeight(float $otherMass) : float + { return $otherMass * $this->surfaceGravity(); } } diff --git a/examples/Enum/Rectangle.php b/examples/Enum/Rectangle.php new file mode 100644 index 0000000..38b7dee --- /dev/null +++ b/examples/Enum/Rectangle.php @@ -0,0 +1,20 @@ +pointA = $pointA; + $rectangle->pointB = $pointB; + return $rectangle; + } +} diff --git a/examples/Enum/Shape.php b/examples/Enum/Shape.php new file mode 100644 index 0000000..90f16b7 --- /dev/null +++ b/examples/Enum/Shape.php @@ -0,0 +1,26 @@ + + */ +abstract class Shape extends Enumeration +{ + /** @var Circle */ + public static $circle; + /** @var Rectangle */ + public static $rectangle; + + protected static function initializeValues(): void + { + self::$circle = new Circle(); + self::$rectangle = new Rectangle(); + } +} diff --git a/examples/Struct/Point.php b/examples/Struct/Point.php new file mode 100644 index 0000000..15a3cee --- /dev/null +++ b/examples/Struct/Point.php @@ -0,0 +1,16 @@ +x = $x; + $this->y = $y; + } +} diff --git a/examples/day.php b/examples/day.php index a5b425f..dbdca3b 100644 --- a/examples/day.php +++ b/examples/day.php @@ -20,7 +20,8 @@ public function __construct(Day $day) { $this->day = $day; } - public function tellItLikeItIs() { + public function tellItLikeItIs(): void + { switch ($this->day) { case Day::$monday: echo "Mondays are bad.\n"; diff --git a/examples/flag.php b/examples/flag.php index 1ff8f28..5d2cf5a 100644 --- a/examples/flag.php +++ b/examples/flag.php @@ -12,7 +12,7 @@ assert(Flag::$ok < Flag::$unavailable); assert(Flag::$notOk < Flag::$unavailable); -set_error_handler(function ($errno, $errstr) { +set_error_handler(static function (int $errno, string $errstr) { assert($errstr === sprintf('Object of class %s could not be converted to int', Flag::class)); }); // Operators overloading is not supported by PHP (see https://wiki.php.net/rfc/operator-overloading) diff --git a/examples/option.php b/examples/option.php new file mode 100644 index 0000000..2864ee0 --- /dev/null +++ b/examples/option.php @@ -0,0 +1,55 @@ += 7.4', E_USER_NOTICE); + return; +} + +$composer = require_once(__DIR__ . '/../vendor/autoload.php'); +$loader = new StaticConstructorLoader($composer); + +/** + * @template T + * @psalm-param bool $returnResult + * @psalm-param T $value + * @psalm-return Option|Option + */ +function getResult(bool $returnResult, $value) +{ + if ($returnResult) { + return (Option::$some)($value); + } + return Option::$none; +} + +/** + * @psalm-param Option|Option $option + */ +function printResult(Option $option) : void +{ + if ($option instanceof Option::$some) { + /** + * @psalm-suppress PossiblyNullOperand psalm can not properly determine that it is a Some and use int|null type. + * We need to write custom type inference from $option if it is instance of Option::$some and same for Option::$none + */ + echo 'Return some value = ' . ($option->unwrap() + 1) . PHP_EOL; + } elseif ($option instanceof Option::$none) { + echo 'Return none' . PHP_EOL; + } else { + throw new Exception('Can not determine the result type'); + } +} + +$option1 = getResult(true, '1'); +/** @psalm-suppress PossiblyInvalidArgument Psalm correctly catches incorrect passed type */ +printResult($option1); +$option1 = getResult(true, 1); +printResult($option1); +$option2 = getResult(false, 1); +printResult($option2); + + diff --git a/examples/planet.php b/examples/planet.php index 9ecd08e..afdfabc 100644 --- a/examples/planet.php +++ b/examples/planet.php @@ -15,5 +15,5 @@ $earthWeight = 175; $mass = $earthWeight / Planet::$earth->surfaceGravity(); foreach (Planet::values() as $p) { - printf("Your weight on %s is %s\n", $p, $p->surfaceWeight($mass)); + printf("Your weight on %s is %s\n", (string) $p, $p->surfaceWeight($mass)); } diff --git a/examples/shape.php b/examples/shape.php new file mode 100644 index 0000000..fc80a85 --- /dev/null +++ b/examples/shape.php @@ -0,0 +1,33 @@ += 7.4', E_USER_NOTICE); + return; +} + +$composer = require_once(__DIR__ . '/../vendor/autoload.php'); +$loader = new StaticConstructorLoader($composer); + +try { + // Unfortunately, this will not be possible because of https://www.php.net/manual/en/migration70.incompatible.php#migration70.incompatible.variable-handling.indirect + /** @psalm-suppress UndefinedGlobalVariable */ + $circ1 = Shape::$circle(new Point(1.0, 1.0), 5.0); + assert(false); +} catch (Error $e) { + assert(true); +} +$circ1 = (Shape::$circle)(new Point(1.0, 1.0), 5.0); +$rect1 = (Shape::$rectangle)(new Point(1.0, 1.0), new Point(2.0, 2.0)); +assert($circ1 instanceof Shape); +assert($rect1 instanceof Shape); +assert($circ1 !== $rect1); +assert($circ1 != $rect1); +assert($circ1 === $circ1); +assert($circ1 == $circ1); +assert($circ1 !== Shape::$circle); +assert($circ1 != Shape::$circle); diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..2e8d576 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Enumeration/Enumeration.php b/src/Enumeration/Enumeration.php index 0371157..b8e2d06 100644 --- a/src/Enumeration/Enumeration.php +++ b/src/Enumeration/Enumeration.php @@ -24,10 +24,11 @@ abstract class Enumeration implements StaticConstructorInterface, Serializable const INITIAL_ORDINAL = 0; /** - * @var int + * @var int|null */ protected $ordinal; + /** @var array */ private static $initializedEnums = []; final public static function __constructStatic() : void @@ -149,7 +150,17 @@ public function __toString() : string final public function name() : string { - return array_search($this, static::values(), true); + $name = array_search($this, static::values(), true); + if (false === $name) { + throw new EnumerationException( + sprintf( + 'Can not find $this in $s::values(). ' . + 'It seems that the static property was overwritten. This is not allowed.', + get_class($this) + ) + ); + } + return $name; } final public function __clone() diff --git a/src/StaticConstructorLoader/StaticConstructorInterface.php b/src/StaticConstructorLoader/StaticConstructorInterface.php index 2f12960..aa31047 100644 --- a/src/StaticConstructorLoader/StaticConstructorInterface.php +++ b/src/StaticConstructorLoader/StaticConstructorInterface.php @@ -13,5 +13,5 @@ interface StaticConstructorInterface * Unfortunately, PHP does not support static initialization. * See static init RFC: https://wiki.php.net/rfc/static_class_constructor */ - public static function __constructStatic(); + public static function __constructStatic() : void; } diff --git a/src/StaticConstructorLoader/StaticConstructorLoader.php b/src/StaticConstructorLoader/StaticConstructorLoader.php index 9738d04..33ad1f3 100644 --- a/src/StaticConstructorLoader/StaticConstructorLoader.php +++ b/src/StaticConstructorLoader/StaticConstructorLoader.php @@ -53,7 +53,7 @@ public function __construct(ClassLoader $classLoader) array_map('spl_autoload_register', $loadersToRestore, $flagTrue, $flagTrue); } - public function loadClass($className) + public function loadClass(string $className): ?bool { $result = $this->classLoader->loadClass($className); if ($result === true && $className !== StaticConstructorInterface::class && is_a($className, StaticConstructorInterface::class, true)) { diff --git a/tests/Enumeration/EnumerationTest.php b/tests/Enumeration/EnumerationTest.php index 87f20e4..5b633e0 100644 --- a/tests/Enumeration/EnumerationTest.php +++ b/tests/Enumeration/EnumerationTest.php @@ -191,4 +191,15 @@ public function testCustomStaticProperties() $this->expectException(InvalidArgumentException::class); ActionWithCustomStaticProperty::valueOf('customProperty'); } + + public function testNameWhenIncorrectlyInitilizedProperies() + { + Flag::initialize(); + + $notOk = Flag::$notOk; + Flag::$notOk = Flag::$noState; + + $this->expectException(EnumerationException::class); + $notOk->name(); + } } diff --git a/tests/StaticConstructorLoader/Fixtures/Action.php b/tests/StaticConstructorLoader/Fixtures/Action.php index 83cc2bb..5cf0c43 100644 --- a/tests/StaticConstructorLoader/Fixtures/Action.php +++ b/tests/StaticConstructorLoader/Fixtures/Action.php @@ -10,7 +10,7 @@ class Action implements StaticConstructorInterface { public static $instance; - public static function __constructStatic() + public static function __constructStatic() : void { static::$instance = new static(); }