Typ wyliczeniowy – SplEnum i alternatywy

We wpisie Magiczne „cyferki” pisałem o używaniu stałych zamiast magicznych cyfr, które dla osoby nie znającej szczegółów projektu nic nie znaczą. Stałe zamiast cyfr są postępem, jednak pozostaje problem poprawności danych. Na przykład wartość 1 może mieć status obiektu firmy, osoby i wielu innych obiektów w aplikacji. W dodatku dla każdego obiektu może on oznaczać coś zupełnie innego. Z drugiej strony przy większej liczbie statusów w jednym obiekcie i mniejszej w innych można przez pomyłkę przypisać status innego obiektu, który jest poza zakresem danego obiektu.

Krótko mówiąc pozostaje problem ograniczenia możliwości ustawienia statusu tylko z określonego zakresu/typu. Typ jest tutaj istotną wskazówką – m.in. w Javie są tytułowe typy wyliczeniowe, które pozawalają zdefiniować nowy typ, dla określonego zbioru wartości, a język poprzez typowanie parametrów metody przypilnuje, żeby ustawiać statusy tylko dozwolonego typu.

W PHP również jest typowanie parametrów (tylko dla typów złożonych) i jak się okazuje w bibliotece SPL znajduje się nawet klasa dla typów wyliczeniowych – SplEnum. Jej budowa jest dość prosta a przykłady z manuala klarowne. Jest jednak jeden problem – nie jest standardowym elementem języka i wymaga doinstalowania rozszerzenia PECL na serwerze. Dla posiadaczy serwerów dedykowanych/VPS nie stanowi to problemu, jednak przy uruchamianiu aplikacji na hostingu współdzielonym zainstalowanie tego rozszerzenia zależy od administratora.

Jak wspominałem trochę wyżej budowa klasy jest dość prosta, więc zamiast zastanawiać się nad dostępnością/możliwością instalacji rozszerzenia na serwerze można napisać odpowiednik tej klasy samemu, poniżej przykładowa implementacja.

<?php

/**
 * My implementation of alternative SplEnum 
 * @link http://php.net/manual/en/class.splenum.php
 */
class MyEnum 
{
    /**
     * Value of object
     * @var mixed
     */
    private $value;
    
    /**
     * Name of const with default value
     * @var string
     */
    private $defaultName = '__default';
    
    /**
     * Reflection object
     * @var ReflectionClass
     */
    private $reflection;

    /**
     * Create object
     * @param mixed $initialValue
     * @throws UnexpectedValueException
     */
    public function __construct($initialValue = null)
    {
        if ($initialValue && in_array($initialValue, $this->getConstList())) {
            $this->value = $initialValue;
        } elseif (!$initialValue && $this->getDefault()) {
            $this->value = $this->getDefault();
        } else {
            throw new UnexpectedValueException('Value not a const in enum '. get_class($this));
        }
    }
    
    /**
     * Get array of class consts
     * @param boolean $includeDefault
     * @return array
     */
    public function getConstList($includeDefault = false)
    {
        $tmp = $this->getReflection()->getConstants();
        if (!$includeDefault && array_key_exists($this->defaultName, $tmp)) {
            unset($tmp[$this->defaultName]);
        }
        return $tmp;
    }
    
    /**
     * Get value of object
     * @return mixed
     */
    public function getValue()
    {
        return $this->value;
    }
    
    public function __toString()
    {
        return (string) $this->getValue();
    }
    
    /**
     * Get default value from class
     * @return mixed
     */
    protected function getDefault()
    {
        return $this->getReflection()->getConstant($this->defaultName);
    }

    /**
     * Get instance of class reflection
     * @return ReflectionClass
     */
    protected function getReflection()
    {
        if (!$this->reflection) {
            $this->reflection = new ReflectionClass($this);
        }
        
        return $this->reflection;
    }
}

Myślę, że kod jest czytelny i jasny. Dla przypomnienia odczytanie stałych zadeklarowanych w klasie jest realizowane z wykorzystaniem mechanizmu refleksji, który pozwala na analizowanie struktury/budowy obiektu w trakcie wykonania kodu.

Teraz bardzo prosty kod z przykładem użycia:

<?php
function processOption(Opcje $opcja)
{
    echo($opcja);
}

class Opcje extends MyEnum
{
    const __default = 2;
    
    const OPCJA1 = 1;
    const OPCJA2 = 2;
}

$obj = new Opcje();
processOption($obj);

Powyżej znajduje się definicja funkcji, która przyjmuje parametr typu Opcje i definicja tej klasy. Klasa ogranicza się jedynie do dziedziczenia po MyEnum i deklaracji stałych, cała funkcjonalność jest dziedziczona. Przykładowy kod wyświetli tylko 2, ponieważ taka jest wartość domyślna typu wyliczeniowego i typ zgadza się z oczekiwanym typem parametru funkcji.

W przypadku próby utworzenia obiektu z wartością spoza zdefiniowanych stałych zostanie rzucony wyjątek UnexpectedValueException, natomiast przy próbie przekazania do funkcji parametru innego typu:

<?php
processOption(2);
// wynik: Catchable fatal error: Argument 1 passed to processOption() 
// must be an instance of Opcje, integer given, called in...

Wygląda na to, że problem jest rozwiązany – nie można pomyłkowo przypisać innej wartości i tego nie zauważyć.

Dodatkowo, na polskiej planecie PHP pojawił się jakiś czas temu wpis z pomysłem implementacji typów wyliczeniowych w oparciu o koncepcję z Javy – odsyłam do tego wpisu (autorem jest Sebastian Malaca). Warto również przyjrzeć się komentarzom pod tym wpisem, ponieważ można tam znaleść kolejną/bardziej rozbudowaną alternatywę dla SplEnum.

Dodaj komentarz