Комментарии 51
В пером примере страшно неопримизированный код, от условия в 4 строки текут кровавые слезы. Дальше, честно, читал по диагонали. Можно было сделать проще: массивом или методом, который возвращает все доступные варианты. Можно было так же реализовать некий интерфейс или абстрактный класс, который бы описал основные возможности такого «Enum» класса.
Эта реализация так-же опирается на механизм рефлексии. В методе toArray() происходит поиск и сохранение всех констант класса. Впоследствии эти значения используются как варианты перечисления.
Оптимизация всегда имеет цель. Примеры оптимизированы не с целью уменьшения количества строк кода, а с целью увеличения очевидности.
Прям не хотелось это писать, но вырвалось, извините.
Работают строгие сравнения (===, 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)
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, нет?
class Food {
public const BEER = 'Food::BEER';
}
class Waste {
public const BEER = 'Waste::BEER';
}
<?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;
}
Для себя этим кодом и пользовался, можно развить/улучшить, но лично мне и лично для меня — хватало.
* Нет контроля типов (т.е если унести свитч в отедльный метод\набор методов, то там будет таки (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 и статанализаторы не столько помогать будут работать с этим, сколько мешать.
<?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 это не является какой-то из ряда вон выходящей вещью. Другой вопрос зачем вам это надо?
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.
/**
* @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];
}
}
public function someFunc(TheEnum $enum): void {}
куда безопасней, наглядней и понятней, чем в таком
public function someFunc(string $enum): void {}
Мы пришли к тому что бы использовать одиночку (при строгом и не строгом сравнивании был один результат) при этом реализация одиночки была положена в абстрактный класс Enum, а вот наследники должны были реализовать определенный метод, в котором весь пул доступных значений.
К сожалению приходилось перечислять все статические методы, но без этого ни куда.
Правда мы не учитывали кейсы где мы используем serialize или unserialize т.к. мы используем JMS сериалайзер и сериализуем в JSON, с кастомными handler-ами.
Я просто оставлю это здесь
https://www.php.net/manual/ru/class.splenum.php
в таком виде, в каком приведены были примеры — перечисления нафиг не нужны в РНР. Сила РНР в динамичности всего и вся, а наворачивая перечисления, вы попросту хардкодите часть логики в коде.
Я бы наоборот, держал бы все инстансы перечисления в базе и подгружал бы.
Вместо хардкода Season::autum, просто буду работать с объектом класса Season, и пусть он сам решает что ему положено делать.
Перечисления имеют смысл только в компилируемых языках, имхо
Не понимаю, почему до сих пор в pho нет типа перечисления, очень нужная вещь…
Про дженерики уже молчу..
Перечисления в PHP