Symfony Uid: UUID и ULID в PHP — мастерская и 8 практических задач
От генерации ID до SQLite и проверки доступа: UUIDv4/v7, ULID, UUIDv5, форматы и ошибки. Восемь задач, реальные записи PHP и исходники.

UUID и ULID в реальном PHP: генерация, время, форматы, SQLite и права API. Восемь задач, двадцать один вариант и исходники Windows/Linux.
Symfony Uid: зачем приложению собственные идентификаторы
Идентификатор связывает запись в базе, адрес страницы, событие очереди и строку журнала. Пока приложение живёт в одной таблице, обычный числовой ключ часто полностью решает задачу. Но при импорте, работе нескольких сервисов или создании записи до обращения к базе нужен другой способ получить устойчивую ссылку на объект. Здесь полезны UUID и ULID.
Symfony Uid предоставляет генерацию, разбор, сравнение и преобразование идентификаторов в PHP. Полный Symfony для этого не нужен: компонент устанавливается через Composer. В статье разберём восемь прикладных задач, а в мастерской сравним двадцать один вариант настоящего выполнения PHP. Исходники можно скачать и запустить на Windows или Linux.
Практический результат — понятный контракт: какой ID создаёт приложение, что принимает маршрут API, как значение хранится в SQLite и кто имеет право читать запись. Отдельно покажем ошибки. Правильный формат строки ещё не означает, что объект существует, а сложность угадывания адреса не заменяет проверку доступа.
Когда UUID полезен, а когда достаточно числового ключа
UUID удобен, когда идентификатор нужен раньше сохранения записи. Например, клиент формирует заявку и связанные вложения, импорт собирает данные из нескольких источников, а обработчик очереди должен ссылаться на будущий объект. Генерация не требует сначала получать следующий номер из общей таблицы. У этого решения есть цена: более длинные значения, дополнительное место в индексах и менее удобное ручное чтение.
Числовой ключ остаётся хорошим вариантом для простой внутренней базы. Не нужно менять его только ради современного названия. Иногда достаточно оставить внутренний INTEGER PRIMARY KEY, а для публичного API добавить отдельный уникальный UUID. Это два разных назначения: эффективная локальная связь и внешний устойчивый идентификатор.
Перед выбором ответьте на вопросы: кто создаёт ID, нужно ли воспроизводить его при повторном импорте, допустимо ли раскрывать время и какой порядок действительно требуется. Сортировка по идентификатору не заменяет явные created_at, статус обработки или номер версии бизнес-события. Эти поля описывают разные свойства объекта.
Версии проекта и установка без Symfony Framework
В проекте зафиксирован symfony/uid 7.4.21, установленный вместе с polyfill-uuid 1.43.0. Выбрана ветка Symfony 7.4 для PHP 8.2 и новее. В публичной части используется локальный Bootstrap 5.3.9. Конкретный набор зависимостей хранится в composer.lock; дата проверки версий — 13 ноября 2026 года.
Для примера с базой необходим PDO_SQLITE. Схема использует STRICT-таблицу, поэтому нужна SQLite не ниже 3.37. Проверяйте версию именно в том PHP-процессе, который запускает приложение: системная команда sqlite3 и встроенная библиотека PDO могут отличаться. В архиве тесты сами обращаются к настоящей SQLite.
Если начинаете проект самостоятельно, установите компонент командой require. Если скачали готовый архив, используйте install: он восстанавливает зафиксированный набор, а не подбирает новые версии. Автозагрузка обязательна. Подробности установки и lock-файла разобраны в нашей статье о Composer.
composer require symfony/uid:^7.4
# В скачанном проекте:
composer install --no-interaction --no-plugins --no-scripts
php tests/run.php
Мини-мастерская: что здесь выполнялось по-настоящему
Мастерская показывает запись реального выполнения PHP. Это позволяет безопасно открыть примеры на обычной странице без запуска пользовательского кода на хостинге. Выберите Windows или Linux, задачу и вариант входных данных. Посмотрите исходные значения, полученные строки, проверки и соответствующий PHP-файл. Кнопка пошагового разбора открывает пояснения, а поиск фильтрует уже записанные строки.
На странице нельзя получить новый случайный UUID нажатием кнопки: для этого предназначен скачиваемый проект. Генерация, преобразования, SQL и проверка владельца действительно выполнялись при подготовке записей. В каждом опыте показаны версия PHP, версия компонента, время записи и SHA-256 исходника. Windows и Linux содержат разные случайные значения, что ожидаемо.
Начните с UUIDv4, затем сравните порядок UUIDv7, откройте ошибочный формат и завершите запросом чужого объекта. Скачайте JSON выбранного запуска и исходник. Так вы получаете не только красивую строку, но и воспроизводимый пример поведения, включая отказ базы или функции маршрута.
UUID, ULID и представление: различаем три понятия
UUID — 128-битное значение с определённым форматом и версией. Привычная запись содержит 36 символов: шестнадцатеричные цифры и дефисы. Версия объясняет способ формирования, например случайный UUIDv4 или временной UUIDv7. ULID также занимает 128 бит, но обычно записывается двадцатью шестью символами Base32 и имеет другой внутренний формат.
Base32, Base58 и binary — способы представить данные. Преобразование одного UUID в Base58 не создаёт другой объект и не увеличивает его уникальность. В опыте хранения одна и та же сущность показана несколькими строками; затем библиотека восстанавливает исходные байты и сравнивает результат.
Важно договориться о формате на границе системы. Если маршрут принимает обычную UUID-строку, не следует неожиданно разрешать там все представления, которые понимает библиотека. Удобный внутренний парсер и публичный API имеют разные требования. Канонический формат уменьшает число вариантов адреса, упрощает журналирование и помогает заметить ошибки интеграции.
require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\Uid\Uuid;
$id = Uuid::v4();
echo (string) $id; // обычная UUID-строка
echo $id->toBase58(); // другое представление тех же 128 бит
$binary = $id->toBinary();
var_dump(strlen($binary)); // 16
Опыт 1. UUIDv4: независимое создание новой записи
Первый опыт делает пять отдельных вызовов Uuid::v4(). В записи видны пять разных значений, правильная версия и длина binary. Второй вариант намеренно получает объект один раз, а затем повторно использует ту же переменную. Результат содержит пять строк с одним ID. Это полезное различие при копировании объекта или подготовке пакетного импорта.
Если новая запись должна иметь новую идентичность, генерацию следует выполнять в момент её создания. Повторное использование уже выделенного ID, наоборот, помогает связать несколько частей одной операции. Например, строка заказа и сообщение о его создании должны ссылаться на один объект. Не генерируйте случайный ID заново при каждом повторе доставки одного сообщения без продуманного правила.
Набор из пяти разных значений не является математическим доказательством отсутствия коллизий. В базе всё равно нужен уникальный ключ, а приложение должно корректно обработать ошибку сохранения. UUID решает задачу распределённой генерации, но не отменяет ограничения данных и транзакции.
$id = Uuid::v4();
$order = ['id' => (string) $id, 'title' => 'Проверка оборудования'];
$event = ['order_id' => (string) $id, 'type' => 'order.created'];
// Один объект и событие о нём используют один и тот же ID.
// Для другого заказа нужен новый вызов Uuid::v4().
Опыт 2. UUIDv7: сортировка и время создания
UUIDv7 содержит временную часть, благодаря которой однотипные значения удобно упорядочивать. В опыте явно заданы три момента времени. Настоящий генератор создаёт идентификаторы, библиотека извлекает даты обратно, затем PHP сортирует канонические строки. Во втором варианте записи поступают в обратном порядке: порядок получения становится 3–2–1, а сортировка по ID возвращает 1–2–3.
Стандартное поле времени UUIDv7 измеряется в миллисекундах. Symfony 7.4 дополнительно использует часть доступных битов для микросекундной точности; это свойство выбранной реализации, а не обещание одинаковой точности у любого генератора UUIDv7. В лаборатории проверяется восстановление фиксированных дат именно установленной библиотекой.
Не делайте из сортируемого ID глобальный порядковый номер. Разные машины имеют разные часы, сообщения задерживаются, а транзакции фиксируются независимо. Для отчёта по бизнес-времени храните отдельное поле события. Для строгой последовательности внутри агрегата нужен свой номер версии или иной согласованный механизм.
use Symfony\Component\Uid\UuidV7;
$time = new DateTimeImmutable('2026-11-01T08:00:00.123456Z');
$id = new UuidV7(UuidV7::generate($time));
echo (string) $id;
echo $id->getDateTime()->format('Y-m-d\TH:i:s.u\Z');
// В обычной работе: Uuid::v7(), время берётся из окружения.
Порядок времени не равен порядку доставки
Представьте два обработчика, которые создают события почти одновременно. Даже если идентификаторы можно отсортировать по времени, потребитель очереди может сначала получить более позднее сообщение. Другой пример — создание ID перед долгой транзакцией: запись с ранним ID может стать видимой в базе позже следующей. Это не ошибка библиотеки, а различие между несколькими моментами жизненного цикла.
Поэтому у событий полезно явно разделять occurred_at, received_at и processed_at. В обычном каталоге может хватить created_at и updated_at, а в учёте изменений нужен порядковый номер версии сущности. Не извлекайте бизнес-правило из формата ID, если его можно выразить отдельным полем с понятным назначением.
В пределах одной реализации генератор может поддерживать монотонность близких вызовов. Но переносить это свойство на разные процессы, языки и ручные импорты без проверки нельзя. Наша мастерская демонстрирует известные моменты и сортировку конкретных значений, не измеряет распределённую согласованность или пропускную способность кластера.
Опыт 3. ULID: короткая строка и обратное преобразование
ULID часто удобен для идентификаторов событий и публичных записей, когда хочется получить сравнительно короткую строку с временным порядком. В первом варианте библиотека генерирует ULID для заданного момента. Во втором разбирается известное значение в нижнем регистре. Результат приводится к канонической форме, показываются время, Base58 и исходные байты.
Проверьте roundTrip: значение превращается в binary, затем восстанавливается и сравнивается методом equals. Это важнее визуального сходства двух строк. Ошибка кодировки, случайное добавление пробела или неверный способ передачи binary могут испортить идентификатор, даже если большинство символов осталось прежним.
ULID содержит временную информацию, поэтому не подходит, если сам факт времени создания должен быть скрыт. Его строка не является секретом или разрешением на операцию. При выборе между UUIDv7 и ULID учитывайте уже существующие API, типы базы, инструменты поддержки и договорённости команды, а не только длину представления.
use Symfony\Component\Uid\Ulid;
$id = Ulid::fromString('01arz3ndektsv4rrffq69g5fav');
$restored = Ulid::fromBinary($id->toBinary());
echo (string) $id; // 01ARZ3NDEKTSV4RRFFQ69G5FAV
var_dump($restored->equals($id)); // true
Опыт 4. Разбор ID: формат и требования маршрута
Публичному обработчику недостаточно проверить, что строка похожа на идентификатор. Нужно определить допустимое представление и версию. В нашем примере маршрут принимает только каноническую UUID-строку и только UUIDv7. Корректный UUIDv4 проходит синтаксическую проверку, но отклоняется политикой маршрута. Повреждённая строка отбрасывается ещё раньше.
Верхний регистр шестнадцатеричных цифр допустим и нормализуется библиотекой. Пробелы вокруг значения наш контракт не принимает: сервер не должен молча угадывать намерение отправителя. Для другой формы вы можете явно выбрать trim, но это должно быть осознанным правилом ввода, одинаковым во всех точках приложения.
Нулевой UUID показывает ещё одну границу: специальное значение может иметь корректный формат, но не подходить в качестве рабочей ссылки. Проверка синтаксиса не обращается к базе и не устанавливает наличие объекта. После разбора запрос к хранилищу и проверка прав остаются отдельными шагами.
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\UuidV7;
if (!Uuid::isValid($input, Uuid::FORMAT_RFC_9562)) {
throw new InvalidArgumentException('Expected UUID text');
}
$id = Uuid::fromString($input);
if (!$id instanceof UuidV7) {
throw new InvalidArgumentException('Expected UUIDv7');
}
Сравнение, нормализация и значение nil
После разбора используйте объект идентификатора или одно выбранное каноническое представление. Сравнивать исходные строки разных форматов неудобно: один UUID может прийти с другим регистром или быть представлен Base58. Метод equals выражает сравнение самих идентификаторов, а toBinary позволяет проверить реальные байты.
Нормализация особенно важна перед уникальным сохранением. Если хранить произвольные текстовые формы без правил, база может считать две строки разными, хотя приложение считает их одной идентичностью. В опыте SQLite верхний регистр сначала разбирается в UUID, затем превращается в те же 16 байт; повторная вставка правильно нарушает уникальность.
Не используйте nil как незаметную замену отсутствующего значения. Если поле необязательное, null часто лучше передаёт смысл. Если ID обязателен, ошибку нужно показать или вернуть вызывающей стороне. Подстановка специального значения способна связать разные ошибочные операции с одним ложным объектом и затруднить диагностику.
$first = Uuid::fromString('01890f3e-8a22-7c10-9123-123456789abc');
$second = Uuid::fromString('01890F3E-8A22-7C10-9123-123456789ABC');
var_dump($first->equals($second));
var_dump($first->toBinary() === $second->toBinary());
Опыт 5. UUIDv5: повторяемая идентичность импорта
При регулярном импорте часто нужно узнавать уже существующую запись. Если внешний источник имеет устойчивый ключ, можно определить namespace и получать UUIDv5 из пары namespace + имя. Повтор одинаковой пары даст то же значение. В первом варианте мастерской это проверяется двумя настоящими вызовами библиотеки.
Во втором варианте меняется регистр имени, затем пространство имён. Получаются другие идентификаторы. Это напоминает, что перед вычислением нужен контракт: точный источник, формат ключа, правила регистра, пробелов и кодировки. Нельзя начать с одного правила, а через месяц незаметно изменить нормализацию: прежние объекты получат новые ID.
UUIDv5 использует SHA-1 как часть стандартизированного алгоритма идентификатора. Это не делает его паролем, подписью или способом скрыть персональные данные. Если исходные имена можно перебрать, результат также воспроизводим. Для секрета или криптографического доказательства нужны другие механизмы.
$namespace = Uuid::fromString(Uuid::NAMESPACE_URL);
$name = 'erp:order:42';
$id = Uuid::v5($namespace, $name);
$again = Uuid::v5($namespace, $name);
var_dump($id->equals($again)); // true
// В рабочей интеграции namespace и схема имени являются частью контракта.
UUIDv5 и идемпотентность: похожие, но разные задачи
Повторяемый ID помогает найти одну и ту же сущность, но сам по себе не гарантирует однократное выполнение операции. Импорт может дважды списать остаток, отправить письмо или создать платёж, даже если использует одинаковый идентификатор заказа. Идентичность объекта и идемпотентность действия следует описывать отдельно.
Для повторяемой операции сохраните её ключ, состояние и результат в согласованной транзакции. Повторный запрос должен либо вернуть уже полученный ответ, либо безопасно продолжить незавершённую работу. Уникальное ограничение помогает обнаружить повтор, но реакцию на него определяет приложение. Нельзя просто поймать любую ошибку INSERT и считать операцию успешной.
Полезный порядок внедрения: сначала определить, что считается одним объектом, затем — что считается одной операцией над ним. После этого выбрать UUIDv5, отдельный idempotency key или обычную таблицу соответствия внешних и внутренних ключей. Последний вариант бывает понятнее, если внешние источники меняют свои правила.
Опыт 6. Текстовое и бинарное хранение
UUID-строка занимает 36 ASCII-символов, binary — 16 байт. Hex-показ binary содержит 32 символа, но это только читаемое представление, а не размер сохранённых данных. Мастерская специально выводит оба числа, чтобы не спутать байты с их отображением. Для ULID каноническая строка имеет 26 символов, а исходное значение также занимает 16 байт.
Текст удобен при ручном просмотре базы, логах и обмене JSON. Binary может уменьшить размер ключей и индексов, но требует последовательного преобразования на всех границах. Универсального обещания ускорения здесь нет: реальный результат зависит от СУБД, схемы, запросов, объёма и характера вставок.
Не отправляйте произвольные binary-байты как строку UTF-8 в JSON. Для внешнего API верните канонический текст. При миграции существующего поля сначала проверьте все значения, найдите дубликаты после нормализации и подготовьте откат. Смена физического формата поля не обязана менять публичные URL.
$binary = $id->toBinary();
echo strlen($binary); // 16
echo strlen(bin2hex($binary)); // 32: длина hex-представления
$restored = Uuid::fromBinary($binary);
echo json_encode(['id' => (string) $restored], JSON_THROW_ON_ERROR);
Опыт 7. Настоящая SQLite через PDO
В седьмой задаче PHP действительно создаёт SQLite в памяти, таблицу STRICT и запись. BLOB-ключ имеет ограничение длины 16 байт; владелец и заголовок хранятся отдельными полями. INSERT выполняется подготовленным запросом, а ID передаётся через PDO::PARAM_LOB. Затем SELECT показывает typeof, length и hex, подтверждая реальный формат хранения.
Обратное чтение превращает BLOB в UUID и возвращает обычную строку для ответа. Это небольшой законченный слой доступа к данным: вызывающий код работает с идентификатором, а особенности хранения остаются внутри Repository. В рабочем проекте создание схемы нужно перенести в миграцию, а не повторять при каждом запросе.
Если PDO пока незнаком, начните с нашего раздела «SQL и PDO с нуля». Продолжение с реальным приложением находится в главе «PDO_SQLITE: локальный каталог, доступ и связанный импорт». Это прямые ссылки на нужные части большого PHP-мануала сайта.
$db = new PDO('sqlite::memory:');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('CREATE TABLE orders (
id BLOB PRIMARY KEY NOT NULL CHECK(length(id)=16),
owner_id INTEGER NOT NULL,
title TEXT NOT NULL
) STRICT');
Подготовленные запросы и явный тип BLOB
Подготовленный запрос отделяет структуру SQL от значений. В Repository используются именованные параметры, а типы передаются явно: binary как PARAM_LOB, владелец как PARAM_INT, заголовок как PARAM_STR. Такой код проще проверять, чем сборку SQL через соединение строк, особенно при наличии произвольного пользовательского текста.
При SELECT используется то же бинарное представление. Частая ошибка — сохранить 16 байт, а искать по 36-символьной строке без преобразования. Запрос будет выглядеть правильным, но не найдёт запись. Поэтому проверка round-trip и тест полного пути INSERT → SELECT → UUID входят в проект.
Ограничение поля и проверка приложения дополняют друг друга. Код валидирует вход заранее и даёт понятную ошибку, база защищает целостность при других путях записи. Не стоит полагаться только на HTML-форму: запрос может прийти от интеграции, очереди, консольной команды или нового обработчика, о котором текущая форма ничего не знает.
$query = $db->prepare('INSERT INTO orders(id,owner_id,title)
VALUES(:id,:owner,:title)');
$query->bindValue(':id', $id->toBinary(), PDO::PARAM_LOB);
$query->bindValue(':owner', 7, PDO::PARAM_INT);
$query->bindValue(':title', 'Проверка оборудования', PDO::PARAM_STR);
$query->execute();
Ошибки SQLite: дубль и неверная длина
Второй вариант опыта пытается вставить тот же UUID в другом регистре. После разбора это те же байты, поэтому срабатывает первичный ключ. Третий вариант передаёт короткий BLOB: ограничение CHECK отклоняет его. В обоих случаях исходная запись остаётся единственной. Мастерская показывает фактический SQLSTATE и классификацию ожидаемого отказа.
В прикладном API техническое сообщение базы обычно не следует возвращать пользователю целиком. Определите, какой конфликт можно объяснить как повтор запроса, а какой означает ошибку программы. Сохраняйте диагностический контекст без лишних персональных данных, а клиенту возвращайте устойчивый код и понятное описание.
Эти небольшие опыты не проверяют нагрузку, резервное копирование или конкурентную работу вашего сервера. Они подтверждают конкретный контракт хранения. Для блокировок, снимков и планов запросов есть отдельная глава PHP-мануала; для изменения схемы пригодится материал о миграциях Phinx. UUID не освобождает приложение от обычной эксплуатации базы.
PDO_SQLITE: конкуренция и результаты запросов · Миграции Phinx
Опыт 8. Идентификатор в API и отдельная авторизация
Последний опыт выполняет настоящую PHP-функцию обработчика, которая обращается к SQLite. Сетевая HTTP-служба в мастерской не запускается: показан записанный результат функции с выбранным статусом. Собственная запись даёт 200, чужая — 404, неправильный ID — 400, отсутствие установленной учётной записи — 401.
Параметр authenticatedActor предполагается результатом уже выполненной серверной аутентификации. Его нельзя брать из owner_id, присланного клиентом. После проверки формата SELECT ищет одновременно идентификатор и владельца. Поэтому знание чужого UUID не открывает объект. В выбранной политике чужая и отсутствующая запись выглядят одинаково; другой сервис может использовать собственный согласованный контракт ошибок.
Сложный ID лишь меняет удобство перебора. Он не доказывает право читать, изменять или удалять данные. Проверяйте конкретную операцию и границы организации на сервере. Удаление или экспорт могут требовать дополнительных прав даже у пользователя, которому разрешён обычный просмотр карточки.
$query = $db->prepare('SELECT id,title FROM orders
WHERE id=:id AND owner_id=:owner');
$query->bindValue(':id', $id->toBinary(), PDO::PARAM_LOB);
$query->bindValue(':owner', $authenticatedActor, PDO::PARAM_INT);
$query->execute();
$row = $query->fetch(PDO::FETCH_ASSOC);
// Нет совпадения — выбранная политикой ошибка not_found.
Windows: запустить проект и проверить собственный ID
Распакуйте архив в обычный рабочий каталог. Проверьте PHP CLI, Composer и наличие pdo_sqlite. Команда php –ini показывает конфигурацию консоли: она может отличаться от настроек Apache или PHP-FPM. После установки зависимостей сначала запустите тесты, затем один опыт, чтобы убедиться в работоспособности окружения.
Команда inspect принимает ваш текстовый UUID или ULID, разбирает его настоящей библиотекой и возвращает JSON с представлениями. Успех имеет код завершения 0; неправильный формат — 2 и сообщение в stderr. Это удобно для небольшого инструмента поддержки или проверки входного файла без веб-интерфейса.
Для просмотра уже готовой public достаточно локального HTTP-сервера. Адрес привязан к loopback; после работы остановите только свой процесс через Ctrl+C. Открытие файла напрямую через file:// не подходит для загрузки JSON-записей. Сам просмотр статической мастерской не требует запуска PHP-обработчика на публичном сайте.
php -v
php --ini
php -m
composer install --no-interaction --no-plugins --no-scripts
php tests/run.php
php bin/inspect.php 01890f3e-8a22-7c10-9123-123456789abc
php bin/run.php sqlite 0
php -S 127.0.0.1:8531 -t public
Linux: повторить выполнение и собрать запись
В Linux используются те же PHP-файлы и lock-зависимости. Установите поддерживаемый PHP нужной версии и расширение SQLite для своего дистрибутива. Для Debian/Ubuntu ориентиром служат php-cli и php-sqlite3, но конкретные версии нужно проверить после установки. Composer запускайте от обычного пользователя в каталоге проекта.
capture формирует JSON реального выполнения и хеши исходников. build-public обновляет просмотр кода, README и лицензии. Случайные идентификаторы в новой записи закономерно отличаются от прежних, поэтому сравнивать нужно проверяемые свойства: версию, длину, обратное преобразование, сортировку фиксированных дат и коды отказов.
Наши тесты прошли на Windows PHP 8.3.33 и в чистом Linux-контейнере PHP 8.5.12. Это не означает, что автоматически настроен ваш внешний VPS: права каталогов, конфигурацию FPM, резервное копирование и сетевой доступ нужно проверять в целевом окружении. В архиве граница этих проверок указана отдельно.
sudo apt update
sudo apt install php-cli php-sqlite3 unzip
php -v
composer install --no-interaction --no-plugins --no-scripts
php tests/run.php
php bin/capture.php recording-linux.json
php bin/build-public.php
Linux/VPS: где хранить базу и как подключить приложение
Компонент Uid не требует отдельной службы. Он работает внутри PHP-процесса. Если вам нужна только опубликованная мастерская, раздайте public существующим Nginx или Caddy. Если переносите Repository в приложение, выберите абсолютный путь к SQLite вне web-root и отдельного пользователя с доступом к файлу и его каталогу.
SQLite создаёт вспомогательные файлы рядом с базой, поэтому одной возможности прочитать data.sqlite недостаточно. Не решайте проблему прав через chmod 777. Настройте владельца и группу под конкретный процесс, проверьте запись, блокировки и восстановление резервной копии. CLI и FPM могут иметь разные права и разные конфигурации PHP.
Схему перенесите в миграции, параметры подключения — в конфигурацию окружения. Внешний API продолжайте отдавать в понятном строковом формате, даже если внутри хранится BLOB. Ошибки преобразования и дубликаты после нормализации нужно выявить до переключения действующей базы, желательно на её проверенной копии.
$db = new PDO('sqlite:/var/lib/my-app/data.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('PRAGMA busy_timeout = 3000');
// Схема создаётся миграцией, а не произвольным запросом посетителя.
Пакетный импорт, cron и обработка повторов
Для фонового процесса важны не только идентификаторы, но и окружение запуска. В cron укажите абсолютный путь PHP и рабочий каталог. Проверьте выполнение под тем же пользователем, который будет запускать расписание. Отдельно направьте результат и ошибки, предусмотрите ротацию файлов и сигнал о неуспешном завершении.
Учебная команда ниже сохраняет отчёт SQLite-опыта, а не обрабатывает реальные заказы. При переносе на импорт добавьте транзакции, ограничения партии и контроль повторов. Если два запуска могут пересечься, нужен отдельный механизм блокировки. Новый UUID для каждой попытки способен только скрыть повтор, а не исправить его.
Хороший журнал операции содержит собственный идентификатор запуска, ключ обрабатываемого объекта и понятный результат. Не смешивайте их в одном поле. Тогда можно отличить повторную доставку сообщения, новую бизнес-операцию и повторный технический запуск одного обработчика. Для этого UUID полезен как инструмент связи, а не как замена модели процесса.
# Каталоги подготовьте заранее с нужным владельцем.
15 3 * * * cd /opt/uid-lab && /usr/bin/php bin/run.php sqlite 0 > /var/lib/uid-lab/last-demo.json 2> /var/lib/uid-lab/last-demo.err
Проверки проекта и границы воспроизводимости
Проект содержит 93 проверки на каждой ОС. Они выполняют все восемь задач и двадцать один вариант, проверяют формат и длину, восстановление даты, нормализацию, namespace UUIDv5, обратное преобразование binary, SQL-ограничения, принадлежность записи и реальные exit code консольного inspect. Это проверки поведения установленной библиотеки и нашего прикладного кода.
В записях есть SHA-256 PHP-файлов и Composer lock. Поэтому можно проверить, каким исходником получен результат. Случайные значения не сравниваются между ОС как неизменяемые константы: сравниваются свойства и смысл результата. Фиксированные входы UUIDv5, хранения и SQLite дают устойчивые данные, а версии SQLite указаны отдельно.
Не выводите из этого набора доказательство отсутствия всех коллизий, безопасности вашего приложения или производительности большой базы. Перед интеграцией добавьте свои ограничения, миграционные данные, права и сценарии повторного выполнения. Отдельно проверяйте внешний API и интерфейс: успешно разобранный ID может всё ещё вести к непонятной ошибке для пользователя.
composer validate --strict
composer audit
php tests/run.php
php bin/capture.php
php bin/build-public.php
# Windows/Linux: сравнивайте sourceHashes и проверяемые свойства,
# а не требуйте равенства новых случайных UUID.
Что скачать и какие источники использовать дальше
В архиве находятся компонент с lock-файлом, восемь PHP-сценариев, слой SQLite/PDO, две консольные команды, тесты, записи Windows/Linux и статический интерфейс. Авторский код распространяется по MIT; тексты лицензий зависимостей сохранены рядом. Начать можно с одного собственного поля ID, не перестраивая приложение целиком.
Для API компонента используйте документацию Symfony Uid 7.4. Устройство UUID и границы порядка описаны в RFC 9562, формат ULID — в спецификации ULID. Работа расширения базы находится в официальном руководстве PDO_SQLITE.
При выборе решения закрепите три правила: как создаётся идентичность, в каком формате она хранится и кто может выполнять операцию. Затем повторите успешный и отрицательный сценарий на своих данных. Именно это превращает генератор красивых строк в полезную часть приложения, которую можно сопровождать и объяснить следующему разработчику.


