пятница, 17 июня 2022 г.

Константы и пространства имен в php

Сегодня поисследуем использование пространств имен (они же неймспейсы) для объявления констант и обращения к ним.

Как сказано в мануале:

классы (включая абстрактные и трейты), интерфейсы, функции и константы зависят от пространства имен.

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

Проверим:

namespace FirstNs {
    const SUPER_VALUE = 'NS-1';
}

namespace SecondNs {
    const SUPER_VALUE = 'NS-2';
}

namespace {
    const SUPER_VALUE = 'NS-GLOBAL';
    var_dump(
        SUPER_VALUE,
        FirstNs\SUPER_VALUE,
        SecondNs\SUPER_VALUE,
    );
}

// Вывод:
// string(9) "NS-GLOBAL"
// string(4) "NS-1"
// string(4) "NS-2"

Ровно то, что нужно.

Но что если мы хотим определить константы методом define.

Не проблема, пробуем написать так:

namespace FirstNs {
    define('SUPER_VALUE', 'NS-1');
}

namespace SecondNs {
    define('SUPER_VALUE', 'NS-2');
}

namespace {
    define('SUPER_VALUE', 'NS-GLOBAL');
    var_dump(
        SUPER_VALUE,
        FirstNs\SUPER_VALUE,
        SecondNs\SUPER_VALUE,
    );
}

Запускаем и видим:

Warning: Constant SUPER_VALUE already defined in ...
Warning: Constant SUPER_VALUE already defined in ...
Fatal error: Uncaught Error: Undefined constant "FirstNs\SUPER_VALUE" in ...

Судя по всему, мы пытаемся три раза определить одну и ту же константу в глобальном неймспейсе. Что же делать?

А просто указать нужный неймспейс при определении константы, благо php это позволяет:

namespace FirstNs {
    define('FirstNs\\SUPER_VALUE', 'NS-1');
    // или
    define(__NAMESPACE__ . '\\SUPER_VALUE', 'NS-1');
    
}

namespace SecondNs {
    define('SecondNs\\SUPER_VALUE', 'NS-2');
}

namespace {
    define('SUPER_VALUE', 'NS-GLOBAL');
    var_dump(
        SUPER_VALUE,
        FirstNs\SUPER_VALUE,
        SecondNs\SUPER_VALUE,
    );
}

// Вывод:
// string(9) "NS-GLOBAL"
// string(4) "NS-1"
// string(4) "NS-2"

Можно даже пойти дальше и в одном неймспейсе определить константу другого неймспейса:

namespace FirstNs {
    define('SecondNs\\SUPER_VALUE', 'NS-2-1');
}

namespace SecondNs {
    define('FirstNs\\SUPER_VALUE', 'NS-1-2');
}

Делать так в продакшене категорически не рекомендуется.

Итак, сегодня мы разобрались с тем, как определять константы с учетом неймспейсов, используя как ключевое слово const, так и функцию define.

вторник, 8 февраля 2022 г.

Длина строк в Go

Этот пост родился из-за странного поведения валидатора Length из пакета ozzo-validation.

Следуя руководству, можно зарегистрировать, например, такой валидатор для строк:

data := "Некоторая строка"
err := validation.Validate(data,
    validation.Required,
    validation.Length(10, 16),
)
fmt.Println(err)

И так как в строке у нас кириллические символы, и более того - кодировка строки utf-8, то валидация не пропускает строку, падая с ошибкой "the length must be between 10 and 16". Почему так происходит, ведь валидируемая строка содержит ровно 16 символов?

Для того, чтобы понять почему валидация выдает ошибку - придется сходить в исходный код валидатора. Поиски приводят к тому, что для определения длины валидируемой строки используется метод Len() из типа reflect#Value. Путешествие дальше приводит нас к тому, что свойство Len вычисляется с помощью встроенной функции len. А как сообщает мануал:

The len built-in function returns the length of v, according to its type:
String: the number of bytes in v.

Следовательно, валидация на основании длины строки в байтах нас не устраивает, так как в случае многобайтных кодировок (коей является utf-8) число символов в строке не равно числу байт в этой же строке.

Хотелось бы, чтобы был метод типа utfLen(). И он есть в пакете unicode/utf8, правда, с немного другим названием - RuneCountInString. И о чудо, данный метод даже используется в валидаторе, если установить дополнительный флаг rune. И это можно сделать применив валидатор RuneLength.

Обновленный код:

data := "Некоторая строка"
err := validation.Validate(data,
    validation.Required,
    validation.RuneLength(10, 16),
)
fmt.Println(err)

Валидация отрабатывает как и ожидается, выводя <nil>.

И не забудьте написать для этого случая тест (ну или хотя бы комментарий), чтобы пытливый программист, использующий код после вас, не смог бы заменить RuneLength на Length, думая, что "и так сойдёт".

P.S. Больше разъяснений про длины строк можно почитать в этом ответе на stackoverflow.

P.P.S. Отличное видео на Youtube, раскрывающее суть кодировок.