Skip to content

Implemented Rust options and regular enum usage with Psalm generics usage #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
{
Expand All @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
3 changes: 3 additions & 0 deletions examples/Enum/CardType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions examples/Enum/Circle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);

namespace Dbalabka\Enumeration\Examples\Enum;

use Dbalabka\Enumeration\Examples\Struct\Point;

class Circle extends Shape
{
private Point $point;
private float $radius;

public function __invoke(Point $point, float $radius)
{
$circle = new static();
$circle->point = $point;
$circle->radius = $radius;
return $circle;
}
}
3 changes: 3 additions & 0 deletions examples/Enum/Color.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions examples/Enum/Flag.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
121 changes: 121 additions & 0 deletions examples/Enum/Option.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);

namespace Dbalabka\Enumeration\Examples\Enum;

use Dbalabka\Enumeration\Enumeration;

/**
* Rust Option implementation
*
* @author Dmitry Balabka <dmitry.balabka@gmail.com>
* @template T
*/
class Option extends Enumeration
{
/**
* @psalm-var Option<mixed>
*/
public static $some;

/**
* @psalm-var Option<null>
*/
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<G>
*/
public function __invoke($value)
{
if ($this instanceof Option::$none) {
throw new \Exception('Can not instantiate option of none');
}
return new static($value);
}
}
3 changes: 2 additions & 1 deletion examples/Enum/Planet.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
20 changes: 20 additions & 0 deletions examples/Enum/Rectangle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);

namespace Dbalabka\Enumeration\Examples\Enum;

use Dbalabka\Enumeration\Examples\Struct\Point;

class Rectangle extends Shape
{
private Point $pointA;
private Point $pointB;

public function __invoke(Point $pointA, Point $pointB)
{
$rectangle = new static();
$rectangle->pointA = $pointA;
$rectangle->pointB = $pointB;
return $rectangle;
}
}
26 changes: 26 additions & 0 deletions examples/Enum/Shape.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);

namespace Dbalabka\Enumeration\Examples\Enum;

use Dbalabka\Enumeration\Enumeration;
use Dbalabka\Enumeration\Examples\Struct\Point;

/**
* Class Shape
*
* @author Dmitry Balabka <dmitry.balabka@gmail.com>
*/
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();
}
}
16 changes: 16 additions & 0 deletions examples/Struct/Point.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);

namespace Dbalabka\Enumeration\Examples\Struct;

class Point
{
protected float $x;
protected float $y;

public function __construct(float $x, float $y)
{
$this->x = $x;
$this->y = $y;
}
}
3 changes: 2 additions & 1 deletion examples/day.php
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion examples/flag.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading