|
1 |
| -# php-enumeration |
2 |
| -Implementation of enumeration classes in PHP. The better alternative for enums |
| 1 | +# PHP Enumeration classes |
| 2 | +Implementation of [Enumeration Classes](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types) in PHP. |
| 3 | + |
| 4 | +In contrast to [existing solutions](#existing-solutions) this implementation avoid usage of [Magic methods](https://www.php.net/manual/en/language.oop5.magic.php) and |
| 5 | +[Reflection](https://www.php.net/manual/en/book.reflection.php) to provide better performance and code autocompletion. |
| 6 | +Also, we use static properties that can utilize the power of [Typed Properties](https://wiki.php.net/rfc/typed_properties_v2). |
| 7 | +The Enumeration Classes is much closer to other language implementations like [Java Enums](https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html) |
| 8 | +and [Python Enums](https://docs.python.org/3/library/enum.html). |
| 9 | + |
| 10 | + |
| 11 | +## Declaration |
| 12 | + |
| 13 | +Basic way to declare named Enumeration class: |
| 14 | +```php |
| 15 | +<?php |
| 16 | +use Dbalabka\Enumeration; |
| 17 | + |
| 18 | +final class Action extends Enumeration |
| 19 | +{ |
| 20 | + public static $view; |
| 21 | + public static $edit; |
| 22 | +} |
| 23 | +// to avoid manual initialization you can setup "vladimmi/construct-static" custom loader |
| 24 | +Action::initialize(); |
| 25 | +``` |
| 26 | + |
| 27 | +with Typed Properties support: |
| 28 | +```php |
| 29 | +<?php |
| 30 | +final class Day extends Enumeration |
| 31 | +{ |
| 32 | + public static Day $sunday; |
| 33 | + public static Day $monday; |
| 34 | + public static Day $tuesday; |
| 35 | + public static Day $wednesday; |
| 36 | + public static Day $thursday; |
| 37 | + public static Day $friday; |
| 38 | + public static Day $saturday; |
| 39 | +} |
| 40 | +Day::initialize(); |
| 41 | +``` |
| 42 | + |
| 43 | +By default enumeration class does not require the value to be provided. You can use constructor to set any types of values. |
| 44 | +1. Flag enum implementation example: |
| 45 | + ```php |
| 46 | + <?php |
| 47 | + final class Flag extends Enumeration |
| 48 | + { |
| 49 | + public static Flag $ok; |
| 50 | + public static Flag $notOk; |
| 51 | + public static Flag $unavailable; |
| 52 | + |
| 53 | + private int $flagValue; |
| 54 | + |
| 55 | + protected function __construct() |
| 56 | + { |
| 57 | + $this->flagValue = 1 << $this->ordinal(); |
| 58 | + } |
| 59 | + |
| 60 | + public function getFlagValue() : int |
| 61 | + { |
| 62 | + return $this->flagValue; |
| 63 | + } |
| 64 | + } |
| 65 | + ``` |
| 66 | +2. You should override `initializeValues()` method to set custom values for each Enum element. |
| 67 | + ```php |
| 68 | + <?php |
| 69 | + |
| 70 | + final class Planet extends Enumeration |
| 71 | + { |
| 72 | + public static Planet $mercury; |
| 73 | + public static Planet $venus; |
| 74 | + public static Planet $earth; |
| 75 | + public static Planet $mars; |
| 76 | + public static Planet $jupiter; |
| 77 | + public static Planet $saturn; |
| 78 | + public static Planet $uranus; |
| 79 | + public static Planet $neptune; |
| 80 | + |
| 81 | + private float $mass; // in kilograms |
| 82 | + private float $radius; // in meters |
| 83 | + |
| 84 | + // universal gravitational constant (m3 kg-1 s-2) |
| 85 | + private const G = 6.67300E-11; |
| 86 | + |
| 87 | + protected function __construct(float $mass, float $radius) |
| 88 | + { |
| 89 | + $this->mass = $mass; |
| 90 | + $this->radius = $radius; |
| 91 | + } |
| 92 | + |
| 93 | + protected static function initializeValues() : void |
| 94 | + { |
| 95 | + self::$mercury = new self(3.303e+23, 2.4397e6); |
| 96 | + self::$venus = new self(4.869e+24, 6.0518e6); |
| 97 | + self::$earth = new self(5.976e+24, 6.37814e6); |
| 98 | + self::$mars = new self(6.421e+23, 3.3972e6); |
| 99 | + self::$jupiter = new self(1.9e+27, 7.1492e7); |
| 100 | + self::$saturn = new self(5.688e+26, 6.0268e7); |
| 101 | + self::$uranus = new self(8.686e+25, 2.5559e7); |
| 102 | + self::$neptune = new self(1.024e+26, 2.4746e7); |
| 103 | + } |
| 104 | + |
| 105 | + public function surfaceGravity() : float |
| 106 | + { |
| 107 | + return self::G * $this->mass / ($this->radius * $this->radius); |
| 108 | + } |
| 109 | + |
| 110 | + public function surfaceWeight(float $otherMass) { |
| 111 | + return $otherMass * $this->surfaceGravity(); |
| 112 | + } |
| 113 | + } |
| 114 | + ``` |
| 115 | + |
| 116 | +Declaration rules that developer should follow: |
| 117 | +1. You should always declare the Enum resulting enum class as `final`. |
| 118 | + > ...Allowing subclassing of enums that define members would lead to a violation of some important invariants of types and instances. |
| 119 | + > On the other hand, it makes sense to allow sharing some common behavior between a group of enumerations... |
| 120 | + > (from [Python Enum documentation](https://docs.python.org/3/library/enum.html#restricted-enum-subclassing)) |
| 121 | +2. Constructor should always be declared as non-public (`private` or `protected`) to avoid unwanted class instantiation. |
| 122 | +3. Implementation is based on assumption that all class static properties are elements of Enum. If there is a need to declare |
| 123 | +any static property that isn't Enum element than you should override that `\Dbalabka\Enumeration::getStaticVars`. |
| 124 | +4. You must call `Dbalabka\Enumeration::initialize()` after each Enumeration class declaration or use |
| 125 | +[vladimmi/construct-static](https://github.com/vladimmi/construct-static) custom loader |
| 126 | + |
| 127 | +## Usage |
| 128 | +```php |
| 129 | +<?php |
| 130 | +$viewAction = Action::$view; |
| 131 | + |
| 132 | +// it is possible to compare Enum elements |
| 133 | +var_dump($viewAction === Action::$view); |
| 134 | + |
| 135 | +// you can get Enum element by name |
| 136 | +$editAction = Action::valueOf('edit'); |
| 137 | + |
| 138 | +// iterate over all Enum elements |
| 139 | +foreach (Action::values() as $name => $action) { |
| 140 | + echo $action; |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +## Known issues |
| 145 | +### Readonly Properties |
| 146 | +In the current implementation, static property value might be occasionally replaced. |
| 147 | +[Readonly Properties](https://wiki.php.net/rfc/readonly_properties) is aimed to solve this issue. |
| 148 | +In ideal world Enum values should be declared as a constants. Unfortunately, it is not possible in PHP right now. |
| 149 | +```php |
| 150 | +<?php |
| 151 | +// It is possible but don't do it |
| 152 | +Action::$view = Action::$edit; |
| 153 | +// Following isn't possible in PHP 7.4 with declared properties types |
| 154 | +Action::$view = null; |
| 155 | +``` |
| 156 | + |
| 157 | +### Class static initialization |
| 158 | +Implementation rely on class static initialization which was proposed in [Static Class Constructor](https://wiki.php.net/rfc/static_class_constructor). |
| 159 | +RFC describes the possible workarounds. Simplest is to call initialization method right after class declaration, |
| 160 | +but it requires keep this in mind. Thanks to [Typed Properties](https://wiki.php.net/rfc/typed_properties_v2) |
| 161 | +we can control not initialized properties - PHP will throw and error in case of access to not initialized property. |
| 162 | +It might be automated with custom autoloader implemented in [vladimmi/construct-static](https://github.com/vladimmi/construct-static) library. |
| 163 | +```php |
| 164 | +<?php |
| 165 | +// You should always call initialize() method right after class declaration |
| 166 | +// To avoid manual initialization you can setup "vladimmi/construct-static" custom loader |
| 167 | +Action::initialize(); |
| 168 | +``` |
| 169 | +See [examples/class_static_construct.php](examples/class_static_construct.php) for example to overcome this limitation. |
| 170 | + |
| 171 | +### Serialization |
| 172 | +There no possibility to serialize the singleton. As a result, we have to restrict direct Enum object serialization. |
| 173 | +[New custom object serialization mechanism](https://wiki.php.net/rfc/custom_object_serialization) does not help with direct Enum serialization |
| 174 | +but it give the possibility to control this in class which hold the reference to Enums instances. Also, it can be workaround |
| 175 | +with [Serializable Interface](https://www.php.net/manual/en/class.serializable.php) in similar way. So this problem somehow |
| 176 | +solves with worse developer experience. [TODO: clarify] Probably, [similar to Java Enums](https://stackoverflow.com/questions/15521309/is-custom-enum-serializable-too) |
| 177 | +the PHP Enums should not be serializable at all. The only way to serialize the Enum is to obtain the name of Enum constant |
| 178 | +and use valueOf() method to obtain the Enum constant. |
| 179 | +```php |
| 180 | +<?php |
| 181 | +// Following line will throw an exception |
| 182 | +serialize(Action::$view); |
| 183 | +``` |
| 184 | +See [examples/serialization_php74.php](examples/serialization_php74.php) to overcome this limitation. |
| 185 | +It is possible to submit RFC to implement singleton serialization in PHP. For example [Java Enums](https://docs.oracle.com/javase/7/docs/api/java/lang/Enum.html) |
| 186 | +implements Serializable interface and replace class instance during unserialization in [readResolve()](https://docs.oracle.com/javase/7/docs/api/java/io/Serializable.html) method. |
| 187 | + |
| 188 | +## Existing solutions |
| 189 | +In contrast to existing solutions and RFCs like |
| 190 | +* https://github.com/myclabs/php-enum |
| 191 | +* https://github.com/marc-mabe/php-enum |
| 192 | +* https://www.php.net/manual/en/class.splenum.php |
| 193 | +* [PHP RFC: Enumerated Types](https://wiki.php.net/rfc/enum) |
| 194 | + |
| 195 | +(there a lot of [other PHP implementations](https://packagist.org/search/?query=php-enum)) |
| 196 | + |
| 197 | +## References |
| 198 | +* [Enum Types](https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html) - The Java™ Tutorials |
| 199 | +* [Enum — Support for enumerations](https://docs.python.org/3/library/enum.html) - The Python Standard Library |
| 200 | + * [Class Enum<E extends Enum<E>>](https://docs.oracle.com/javase/7/docs/api/java/lang/Enum.html) - Java™ Platform, Standard Edition 7 API Specification |
| 201 | +* [Use enumeration classes instead of enum types](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types) - .NET microservices - Architecture e-book |
| 202 | + * [Enumeration Class implementation](https://github.com/dotnet-architecture/eShopOnContainers/blob/8960db40d43d79ad475799dedfe311ebc49cab99/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs) - Microservices Architecture and Containers based Reference Application |
0 commit comments