W programowaniu bardzo często można trafić na sytuację, w której jakiś obiekt może istnieć w wielu stanach. W różnych stanach mogą być np. umowy (szkic, zaakceptowana, podpisana), posty (szkic, opublikowany, zaplanowany), konta (niepotwierdzone, potwierdzone). W niektórych przypadkach może wyniknąć potrzeba rozdzielenia konkretnych stanów na osobne klasy, w jeszcze innych wykorzystanie tzw. maszyny stanów, ale nie będę się na tym dziś skupiał. Chciałbym się skupić na wyrażaniu stanu przy pomocy wartości.

Po co enum?

Jeśli zajrzymy sobie na chwilę do Javy, C# czy C++, możemy znaleźć jeden dodatkowy typ, którego w podstawowej implementacji PHP nie znajdziemy. Ten typ to enum. Enum, czyli enumerable, lub typ wyliczeniowy to typ, który może przyjąć tylko te wartości, które zawarliśmy w jego definicji. Jest to świetne rozwiązanie, ponieważ zabezpieczamy się dzięki temu na wypadek wpisania jakiejś kompletnie nieprzewidzianej wartości w pole tego typu.

Bez zależności

Najprostszy sposób wyrażenia stanu np. obiektu w PHP to dodanie do niego pola przechowującego aktualny stan:

class Article {
    public const PUBLISHED = 1;
    public const DRAFT = 2;

    private int $state;
    // inne pola obiektu

    public function setState(int $state): void
    {
        $this->state = $state;
    }

    // inne metody obiektu
}

$article = new Article();
$article->setState(Article::PUBLISHED);

W powyższym przykładzie widzimy obiekt Article, który posiada swój stan ustawiany przy pomocy settera. Wszystko niby spoko, pole nie jest publiczne, mamy stałe określające stany, jednak ten kod mimo ustawienia pola jako private pozwala nam wpisać w pole state dowolną liczbę całkowitą. I tutaj właśnie przyda nam się enum.

W podstawowym PHP, niestety nie znajdziemy typu enum. Są jednak pewne implementacje, które możemy wykorzystać.

Standard PHP Library

Pierwszą z nich, jest rozszerzenie PHP o nazwie SplTypes. Poza klasą SplEnum dodaje jeszcze kilka mechanizmów pozwalających na silniejsze typowanie zmiennych. To rozwiązanie ma jednak dwie spore wady. Po pierwsze jest rozszerzeniem języka, które musimy sami doinstalować, a nie zawsze jest to łatwe / możliwe. Drugą wadą jest to, że jest ono w fazie eksperymentalnej i to chyba już od 10 lat i nie wiadomo co się z tym stanie w przyszłości.

Powyższy przykład z wykorzystaniem tego rozszerzenia może wyglądać tak:

<?php
class Article {
    private ArticleState $state;
    // inne pola obiektu

    public function setState(ArticleState $state): void
    {
        $this->state = $state;
    }

    // inne metody obiektu
}

class ArticleState extends SplEnum {
    const __default = self::DRAFT;
    
    public const PUBLISHED = 1;
    public const DRAFT = 2;
}

$article = new Article();
$article->setState(new ArticleState(ArticleState::PUBLISHED));

W powyższym przykładzie jesteśmy już bezpieczni. Już sam kod programu dzięki typowaniu zabezpiecza nas przed popełnieniem błędu i wpisaniu w pole stanu jakiejś przypadkowej lub błędnej wartości.

MyClabs\PHPEnum

Ostatnim i moim zdaniem najlepszym z kilku względów rozwiązaniem jest zastosowanie zewnętrznej biblioteki. Moim faworytem jest biblioteka myclabs/php-enum. Biblioteka ta jest najczęściej instalowaną tego typu biblioteką z packagista. Posiada integrację do PHPStan’a i daje się mapować np. w Doctrine czy Yii. Biblioteka ta dostarcza proste i intuicyjne API do pracy z enumami. Wystarczy ją zainstalować z composera:

composer require myclabs/php-enum

I już możemy ją wykorzystać w naszym przykładzie:

<?php
class Article {
    private ArticleState $state;
    // inne pola obiektu

    public function setState(ArticleState $state): void
    {
        $this->state = $state;
    }

    // inne metody obiektu
}

class ArticleState extends MyCLabs\Enum\Enum {
    public const PUBLISHED = 1;
    public const DRAFT = 2;
}

$article = new Article();
$article->setState(ArticleState::PUBLISHED());

Użycie jest nawet prostsze niż w enumie z SplTypes, a do tego nie wymaga żadnych zależności interpretera. Jeśli ktoś korzysta z IDE, np. PHPStorm, może zauważyć, że w tym kodzie może wystąpić problem z podpowiedziami, ale jest na to rada prosto z dokumentacji php-enum. Można zadeklarować metody statyczne dla każdego z parametrów lub użyć PHPDoc:

/**
 * @method static ArticleState PUBLISHED()
 * @method static ArticleState DRAFT()
 */
class ArticleState extends MyCLabs\Enum\Enum {
    public const PUBLISHED = 1;
    public const DRAFT = 2;
}

I już mamy podpowiedzi :D!

Enumy w bazie danych

Relacyjne bazy danych np. MySQL, mają możliwość zadeklarowania pola typu enum. Jest to fajna opcja i dodatkowe zabezpieczenie przed problemem ze spójnością. Istnieje wiele zdań na temat tego, czy warto używać pola tego typu. Z mojego doświadczenia wiem, że jego użycie bywa problematyczne, chociażby z powodu konieczności wykonania migracji przy dodaniu nowej opcji.

Czasem jednak może pojawić się wymaganie co do czytelności bo np. ktoś generuje raport z danych wyciągniętych bezpośrednio z bazy. Można wtedy użyć enuma, lub też utworzyć dodatkową tabelę z wartościami i w zapytaniach do bazy łączyć wartość z intem. Jednym z rozwiązań może być użycie pola VARCHAR jednak jest ono bardziej kosztowne jeśli chodzi o rozmiar szczególnie przy dużej ilości rekordów.

Warto dobrze przemyśleć zastosowanie typu enum w bazie, bo każdy przypadek jest na swój sposób specyficzny. U mnie często kończyło się migracją pola do tinyint.

Podsumowując

Warto rozważyć użycie którejś z implementacji typu enum w PHP. Podniesie to na pewno jakość kodu, dzięki silniejszemu sprawdzaniu typów. To z kolei pomoże nam to uniknąć wielu błędów związanych z literówkami, błędnymi wartościami, czy pozostałościami z debugowania naszego kodu.