Skip to content

Commit a670ce7

Browse files
committed
Implement sqids:check and sqids:alphabet commands
1 parent 6b0cc6a commit a670ce7

File tree

6 files changed

+307
-0
lines changed

6 files changed

+307
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RossBearman\Sqids\Console\Commands;
6+
7+
use Illuminate\Console\Command;
8+
use Illuminate\Support\Collection;
9+
use Illuminate\Support\Str;
10+
use RossBearman\Sqids\Codecs\SqidCodec;
11+
use RossBearman\Sqids\Sqids;
12+
13+
final class AlphabetCommand extends Command
14+
{
15+
protected $signature = 'sqids:alphabet
16+
{ models?* : The FQDNs of the models to generate an alphabet for }
17+
{ --key= : A secure key to use when shuffling the alphabet }
18+
{ --P|plain : Get key pairs without any instructions }';
19+
20+
protected $description = 'Generate a new alphabet for a specific model';
21+
22+
public function handle(Sqids $sqids): void
23+
{
24+
if (count($this->argument('models')) === 0) {
25+
$this->info((string) $sqids->shuffleDefaultAlphabet($this->option('key') ?: bin2hex(random_bytes(32))));
26+
27+
return;
28+
}
29+
30+
$alphabets = collect($this->argument('models'))->mapWithKeys(
31+
fn (string $model) => [$model => $sqids->fromClass($model)]
32+
);
33+
34+
$this->option('plain') ? $this->printPlain($alphabets) : $this->printInstructions($alphabets);
35+
}
36+
37+
/** @param Collection<string, SqidCodec> $codecs */
38+
protected function printPlain(Collection $codecs): void
39+
{
40+
foreach ($codecs as $model => $codec) {
41+
$this->info("{$model}: {$codec->alphabet}");
42+
}
43+
}
44+
45+
/** @param Collection<string, SqidCodec> $codecs */
46+
protected function printInstructions(Collection $codecs): void
47+
{
48+
$this->warn('Update your `sqids.php` config file to include the following items:');
49+
$this->newLine();
50+
$this->info("'alphabets' => [");
51+
52+
foreach ($codecs as $model => $codec) {
53+
$this->info(" {$model}::class => env('{$this->getEnvKey($model)}')");
54+
}
55+
56+
$this->info("'];");
57+
$this->newLine();
58+
$this->warn('And add these keys to your .env:');
59+
$this->newLine();
60+
61+
foreach ($codecs as $model => $codec) {
62+
$this->info("{$this->getEnvKey($model)}={$codec->alphabet}");
63+
}
64+
}
65+
66+
protected function getModelName(string $model): string
67+
{
68+
return Str::afterLast($model, '\\');
69+
}
70+
71+
protected function getEnvKey(string $model): string
72+
{
73+
return 'SQIDS_ALPHABET_' . Str::of($this->getModelName($model))->snake()->upper();
74+
}
75+
}

src/Console/Commands/CheckCommand.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RossBearman\Sqids\Console\Commands;
6+
7+
use Illuminate\Console\Command;
8+
use RossBearman\Sqids\Sqids;
9+
use RossBearman\Sqids\Support\ConfigResolver;
10+
11+
final class CheckCommand extends Command
12+
{
13+
protected $signature = 'sqids:check
14+
{ models?* : The FQDNs of the models to check }';
15+
16+
protected $description = 'Check the models have a fixed alphabet registered in config';
17+
18+
public function handle(ConfigResolver $config): void
19+
{
20+
/**
21+
* ```shell
22+
* php artisan sqids:check App\Models\Customer
23+
* ```
24+
*
25+
* If set correctly, you will see output similar to the following, without any warnings.
26+
*
27+
* ```shell
28+
* App\Models\Customer found in app\Models\Customer.php
29+
* Fixed alphabet found in config/sqids.php
30+
*
31+
* Alphabet: s7tc0TE3AfrqyMjFvbgunkhZDBp6NCIRJoQldLm8wYxHa5iWzVeP124SXUOK9G
32+
* ````
33+
*/
34+
if (count($this->argument('models')) === 0) {
35+
if (empty($config->getAlphabets())) {
36+
$this->error('No alphabets have been defined for models in `sqids.php`');
37+
38+
return;
39+
}
40+
41+
$alphabets = [];
42+
foreach ($config->getAlphabets() as $model => $alphabet) {
43+
class_exists($model) ?
44+
$alphabets['found'][] = ['model' => $model, 'alphabet' => (string) $alphabet] :
45+
$alphabets['not_found'][] = ['model' => $model, 'alphabet' => (string) $alphabet];
46+
}
47+
48+
if (!empty($alphabets['found'])) {
49+
$this->info('The following classes have alphabets defined in the config');
50+
$this->table(['Model', 'Alphabet'], $alphabets['found']);
51+
$this->newLine();
52+
}
53+
54+
if (!empty($alphabets['not_found'])) {
55+
$this->error('The following entries were found in the config, but the class could not be resolved');
56+
$this->table(['Model', 'Alphabet'], $alphabets['not_found']);
57+
$this->newLine();
58+
}
59+
}
60+
}
61+
}

src/SqidsServiceProvider.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use Illuminate\Contracts\Foundation\Application;
88
use Illuminate\Support\ServiceProvider;
9+
use RossBearman\Sqids\Console\Commands\AlphabetCommand;
10+
use RossBearman\Sqids\Console\Commands\CheckCommand;
911
use RossBearman\Sqids\Support\ConfigResolver;
1012

1113
final class SqidsServiceProvider extends ServiceProvider
@@ -15,6 +17,12 @@ public function register(): void
1517
$this->app->singleton(Sqids::class, function (Application $app) {
1618
return new Sqids(new ConfigResolver($app['config']['sqids']));
1719
});
20+
21+
$this->app->when(CheckCommand::class)
22+
->needs(ConfigResolver::class)
23+
->give(function (Application $app) {
24+
return new ConfigResolver($app['config']['sqids']);
25+
});
1826
}
1927

2028
public function boot(): void
@@ -27,6 +35,11 @@ public function boot(): void
2735
$this->publishes([
2836
__DIR__ . '/../config/sqids.php' => config_path('sqids.php'),
2937
]);
38+
39+
$this->commands([
40+
AlphabetCommand::class,
41+
CheckCommand::class,
42+
]);
3043
}
3144
}
3245
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RossBearman\Sqids\Tests\Feature\Console\Commands;
6+
7+
use Illuminate\Support\Facades\Artisan;
8+
use Illuminate\Support\Facades\Config;
9+
use Orchestra\Testbench\Concerns\WithWorkbench;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use PHPUnit\Framework\Attributes\Test;
12+
use RossBearman\Sqids\Console\Commands\AlphabetCommand;
13+
use RossBearman\Sqids\Facades\Sqids;
14+
use RossBearman\Sqids\Tests\TestCase;
15+
16+
final class AlphabetCommandTest extends TestCase
17+
{
18+
use WithWorkbench;
19+
20+
#[Test]
21+
public function it_can_generate_consistent_alphabets_for_models(): void
22+
{
23+
$status = Artisan::call(AlphabetCommand::class, [
24+
'models' => ['App\Models\Customer', 'App\Models\Order'],
25+
'--plain' => true,
26+
]);
27+
$this->assertEquals(0, $status);
28+
29+
$alphabets = collect(explode("\n", Artisan::output()))
30+
->filter()
31+
->map(function ($pair) {
32+
[$model, $alphabet] = explode(': ', $pair);
33+
34+
return ['name' => $model, 'alphabet' => $alphabet];
35+
});
36+
37+
$status = Artisan::call(AlphabetCommand::class, [
38+
'models' => ['App\Models\Customer', 'App\Models\Order'],
39+
'--plain' => true,
40+
]);
41+
$this->assertEquals(0, $status);
42+
43+
$output = Artisan::output();
44+
45+
foreach ($alphabets as $model) {
46+
$this->assertStringContainsString("{$model['name']}: {$model['alphabet']}", $output);
47+
}
48+
}
49+
50+
#[Test]
51+
public function it_outputs_shuffled_alphabet_with_no_input(): void
52+
{
53+
$alphabet = Config::get('sqids.alphabet');
54+
55+
Artisan::call(AlphabetCommand::class);
56+
57+
$output = rtrim(Artisan::output());
58+
59+
$this->assertNotSame($alphabet, $output);
60+
$this->assertSame(strlen($alphabet), strlen($output));
61+
62+
foreach (str_split($alphabet) as $element) {
63+
$this->assertStringContainsString($element, $output);
64+
}
65+
}
66+
67+
#[Test]
68+
public function it_outputs_consistently_shuffled_alphabet_with_key(): void
69+
{
70+
Artisan::call(AlphabetCommand::class, ['--key' => 'test-key']);
71+
$outputOne = Artisan::output();
72+
73+
Artisan::call(AlphabetCommand::class, ['--key' => 'test-key']);
74+
$outputTwo = Artisan::output();
75+
76+
$this->assertSame($outputOne, $outputTwo);
77+
}
78+
79+
#[Test]
80+
#[DataProvider('modelProvider')]
81+
public function it_prints_config_instructions_for_models(string $model, string $envKey): void
82+
{
83+
$alphabet = Sqids::fromClass($model)->alphabet->value;
84+
85+
$status = Artisan::call(AlphabetCommand::class, [
86+
'models' => [$model],
87+
]);
88+
$this->assertEquals(0, $status);
89+
90+
$output = Artisan::output();
91+
$this->assertStringContainsString("{$model}::class => env('{$envKey}')", $output);
92+
$this->assertStringContainsString("{$envKey}={$alphabet}", $output);
93+
}
94+
95+
public static function modelProvider(): array
96+
{
97+
return [
98+
['Model', 'SQIDS_ALPHABET_MODEL'],
99+
['\\Model', 'SQIDS_ALPHABET_MODEL'],
100+
['App\\Model', 'SQIDS_ALPHABET_MODEL'],
101+
['\\App\\Model', 'SQIDS_ALPHABET_MODEL'],
102+
['App\\Models\\Model', 'SQIDS_ALPHABET_MODEL'],
103+
['App\\Models\\Model\\Model', 'SQIDS_ALPHABET_MODEL'],
104+
['ThreeWordModel', 'SQIDS_ALPHABET_THREE_WORD_MODEL'],
105+
['App\\Models\\ThreeWordModel', 'SQIDS_ALPHABET_THREE_WORD_MODEL'],
106+
];
107+
}
108+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RossBearman\Sqids\Tests\Feature\Console\Commands;
6+
7+
use Illuminate\Support\Facades\Artisan;
8+
use Orchestra\Testbench\Concerns\WithWorkbench;
9+
use PHPUnit\Framework\Attributes\Test;
10+
use RossBearman\Sqids\Console\Commands\CheckCommand;
11+
use RossBearman\Sqids\Tests\TestCase;
12+
13+
final class CheckCommandTest extends TestCase
14+
{
15+
use WithWorkbench;
16+
17+
#[Test]
18+
public function it_can_list_the_models_that_have_fixed_alphabets(): void
19+
{
20+
config()->set('sqids.alphabets', [
21+
'RossBearman\Sqids\Tests\Testbench\Models\Calamari' => 'abcdefg',
22+
'App\Models\Order' => 'hijklmn',
23+
]);
24+
25+
$status = Artisan::call(CheckCommand::class);
26+
$this->assertEquals(0, $status);
27+
28+
$output = Artisan::output();
29+
30+
$this->assertStringContainsString('The following classes have alphabets defined in the config', $output);
31+
$this->assertStringContainsString('RossBearman\Sqids\Tests\Testbench\Models\Calamari', $output);
32+
$this->assertStringContainsString('abcdefg', $output);
33+
34+
$this->assertStringContainsString('The following entries were found in the config, but the class could not be resolved', $output);
35+
$this->assertStringContainsString('App\Models\Order', $output);
36+
$this->assertStringContainsString('hijklmn', $output);
37+
}
38+
39+
#[Test]
40+
public function it_warns_user_if_no_fixed_alphabets_are_defined(): void
41+
{
42+
$status = Artisan::call(CheckCommand::class);
43+
$this->assertEquals(0, $status);
44+
45+
$output = Artisan::output();
46+
47+
$this->assertStringContainsString('No alphabets have been defined for models', $output);
48+
}
49+
}

tests/TestCase.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ protected function setUp(): void
1919
{
2020
parent::setUp();
2121

22+
$this->withoutMockingConsoleOutput();
2223
$this->withoutExceptionHandling();
2324

2425
$this->loadMigrationsFrom(__DIR__ . '/Testbench/database/migrations');

0 commit comments

Comments
 (0)