Skip to content

Commit 11b507f

Browse files
authored
Implemented Rust options and regular enum usage with Psalm generics usage (#13)
* Implemented Rust options and regular enum usage * Integrated Psalm and code fixes * Improved README
1 parent 4a15f6a commit 11b507f

File tree

23 files changed

+444
-12
lines changed

23 files changed

+444
-12
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@ install:
1919
- chmod +x coverage/bin/phpcov
2020

2121
script:
22+
- php vendor/bin/psalm
2223
- phpdbg -qrr vendor/bin/phpunit --coverage-php coverage/cov/main.cov
2324

2425
- php examples/card_type.php
2526
- php examples/class_static_construct.php
2627
- php examples/day.php
2728
- php examples/flag.php
29+
- php examples/option.php
2830
- php examples/php-enum_comparision.php
2931
- php examples/planet.php
3032
- php examples/serialization_php74.php
33+
- php examples/shape.php
3134

3235
after_script:
3336
- curl -OL https://github.com/php-coveralls/php-coveralls/releases/download/v1.0.0/coveralls.phar

README.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,15 @@ foreach (Action::values() as $name => $action) {
154154
}
155155
```
156156

157+
More usage examples:
158+
* [Static constructor usage](./examples/class_static_construct.php)
159+
* [Option enum](./examples/day.php) similar to Rust enum
160+
* [Shape enum](./examples/shape.php) similar to Rust enum
161+
* [Serialization restriction](./examples/serialization_php74.php)
162+
* [Day enum](./examples/day.php)
163+
* [Flag enum](./examples/day.php)
164+
* [Planet enum](./examples/planet.php)
165+
157166
## Known issues
158167
### Readonly Properties
159168
In the current implementation, static property value can be occasionally replaced.
@@ -181,6 +190,14 @@ use Dbalabka\StaticConstructorLoader\StaticConstructorLoader;
181190
$composer = require_once(__DIR__ . '/vendor/autoload.php');
182191
$loader = new StaticConstructorLoader($composer);
183192
```
193+
Also, it would be very helpful to have expression based properties initialization:
194+
```php
195+
class Enum {
196+
// this is not allowed
197+
public static $FOO = new Enum();
198+
public static $BAR = new Enum();
199+
}
200+
```
184201
See [examples/class_static_construct.php](examples/class_static_construct.php) for example.
185202

186203
### Serialization
@@ -195,7 +212,7 @@ but it gives the possibility to control this in the class which holds the refere
195212
with [Serializable Interface](https://www.php.net/manual/en/class.serializable.php) in a similar way.
196213
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).
197214
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.
198-
So this problem somewhat resolved a the cost of a worse developer experience. Hope it will be solved in future RFCs.
215+
So this problem somewhat resolved the cost of a worse developer experience. Hope it will be solved in future RFCs.
199216
```php
200217
class SomeClass
201218
{
@@ -212,7 +229,36 @@ class SomeClass
212229
}
213230
}
214231
```
215-
See complete example in [examples/serialization_php74.php](examples/serialization_php74.php).
232+
See complete example in [examples/serialization_php74.php](examples/serialization_php74.php).
233+
234+
### Callable static properties syntax
235+
Unfortunately, you can not simply use static property as callable. There was a
236+
[syntax change](https://wiki.php.net/rfc/uniform_variable_syntax#backward_incompatible_changes) since PHP 7.0.
237+
```php
238+
// Instead of using syntax
239+
Option::$some('1'); // this line will rise an error "Function name must be a string"
240+
// you should use
241+
(Option::$some)('1');
242+
```
243+
It might be that using static variables isn't best option.
244+
245+
Probably the another option is to use magic calls with `__callStatic` but this variant suffers from missing autosuggestion,
246+
negative performance impact and missing static analysis that might be overcome by additional manually added annotations.
247+
```php
248+
Option::some('1');
249+
```
250+
251+
The best option is native PHP implementation. Unfortunately, it might be complicated task. It might seem that a quick solution it would be
252+
helpful to have late (in runtime) constants initialization or/and expression based class constants initialization:
253+
```php
254+
class Enum {
255+
// this is not allowed
256+
public const FOO = new Enum();
257+
public const BAR = new Enum();
258+
}
259+
```
260+
Still, calling `Enum::FOO()` will try to find a method instead of trying to treat constant's value as a callable.
261+
So we make a conclusion that enum syntax that use constants will not work but static properties will.
216262

217263
## Existing solutions
218264
Libraries:

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"require-dev": {
1616
"myclabs/php-enum": "^1.0",
1717
"phpunit/phpunit": "^7.5",
18-
"phpbench/phpbench": "^0.16.9"
18+
"phpbench/phpbench": "^0.16.9",
19+
"vimeo/psalm": "^3.5"
1920
},
2021
"autoload": {
2122
"psr-4": {

examples/Enum/CardType.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77

88
final class CardType extends Enumeration
99
{
10+
/** @var $this */
1011
public static $amex;
12+
/** @var $this */
1113
public static $visa;
14+
/** @var $this */
1215
public static $masterCard;
1316

1417
protected static function initializeValues() : void

examples/Enum/Circle.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Dbalabka\Enumeration\Examples\Enum;
5+
6+
use Dbalabka\Enumeration\Examples\Struct\Point;
7+
8+
class Circle extends Shape
9+
{
10+
private Point $point;
11+
private float $radius;
12+
13+
public function __invoke(Point $point, float $radius)
14+
{
15+
$circle = new static();
16+
$circle->point = $point;
17+
$circle->radius = $radius;
18+
return $circle;
19+
}
20+
}

examples/Enum/Color.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77

88
final class Color extends Enumeration
99
{
10+
/** @var $this */
1011
public static $red;
12+
/** @var $this */
1113
public static $green;
14+
/** @var $this */
1215
public static $blue;
1316

1417
private $value;

examples/Enum/Flag.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77

88
final class Flag extends Enumeration
99
{
10+
/** @var $this */
1011
public static $noState;
12+
/** @var $this */
1113
public static $ok;
14+
/** @var $this */
1215
public static $notOk;
16+
/** @var $this */
1317
public static $unavailable;
1418

19+
/** @var int */
1520
private $flagValue;
1621

1722
protected function __construct()

examples/Enum/Option.php

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Dbalabka\Enumeration\Examples\Enum;
5+
6+
use Dbalabka\Enumeration\Enumeration;
7+
8+
/**
9+
* Rust Option implementation
10+
*
11+
* @author Dmitry Balabka <dmitry.balabka@gmail.com>
12+
* @template T
13+
*/
14+
class Option extends Enumeration
15+
{
16+
/**
17+
* @psalm-var Option<mixed>
18+
*/
19+
public static $some;
20+
21+
/**
22+
* @psalm-var Option<null>
23+
*/
24+
public static $none;
25+
26+
/**
27+
* @psalm-var T
28+
*/
29+
private $value;
30+
31+
/**
32+
* @psalm-param T $value
33+
*/
34+
protected function __construct($value)
35+
{
36+
$this->value = $value;
37+
}
38+
39+
protected static function initializeValues(): void
40+
{
41+
self::$some = new class (null) extends Option { };
42+
self::$none = new class (null) extends Option { };
43+
}
44+
45+
/**
46+
* @psalm-return T
47+
*/
48+
public function unwrap()
49+
{
50+
if ($this->isSome()) {
51+
return $this->value;
52+
}
53+
throw new \Exception('Called `Option::unwrap()` on a `Option::$none` value');
54+
}
55+
56+
/**
57+
* @psalm-param T $default
58+
* @psalm-return T
59+
*/
60+
public function unwrapOr($default)
61+
{
62+
return $this->isSome() ? $this->value : $default;
63+
}
64+
65+
/**
66+
* @psalm-param callable():T $func
67+
* @psalm-return T
68+
*/
69+
public function unwrapOrElse(callable $func)
70+
{
71+
if ($this->isSome()) {
72+
return $this->value;
73+
}
74+
return $func();
75+
}
76+
77+
public function isSome():bool
78+
{
79+
return $this instanceof Option::$some;
80+
}
81+
82+
public function isNone():bool
83+
{
84+
return !$this->isSome();
85+
}
86+
87+
/**
88+
* @psalm-param T $x
89+
*/
90+
public function contains($x): bool
91+
{
92+
if ($this->isSome()) {
93+
return $this->value === $x;
94+
}
95+
return false;
96+
}
97+
98+
/**
99+
* @psalm-return T
100+
*/
101+
public function expect(string $message)
102+
{
103+
if ($this->isSome()) {
104+
return $this->value;
105+
}
106+
throw new \Exception($message);
107+
}
108+
109+
/**
110+
* @template G
111+
* @psalm-param G $value
112+
* @psalm-return Option<G>
113+
*/
114+
public function __invoke($value)
115+
{
116+
if ($this instanceof Option::$none) {
117+
throw new \Exception('Can not instantiate option of none');
118+
}
119+
return new static($value);
120+
}
121+
}

examples/Enum/Planet.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ public function surfaceGravity() : float
4545
return self::G * $this->mass / ($this->radius * $this->radius);
4646
}
4747

48-
public function surfaceWeight(float $otherMass) {
48+
public function surfaceWeight(float $otherMass) : float
49+
{
4950
return $otherMass * $this->surfaceGravity();
5051
}
5152
}

examples/Enum/Rectangle.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Dbalabka\Enumeration\Examples\Enum;
5+
6+
use Dbalabka\Enumeration\Examples\Struct\Point;
7+
8+
class Rectangle extends Shape
9+
{
10+
private Point $pointA;
11+
private Point $pointB;
12+
13+
public function __invoke(Point $pointA, Point $pointB)
14+
{
15+
$rectangle = new static();
16+
$rectangle->pointA = $pointA;
17+
$rectangle->pointB = $pointB;
18+
return $rectangle;
19+
}
20+
}

examples/Enum/Shape.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Dbalabka\Enumeration\Examples\Enum;
5+
6+
use Dbalabka\Enumeration\Enumeration;
7+
use Dbalabka\Enumeration\Examples\Struct\Point;
8+
9+
/**
10+
* Class Shape
11+
*
12+
* @author Dmitry Balabka <dmitry.balabka@gmail.com>
13+
*/
14+
abstract class Shape extends Enumeration
15+
{
16+
/** @var Circle */
17+
public static $circle;
18+
/** @var Rectangle */
19+
public static $rectangle;
20+
21+
protected static function initializeValues(): void
22+
{
23+
self::$circle = new Circle();
24+
self::$rectangle = new Rectangle();
25+
}
26+
}

examples/Struct/Point.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Dbalabka\Enumeration\Examples\Struct;
5+
6+
class Point
7+
{
8+
protected float $x;
9+
protected float $y;
10+
11+
public function __construct(float $x, float $y)
12+
{
13+
$this->x = $x;
14+
$this->y = $y;
15+
}
16+
}

examples/day.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public function __construct(Day $day) {
2020
$this->day = $day;
2121
}
2222

23-
public function tellItLikeItIs() {
23+
public function tellItLikeItIs(): void
24+
{
2425
switch ($this->day) {
2526
case Day::$monday:
2627
echo "Mondays are bad.\n";

examples/flag.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
assert(Flag::$ok < Flag::$unavailable);
1313
assert(Flag::$notOk < Flag::$unavailable);
1414

15-
set_error_handler(function ($errno, $errstr) {
15+
set_error_handler(static function (int $errno, string $errstr) {
1616
assert($errstr === sprintf('Object of class %s could not be converted to int', Flag::class));
1717
});
1818
// Operators overloading is not supported by PHP (see https://wiki.php.net/rfc/operator-overloading)

0 commit comments

Comments
 (0)