Shortify

Сокращение URL: модель, сценарий и контракт хранения на PHP 8.3.

Небольшая PHP-библиотека как практический разбор DDD: валидация URL, фасад shorten/expand, репозиторий и явные ограничения генерации кода.

PHP · DDD · URL Shortener · Architecture

Моя роль

Автор библиотеки: модель URL, сервис сокращения, фасад, репозиторий и тесты.

Open source · Сентябрь 2026. Разбор опубликованного кода на коммите 779d5e59. Ссылки закреплены на этой версии; ограничения описаны для неё.

Небольшой домен с понятной границей хранения

Я написал Shortify как PHP-библиотеку для двух операций: получить короткий код исходного URL и найти исходный URL по коду. Это удобная задача для проверки архитектуры на небольшом масштабе: в ней есть валидация, генерация значения, хранение и обработка отсутствующей записи. Все эти обязанности можно увидеть в нескольких классах и обсудить по конкретному коду.

Моя роль — автор модели URL, сервиса сокращения, фасада, интерфейса репозитория и тестов. Пакет требует PHP ^8.3; это подтверждается composer.json. Сторонних runtime-библиотек в разделе require нет. Здесь DDD выражается в названных операциях и явных зависимостях.

Путь создания короткого кода
  1. ShortifyФасад shorten / expand
  2. CoreUrl, Shortener и CodeGenerator
  3. RepositoryКонтракт хранения и выбранный адаптер

В комплекте есть InMemoryRepository для локального сценария. Постоянное хранилище и HTTP-перенаправление подключает приложение.

Сценарий: сократить и раскрыть одну ссылку

Для знакомства с библиотекой не нужна база данных. InMemoryRepository из исходников хранит объекты в массиве текущего процесса. Фасад получает этот репозиторий, создаёт URL с коротким кодом и возвращает исходный адрес при последующем вызове expand. Ниже полный пример после установки пакета.

<?php
require __DIR__ . '/vendor/autoload.php';

use Tenqz\Shortify\Shortify;
use Tenqz\Shortify\Infrastructure\Repository\InMemoryRepository;

$shortify = new Shortify(new InMemoryRepository());
$original = 'https://example.com/articles/php-boundaries';
$url = $shortify->shorten($original);
$code = $url->getShortCode();
$expanded = $shortify->expand($code);

echo $code . PHP_EOL;
echo $expanded . PHP_EOL;
if ($expanded !== $original) {
    throw new RuntimeException('Round trip failed');
}

Первая строка результата — сгенерированный код, который не следует заранее фиксировать в примере. Вторая должна совпасть с исходным URL; эта проверка записана прямо в коде. После завершения процесса соответствие исчезнет. Публичного короткого адреса пока нет: домен, маршрут и HTTP-ответ перенаправления создаёт веб-приложение.

Решение 1: проверять URL при создании объекта

Url проверяет формат через FILTER_VALIDATE_URL, разрешает только http и https и требует host. Неудачная проверка даёт InvalidUrlException до сохранения. Это единая точка проверки входа для сценария сокращения. Объект изменяемый: исходный адрес задаётся в конструкторе, а короткий код назначается через setShortCode.

Решение 2: зависеть от контракта репозитория

Shortener получает UrlRepositoryInterface с методами save, findByCode и exists. Сервис не содержит SQL и не выбирает базу. Адаптер приложения может сохранять те же объекты в подходящем хранилище, сохраняя публичные операции библиотеки. Интерфейс размещён в namespace Infrastructure; сервис Core обращается к этому контракту, а конкретный адаптер передаётся через конструктор.

Решение 3: отделить сценарий от точки входа

Фасад Shortify собирает Shortener и генератор кода. Внутри сервиса последовательность видна непосредственно: создать Url, сгенерировать код, назначить его и сохранить. Обратная операция ищет запись и бросает UrlNotFoundException, если её нет. Приложение само преобразует эту ошибку в HTTP 404 или другое поведение своего интерфейса.

Что реализовано и как это проверяется

Результат — отдельный Composer-пакет с коротким входным API и заменяемым хранилищем. В интеграционном тесте есть сценарий shorten → expand с in-memory репозиторием, ошибочный URL и отсутствующий код. Unit-тест сервиса проверяет взаимодействие с репозиторием и генератором. Реализация Shortener позволяет сопоставить эти ожидания с последовательностью действий.

Проверяемый контракт результата — сохранённый код раскрывается в исходный URL, неверный адрес отклоняется, отсутствующий код даёт отдельное исключение. Интеграционная проверка охватывает взаимодействие классов внутри одного процесса. Конкурентная запись в постоянное хранилище требует отдельной проверки адаптера.

Что потребуется для публичного сервиса

Генератор по умолчанию берёт шесть символов из MD5-строки, построенной с URL, временем и случайными байтами. Хотя исходный алфавит содержит буквы и цифры, итоговая строка шестнадцатеричная. Код не является токеном доступа или гарантированно уникальным идентификатором.

В Shortener нет проверки коллизии и повторной генерации; exists в этом сценарии не вызывается. InMemoryRepository при совпадении ключа заменяет запись. Для постоянного сервиса нужен уникальный индекс и отдельная политика обработки конфликта с ограниченным числом попыток. Проверка «сначала exists, потом save» сама по себе не защищает от параллельных запросов.

Также вне библиотеки остаются срок жизни ссылок, блокировка нежелательных адресов, аналитика и управление доступом. Валидация синтаксиса URL не проверяет безопасность содержимого по этому адресу.

Как попробовать

composer require tenqz/shortify

На PHP 8.3 или новее сохраните пример рядом с vendor и запустите его. Затем изучите контракт репозитория и адаптер в памяти. Полный проект доступен в tenqz/shortify; начинать расширение стоит с требований к хранению и коллизиям.

GitHub

Давайте делать сложное понятным.

Архитектура, инженерное лидерство и AI в разработке — когда система слишком важна, чтобы её упрощать, и слишком дорогая, чтобы ею не владеть.

LinkedIn Telegram Email