marc-mabe / php-enum

Simple and fast implementation of enumerations with native PHP
BSD 3-Clause "New" or "Revised" License
464 stars 36 forks source link

Rethink about enum inheritance #36

Closed marc-mabe closed 9 years ago

marc-mabe commented 10 years ago

http://stackoverflow.com/questions/1414755/java-extend-enum https://blogs.oracle.com/darcy/entry/enums_and_mixins

In Java it's not possible to extend an enum because a class expecting a specialized enum type doesn't know about extended values.

From an internal development point of view it would be more simple to allow only enumerations extending the base class directly because no class name handling (called class) will be required.

On the side enumeration classes can have additional functionalities and are not limited to value definitions alone. In such cases it could make sense to add or overwrite functionalities and extending would be the way to go.

0xPaul commented 10 years ago

http://php.net/manual/en/language.oop5.final.php

marc-mabe commented 10 years ago

@0xPaul the final keyword doesn't help in this case because the enumeration class itself doesn't know the implementator. For example an HttpMethodEnum defines standard HTTP methods like GET, POST ... and it will be extended by WebDavMethodEnum. This will be very valid but a HTTP client expecting HttpMethodEnum does or does not handle WebDavMethodEnum. The client can type-hint HttpMethodEnum but then all extended enumerations will pass. Using the currently recommended way to check via HttpMethodEnum::get($enum) the client makes sure to not get an extended version but that breaks OOP inheritance - e.g. you can't overwrite __toString method.

Another issue with inheritance is the single instance per value rule. If you have an extended version like the one described above the following produces WTF moments:

$get1 = HttpMethodEnum::get('GET');
$get2 = WebDavMethodEnum::get('GET');
var_dump($get1 === $get2);  // false
var_dump($get1->is($get2)); // true
var_dump($get2->is($get1)); // false
marc-mabe commented 10 years ago

The third comparison $get2->is($get1) could be solved by changing the following:

final public function is($enum)
{
    return $this->value === $enum || ($enum instanceof static && $this->value === $enum->getValue());
}
// to
final public function is($enum)
{
    return $this->value === $enum || (($enum instanceof static || $this instanceof $enum) && $this->value === $enum->getValue());
}
marc-mabe commented 9 years ago

I have decided to basically allow inheritance but this library will handle child enumerations as a different type.

Example:

class EnumA extends Enum {
    const A = 'A';
}

class EnumB extends EnumA {
    const B = 'B';
}

$aa = EnumA::A();
$ba = EnumB::A();

$aa === $ba; // false -> different instances
$aa->is($ba); // false -> different enumerations
$ba->is($aa); // false -> different enumerations
$aa->is($ba->getValue()); // true -> Because the enumerator of type EnumA will be checked against a matching scalar value
$ba->is($aa->getValue()); // true -> Because the enumerator of type EnumB will be checked against a matching scalar value

EnumA::get($ba); // -> InvalidArgumentException
EnumB::get($aa); // -> InvalidArgumentException
EnumA::get($ba->getValue()); // -> returns same as $aa
EnumB::get($aa->getValue()); // -> returns same as $ba

By sure inheritance works the same as for others. So type-hints will allow inherited enumeration types as well and properties of an enumeration will be inherited by well known rules. The developer has to be in mind of it.

PS: I'll create own issues to match this rules.