Идентификатор ресурса
- ·
-
При создании ресурсо-ориентированных API скорее всего каждый ресурс будет иметь свой идентификатор, чтобы пользователь, в любой момент времени, при достаточном уровне прав, смог запросить требуемый ресурс. Типичный идентификатор ресурса в большинстве проектов имеет целочисленное значение. И на это есть несколько причин, во-первых, используется реляционная база данных(напр. MySQL, PostgreSQL) которая предоставляют возможность генерировать идентификаторы на стороне базы данных что решает проблемы с уникальностью, а во-вторых, целочисленные идентификаторы позволяют данным РСУБД эффективно выполнять запросы и хранить данные на диске. Реже встречаются строковые идентификаторы, а если и встречаются, то это идентификатор сгенерированные с помощью библиотеки в формате UUID.
И если UUID избыточен, то целочисленный идентификатор просто плохой. А что делает идентификатор хорошим? Давайте рассмотрим атрибуты хорошего идентификатора.
Атрибуты хорошего идентификатора
Существует список атрибутов который делают идентификатор ваших ресурсов хорошим. Возможно он не исчерпывающий, но вполне достаточен.
Простота использования
Идентификаторы должны быть просты в большинстве типичных сценариев. Запрос на получение ресурса по его идентификатору должно осуществляться просто дабы минимизировать вероятность ошибки.
Уникальность
Не будет секретом если я скажу что каждый идентификатор должен быть уникальным. Не обязательно в пределах всей системы, он может быть уникальным в рамках определенного контекста. Добиться абсолютной уникальности практически нереально но, в пределах своего приложения вполне осуществимо.
Постоянство
Есть негласное правило что идентификаторы после присваивания изменяться не должны. В первую очередь это обусловлено тем что это чревато появлением проблем. Проблем связанных с отношениями между ресурсами, кешированием и т.п. Некоторые источники рекомендуют не использовать ID уже удаленных ресурсов.
Быстрая генерация
Мы живем в мире высокой нагрузки, и будет проблемой если генерация ID потребляет большое количество ресурсов CPU. Лучше сохранить эти ресурсы для задач в которых мы не можем экономить.
Непредсказуемость
Хорошим атрибутом идентификатора так же является его непредсказумость. В первую очередь это касается безопасности, дабы злоумышленник не имел возможности предугадывать следующий идентификатор в поисках потенциальных уязвимостей.
Читабельность
В большинстве случае идентификатор будет использоваться программами но, существуют ситуации когда их будут читать люди. По этому желательно не использовать в идентификаторе символы которые легко спутать. Например символы 1 и L(в нижнем регистре), 1 и |, l и |, 0 и O и так далее. Например в таком идентификаторе 1lllll|l00o0llll1| легко перепутать символы из примеров. Здесь они бросаются в глаза из-за шрифта но, нет гарантий что в другом шрифте разница будет так очевидна. Поэтому дабы минимизировать вероятность ошибки рекомендуется не использовать данные символы.
Информационная плотность
В идентификатор требуется вложить максимум информации при минимальном значении. Например при использовании целочисленных идентификаторов, мы может использовать до 10 вариантов каждого символа, а в случае Base64 — до 64.
Возможность проверки
Некоторые форматы идентификаторов имеют контрольную сумму. Например в идентификаторе ISBN(Международный стандартный книжный номер), последняя цифра в данном идентификаторе является контрольной суммой, которая подтверждает предыдущие цифры.
Пример ISBN идентификаторов:
- ISBN-10: 0-306-40615-2
- ISBN-10: 1-4028-9462-7
- ISBN-10: 3-16-148410-0
Как выглядит хороший идентификатор
Теперь вы знаете атрибуты хорошего идентификатора, настало время показать как он выглядит.
Тип данных
Предпочитаемый тип данных для идентификатора будет строкой. Строки являются наиболее гибким вариантом, которые предоставляют найбольшую плотность информации и вариативность, чтобы сделать их читабельными и удобными в использовании.
Набор символов
Для набора символов мы будем опираться на всеми известный стандарт ASCII(American standard code for information interchange).
Формат
Так как в нашем распоряжении 128 символов стандарта ASCII, все символы не рекомендуется использовать, так как это будет ухудшать читабельность нашего идентификатора. Ранее объяснял в разделе Читабельность.
Рекомендую обратить ваше внимание на формат сериализации разработанный Дугласом Крокфордом Base32.
Base32 опирается на использование 32 из 128 символов ASCII. Сюда входят буквы от A-Z, 0-9 но отбрасываются |, I, L, o и U. Данный формат имеет форму перечисленных символов в верхнем регистре. Дефисы рассматриваются как необязательные и отбрасываются при декодировании. Данный формат позволяет строить достаточно гибкие и читабельные идентификаторы.
Контрольная сумма
Один из основных атрибутов хорошего идентификатора это возможность проверки. Возможность отличить отсутствующий(удаленный) идентификатор и тот который в принципе не мог существовать. Достигается это за счет контрольной суммы. Контрольная сумма обычно выражается в дополнительном символе(символах) в конце строки идентификатора и создается на основе содержимого идентификатора.
Тип ресурса
Уделяя внимание атрибуту уникальности мы можем добавлять в идентификатор тип ресурса. Тип ресурса можно добавить как префикс, например: user-8hgpw-ybcdx-k6c, либо user:8hgpw-ybcdx-k6c. Тип ресурса в идентификаторе, требование необязательное но, оно дает дополнительную полезную информацию.
Реализация
Мы рассмотрели атрибуты хорошего идентификатора и как он выглядит. Давайте теперь займемся реализацией.
Размер
Очевидно что размер идентификатора будет зависеть от ситуации и скорее всего изменяться со временем. Ресурс может быть уникальным в пределах другого ресурса, в пределах приложения или уникальным глобально. Рассмотрим их.
В пределах ресурса
Например у нас существует ресурс Article(статья), у статьи могут быть Comment(комментарии). Скорее всего размер идентификатора будет небольшим.
В пределах приложения
Обратимся к тому же ресурсу Article(статья). Размер идентификатора должен быть достаточно большой но все так же зависит от использования вашего приложения.
Уникально глобально
Размер такого идентификатора должен быть настолько большим чтобы количество возможных идентификаторов был такой же у UUID.
Генерация идентификатора
Для генерации (псевдо)случайного набора байтов вы можете использовать любой алгоритм на Ваше усмотрение. Я воспользуюсь функцией random_bytes. Ниже реализован базовый алгоритм Crockford Base32.
function generateId(int $size): string {
$bytes = random_bytes($size);
$bin = gmp_strval(gmp_import($bytes), 2);
$bin = str_pad($bin, strlen($bytes) * 8, '0', STR_PAD_LEFT);
$bin = str_split($bin, 5);
$last = array_pop($bin);
if (! is_null($last)) {
$bin[] = str_pad($last, 5, '0', STR_PAD_RIGHT);
}
$alphabet = [
'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T',
'V', 'W', 'X', 'Y', 'Z'
];
return implode(
'',
array_map(
fn ($bits) => $alphabet[(bindec($bits))],
$bin
)
);
}
Вычисление контрольной суммы
Чтобы вычислить контрольную сумму достаточно взять набор байтов как целое число и поделить по модулю на 37. Делим на 37 потому что мы к алфавиту Base32 добавляем еще 5 символов.
function getCharForChecksum(int $index): string {
$crockfordAlphabet = [
'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T',
'V', 'W', 'X', 'Y', 'Z'
];
$alphabet = array_merge($crockformAlphabet, [
'*', '~', '$', '=', 'U'
]);
return $alphabet[$index];
}
function calculateChecksum(string $bytes): int {
$bigInt = gmp_strval(gmp_import($bytes));
$checksum = gmp_mod($bigInt, 37);
return intval($checksum);
}
Теперь реализуем функцию генерации идентификатора с контрольной суммой.
function generateIdWithChecksum(int $size): string {
$bytes = random_bytes($size);
$bin = gmp_strval(gmp_import($bytes), 2);
$bin = str_pad($bin, strlen($bytes) * 8, '0', STR_PAD_LEFT);
$bin = str_split($bin, 5);
$last = array_pop($bin);
if (! is_null($last)) {
$bin[] = str_pad($last, 5, '0', STR_PAD_RIGHT);
}
$alphabet = [
'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T',
'V', 'W', 'X', 'Y', 'Z'
];
$encoded = implode(
'',
array_map(
fn ($bits) => $alphabet[(bindec($bits))],
$bin
)
);
$checksum = calculateChecksum($bytes);
return $encoded . getCharForChecksum($checksum);
}
Верификация идентификатора
Реализуем функцию декодирования из формата Crockford Base32. Это нужно для повторного вычисления контрольной суммы.
function decodeId(string $id) {
$normalizedId = strtoupper($id);
$normalizedId = str_replace(['O', 'L', 'I', '-'], ['0', '1', '1', ''], $normalizedId);
$normalizedId = str_split($normalizedId);
$alphabet = [
'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T',
'V', 'W', 'X', 'Y', 'Z'
];
$bin = implode(
'',
array_map(
fn ($char) => sprintf('%05b', array_search($char, $alphabet)),
$normalizedId
)
);
$normalizedId = str_split($bin, 8);
$last = array_pop($normalizedId);
if (! is_null($last) && strlen($last) === 8) {
$normalizedId[] = $last;
}
return implode(
'',
array_map(
fn ($bits) => chr(intval(bindec($bits))),
$normalizedId
)
);
}
function verifyId(string $id): bool {
$idWithoutChecksum = substr($id, 0, -1);
$checksumChar = substr($id, -1);
$bytes = decodeId($idWithoutChecksum);
return $checksumChar === getCharForChecksum(calculateChecksum($bytes));
}
Итоговый код
Вот для Вас итоговый вариант реализации в объектно-ориентированном стиле. Это встроенный интерпретатор и Вы можете проводить свои эксперименты. Удачи:)