Pull to refresh

Comments 51

Тема не раскрыта полностью, к примеру, никакой информации по поводу готовых решений в этом направлении, допустим github.com/myclabs/php-enum

В пером примере страшно неопримизированный код, от условия в 4 строки текут кровавые слезы. Дальше, честно, читал по диагонали. Можно было сделать проще: массивом или методом, который возвращает все доступные варианты. Можно было так же реализовать некий интерфейс или абстрактный класс, который бы описал основные возможности такого «Enum» класса.

Эта реализация так-же опирается на механизм рефлексии. В методе toArray() происходит поиск и сохранение всех констант класса. Впоследствии эти значения используются как варианты перечисления.


Оптимизация всегда имеет цель. Примеры оптимизированы не с целью уменьшения количества строк кода, а с целью увеличения очевидности.

Очевидность? Если вы имеете в виду совсем неопытных ребят, то ваш пример плох еще и тем, что учит писать некрасиво. Если же брать более опытных людей, то они, как правило, вычитывают куда более сложный код на ревью.

Прям не хотелось это писать, но вырвалось, извините.
Вся эта реализация просто бессмысленна. А главное преследует цель эстетизации, а не оптимизации. И чем вам помешали массивы? Учитывая, что набор функций для их обработки уже собран. Напоминает типичную ооп'о'фагию…
Мы когда то дошли до вот такого решения. github.com/paillechat/php-enum/blob/master/src/Enum.php

Работают строгие сравнения (===, in_array($value, [MyEnum::NAME1(), MyEnum::NAME2()], true) и пр.)

Косяка из комента ниже нет habr.com/ru/post/517752/#comment_22031208
Food::BEER() === Waste::BEER() не пройдет (это разные инстансы)

Обращения к рефлексии кэшируются в памяти класса и повторно используется мемори кэш вместо рефлексии при обращении к инстансам.

В целом можно было бы сделать и без рефлексии (похожий пример ниже в коментах), но первая версия этой либы работала на текстовых константах (new MyEnum(MyEnum::NAME1) ) и было решено оставить работу со списком значений через константы, а не через массив допустимых значений
Ну, кстати, об всём этом написано в статье.
И с какой точки зрения неоптимизирован код первого примера? Он с точки зрения читабельности не очень, но зачастую перечисления никогда не меняются, вероятно, там никогда не станет сто элементов вместо четырёх. Зато он очень лёгкий. Перечисления это сахарок, хотелось бы, чтобы они (раз уж не реализованы на уровне языка), не тратили зря ресурсы. Впрочем, первый пример вообще неудобен в применении и мало что даёт. Дальше лучше.

Массив из констант можно было сделать. Или даже ассоциативный self::SUMMER => true и проверять в конструкторе как if (self::VALUES[*value]?? false)

С точки зрения читабельности, даже, заменить 8 строк 2мя имеет смысл еще какой. Эллементарно, все константы оборачиваются в массив и дальше через in_array и implode. Такой варинт будет работать так же быстро как и тот, что представлен в примере.
Да я бы тоже скорее всего написал именно так. Тем более, что преждевременные оптимизации вредны. Просто в данном случае это не слишком важно. И речь в примере не об этом, а об одной из простейших реализаций перечислений.

Это не оптимизация же в привычном понимании, а улучшение читаемости

Я имел ввиду под оптимизацией то что написано в статье и то что я пытался оправдать.

А вообще тема, кажется, вечная. Как PHP4 вышел, так началось.

Зачем это?
Ради чего? Этот жуткий колхоз. Использовать объект со всеми вытеуающими для эмуляции человеко ориентированного описания числовых констант.
Я полагаю, что если суммировать занимаемую памть всеми перечислениями в каком нибудь огромном проекте, то она будет меньше занимаемой памяти одним этим классом.
Как дети. Ей богу.

Контроль типов в значениях. Чтобы по сигнатуры метода какого-то другого объекта сразу было видно, что он не просто строку или число применяет, а узко ограниченный набор значений.

В php? Контроль типов? Если нужен ЯП умеюший в нормалтный ООП надо выбирать подходящий инструмент, а не городить огород.
Используйте массивы или константы

Весь проект переписать на другой ЯП в тот момент когда понадобились перечисления — такая себе затея. Давайте будем честны, иногда работаем с чем есть, проекты не только создают, но и развивают существующие.

Php 7 вышел почти 5 лет назад и в нём с контролем типов ситуация значительно улучшилась, осталась только проблема с контролем содержимого массивов.
Чем вам ООП не угодило не понимаю — если вы про обобщения, так они к ООП отношения не имеют, а перекрёстное наследование «как в Си++» только мазохистам нужно, в остальном же ООП в пыхе стандартное.

class Season extends Enum {
    public const WINTER = 'winter';
    public const SPRING = 'spring';
    public const SUMMER = 'summer';
    public const AUTUMN = 'autumn';
}


$now = Season::AUTUMN(); // Autocomplete works as Season::AUTUMN exists
var_export($now->is(Season::AUTUMN)); // true
var_export("$now" === Season::AUTUMN); // true
var_export($now == Season::AUTUMN); // true
var_export($now == Season::SPRING); // false
echo "$now"; // autumn


class Enum {
    protected string $_value;

    protected function __construct(string $value) {
        $this->_value = $value;
    }

    public function is($key)
    {
        return $this->_value === $key;
    }

    public static function __callStatic($name, $params) {
        $value = constant("static::$name");
        if (!$value) {
            throw new \InvalidArgumentException(static::class . " can't be $name");
        }
        return new static($value);
    }

    public function __toString() {
        return $this->_value;
    }
}

Можно еще таким примером продолжить ряд. IDE это нравится (начав писать Season видим список возможных значений), стринговые ключи можно придумать те, которые нужны (например, чтобы согласовать Snake case и Camel case в стиле кода и там, где используется строковая составляющая), плюс макросы IDE позволяют писать одновременно имя и значение константы. Плюс этим можно пользоваться без создания объекта там, где он не нужен и достаточно лишь строковой константы.
class Food {
  public const BEER = 'beer';
}
class Waste {
  public const BEER = 'beer';
}
 
// ...

$this->assertTrue(Waste::BEER === Food::BEER);

Т — типизация

В решении выше Вы получаете enum-ы в стиле си, когда они есть просто псевдонимы для констант примитивного типа, и отбрасываете типизацию.
Это скорее приведение типов :)
Вроде бы в том же C[++] они тоже будут равны, т.к. по сути int, нет?
В С++ enum class не равны, если для них не определён пользовательский operator==
Простой enum — это такой же кривой дизайн, как и строки выше.
class Food {
  public const BEER = 'Food::BEER';
}
class Waste {
  public const BEER = 'Waste::BEER';
}
Помню, баловался с перечислениями в php, если кратко, то получилось что-то типо такого:
<?php

    function createEnum(...$vals) {
        $enum = [];
        $i = 0;
        foreach( $vals as $v ) {
            $enum[$v] = ++$i;
        }
        
        return (object) $enum;
    }
    $eAction = createEnum('jump', 'run', 'kick', 'die');
    
    /*
        много кода,в котором выясняется, что будет прыжок
    */
    
    $action = $eAction->jump;
    
    switch($action) {
        case $eAction->jump: // 1
            echo "You jump!\n";
            break;
        
        case $eAction->run: // 2
            echo "You run!\n";
            break;
        
        case $eAction->kick: // 3
            echo "You kicked enemy!\n";
            break;
        
        case $eAction->die: //4
            echo "Game over!\n";
            break;
    }

Что-то туплю с утра, было без каста массива в объект
<?php
    function createEnum(...$vals) {
        $enum = new stdClass();
        $i = 1;
        foreach( $vals as $v ) {
            $enum->$v = $i++;
        }        
        return $enum;
    }

Для себя этим кодом и пользовался, можно развить/улучшить, но лично мне и лично для меня — хватало.
Автокомплитик бы ещё к этому делу.
Это да, но автокомплит std объектов можно устроить в ide'шках. Но я так не заморачивался никогда)
понимаю, что пример наколеночный, но вижу два недостатка:
* Нет контроля типов (т.е если унести свитч в отедльный метод\набор методов, то там будет таки (string $action) в сигнатуре.
* Енам либо придется делать глобальным, либо инстанциировать в каждом месте, где его надо обработать, т.е. опять же если унести этот свитч в отдельный метод, то для проверки значений придется инстанциировать енам заново, что будет плодить этот набор значений по кодовой базе
Да, я понимал это, но мне нравилась такая простая и (как мне кажется) элегантные реализация/использование, что с этими двумя недостатками я уживался.
С контролем типов вообще проблем не было, а enum'ы хранил в глобальном объекте приложения
Если не нужен контроль типов, то кажется лучше просто набор констант тогда?
define("ACTION_JUMP ", 1);
define("ACTION_RUN", 2);
define("ACTION_WALK", 3);

Типо такого?
Думаю, что если бы я не хотел сделать подобие перечислений, которых увидел в Gamemaker Studio 2, то да — заюзал бы константы, а в качестве группировки была бы часть до первого символа подчеркивания.
Если нужна группировка, то можно раскошелиться и на класс
final class Action {
  public const JUMP = 1;
  public const RUN = 2;
  public const WALK = 3;
}
(Злая шутка)
А если у него на хостинге только PHP4?
Эх, как же не хочется использовать класс, но это реально в пхп самый лучший способ имитировать перечисления. В проде такое и юзал бы
Не обязательно. Можно заюзать пространства имён.
Когда в С++ не было enum class, я извращался примерно так:

namespace EnumName
{
  enum Value {A, B, C};
}

void foo(EnumName::Value value);

foo(EnumName::A);

Можно так:
struct EnumName
{
  enum Value {A, B, C};
  EnumName(Value value) : value(value){}
  operator Value() const { return value; }
private:
  Value value;
}

void foo(EnumName value);

foo(EnumName::A);

Увы, в PHP нельзя указать тайпхинт "все константы нэймспэйса A\B\C"

Чести ради и «все константы класса» тоже нельзя, только через рефлексию. Поправьте, если ошибаюсь. Но эту ветку мы обсуждаем исходя из того что контроль типов не нужен человеку, только группировка значений

habr.com/ru/post/517752/#comment_22041126

Не ошибаетесь. Просто у вас в примерах вроде как объявления foo с типизацией…


А группировку констант с нэймспэйсами можно на PHP, типа


namespace A\B {
    const TYPE1 = 'TYPE1';
    const TYPE2 = 'TYPE2';
    const TYPE3 = 'TYPE3';
}

namespace C {
    use A\B;

    echo B\TYPE1;
}

Но, думаю, многим очень непонятно будет и IDE и статанализаторы не столько помогать будут работать с этим, сколько мешать.

Да, действительно, не подумал об этом.
Посоветуйте что делать с этим enum на PHP, когда необходимо его использовать как ключ в массиве?
Size из статьи и его применение
<?php

class Size
{
    public const SIZES = ['xxs', 'xs', 's', 'm', 'l', 'xl', 'xxl'];

    private string $value;

    private function __construct(string $value)
    {
        $this->value = $value;
    }

    public function __toString(): string
    {
        return $this->value;
    }

    public static function __callStatic($name, $arguments)
    {
        $value = strtolower($name);
        if (!in_array($value, self::SIZES)) {
            throw new BadMethodCallException("Method '$name' not found.");
        }

        if (count($arguments) > 0) {
            throw new InvalidArgumentException("Method '$name' expected no arguments.");
        }

        return new self($value);
    }
};

$ar = array(Size::xxs()=>1);
var_dump($ar);
?>




Понятно, что можно взять enum из первого примера, когда это строковые константы. но есть необходимость в подобном классе
Так
/**
 * Class Size
 * @method static Size xxs()
 * @method static Size xs()
 * @method static Size s()
 * @method static Size m()
 * @method static Size l()
 * @method static Size xl()
 */
class Size
{
    public const SIZES = ['xxs', 'xs', 's', 'm', 'l', 'xl', 'xxl'];

    private $value;

    private function __construct(string $value)
    {
        $this->value = $value;
    }

    public function getValue(): string
    {
        return $this->value;
    }

    public function __toString(): string
    {
        return $this->value;
    }

    public static function __callStatic($name, $arguments)
    {
        $value = strtolower($name);
        if (!in_array($value, self::SIZES)) {
            throw new BadMethodCallException("Method '$name' not found.");
        }

        if (count($arguments) > 0) {
            throw new InvalidArgumentException("Method '$name' expected no arguments.");
        }

        return new self($value);
    }
};

$ar = [Size::xxs()->getValue() => 1];

Или так
/**
 * Class Size
 * @method static Size xxs()
 * @method static Size xs()
 * @method static Size s()
 * @method static Size m()
 * @method static Size l()
 * @method static Size xl()
 */
class Size
{
    public const SIZES = ['xxs', 'xs', 's', 'm', 'l', 'xl', 'xxl'];

    private $value;

    private function __construct(string $value)
    {
        $this->value = $value;
    }

    public function __toString(): string
    {
        return $this->value;
    }

    public static function __callStatic($name, $arguments)
    {
        $value = strtolower($name);
        if (!in_array($value, self::SIZES)) {
            throw new BadMethodCallException("Method '$name' not found.");
        }

        if (count($arguments) > 0) {
            throw new InvalidArgumentException("Method '$name' expected no arguments.");
        }

        return new self($value);
    }
};

$ar = [(string)Size::xxs() => 1];

Но по мне всё это выглядит костыльно. Почему нельзя просто сделать так?


$ar = ['xxs' => 1];

Ничего. Дробные числа тоже нельзя применять как ключи. В рамках PHP это не является какой-то из ряда вон выходящей вещью. Другой вопрос зачем вам это надо?

Массив пхп не позволяет держать в ключах ничего кроме строк и интов. Для того, чтобы использовать в качестве объектов нужен некий Map, например такой.

www.php.net/manual/ru/class.ds-map.php

Я бы наверное в какую-то такую сторону смотрел


class Seasons{
    private static array $mapping = [];

    private string $name;

    private function __construct(string $name){
        $this->name = $name;
        self::$mapping[$name] = $this;
    }

     public static function SUMMER(): Seasons
     {
         if(isset(self:$mapping['summer']){
               return self:$mapping['summer'];
         }

         return new self('summer');
     }
}

Позвольте мне немного улучшить ваш код:


class Seasons {
    private static array $mapping = [];

    private string $name;

    private function __construct(string $name) {
        $this->name = $name;
    }

     public static function SUMMER(): Seasons
     {
         if (!isset(self:$mapping['summer']) {
               self:$mapping['summer'] = new self('summer');
         }

         return self:$mapping['summer'];
     }
}

Как результат получается перечисление как одиночка. С той лишь разницей, что конкретные экземпляры хранятся не в отдельных статических полях, а в массиве $mapping.

Пойдем еще дальше и добавим контроль допустимых значений через массив, а не через наличие метода. После этого можно вынести все кроме static $values в базовый родительский класс и у нас получился неплохой переиспользуемый енам

/**
 * @method static static SUMMER()
 * @method static static AUTUMN()
 * @method static static WINTER()
 * @method static static SPRING()
 */
final class Seasons
{
    private static array $mapping = [];

    private static array $values = [
        'SUMMER',
        'AUTUMN',
        'WINTER',
        'SPRING',
    ];

    private string $name;

    private function __construct(string $name)
    {
        $this->name = $name;
    }

    public static function __callStatic(string $name, array $args): self
    {
        if (!in_array($name, self::$values, true)) {
            throw new \BadMethodCallException("Value $name is not allowed");
        }

        if (!isset(self::$mapping[$name])) {
            self::$mapping[$name] = new self($name);
        }

        return self::$mapping[$name];
    }
}

К сожалению PHP не имеет нормального инструмента для реализации перечислений и приходилось выкручиваться, а использовать в таком виде
  public function someFunc(TheEnum $enum): void {}

куда безопасней, наглядней и понятней, чем в таком

  public function someFunc(string $enum): void {}


Мы пришли к тому что бы использовать одиночку (при строгом и не строгом сравнивании был один результат) при этом реализация одиночки была положена в абстрактный класс Enum, а вот наследники должны были реализовать определенный метод, в котором весь пул доступных значений.
К сожалению приходилось перечислять все статические методы, но без этого ни куда.

Правда мы не учитывали кейсы где мы используем serialize или unserialize т.к. мы используем JMS сериалайзер и сериализуем в JSON, с кастомными handler-ами.

в таком виде, в каком приведены были примеры — перечисления нафиг не нужны в РНР. Сила РНР в динамичности всего и вся, а наворачивая перечисления, вы попросту хардкодите часть логики в коде.
Я бы наоборот, держал бы все инстансы перечисления в базе и подгружал бы.
Вместо хардкода Season::autum, просто буду работать с объектом класса Season, и пусть он сам решает что ему положено делать.


Перечисления имеют смысл только в компилируемых языках, имхо

Не понимаю, почему до сих пор в pho нет типа перечисления, очень нужная вещь…
Про дженерики уже молчу..

Я извиняюсь, но я не совсем понял, что такое дженерик.

Они же обобщённые типы. Типы параметризуемые другими типами. Если вы не знакомы с парадигмой обобщённого программирования, то лучше погуглить, в двух словах объяснить непросто. (Не советую читать Википедию об этом!)
Sign up to leave a comment.

Articles