From 2d6d7f21a5669ec0b18cb9dab67c8a3629484b6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:48:45 +0000 Subject: [PATCH 01/18] Modernize core package tooling Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- .github/workflows/ci.yml | 35 ++++++++ .gitignore | 5 +- .travis.yml | 19 ----- README.md | 84 +++++++++--------- bin/genset | 6 +- composer.json | 43 ++++++++-- phpunit.xml | 42 ++------- src/ZackKitzmiller/InvalidCharacterSet.php | 11 +++ src/ZackKitzmiller/Tiny.php | 99 ++++++++++++++-------- tests/TinyTest.php | 85 +++++++++++++------ 10 files changed, 259 insertions(+), 170 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml create mode 100644 src/ZackKitzmiller/InvalidCharacterSet.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5032934 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ['8.1', '8.2', '8.3', '8.4'] + + steps: + - name: Check out repository + uses: actions/checkout@v7.0.1 + + - name: Set up PHP + uses: shivammathur/setup-php@2.37.2 + with: + php-version: ${{ matrix.php-version }} + coverage: none + + - name: Validate composer configuration + run: composer validate --strict + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run test suite + run: composer test diff --git a/.gitignore b/.gitignore index 914bf13..55a6c02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -vendor +vendor/ composer.phar composer.lock .DS_Store -build +build/ +.phpunit.cache/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b9326f2..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: php - -php: - - hhvm - - 5.6 - - 5.5 - - 5.4 - - 5.3 - -before_script: - - composer self-update - - composer install - -script: - - phpunit --coverage-text - -matrix: - allow_failures: - - php: hhvm diff --git a/README.md b/README.md index 309003d..156b821 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,31 @@ -[![Build Status](https://travis-ci.org/zackkitzmiller/tiny-php.png?branch=master)](https://travis-ci.org/zackkitzmiller/tiny-php) -[![Coverage Status](https://coveralls.io/repos/zackkitzmiller/tiny-php/badge.png?branch=master)](https://coveralls.io/r/zackkitzmiller/tiny-php?branch=master) +[![CI](https://github.com/zackkitzmiller/tiny-php/actions/workflows/ci.yml/badge.svg)](https://github.com/zackkitzmiller/tiny-php/actions/workflows/ci.yml) # Tiny -A reversible base62 ID obfuscater +Tiny is a small reversible ID obfuscator for PHP. It encodes integers into a custom alphabet and decodes them back again. -## Authors +## What's modernized -Originally by Jacob DeHart, with Ruby and Python ports by Kyle Bragger - -Now maintained by [Zack Kitzmiller](https://github.com/zackkitzmiller). +- PHP 8.1+ baseline +- PSR-4 autoloading +- PHPUnit 11 test suite +- GitHub Actions CI +- Safer alphabet validation and decoding errors ## Installation -Install via Composer - -```json -{ - "require": { - "zackkitzmiller/tiny": "1.2.0" - }, -} +```bash +composer require zackkitzmiller/tiny ``` ## Usage ```php -$tiny = new \ZackKitzmiller\Tiny('5SX0TEjkR1mLOw8Gvq2VyJxIFhgCAYidrclDWaM3so9bfzZpuUenKtP74QNH6B'); +to(5); // E @@ -39,47 +38,42 @@ echo $tiny->to(126); echo $tiny->from('XX'); // 126 +``` -echo $tiny->to(999); -// vk +## Character set rules -echo $tiny->from('vk'); -// 999 +Your alphabet must: -``` +- contain at least 2 characters +- only contain unique characters +- stay fixed forever once you start issuing encoded IDs -## Configuration +Tiny throws an exception when the alphabet is invalid or when you try to decode characters that are not in the configured alphabet. -You must instanciate a new instance of Tiny with a random alpha-numeric set where each character must only be used exactly once. Do **NOT** change this once you start using Tiny, as you won't be able to reverse. +## Generate an alphabet -You can generate a random set from the commandline with `$ ./bin/genset` +After installing dependencies, generate a fresh 62-character alphabet with: -## Using laravel? +```bash +./bin/genset +``` -If you're using laravel and want to use a more laravel-like and cleaner syntax you only have to follow these steps. +You can also generate one in code: -First open your ``app/config/app.php`` file and scroll down to your providers and add -```php -'providers' => array( - ... - 'ZackKitzmiller\TinyServiceProvider', -) -``` -and then this to aliases ```php -'aliases' => array( - ... - 'Tiny' => 'ZackKitzmiller\Facades\Tiny', -) +$set = Tiny::generateSet(); ``` -Lastly you run ``php artisan config:publish zackkitzmiller/tiny`` to publish the configuration file and then run ``php artisan tiny:generate`` to create a valid key. +The legacy `Tiny::generate_set()` helper is still available for backwards compatibility. -### Usage in Laravel -```php -echo Tiny::to(999); -// echos vk +## Development -echo Tiny::from('E'); -// echos 5 +```bash +composer install +composer test +composer check ``` + +## Legacy Laravel integration + +The repository still includes the original Laravel integration classes for older applications, but the package is now centered on the framework-agnostic core library. diff --git a/bin/genset b/bin/genset index 92541dd..d36a8e6 100755 --- a/bin/genset +++ b/bin/genset @@ -2,13 +2,13 @@ =5.3.0" + "php": "^8.1" }, "require-dev": { - "phpunit/phpunit": "~3.7.0", - "league/phpunit-coverage-listener": "~1.1" + "phpunit/phpunit": "^11.5" }, "autoload": { - "psr-0": {"ZackKitzmiller": "src/"} + "psr-4": { + "ZackKitzmiller\\": "src/ZackKitzmiller/" + } + }, + "autoload-dev": { + "classmap": [ + "tests/" + ] + }, + "bin": [ + "bin/genset" + ], + "scripts": { + "test": "phpunit", + "test:coverage": "phpunit --coverage-text", + "check": [ + "@composer validate --strict", + "@test" + ] + }, + "config": { + "sort-packages": true } } diff --git a/phpunit.xml b/phpunit.xml index 3a1d951..9e8aa6c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,43 +1,13 @@ - - + tests - - - - - - src/ZackKitzmiller/Tiny.php - - - - - - - - - - - - - - ZackKitzmiller - - - 4GggSuZVGvVaxBi8E9IZTqG31EXFvjMe7 - - - https://coveralls.io/api/v1/jobs - - - build/ - - - - - + + + src/ZackKitzmiller + + diff --git a/src/ZackKitzmiller/InvalidCharacterSet.php b/src/ZackKitzmiller/InvalidCharacterSet.php new file mode 100644 index 0000000..04f4dc9 --- /dev/null +++ b/src/ZackKitzmiller/InvalidCharacterSet.php @@ -0,0 +1,11 @@ +set = $set; } - public function to($id) + public function to(int|string $id): string { - $set = $this->set; - - $hexn = ''; - $id = floor(abs(intval($id))); - $radix = strlen($set); - while (true) { - $r = $id % $radix; - $hexn = $set[$r] . $hexn; - $id = ($id - $r) / $radix; - if ($id == 0) { - break; - } - } - return $hexn; + $encoded = ''; + $value = abs((int) $id); + $radix = strlen($this->set); + + do { + $remainder = $value % $radix; + $encoded = $this->set[$remainder] . $encoded; + $value = intdiv($value - $remainder, $radix); + } while ($value > 0); + + return $encoded; } - public function from($str) + public function from(string $value): int { - $set = $this->set; + if ($value === '') { + return 0; + } + + $radix = strlen($this->set); + $decoded = 0; + + foreach (str_split($value) as $character) { + $position = strpos($this->set, $character); - $radix = strlen($set); - $strlen = strlen($str); - $n = 0; - for ($i = 0; $i < $strlen; $i++) { - $n += strpos($set, $str[$i]) * pow($radix, ($strlen - $i - 1)); + if ($position === false) { + throw new InvalidArgumentException(sprintf('Character "%s" is not in the Tiny character set.', $character)); + } + + $decoded = ($decoded * $radix) + $position; } - return $n; + + return $decoded; } - public static function generate_set() + public static function generateSet(): string { - $arr = array(); + $characters = [ + ...range('A', 'Z'), + ...range('a', 'z'), + ...range('0', '9'), + ]; - for ($i = 65; $i <= 122; $i++) { - if ($i < 91 || $i > 96) $arr[] = chr($i); - } + shuffle($characters); - $arr = array_merge($arr, range(0, 9)); - shuffle($arr); + return implode('', $characters); + } - return join('', $arr); + public static function generate_set(): string + { + return self::generateSet(); } + private static function assertValidSet(string $set): void + { + if (strlen($set) < 2) { + throw new InvalidCharacterSet('Tiny requires at least two unique characters in the set.'); + } + + if (count(array_unique(str_split($set))) !== strlen($set)) { + throw new InvalidCharacterSet('Tiny character sets must only contain unique characters.'); + } + } } diff --git a/tests/TinyTest.php b/tests/TinyTest.php index e65699f..7f7dbe4 100644 --- a/tests/TinyTest.php +++ b/tests/TinyTest.php @@ -1,45 +1,80 @@ tiny = new Tiny('5SX0TEjkR1mLOw8Gvq2VyJxIFhgCAYidrclDWaM3so9bfzZpuUenKtP74QNH6B'); } - public function testToTiny() { - $converted = $this->tiny->to(5); - $this->assertEquals('E', $converted); + public function testToTiny(): void + { + self::assertSame('E', $this->tiny->to(5)); } - public function testFromTiny() { - $reversed = $this->tiny->from('E'); - $this->assertEquals(5, $reversed); + public function testFromTiny(): void + { + self::assertSame(5, $this->tiny->from('E')); } - public function testReversingRandomInt() { - for ($i = 0; $i <= 100; $i++) { - $this->assertEquals($this->tiny->from($this->tiny->to($i)), $i); + public function testReversingRandomInt(): void + { + for ($i = 0; $i <= 1000; $i++) { + self::assertSame($i, $this->tiny->from($this->tiny->to($i))); } } - public function testGenerateRandomSetsWork() { - for ($i = 0; $i <= 1000; $i++) { - $tiny = new Tiny(Tiny::generate_set()); - $this->assertEquals($tiny->from($tiny->to($i)), $i); - } + public function testNegativeValuesAreNormalized(): void + { + self::assertSame($this->tiny->to(25), $this->tiny->to(-25)); } - public function testGenerateSetUnique() { - $set = Tiny::generate_set(); - $set_parts = str_split($set); - $used = array(); - foreach ($set_parts as $char) { - $this->assertArrayNotHasKey($char, $used); - $used[$char] = $char; + public function testGenerateRandomSetsWork(): void + { + for ($i = 0; $i <= 100; $i++) { + $tiny = new Tiny(Tiny::generateSet()); + self::assertSame($i, $tiny->from($tiny->to($i))); } } + + public function testGenerateSetAliasRemainsAvailable(): void + { + self::assertSame(62, strlen(Tiny::generate_set())); + } + + public function testGenerateSetIsUnique(): void + { + $set = Tiny::generateSet(); + self::assertCount(62, array_unique(str_split($set))); + } + + public function testConstructorRejectsDuplicateCharacters(): void + { + $this->expectException(InvalidCharacterSet::class); + + new Tiny('aa'); + } + + public function testConstructorRejectsSmallCharacterSets(): void + { + $this->expectException(InvalidCharacterSet::class); + + new Tiny('a'); + } + + public function testFromRejectsUnknownCharacters(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->tiny->from('!'); + } } From 807f44377bbd6a30ba876a75602fbad8657ae06b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:50:33 +0000 Subject: [PATCH 02/18] Modernize library implementation Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- .gitignore | 1 + bin/genset | 5 +- phpunit.xml | 7 +-- src/ZackKitzmiller/Facades/Tiny.php | 13 +++-- .../Laravel5/TinyGenerateCommand.php | 57 +++++++++++++------ .../Laravel5/TinyServiceProvider.php | 27 ++++++--- src/ZackKitzmiller/TinyGenerateCommand.php | 44 +++++++------- src/ZackKitzmiller/TinyServiceProvider.php | 31 +++++----- src/config/config.php | 7 +-- tests/TinyTest.php | 3 +- 10 files changed, 113 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index 55a6c02..ae6eb3e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ composer.lock .DS_Store build/ .phpunit.cache/ +.phpunit.result.cache diff --git a/bin/genset b/bin/genset index d36a8e6..a8c8caa 100755 --- a/bin/genset +++ b/bin/genset @@ -1,8 +1,5 @@ #!/usr/bin/env php - - - + tests - - - src/ZackKitzmiller - - diff --git a/src/ZackKitzmiller/Facades/Tiny.php b/src/ZackKitzmiller/Facades/Tiny.php index d1cd555..2da7d84 100644 --- a/src/ZackKitzmiller/Facades/Tiny.php +++ b/src/ZackKitzmiller/Facades/Tiny.php @@ -1,12 +1,15 @@ -info("Tiny key [$key] has been set."); + if (file_put_contents($path, $content) === false) { + throw new RuntimeException(sprintf('Unable to write environment file at %s.', $path)); + } + + $this->info("Tiny key [{$key}] has been set."); + + return self::SUCCESS; } + public function fire(): int + { + return $this->handle(); + } } diff --git a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php index 852bdcd..d818e2e 100644 --- a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php +++ b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php @@ -1,30 +1,39 @@ -app->singleton('tiny.generate', function () { + $this->app->singleton('tiny.generate', static function () { return new TinyGenerateCommand(); }); $this->commands('tiny.generate'); } - public function register() + public function register(): void { - $this->app->singleton('tiny', function () { - $key = getenv('LEAGUE_TINY_KEY'); + $this->app->singleton('tiny', static function () { + $key = getenv('TINY_KEY') ?: getenv('LEAGUE_TINY_KEY'); + + if (! is_string($key) || $key === '') { + throw new RuntimeException('A Tiny character set must be configured via TINY_KEY or LEAGUE_TINY_KEY.'); + } return new Tiny($key); }); } - public function provides() + public function provides(): array { - return array('tiny'); + return ['tiny']; } } diff --git a/src/ZackKitzmiller/TinyGenerateCommand.php b/src/ZackKitzmiller/TinyGenerateCommand.php index 8147784..1cf4435 100644 --- a/src/ZackKitzmiller/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/TinyGenerateCommand.php @@ -1,43 +1,49 @@ -files = $files; } - public function fire() + public function handle(): int { - list($path, $contents) = $this->getKeyFile(); - - $key = Tiny::generate_set(); + [$path, $contents] = $this->getKeyFile(); + $key = Tiny::generateSet(); + $currentKey = (string) $this->laravel['config']['zackkitzmiller/tiny::key']; - $contents = str_replace($this->laravel['config']['zackkitzmiller/tiny::key'], $key, $contents); + $contents = str_replace($currentKey, $key, $contents); $this->files->put($path, $contents); - $this->laravel['config']['zackkitzmiller/tiny::key'] = $key; - $this->info("Tiny key [$key] has been set."); + $this->info("Tiny key [{$key}] has been set."); + + return self::SUCCESS; } - protected function getKeyFile() + public function fire(): int { - $env = $this->option('env') ? $this->option('env').'/' : ''; + return $this->handle(); + } - $contents = $this->files->get($path = $this->laravel['path']."/config/packages/zackkitzmiller/tiny/{$env}config.php"); + protected function getKeyFile(): array + { + $env = $this->option('env') ? $this->option('env') . '/' : ''; + $path = $this->laravel['path'] . "/config/packages/zackkitzmiller/tiny/{$env}config.php"; - return array($path, $contents); + return [$path, $this->files->get($path)]; } - } diff --git a/src/ZackKitzmiller/TinyServiceProvider.php b/src/ZackKitzmiller/TinyServiceProvider.php index cb3ea58..8c8c24a 100644 --- a/src/ZackKitzmiller/TinyServiceProvider.php +++ b/src/ZackKitzmiller/TinyServiceProvider.php @@ -1,34 +1,33 @@ -package('zackkitzmiller/tiny', 'zackkitzmiller/tiny', __DIR__.'/../'); + $this->package('zackkitzmiller/tiny', 'zackkitzmiller/tiny', __DIR__ . '/../'); - $this->app['tiny.generate'] = $this->app->share(function($app) - { + $this->app['tiny.generate'] = $this->app->share(function ($app) { return new TinyGenerateCommand($app['files']); }); $this->commands('tiny.generate'); } - public function register() + public function register(): void { - $this->app['tiny'] = $this->app->share(function($app) - { - $key = $app['config']['zackkitzmiller/tiny::key']; - - return new Tiny($key); + $this->app['tiny'] = $this->app->share(function ($app) { + return new Tiny((string) $app['config']['zackkitzmiller/tiny::key']); }); } - public function provides() + public function provides(): array { - return array('tiny'); + return ['tiny']; } - } \ No newline at end of file diff --git a/src/config/config.php b/src/config/config.php index 481756e..226a41e 100644 --- a/src/config/config.php +++ b/src/config/config.php @@ -1,7 +1,8 @@ '', - -); \ No newline at end of file +]; \ No newline at end of file diff --git a/tests/TinyTest.php b/tests/TinyTest.php index 7f7dbe4..7b13285 100644 --- a/tests/TinyTest.php +++ b/tests/TinyTest.php @@ -5,7 +5,6 @@ use PHPUnit\Framework\TestCase; use ZackKitzmiller\InvalidCharacterSet; use ZackKitzmiller\Tiny; -use InvalidArgumentException; final class TinyTest extends TestCase { @@ -73,7 +72,7 @@ public function testConstructorRejectsSmallCharacterSets(): void public function testFromRejectsUnknownCharacters(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); $this->tiny->from('!'); } From 47d840393ed37494f368ba7241a56a0f338d81fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:52:04 +0000 Subject: [PATCH 03/18] Finalize repo modernization Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- .github/workflows/ci.yml | 3 ++ src/ZackKitzmiller/EnvironmentKeyUpdater.php | 35 ++++++++++++ .../Laravel5/TinyGenerateCommand.php | 53 ++++++++++--------- .../Laravel5/TinyServiceProvider.php | 19 +++++-- src/ZackKitzmiller/Tiny.php | 15 +++++- src/ZackKitzmiller/TinyGenerateCommand.php | 31 +++++++---- src/ZackKitzmiller/TinyServiceProvider.php | 23 ++++++-- src/config/config.php | 2 +- tests/TinyTest.php | 33 ++++++++++++ 9 files changed, 167 insertions(+), 47 deletions(-) create mode 100644 src/ZackKitzmiller/EnvironmentKeyUpdater.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5032934..fe03f52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,9 @@ on: - master pull_request: +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest diff --git a/src/ZackKitzmiller/EnvironmentKeyUpdater.php b/src/ZackKitzmiller/EnvironmentKeyUpdater.php new file mode 100644 index 0000000..fc53029 --- /dev/null +++ b/src/ZackKitzmiller/EnvironmentKeyUpdater.php @@ -0,0 +1,35 @@ + 0) { + return $updated; + } + } + + if ($trimmed === '') { + return self::PRIMARY_KEY . '=' . $key . PHP_EOL; + } + + return $trimmed . PHP_EOL . self::PRIMARY_KEY . '=' . $key . PHP_EOL; + } +} diff --git a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php index 3146d89..36f1335 100644 --- a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php @@ -5,7 +5,9 @@ namespace ZackKitzmiller\Laravel5; use Illuminate\Console\Command; +use Illuminate\Filesystem\Filesystem; use RuntimeException; +use ZackKitzmiller\EnvironmentKeyUpdater; use ZackKitzmiller\Tiny; class TinyGenerateCommand extends Command @@ -14,35 +16,20 @@ class TinyGenerateCommand extends Command protected $description = 'Generate a valid key'; + public function __construct(private readonly Filesystem $files) + { + parent::__construct(); + } + public function handle(): int { $key = Tiny::generateSet(); $path = base_path('.env'); - $currentKey = getenv('TINY_KEY') ?: getenv('LEAGUE_TINY_KEY') ?: null; - - if (is_file($path)) { - $originalContent = file_get_contents($path); - - if ($originalContent === false) { - throw new RuntimeException(sprintf('Unable to read environment file at %s.', $path)); - } - - if ($currentKey !== null) { - $content = str_replace( - ["TINY_KEY={$currentKey}", "LEAGUE_TINY_KEY={$currentKey}"], - "TINY_KEY={$key}", - $originalContent - ); - } else { - $content = rtrim($originalContent) . PHP_EOL . "TINY_KEY={$key}" . PHP_EOL; - } - } else { - $content = "TINY_KEY={$key}" . PHP_EOL; - } - if (file_put_contents($path, $content) === false) { - throw new RuntimeException(sprintf('Unable to write environment file at %s.', $path)); - } + $content = is_file($path) ? $this->readEnvironmentFile($path) : ''; + $content = EnvironmentKeyUpdater::updateContents($content, $key); + + $this->writeEnvironmentFile($path, $content); $this->info("Tiny key [{$key}] has been set."); @@ -53,4 +40,22 @@ public function fire(): int { return $this->handle(); } + + private function readEnvironmentFile(string $path): string + { + $content = $this->files->get($path); + + if (! is_string($content)) { + throw new RuntimeException(sprintf('Unable to read environment file at %s.', $path)); + } + + return $content; + } + + private function writeEnvironmentFile(string $path, string $content): void + { + if ($this->files->put($path, $content) === false) { + throw new RuntimeException(sprintf('Unable to write environment file at %s.', $path)); + } + } } diff --git a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php index d818e2e..cb5a8c0 100644 --- a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php +++ b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php @@ -6,26 +6,35 @@ use Illuminate\Support\ServiceProvider; use RuntimeException; +use ZackKitzmiller\EnvironmentKeyUpdater; use ZackKitzmiller\Tiny; class TinyServiceProvider extends ServiceProvider { public function boot(): void { - $this->app->singleton('tiny.generate', static function () { - return new TinyGenerateCommand(); + if (function_exists('config_path')) { + $this->publishes([ + dirname(__DIR__) . '/config/config.php' => config_path('tiny.php'), + ], 'tiny-config'); + } + + $this->app->singleton('tiny.generate', static function ($app) { + return new TinyGenerateCommand($app['files']); }); - $this->commands('tiny.generate'); + $this->commands(['tiny.generate']); } public function register(): void { + $this->mergeConfigFrom(dirname(__DIR__) . '/config/config.php', 'tiny'); + $this->app->singleton('tiny', static function () { - $key = getenv('TINY_KEY') ?: getenv('LEAGUE_TINY_KEY'); + $key = config('tiny.key') ?: getenv(EnvironmentKeyUpdater::PRIMARY_KEY) ?: getenv(EnvironmentKeyUpdater::LEGACY_KEY); if (! is_string($key) || $key === '') { - throw new RuntimeException('A Tiny character set must be configured via TINY_KEY or LEAGUE_TINY_KEY.'); + throw new RuntimeException('A Tiny character set must be configured before resolving the Tiny service.'); } return new Tiny($key); diff --git a/src/ZackKitzmiller/Tiny.php b/src/ZackKitzmiller/Tiny.php index 515185d..45c571d 100644 --- a/src/ZackKitzmiller/Tiny.php +++ b/src/ZackKitzmiller/Tiny.php @@ -20,7 +20,7 @@ public function __construct(string $set) public function to(int|string $id): string { $encoded = ''; - $value = abs((int) $id); + $value = $this->normalizeId($id); $radix = strlen($this->set); do { @@ -82,4 +82,17 @@ private static function assertValidSet(string $set): void throw new InvalidCharacterSet('Tiny character sets must only contain unique characters.'); } } + + private function normalizeId(int|string $id): int + { + if (is_int($id)) { + return abs($id); + } + + if (! preg_match('/^[+-]?\d+$/', $id)) { + throw new InvalidArgumentException('Tiny can only encode whole-number integers.'); + } + + return abs((int) $id); + } } diff --git a/src/ZackKitzmiller/TinyGenerateCommand.php b/src/ZackKitzmiller/TinyGenerateCommand.php index 1cf4435..f99b8b0 100644 --- a/src/ZackKitzmiller/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/TinyGenerateCommand.php @@ -9,7 +9,7 @@ class TinyGenerateCommand extends Command { - protected $name = 'tiny:generate'; + protected $signature = 'tiny:generate'; protected $description = 'Generate a valid key'; @@ -20,14 +20,13 @@ public function __construct(private readonly Filesystem $files) public function handle(): int { - [$path, $contents] = $this->getKeyFile(); + $path = base_path('.env'); + $contents = is_file($path) ? $this->readEnvironmentFile($path) : ''; $key = Tiny::generateSet(); - $currentKey = (string) $this->laravel['config']['zackkitzmiller/tiny::key']; + $contents = EnvironmentKeyUpdater::updateContents($contents, $key); - $contents = str_replace($currentKey, $key, $contents); - - $this->files->put($path, $contents); - $this->laravel['config']['zackkitzmiller/tiny::key'] = $key; + $this->writeEnvironmentFile($path, $contents); + $this->laravel['config']['tiny.key'] = $key; $this->info("Tiny key [{$key}] has been set."); @@ -39,11 +38,21 @@ public function fire(): int return $this->handle(); } - protected function getKeyFile(): array + private function readEnvironmentFile(string $path): string { - $env = $this->option('env') ? $this->option('env') . '/' : ''; - $path = $this->laravel['path'] . "/config/packages/zackkitzmiller/tiny/{$env}config.php"; + $content = $this->files->get($path); + + if (! is_string($content)) { + throw new \RuntimeException(sprintf('Unable to read environment file at %s.', $path)); + } - return [$path, $this->files->get($path)]; + return $content; + } + + private function writeEnvironmentFile(string $path, string $content): void + { + if ($this->files->put($path, $content) === false) { + throw new \RuntimeException(sprintf('Unable to write environment file at %s.', $path)); + } } } diff --git a/src/ZackKitzmiller/TinyServiceProvider.php b/src/ZackKitzmiller/TinyServiceProvider.php index 8c8c24a..338e1f9 100644 --- a/src/ZackKitzmiller/TinyServiceProvider.php +++ b/src/ZackKitzmiller/TinyServiceProvider.php @@ -5,24 +5,37 @@ namespace ZackKitzmiller; use Illuminate\Support\ServiceProvider; +use RuntimeException; class TinyServiceProvider extends ServiceProvider { public function boot(): void { - $this->package('zackkitzmiller/tiny', 'zackkitzmiller/tiny', __DIR__ . '/../'); + if (function_exists('config_path')) { + $this->publishes([ + __DIR__ . '/../config/config.php' => config_path('tiny.php'), + ], 'tiny-config'); + } - $this->app['tiny.generate'] = $this->app->share(function ($app) { + $this->app->singleton('tiny.generate', function ($app) { return new TinyGenerateCommand($app['files']); }); - $this->commands('tiny.generate'); + $this->commands(['tiny.generate']); } public function register(): void { - $this->app['tiny'] = $this->app->share(function ($app) { - return new Tiny((string) $app['config']['zackkitzmiller/tiny::key']); + $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'tiny'); + + $this->app->singleton('tiny', function ($app) { + $key = $app['config']->get('tiny.key') ?: getenv(EnvironmentKeyUpdater::PRIMARY_KEY) ?: getenv(EnvironmentKeyUpdater::LEGACY_KEY); + + if (! is_string($key) || $key === '') { + throw new RuntimeException('A Tiny character set must be configured before resolving the Tiny service.'); + } + + return new Tiny($key); }); } diff --git a/src/config/config.php b/src/config/config.php index 226a41e..8b8a3b5 100644 --- a/src/config/config.php +++ b/src/config/config.php @@ -11,5 +11,5 @@ | Key that the Tiny class uses. | */ - 'key' => '', + 'key' => env('TINY_KEY', env('LEAGUE_TINY_KEY', '')), ]; \ No newline at end of file diff --git a/tests/TinyTest.php b/tests/TinyTest.php index 7b13285..aaec087 100644 --- a/tests/TinyTest.php +++ b/tests/TinyTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use PHPUnit\Framework\TestCase; +use ZackKitzmiller\EnvironmentKeyUpdater; use ZackKitzmiller\InvalidCharacterSet; use ZackKitzmiller\Tiny; @@ -37,6 +38,11 @@ public function testNegativeValuesAreNormalized(): void self::assertSame($this->tiny->to(25), $this->tiny->to(-25)); } + public function testNumericStringsAreSupported(): void + { + self::assertSame($this->tiny->to(25), $this->tiny->to('0025')); + } + public function testGenerateRandomSetsWork(): void { for ($i = 0; $i <= 100; $i++) { @@ -76,4 +82,31 @@ public function testFromRejectsUnknownCharacters(): void $this->tiny->from('!'); } + + public function testToRejectsNonIntegerStrings(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->tiny->to('12abc'); + } + + public function testEnvironmentKeyUpdaterReplacesExistingKeys(): void + { + $contents = "APP_ENV=testing\nLEAGUE_TINY_KEY=old-key\n"; + + self::assertSame( + "APP_ENV=testing\nTINY_KEY=new-key\n", + EnvironmentKeyUpdater::updateContents($contents, 'new-key') + ); + } + + public function testEnvironmentKeyUpdaterAppendsMissingKeys(): void + { + $contents = "APP_ENV=testing\n"; + + self::assertSame( + "APP_ENV=testing\nTINY_KEY=new-key\n", + EnvironmentKeyUpdater::updateContents($contents, 'new-key') + ); + } } From 625d188cad1079b88b15a6bdfee8609d8dba21f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:52:44 +0000 Subject: [PATCH 04/18] Fix modernization follow-ups Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- README.md | 2 +- .../Laravel5/TinyGenerateCommand.php | 15 ++++++++++++++- src/ZackKitzmiller/TinyGenerateCommand.php | 15 ++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 156b821..1cbbd5c 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ After installing dependencies, generate a fresh 62-character alphabet with: You can also generate one in code: ```php -$set = Tiny::generateSet(); +$set = \ZackKitzmiller\Tiny::generateSet(); ``` The legacy `Tiny::generate_set()` helper is still available for backwards compatibility. diff --git a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php index 36f1335..fc26f55 100644 --- a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php @@ -24,7 +24,7 @@ public function __construct(private readonly Filesystem $files) public function handle(): int { $key = Tiny::generateSet(); - $path = base_path('.env'); + $path = $this->environmentFilePath(); $content = is_file($path) ? $this->readEnvironmentFile($path) : ''; $content = EnvironmentKeyUpdater::updateContents($content, $key); @@ -58,4 +58,17 @@ private function writeEnvironmentFile(string $path, string $content): void throw new RuntimeException(sprintf('Unable to write environment file at %s.', $path)); } } + + private function environmentFilePath(): string + { + if (method_exists($this->laravel, 'environmentFilePath')) { + return $this->laravel->environmentFilePath(); + } + + if (method_exists($this->laravel, 'basePath')) { + return $this->laravel->basePath('.env'); + } + + return $this->laravel['path.base'] . '/.env'; + } } diff --git a/src/ZackKitzmiller/TinyGenerateCommand.php b/src/ZackKitzmiller/TinyGenerateCommand.php index f99b8b0..e92a0ba 100644 --- a/src/ZackKitzmiller/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/TinyGenerateCommand.php @@ -20,7 +20,7 @@ public function __construct(private readonly Filesystem $files) public function handle(): int { - $path = base_path('.env'); + $path = $this->environmentFilePath(); $contents = is_file($path) ? $this->readEnvironmentFile($path) : ''; $key = Tiny::generateSet(); $contents = EnvironmentKeyUpdater::updateContents($contents, $key); @@ -55,4 +55,17 @@ private function writeEnvironmentFile(string $path, string $content): void throw new \RuntimeException(sprintf('Unable to write environment file at %s.', $path)); } } + + private function environmentFilePath(): string + { + if (method_exists($this->laravel, 'environmentFilePath')) { + return $this->laravel->environmentFilePath(); + } + + if (method_exists($this->laravel, 'basePath')) { + return $this->laravel->basePath('.env'); + } + + return $this->laravel['path.base'] . '/.env'; + } } From 4143042976298d36777b278ea5cebee625394c3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:53:31 +0000 Subject: [PATCH 05/18] Address validation feedback Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- .../Laravel5/TinyServiceProvider.php | 6 ++++-- src/ZackKitzmiller/Tiny.php | 20 ++++++++++++++++++- tests/TinyTest.php | 7 +++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php index cb5a8c0..2fc98d9 100644 --- a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php +++ b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php @@ -11,11 +11,13 @@ class TinyServiceProvider extends ServiceProvider { + private const CONFIG_PATH = __DIR__ . '/../../config/config.php'; + public function boot(): void { if (function_exists('config_path')) { $this->publishes([ - dirname(__DIR__) . '/config/config.php' => config_path('tiny.php'), + self::CONFIG_PATH => config_path('tiny.php'), ], 'tiny-config'); } @@ -28,7 +30,7 @@ public function boot(): void public function register(): void { - $this->mergeConfigFrom(dirname(__DIR__) . '/config/config.php', 'tiny'); + $this->mergeConfigFrom(self::CONFIG_PATH, 'tiny'); $this->app->singleton('tiny', static function () { $key = config('tiny.key') ?: getenv(EnvironmentKeyUpdater::PRIMARY_KEY) ?: getenv(EnvironmentKeyUpdater::LEGACY_KEY); diff --git a/src/ZackKitzmiller/Tiny.php b/src/ZackKitzmiller/Tiny.php index 45c571d..c1ebd49 100644 --- a/src/ZackKitzmiller/Tiny.php +++ b/src/ZackKitzmiller/Tiny.php @@ -86,6 +86,10 @@ private static function assertValidSet(string $set): void private function normalizeId(int|string $id): int { if (is_int($id)) { + if ($id === PHP_INT_MIN) { + throw new InvalidArgumentException('Tiny cannot encode values smaller than PHP_INT_MIN + 1.'); + } + return abs($id); } @@ -93,6 +97,20 @@ private function normalizeId(int|string $id): int throw new InvalidArgumentException('Tiny can only encode whole-number integers.'); } - return abs((int) $id); + $normalized = ltrim($id, '+-'); + $normalized = ltrim($normalized, '0'); + $normalized = $normalized === '' ? '0' : $normalized; + + if ( + strlen($normalized) > strlen((string) PHP_INT_MAX) + || ( + strlen($normalized) === strlen((string) PHP_INT_MAX) + && strcmp($normalized, (string) PHP_INT_MAX) > 0 + ) + ) { + throw new InvalidArgumentException('Tiny can only encode integers within PHP\'s native integer range.'); + } + + return abs((int) $normalized); } } diff --git a/tests/TinyTest.php b/tests/TinyTest.php index aaec087..60df22d 100644 --- a/tests/TinyTest.php +++ b/tests/TinyTest.php @@ -90,6 +90,13 @@ public function testToRejectsNonIntegerStrings(): void $this->tiny->to('12abc'); } + public function testToRejectsOutOfRangeNumericStrings(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->tiny->to((string) PHP_INT_MAX . '0'); + } + public function testEnvironmentKeyUpdaterReplacesExistingKeys(): void { $contents = "APP_ENV=testing\nLEAGUE_TINY_KEY=old-key\n"; From 05356b4d8e1af7765c4cf706205dfc241838a5b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:54:14 +0000 Subject: [PATCH 06/18] Polish modernization docs and provider Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- README.md | 2 ++ .../Laravel5/TinyServiceProvider.php | 15 +++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1cbbd5c..3927247 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ $set = \ZackKitzmiller\Tiny::generateSet(); The legacy `Tiny::generate_set()` helper is still available for backwards compatibility. +This release does introduce a package-level modernization break: Composer autoloading now uses PSR-4 for `ZackKitzmiller\\` classes, so consumers relying on older PSR-0-era assumptions should verify their integration when upgrading. + ## Development ```bash diff --git a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php index 2fc98d9..d644ee2 100644 --- a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php +++ b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php @@ -11,13 +11,11 @@ class TinyServiceProvider extends ServiceProvider { - private const CONFIG_PATH = __DIR__ . '/../../config/config.php'; - public function boot(): void { if (function_exists('config_path')) { $this->publishes([ - self::CONFIG_PATH => config_path('tiny.php'), + self::configPath() => config_path('tiny.php'), ], 'tiny-config'); } @@ -30,10 +28,10 @@ public function boot(): void public function register(): void { - $this->mergeConfigFrom(self::CONFIG_PATH, 'tiny'); + $this->mergeConfigFrom(self::configPath(), 'tiny'); - $this->app->singleton('tiny', static function () { - $key = config('tiny.key') ?: getenv(EnvironmentKeyUpdater::PRIMARY_KEY) ?: getenv(EnvironmentKeyUpdater::LEGACY_KEY); + $this->app->singleton('tiny', static function ($app) { + $key = $app['config']->get('tiny.key') ?: getenv(EnvironmentKeyUpdater::PRIMARY_KEY) ?: getenv(EnvironmentKeyUpdater::LEGACY_KEY); if (! is_string($key) || $key === '') { throw new RuntimeException('A Tiny character set must be configured before resolving the Tiny service.'); @@ -47,4 +45,9 @@ public function provides(): array { return ['tiny']; } + + private static function configPath(): string + { + return dirname(__DIR__, 2) . '/config/config.php'; + } } From c5b8a7d857733ac7f73e99ef55a31740d8e2b5f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:54:50 +0000 Subject: [PATCH 07/18] Handle integer edge cases Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- src/ZackKitzmiller/Tiny.php | 4 ++++ tests/TinyTest.php | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/ZackKitzmiller/Tiny.php b/src/ZackKitzmiller/Tiny.php index c1ebd49..26c97db 100644 --- a/src/ZackKitzmiller/Tiny.php +++ b/src/ZackKitzmiller/Tiny.php @@ -97,6 +97,10 @@ private function normalizeId(int|string $id): int throw new InvalidArgumentException('Tiny can only encode whole-number integers.'); } + if ($id === (string) PHP_INT_MIN) { + throw new InvalidArgumentException('Tiny cannot encode values smaller than PHP_INT_MIN + 1.'); + } + $normalized = ltrim($id, '+-'); $normalized = ltrim($normalized, '0'); $normalized = $normalized === '' ? '0' : $normalized; diff --git a/tests/TinyTest.php b/tests/TinyTest.php index 60df22d..356200b 100644 --- a/tests/TinyTest.php +++ b/tests/TinyTest.php @@ -97,6 +97,13 @@ public function testToRejectsOutOfRangeNumericStrings(): void $this->tiny->to((string) PHP_INT_MAX . '0'); } + public function testToRejectsMinimumIntegerString(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->tiny->to((string) PHP_INT_MIN); + } + public function testEnvironmentKeyUpdaterReplacesExistingKeys(): void { $contents = "APP_ENV=testing\nLEAGUE_TINY_KEY=old-key\n"; From 38245f6cbc0adcd2e8678f6c831728b74da228c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:55:41 +0000 Subject: [PATCH 08/18] Preserve compatibility edge cases Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- .../Laravel5/TinyServiceProvider.php | 10 +++++++++- src/ZackKitzmiller/Tiny.php | 18 ++++++++++-------- src/ZackKitzmiller/TinyServiceProvider.php | 10 +++++++++- tests/TinyTest.php | 5 +++++ 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php index d644ee2..23fb7ac 100644 --- a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php +++ b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php @@ -31,7 +31,15 @@ public function register(): void $this->mergeConfigFrom(self::configPath(), 'tiny'); $this->app->singleton('tiny', static function ($app) { - $key = $app['config']->get('tiny.key') ?: getenv(EnvironmentKeyUpdater::PRIMARY_KEY) ?: getenv(EnvironmentKeyUpdater::LEGACY_KEY); + $key = $app['config']->get('tiny.key'); + + if ($key === null || $key === false) { + $key = getenv(EnvironmentKeyUpdater::PRIMARY_KEY); + } + + if ($key === false) { + $key = getenv(EnvironmentKeyUpdater::LEGACY_KEY); + } if (! is_string($key) || $key === '') { throw new RuntimeException('A Tiny character set must be configured before resolving the Tiny service.'); diff --git a/src/ZackKitzmiller/Tiny.php b/src/ZackKitzmiller/Tiny.php index 26c97db..0ccbea2 100644 --- a/src/ZackKitzmiller/Tiny.php +++ b/src/ZackKitzmiller/Tiny.php @@ -97,24 +97,26 @@ private function normalizeId(int|string $id): int throw new InvalidArgumentException('Tiny can only encode whole-number integers.'); } - if ($id === (string) PHP_INT_MIN) { - throw new InvalidArgumentException('Tiny cannot encode values smaller than PHP_INT_MIN + 1.'); - } - + $isNegative = str_starts_with($id, '-'); $normalized = ltrim($id, '+-'); $normalized = ltrim($normalized, '0'); $normalized = $normalized === '' ? '0' : $normalized; + $limit = $isNegative ? ltrim((string) PHP_INT_MIN, '-') : (string) PHP_INT_MAX; if ( - strlen($normalized) > strlen((string) PHP_INT_MAX) + strlen($normalized) > strlen($limit) || ( - strlen($normalized) === strlen((string) PHP_INT_MAX) - && strcmp($normalized, (string) PHP_INT_MAX) > 0 + strlen($normalized) === strlen($limit) + && strcmp($normalized, $limit) > 0 ) ) { throw new InvalidArgumentException('Tiny can only encode integers within PHP\'s native integer range.'); } - return abs((int) $normalized); + if ($isNegative && $normalized === ltrim((string) PHP_INT_MIN, '-')) { + throw new InvalidArgumentException('Tiny cannot encode values smaller than PHP_INT_MIN + 1.'); + } + + return (int) $normalized; } } diff --git a/src/ZackKitzmiller/TinyServiceProvider.php b/src/ZackKitzmiller/TinyServiceProvider.php index 338e1f9..6fb4225 100644 --- a/src/ZackKitzmiller/TinyServiceProvider.php +++ b/src/ZackKitzmiller/TinyServiceProvider.php @@ -29,7 +29,15 @@ public function register(): void $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'tiny'); $this->app->singleton('tiny', function ($app) { - $key = $app['config']->get('tiny.key') ?: getenv(EnvironmentKeyUpdater::PRIMARY_KEY) ?: getenv(EnvironmentKeyUpdater::LEGACY_KEY); + $key = $app['config']->get('tiny.key'); + + if ($key === null || $key === false) { + $key = getenv(EnvironmentKeyUpdater::PRIMARY_KEY); + } + + if ($key === false) { + $key = getenv(EnvironmentKeyUpdater::LEGACY_KEY); + } if (! is_string($key) || $key === '') { throw new RuntimeException('A Tiny character set must be configured before resolving the Tiny service.'); diff --git a/tests/TinyTest.php b/tests/TinyTest.php index 356200b..cb88144 100644 --- a/tests/TinyTest.php +++ b/tests/TinyTest.php @@ -43,6 +43,11 @@ public function testNumericStringsAreSupported(): void self::assertSame($this->tiny->to(25), $this->tiny->to('0025')); } + public function testNegativeNumericStringsAreNormalized(): void + { + self::assertSame($this->tiny->to(25), $this->tiny->to('-25')); + } + public function testGenerateRandomSetsWork(): void { for ($i = 0; $i <= 100; $i++) { From 2fd2bd22aeb95f804d8241f7c902dfb1b5504dba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:56:31 +0000 Subject: [PATCH 09/18] Harden generated key handling Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php | 2 ++ src/ZackKitzmiller/Tiny.php | 5 ++++- src/ZackKitzmiller/TinyGenerateCommand.php | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php index fc26f55..d2f6d75 100644 --- a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php @@ -30,6 +30,8 @@ public function handle(): int $content = EnvironmentKeyUpdater::updateContents($content, $key); $this->writeEnvironmentFile($path, $content); + $this->laravel['config']['tiny.key'] = $key; + $this->laravel['config']['zackkitzmiller/tiny::key'] = $key; $this->info("Tiny key [{$key}] has been set."); diff --git a/src/ZackKitzmiller/Tiny.php b/src/ZackKitzmiller/Tiny.php index 0ccbea2..a750199 100644 --- a/src/ZackKitzmiller/Tiny.php +++ b/src/ZackKitzmiller/Tiny.php @@ -62,7 +62,10 @@ public static function generateSet(): string ...range('0', '9'), ]; - shuffle($characters); + for ($i = count($characters) - 1; $i > 0; $i--) { + $swapIndex = random_int(0, $i); + [$characters[$i], $characters[$swapIndex]] = [$characters[$swapIndex], $characters[$i]]; + } return implode('', $characters); } diff --git a/src/ZackKitzmiller/TinyGenerateCommand.php b/src/ZackKitzmiller/TinyGenerateCommand.php index e92a0ba..f47f59e 100644 --- a/src/ZackKitzmiller/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/TinyGenerateCommand.php @@ -27,6 +27,7 @@ public function handle(): int $this->writeEnvironmentFile($path, $contents); $this->laravel['config']['tiny.key'] = $key; + $this->laravel['config']['zackkitzmiller/tiny::key'] = $key; $this->info("Tiny key [{$key}] has been set."); From 655f6b5396f81f3f37bce46736c1ecf4e6e1a2af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:57:14 +0000 Subject: [PATCH 10/18] Align Laravel config updates Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php | 4 ++-- src/ZackKitzmiller/Laravel5/TinyServiceProvider.php | 2 +- src/ZackKitzmiller/TinyGenerateCommand.php | 4 ++-- src/ZackKitzmiller/TinyServiceProvider.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php index d2f6d75..8c6419f 100644 --- a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php @@ -30,8 +30,8 @@ public function handle(): int $content = EnvironmentKeyUpdater::updateContents($content, $key); $this->writeEnvironmentFile($path, $content); - $this->laravel['config']['tiny.key'] = $key; - $this->laravel['config']['zackkitzmiller/tiny::key'] = $key; + $this->laravel['config']->set('tiny.key', $key); + $this->laravel['config']->set('zackkitzmiller/tiny::key', $key); $this->info("Tiny key [{$key}] has been set."); diff --git a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php index 23fb7ac..6911f16 100644 --- a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php +++ b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php @@ -13,7 +13,7 @@ class TinyServiceProvider extends ServiceProvider { public function boot(): void { - if (function_exists('config_path')) { + if ($this->app->runningInConsole() && function_exists('config_path')) { $this->publishes([ self::configPath() => config_path('tiny.php'), ], 'tiny-config'); diff --git a/src/ZackKitzmiller/TinyGenerateCommand.php b/src/ZackKitzmiller/TinyGenerateCommand.php index f47f59e..05c4946 100644 --- a/src/ZackKitzmiller/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/TinyGenerateCommand.php @@ -26,8 +26,8 @@ public function handle(): int $contents = EnvironmentKeyUpdater::updateContents($contents, $key); $this->writeEnvironmentFile($path, $contents); - $this->laravel['config']['tiny.key'] = $key; - $this->laravel['config']['zackkitzmiller/tiny::key'] = $key; + $this->laravel['config']->set('tiny.key', $key); + $this->laravel['config']->set('zackkitzmiller/tiny::key', $key); $this->info("Tiny key [{$key}] has been set."); diff --git a/src/ZackKitzmiller/TinyServiceProvider.php b/src/ZackKitzmiller/TinyServiceProvider.php index 6fb4225..37bd4e7 100644 --- a/src/ZackKitzmiller/TinyServiceProvider.php +++ b/src/ZackKitzmiller/TinyServiceProvider.php @@ -11,7 +11,7 @@ class TinyServiceProvider extends ServiceProvider { public function boot(): void { - if (function_exists('config_path')) { + if ($this->app->runningInConsole() && function_exists('config_path')) { $this->publishes([ __DIR__ . '/../config/config.php' => config_path('tiny.php'), ], 'tiny-config'); From 7ce07525152b61dd54752456ef2d1908635b2dc2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:58:44 +0000 Subject: [PATCH 11/18] Finish legacy compatibility fixes Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- src/ZackKitzmiller/EnvironmentKeyUpdater.php | 8 +++- .../Laravel5/TinyServiceProvider.php | 12 +++--- src/ZackKitzmiller/Tiny.php | 4 ++ src/ZackKitzmiller/TinyGenerateCommand.php | 41 +++++++++++++++++-- src/ZackKitzmiller/TinyServiceProvider.php | 12 +++--- tests/TinyTest.php | 19 +++++++++ 6 files changed, 80 insertions(+), 16 deletions(-) diff --git a/src/ZackKitzmiller/EnvironmentKeyUpdater.php b/src/ZackKitzmiller/EnvironmentKeyUpdater.php index fc53029..760e1b6 100644 --- a/src/ZackKitzmiller/EnvironmentKeyUpdater.php +++ b/src/ZackKitzmiller/EnvironmentKeyUpdater.php @@ -19,7 +19,13 @@ public static function updateContents(string $contents, string $key): string ]; foreach ($patterns as $pattern) { - $updated = preg_replace($pattern, self::PRIMARY_KEY . '=' . $key, $contents, 1, $count); + $updated = preg_replace_callback( + $pattern, + static fn (): string => self::PRIMARY_KEY . '=' . $key, + $contents, + 1, + $count + ); if ($updated !== null && $count > 0) { return $updated; diff --git a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php index 6911f16..9040d49 100644 --- a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php +++ b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php @@ -17,19 +17,19 @@ public function boot(): void $this->publishes([ self::configPath() => config_path('tiny.php'), ], 'tiny-config'); - } - - $this->app->singleton('tiny.generate', static function ($app) { - return new TinyGenerateCommand($app['files']); - }); - $this->commands(['tiny.generate']); + $this->commands(['tiny.generate']); + } } public function register(): void { $this->mergeConfigFrom(self::configPath(), 'tiny'); + $this->app->singleton('tiny.generate', static function ($app) { + return new TinyGenerateCommand($app['files']); + }); + $this->app->singleton('tiny', static function ($app) { $key = $app['config']->get('tiny.key'); diff --git a/src/ZackKitzmiller/Tiny.php b/src/ZackKitzmiller/Tiny.php index a750199..bd39251 100644 --- a/src/ZackKitzmiller/Tiny.php +++ b/src/ZackKitzmiller/Tiny.php @@ -48,6 +48,10 @@ public function from(string $value): int throw new InvalidArgumentException(sprintf('Character "%s" is not in the Tiny character set.', $character)); } + if ($decoded > intdiv(PHP_INT_MAX - $position, $radix)) { + throw new InvalidArgumentException('Decoded value exceeds PHP\'s native integer range.'); + } + $decoded = ($decoded * $radix) + $position; } diff --git a/src/ZackKitzmiller/TinyGenerateCommand.php b/src/ZackKitzmiller/TinyGenerateCommand.php index 05c4946..bdbf63c 100644 --- a/src/ZackKitzmiller/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/TinyGenerateCommand.php @@ -20,12 +20,13 @@ public function __construct(private readonly Filesystem $files) public function handle(): int { - $path = $this->environmentFilePath(); - $contents = is_file($path) ? $this->readEnvironmentFile($path) : ''; $key = Tiny::generateSet(); + $environmentPath = $this->environmentFilePath(); + $contents = is_file($environmentPath) ? $this->readEnvironmentFile($environmentPath) : ''; $contents = EnvironmentKeyUpdater::updateContents($contents, $key); - $this->writeEnvironmentFile($path, $contents); + $this->writeEnvironmentFile($environmentPath, $contents); + $this->updateLegacyConfigFile($key); $this->laravel['config']->set('tiny.key', $key); $this->laravel['config']->set('zackkitzmiller/tiny::key', $key); @@ -69,4 +70,38 @@ private function environmentFilePath(): string return $this->laravel['path.base'] . '/.env'; } + + private function updateLegacyConfigFile(string $key): void + { + $path = $this->legacyConfigFilePath(); + + if ($path === null || ! is_file($path)) { + return; + } + + $contents = $this->readEnvironmentFile($path); + $updated = preg_replace_callback( + "/('key'\\s*=>\\s*)([^,]+)(,?)/", + static fn (array $matches): string => $matches[1] . "'" . $key . "'" . $matches[3], + $contents, + 1, + $count + ); + + if ($updated !== null && $count > 0) { + $this->writeEnvironmentFile($path, $updated); + } + } + + private function legacyConfigFilePath(): ?string + { + if (! isset($this->laravel['path'])) { + return null; + } + + $environment = $this->option('env'); + $environment = is_string($environment) && $environment !== '' ? $environment . '/' : ''; + + return $this->laravel['path'] . "/config/packages/zackkitzmiller/tiny/{$environment}config.php"; + } } diff --git a/src/ZackKitzmiller/TinyServiceProvider.php b/src/ZackKitzmiller/TinyServiceProvider.php index 37bd4e7..5d1fc8b 100644 --- a/src/ZackKitzmiller/TinyServiceProvider.php +++ b/src/ZackKitzmiller/TinyServiceProvider.php @@ -15,19 +15,19 @@ public function boot(): void $this->publishes([ __DIR__ . '/../config/config.php' => config_path('tiny.php'), ], 'tiny-config'); - } - - $this->app->singleton('tiny.generate', function ($app) { - return new TinyGenerateCommand($app['files']); - }); - $this->commands(['tiny.generate']); + $this->commands(['tiny.generate']); + } } public function register(): void { $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'tiny'); + $this->app->singleton('tiny.generate', function ($app) { + return new TinyGenerateCommand($app['files']); + }); + $this->app->singleton('tiny', function ($app) { $key = $app['config']->get('tiny.key'); diff --git a/tests/TinyTest.php b/tests/TinyTest.php index cb88144..759b1fc 100644 --- a/tests/TinyTest.php +++ b/tests/TinyTest.php @@ -88,6 +88,15 @@ public function testFromRejectsUnknownCharacters(): void $this->tiny->from('!'); } + public function testFromRejectsValuesThatOverflowNativeIntegers(): void + { + $tiny = new Tiny('ab'); + + $this->expectException(\InvalidArgumentException::class); + + $tiny->from(str_repeat('b', 128)); + } + public function testToRejectsNonIntegerStrings(): void { $this->expectException(\InvalidArgumentException::class); @@ -128,4 +137,14 @@ public function testEnvironmentKeyUpdaterAppendsMissingKeys(): void EnvironmentKeyUpdater::updateContents($contents, 'new-key') ); } + + public function testEnvironmentKeyUpdaterPreservesLiteralSpecialCharacters(): void + { + $contents = "TINY_KEY=old-key\n"; + + self::assertSame( + 'TINY_KEY=value$1\path' . PHP_EOL, + EnvironmentKeyUpdater::updateContents($contents, 'value$1\\path') + ); + } } From cca9120ce8a6f5460bcc198d71ab755b0a10c476 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:59:49 +0000 Subject: [PATCH 12/18] Tighten autoload and updater coverage Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- README.md | 2 ++ composer.json | 1 + src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php | 2 +- src/ZackKitzmiller/Laravel5/TinyServiceProvider.php | 2 +- src/ZackKitzmiller/TinyGenerateCommand.php | 2 +- tests/TinyTest.php | 10 ++++++++++ 6 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3927247..045cb92 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ The legacy `Tiny::generate_set()` helper is still available for backwards compat This release does introduce a package-level modernization break: Composer autoloading now uses PSR-4 for `ZackKitzmiller\\` classes, so consumers relying on older PSR-0-era assumptions should verify their integration when upgrading. +The Laravel integration classes continue to autoload from the `ZackKitzmiller\\Laravel5\\` namespace under `src/ZackKitzmiller/Laravel5/`. + ## Development ```bash diff --git a/composer.json b/composer.json index 6f62005..279adff 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ }, "autoload": { "psr-4": { + "ZackKitzmiller\\Laravel5\\": "src/ZackKitzmiller/Laravel5/", "ZackKitzmiller\\": "src/ZackKitzmiller/" } }, diff --git a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php index 8c6419f..f497a63 100644 --- a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php @@ -12,7 +12,7 @@ class TinyGenerateCommand extends Command { - protected $signature = 'tiny:generate'; + protected $signature = 'tiny:generate {--env=}'; protected $description = 'Generate a valid key'; diff --git a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php index 9040d49..e41a490 100644 --- a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php +++ b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php @@ -56,6 +56,6 @@ public function provides(): array private static function configPath(): string { - return dirname(__DIR__, 2) . '/config/config.php'; + return dirname(__DIR__, 3) . '/src/config/config.php'; } } diff --git a/src/ZackKitzmiller/TinyGenerateCommand.php b/src/ZackKitzmiller/TinyGenerateCommand.php index bdbf63c..937a9ef 100644 --- a/src/ZackKitzmiller/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/TinyGenerateCommand.php @@ -9,7 +9,7 @@ class TinyGenerateCommand extends Command { - protected $signature = 'tiny:generate'; + protected $signature = 'tiny:generate {--env=}'; protected $description = 'Generate a valid key'; diff --git a/tests/TinyTest.php b/tests/TinyTest.php index 759b1fc..b4384b4 100644 --- a/tests/TinyTest.php +++ b/tests/TinyTest.php @@ -147,4 +147,14 @@ public function testEnvironmentKeyUpdaterPreservesLiteralSpecialCharacters(): vo EnvironmentKeyUpdater::updateContents($contents, 'value$1\\path') ); } + + public function testEnvironmentKeyUpdaterPrefersTinyKeyWhenBothKeysExist(): void + { + $contents = "TINY_KEY=old-primary\nLEAGUE_TINY_KEY=old-legacy\n"; + + self::assertSame( + "TINY_KEY=new-key\nLEAGUE_TINY_KEY=old-legacy\n", + EnvironmentKeyUpdater::updateContents($contents, 'new-key') + ); + } } From 4eeb90539bced620b545cff652f38de5fae28e2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 05:00:45 +0000 Subject: [PATCH 13/18] Fix updater edge cases Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- src/ZackKitzmiller/EnvironmentKeyUpdater.php | 28 +++++++++++++------ .../Laravel5/TinyServiceProvider.php | 2 +- src/ZackKitzmiller/TinyGenerateCommand.php | 3 +- tests/TinyTest.php | 10 +++++++ 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/ZackKitzmiller/EnvironmentKeyUpdater.php b/src/ZackKitzmiller/EnvironmentKeyUpdater.php index 760e1b6..d9fed65 100644 --- a/src/ZackKitzmiller/EnvironmentKeyUpdater.php +++ b/src/ZackKitzmiller/EnvironmentKeyUpdater.php @@ -13,21 +13,31 @@ final class EnvironmentKeyUpdater public static function updateContents(string $contents, string $key): string { $trimmed = rtrim($contents); - $patterns = [ - '/^' . self::PRIMARY_KEY . '=.*$/m', - '/^' . self::LEGACY_KEY . '=.*$/m', - ]; + $primaryPattern = '/^' . self::PRIMARY_KEY . '=.*$/m'; + $legacyPattern = '/^' . self::LEGACY_KEY . '=.*$/m'; - foreach ($patterns as $pattern) { + if (preg_match($primaryPattern, $contents) === 1) { $updated = preg_replace_callback( - $pattern, + $primaryPattern, static fn (): string => self::PRIMARY_KEY . '=' . $key, $contents, - 1, - $count + 1 ); - if ($updated !== null && $count > 0) { + if ($updated !== null) { + return $updated; + } + } + + if (preg_match($legacyPattern, $contents) === 1) { + $updated = preg_replace_callback( + $legacyPattern, + static fn (): string => self::PRIMARY_KEY . '=' . $key, + $contents, + 1 + ); + + if ($updated !== null) { return $updated; } } diff --git a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php index e41a490..9040d49 100644 --- a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php +++ b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php @@ -56,6 +56,6 @@ public function provides(): array private static function configPath(): string { - return dirname(__DIR__, 3) . '/src/config/config.php'; + return dirname(__DIR__, 2) . '/config/config.php'; } } diff --git a/src/ZackKitzmiller/TinyGenerateCommand.php b/src/ZackKitzmiller/TinyGenerateCommand.php index 937a9ef..270ab26 100644 --- a/src/ZackKitzmiller/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/TinyGenerateCommand.php @@ -80,9 +80,10 @@ private function updateLegacyConfigFile(string $key): void } $contents = $this->readEnvironmentFile($path); + $escapedKey = addcslashes($key, "\\'"); $updated = preg_replace_callback( "/('key'\\s*=>\\s*)([^,]+)(,?)/", - static fn (array $matches): string => $matches[1] . "'" . $key . "'" . $matches[3], + static fn (array $matches): string => $matches[1] . "'" . $escapedKey . "'" . $matches[3], $contents, 1, $count diff --git a/tests/TinyTest.php b/tests/TinyTest.php index b4384b4..e7075de 100644 --- a/tests/TinyTest.php +++ b/tests/TinyTest.php @@ -157,4 +157,14 @@ public function testEnvironmentKeyUpdaterPrefersTinyKeyWhenBothKeysExist(): void EnvironmentKeyUpdater::updateContents($contents, 'new-key') ); } + + public function testEnvironmentKeyUpdaterPrefersTinyKeyRegardlessOfOrder(): void + { + $contents = "LEAGUE_TINY_KEY=old-legacy\nTINY_KEY=old-primary\n"; + + self::assertSame( + "LEAGUE_TINY_KEY=old-legacy\nTINY_KEY=new-key\n", + EnvironmentKeyUpdater::updateContents($contents, 'new-key') + ); + } } From 01b75518a35c42946004825321f7fd940df8774e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 05:01:31 +0000 Subject: [PATCH 14/18] Polish legacy integration docs Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- README.md | 7 +++++++ src/ZackKitzmiller/EnvironmentKeyUpdater.php | 4 ++-- src/ZackKitzmiller/TinyGenerateCommand.php | 2 +- tests/TinyTest.php | 10 ++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 045cb92..988ffdb 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,10 @@ composer check ## Legacy Laravel integration The repository still includes the original Laravel integration classes for older applications, but the package is now centered on the framework-agnostic core library. + +For Laravel integration: + +- register `ZackKitzmiller\TinyServiceProvider` or `ZackKitzmiller\Laravel5\TinyServiceProvider` +- optionally register the `ZackKitzmiller\Facades\Tiny` facade alias +- publish the config with `php artisan vendor:publish --tag=tiny-config` +- set `TINY_KEY` in your environment or run `php artisan tiny:generate` diff --git a/src/ZackKitzmiller/EnvironmentKeyUpdater.php b/src/ZackKitzmiller/EnvironmentKeyUpdater.php index d9fed65..9ab824a 100644 --- a/src/ZackKitzmiller/EnvironmentKeyUpdater.php +++ b/src/ZackKitzmiller/EnvironmentKeyUpdater.php @@ -13,8 +13,8 @@ final class EnvironmentKeyUpdater public static function updateContents(string $contents, string $key): string { $trimmed = rtrim($contents); - $primaryPattern = '/^' . self::PRIMARY_KEY . '=.*$/m'; - $legacyPattern = '/^' . self::LEGACY_KEY . '=.*$/m'; + $primaryPattern = '/^[ \t]*' . self::PRIMARY_KEY . '=.*$/m'; + $legacyPattern = '/^[ \t]*' . self::LEGACY_KEY . '=.*$/m'; if (preg_match($primaryPattern, $contents) === 1) { $updated = preg_replace_callback( diff --git a/src/ZackKitzmiller/TinyGenerateCommand.php b/src/ZackKitzmiller/TinyGenerateCommand.php index 270ab26..0c8f82d 100644 --- a/src/ZackKitzmiller/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/TinyGenerateCommand.php @@ -82,7 +82,7 @@ private function updateLegacyConfigFile(string $key): void $contents = $this->readEnvironmentFile($path); $escapedKey = addcslashes($key, "\\'"); $updated = preg_replace_callback( - "/('key'\\s*=>\\s*)([^,]+)(,?)/", + "/^([ \\t]*'key'\\s*=>\\s*)([^,]+)(,?[ \\t]*$)/m", static fn (array $matches): string => $matches[1] . "'" . $escapedKey . "'" . $matches[3], $contents, 1, diff --git a/tests/TinyTest.php b/tests/TinyTest.php index e7075de..c7e0237 100644 --- a/tests/TinyTest.php +++ b/tests/TinyTest.php @@ -167,4 +167,14 @@ public function testEnvironmentKeyUpdaterPrefersTinyKeyRegardlessOfOrder(): void EnvironmentKeyUpdater::updateContents($contents, 'new-key') ); } + + public function testEnvironmentKeyUpdaterHandlesIndentedKeys(): void + { + $contents = " TINY_KEY=old-primary\n"; + + self::assertSame( + "TINY_KEY=new-key\n", + EnvironmentKeyUpdater::updateContents($contents, 'new-key') + ); + } } From 375f936a53550d82bb7d08b56e711f4f5dccf34d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 05:02:24 +0000 Subject: [PATCH 15/18] Clarify Laravel fallback behavior Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- README.md | 14 ++++++++++---- .../Laravel5/TinyServiceProvider.php | 8 +++----- src/ZackKitzmiller/TinyServiceProvider.php | 8 +++----- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 988ffdb..d6ec60c 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,13 @@ The repository still includes the original Laravel integration classes for older For Laravel integration: -- register `ZackKitzmiller\TinyServiceProvider` or `ZackKitzmiller\Laravel5\TinyServiceProvider` -- optionally register the `ZackKitzmiller\Facades\Tiny` facade alias -- publish the config with `php artisan vendor:publish --tag=tiny-config` -- set `TINY_KEY` in your environment or run `php artisan tiny:generate` +- Laravel 5+: + - register `ZackKitzmiller\Laravel5\TinyServiceProvider` + - optionally register the `ZackKitzmiller\Facades\Tiny` facade alias + - publish the config with `php artisan vendor:publish --tag=tiny-config` + - set `TINY_KEY` in your environment or run `php artisan tiny:generate` +- older Laravel integrations: + - register `ZackKitzmiller\TinyServiceProvider` + - optionally register the `ZackKitzmiller\Facades\Tiny` facade alias + - publish the package configuration with the framework command your app version expects + - run `php artisan tiny:generate` to persist the generated key diff --git a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php index 9040d49..e29b9eb 100644 --- a/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php +++ b/src/ZackKitzmiller/Laravel5/TinyServiceProvider.php @@ -34,11 +34,9 @@ public function register(): void $key = $app['config']->get('tiny.key'); if ($key === null || $key === false) { - $key = getenv(EnvironmentKeyUpdater::PRIMARY_KEY); - } - - if ($key === false) { - $key = getenv(EnvironmentKeyUpdater::LEGACY_KEY); + $primaryKey = getenv(EnvironmentKeyUpdater::PRIMARY_KEY); + $legacyKey = getenv(EnvironmentKeyUpdater::LEGACY_KEY); + $key = $primaryKey !== false ? $primaryKey : $legacyKey; } if (! is_string($key) || $key === '') { diff --git a/src/ZackKitzmiller/TinyServiceProvider.php b/src/ZackKitzmiller/TinyServiceProvider.php index 5d1fc8b..2664206 100644 --- a/src/ZackKitzmiller/TinyServiceProvider.php +++ b/src/ZackKitzmiller/TinyServiceProvider.php @@ -32,11 +32,9 @@ public function register(): void $key = $app['config']->get('tiny.key'); if ($key === null || $key === false) { - $key = getenv(EnvironmentKeyUpdater::PRIMARY_KEY); - } - - if ($key === false) { - $key = getenv(EnvironmentKeyUpdater::LEGACY_KEY); + $primaryKey = getenv(EnvironmentKeyUpdater::PRIMARY_KEY); + $legacyKey = getenv(EnvironmentKeyUpdater::LEGACY_KEY); + $key = $primaryKey !== false ? $primaryKey : $legacyKey; } if (! is_string($key) || $key === '') { From 9e6be8de79e8373c6be35e4d87a1ae95ade8a92f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 05:07:25 +0000 Subject: [PATCH 16/18] Fix PHP 8.1 test job Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- README.md | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d6ec60c..c17645a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Tiny is a small reversible ID obfuscator for PHP. It encodes integers into a cus - PHP 8.1+ baseline - PSR-4 autoloading -- PHPUnit 11 test suite +- PHPUnit 10/11 test suite - GitHub Actions CI - Safer alphabet validation and decoding errors diff --git a/composer.json b/composer.json index 279adff..6135576 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "php": "^8.1" }, "require-dev": { - "phpunit/phpunit": "^11.5" + "phpunit/phpunit": "^10.5 || ^11.5" }, "autoload": { "psr-4": { From 0fdc45d6b3bbed9a39852a95824898d7c52b8636 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 05:08:55 +0000 Subject: [PATCH 17/18] Restore env-specific generate paths Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- .../Laravel5/TinyGenerateCommand.php | 21 +++ src/ZackKitzmiller/TinyGenerateCommand.php | 25 ++- ...TinyGenerateCommandEnvironmentPathTest.php | 142 ++++++++++++++++++ 3 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 tests/TinyGenerateCommandEnvironmentPathTest.php diff --git a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php index f497a63..13820b4 100644 --- a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php @@ -63,6 +63,20 @@ private function writeEnvironmentFile(string $path, string $content): void private function environmentFilePath(): string { + $environment = $this->environmentOption(); + + if ($environment !== null) { + if (method_exists($this->laravel, 'environmentPath')) { + return rtrim($this->laravel->environmentPath(), '/\\') . '/.env.' . $environment; + } + + if (method_exists($this->laravel, 'basePath')) { + return $this->laravel->basePath('.env.' . $environment); + } + + return $this->laravel['path.base'] . '/.env.' . $environment; + } + if (method_exists($this->laravel, 'environmentFilePath')) { return $this->laravel->environmentFilePath(); } @@ -73,4 +87,11 @@ private function environmentFilePath(): string return $this->laravel['path.base'] . '/.env'; } + + private function environmentOption(): ?string + { + $environment = $this->option('env'); + + return is_string($environment) && $environment !== '' ? $environment : null; + } } diff --git a/src/ZackKitzmiller/TinyGenerateCommand.php b/src/ZackKitzmiller/TinyGenerateCommand.php index 0c8f82d..0bdf67a 100644 --- a/src/ZackKitzmiller/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/TinyGenerateCommand.php @@ -60,6 +60,20 @@ private function writeEnvironmentFile(string $path, string $content): void private function environmentFilePath(): string { + $environment = $this->environmentOption(); + + if ($environment !== null) { + if (method_exists($this->laravel, 'environmentPath')) { + return rtrim($this->laravel->environmentPath(), '/\\') . '/.env.' . $environment; + } + + if (method_exists($this->laravel, 'basePath')) { + return $this->laravel->basePath('.env.' . $environment); + } + + return $this->laravel['path.base'] . '/.env.' . $environment; + } + if (method_exists($this->laravel, 'environmentFilePath')) { return $this->laravel->environmentFilePath(); } @@ -71,6 +85,13 @@ private function environmentFilePath(): string return $this->laravel['path.base'] . '/.env'; } + private function environmentOption(): ?string + { + $environment = $this->option('env'); + + return is_string($environment) && $environment !== '' ? $environment : null; + } + private function updateLegacyConfigFile(string $key): void { $path = $this->legacyConfigFilePath(); @@ -100,8 +121,8 @@ private function legacyConfigFilePath(): ?string return null; } - $environment = $this->option('env'); - $environment = is_string($environment) && $environment !== '' ? $environment . '/' : ''; + $environment = $this->environmentOption(); + $environment = $environment !== null ? $environment . '/' : ''; return $this->laravel['path'] . "/config/packages/zackkitzmiller/tiny/{$environment}config.php"; } diff --git a/tests/TinyGenerateCommandEnvironmentPathTest.php b/tests/TinyGenerateCommandEnvironmentPathTest.php new file mode 100644 index 0000000..bdd282c --- /dev/null +++ b/tests/TinyGenerateCommandEnvironmentPathTest.php @@ -0,0 +1,142 @@ +options[$key] ?? null; + } + + public function setOptions(array $options): void + { + $this->options = $options; + } +} +PHP); +} + +if (! class_exists(\Illuminate\Filesystem\Filesystem::class)) { + eval(<<<'PHP' +namespace Illuminate\Filesystem; + +class Filesystem +{ +} +PHP); +} + +require_once __DIR__ . '/../src/ZackKitzmiller/TinyGenerateCommand.php'; +require_once __DIR__ . '/../src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php'; + +final class TinyGenerateCommandEnvironmentPathTest extends TestCase +{ + /** + * @return array + */ + public static function commandClasses(): array + { + return [ + 'current command' => [\ZackKitzmiller\TinyGenerateCommand::class, '/var/www/current/.env'], + 'laravel5 command' => [\ZackKitzmiller\Laravel5\TinyGenerateCommand::class, '/var/www/legacy/.env'], + ]; + } + + /** + * @dataProvider commandClasses + */ + public function testUsesDefaultEnvironmentFilePathWhenEnvOptionIsMissing(string $commandClass, string $defaultPath): void + { + $command = $this->makeCommand($commandClass, new FakeLaravelApplication(dirname($defaultPath), $defaultPath)); + + self::assertSame($defaultPath, $this->environmentFilePath($command)); + } + + /** + * @dataProvider commandClasses + */ + public function testUsesEnvironmentSpecificFileWhenEnvOptionIsProvided(string $commandClass, string $defaultPath): void + { + $command = $this->makeCommand($commandClass, new FakeLaravelApplication(dirname($defaultPath), $defaultPath), 'staging'); + + self::assertSame(dirname($defaultPath) . '/.env.staging', $this->environmentFilePath($command)); + } + + /** + * @param class-string $commandClass + */ + private function makeCommand(string $commandClass, FakeLaravelApplication $laravel, ?string $environment = null): object + { + $command = new $commandClass(new \Illuminate\Filesystem\Filesystem()); + $command->laravel = $laravel; + $command->setOptions(['env' => $environment]); + + return $command; + } + + private function environmentFilePath(object $command): string + { + $method = new ReflectionMethod($command, 'environmentFilePath'); + $method->setAccessible(true); + + /** @var string */ + return $method->invoke($command); + } +} + +final class FakeLaravelApplication implements ArrayAccess +{ + public function __construct( + private string $basePath, + private string $environmentFilePath, + ) { + } + + public function environmentFilePath(): string + { + return $this->environmentFilePath; + } + + public function environmentPath(): string + { + return $this->basePath; + } + + public function basePath(string $path = ''): string + { + return $path === '' ? $this->basePath : $this->basePath . '/' . ltrim($path, '/'); + } + + public function offsetExists(mixed $offset): bool + { + return $offset === 'path.base'; + } + + public function offsetGet(mixed $offset): mixed + { + return $offset === 'path.base' ? $this->basePath : null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + } + + public function offsetUnset(mixed $offset): void + { + } +} From 6b0c352beb51b3243e90d5230ceef426e58887af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 05:09:40 +0000 Subject: [PATCH 18/18] Harden tiny generate env handling Co-authored-by: zackkitzmiller <111536+zackkitzmiller@users.noreply.github.com> --- src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php | 11 ++++++++++- src/ZackKitzmiller/TinyGenerateCommand.php | 10 +++++++++- tests/TinyGenerateCommandEnvironmentPathTest.php | 12 ++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php index 13820b4..9682de4 100644 --- a/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/Laravel5/TinyGenerateCommand.php @@ -6,6 +6,7 @@ use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; +use InvalidArgumentException; use RuntimeException; use ZackKitzmiller\EnvironmentKeyUpdater; use ZackKitzmiller\Tiny; @@ -92,6 +93,14 @@ private function environmentOption(): ?string { $environment = $this->option('env'); - return is_string($environment) && $environment !== '' ? $environment : null; + if (! is_string($environment) || $environment === '') { + return null; + } + + if (preg_match('/\A[A-Za-z0-9_-]+\z/', $environment) !== 1) { + throw new InvalidArgumentException('The --env option may only contain letters, numbers, dashes, and underscores.'); + } + + return $environment; } } diff --git a/src/ZackKitzmiller/TinyGenerateCommand.php b/src/ZackKitzmiller/TinyGenerateCommand.php index 0bdf67a..5209aa5 100644 --- a/src/ZackKitzmiller/TinyGenerateCommand.php +++ b/src/ZackKitzmiller/TinyGenerateCommand.php @@ -89,7 +89,15 @@ private function environmentOption(): ?string { $environment = $this->option('env'); - return is_string($environment) && $environment !== '' ? $environment : null; + if (! is_string($environment) || $environment === '') { + return null; + } + + if (preg_match('/\A[A-Za-z0-9_-]+\z/', $environment) !== 1) { + throw new \InvalidArgumentException('The --env option may only contain letters, numbers, dashes, and underscores.'); + } + + return $environment; } private function updateLegacyConfigFile(string $key): void diff --git a/tests/TinyGenerateCommandEnvironmentPathTest.php b/tests/TinyGenerateCommandEnvironmentPathTest.php index bdd282c..b26f223 100644 --- a/tests/TinyGenerateCommandEnvironmentPathTest.php +++ b/tests/TinyGenerateCommandEnvironmentPathTest.php @@ -77,6 +77,18 @@ public function testUsesEnvironmentSpecificFileWhenEnvOptionIsProvided(string $c self::assertSame(dirname($defaultPath) . '/.env.staging', $this->environmentFilePath($command)); } + /** + * @dataProvider commandClasses + */ + public function testRejectsUnsafeEnvironmentNames(string $commandClass, string $defaultPath): void + { + $command = $this->makeCommand($commandClass, new FakeLaravelApplication(dirname($defaultPath), $defaultPath), '../staging'); + + $this->expectException(InvalidArgumentException::class); + + $this->environmentFilePath($command); + } + /** * @param class-string $commandClass */