├── LICENSE.md
├── README.md
├── composer.json
├── config
└── sqids.php
├── src
├── Concerns
│ └── HasSqids.php
├── Contracts
│ └── Prefix.php
├── Mixins
│ ├── FindBySqidMixin.php
│ ├── FindBySqidOrFailMixin.php
│ ├── WhereSqidInMixin.php
│ ├── WhereSqidMixin.php
│ └── WhereSqidNotInMixin.php
├── Model.php
├── Prefixes
│ ├── ConsonantPrefix.php
│ └── SimplePrefix.php
├── Sqids.php
├── SqidsServiceProvider.php
└── Support
│ └── Config.php
└── testbench.yaml
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Red Explosion
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | # Laravel Sqids
4 |
5 | [](https://packagist.org/packages/red-explosion/laravel-sqids)
6 | [](https://github.com/red-explosion/laravel-sqids/actions/workflows/tests.yml?query=branch:main)
7 | [](https://github.com/red-explosion/laravel-sqids/actions/workflows/coding-standards.yml?query=branch:main)
8 | [](https://packagist.org/packages/red-explosion/laravel-sqids)
9 |
10 | Laravel Sqids (pronounced "squids") allows you to easily generate Stripe/YouTube looking IDs for your Laravel models.
11 | These IDs are short and are guaranteed to be Collision free.
12 |
13 | For more information on Sqids, we recommend checking out the official Sqids (formerly Hashids) website: [https://sqids.org](https://sqids.org).
14 |
15 | ## Installation
16 |
17 | To get started, install Laravel Sqids via the Composer package manager:
18 |
19 | ```shell
20 | composer require red-explosion/laravel-sqids
21 | ```
22 |
23 | Next, you should publish the Sqids configuration file using the `vendor:publish` artisan command. The `sqids`
24 | configuration file will be placed in your applications `config` directory:
25 |
26 | ```shell
27 | php artisan vendor:publish --provider="RedExplosion\Sqids\SqidsServiceProvider"
28 | ```
29 |
30 | ## Usage
31 |
32 | ### Using Sqids
33 |
34 | To use Laravel Sqids, simply add the `RedExplosion\Sqids\Concerns\HasSqids` trait to your model:
35 |
36 | ```php
37 | use RedExplosion\Sqids\Concerns\HasSqids;
38 |
39 | class User extends Authenticatable
40 | {
41 | use HasSqids;
42 | }
43 | ```
44 |
45 | You will now be able to access the Sqid for the model, by calling the `sqid` attribute:
46 |
47 | ```php
48 | $user = User::first();
49 |
50 | $sqid = $user->sqid; // use_A3EyoEb2TO
51 | ```
52 |
53 | The result of `$sqid` will be encoded value of the models primary key along with the model prefix.
54 |
55 | > [!Tip]
56 | > Only integers can be encoded, and therefore we recommend using this package in conjunction with auto
57 | incrementing IDs.
58 |
59 | If you would like to set a custom prefix for the model, you can override it by setting a `$sqidPrefix` property value
60 | on your model like so:
61 |
62 | ```php
63 | use RedExplosion\Sqids\Concerns\HasSqids;
64 |
65 | class User extends Authenticatable
66 | {
67 | use HasSqids;
68 |
69 | protected string $sqidPrefix = 'user';
70 | }
71 |
72 | $user = User::first();
73 | $sqid = $user->sqid; // user_A3EyoEb2TO
74 | ```
75 |
76 | ### Builder Mixins
77 |
78 | Laravel Sqids provides a number of Eloquent builder mixins to make working with Sqids seamless.
79 |
80 | #### Find by Sqid
81 |
82 | To find a model by a given Sqid, you can use the `findBySqid` method:
83 |
84 | ```php
85 | $user = User::findBySqid('use_A3EyoEb2TO');
86 | ```
87 |
88 | If the model doesn't exist, `null` will be returned. However, if you would like to throw an exception, you can use
89 | the `findBySqidOrFail` method instead which will throw a `ModelNotFoundException` when a model can't be found:
90 |
91 | ```php
92 | $user = User::findBySqidOrFail('use_invalid');
93 | ```
94 |
95 | #### Where Sqid
96 |
97 | To add a where clause to your query, you can use the `whereSqid` method:
98 |
99 | ```php
100 | $users = User::query()
101 | ->whereSqid('use_A3EyoEb2TO')
102 | ->get();
103 | ```
104 |
105 | This will retrieve all users where the Sqid/primary key matches the given value.
106 |
107 | #### Where Sqid in
108 |
109 | To get all models where the Sqid is in a given array, you can use the `whereSqidIn` method:
110 |
111 | ```php
112 | $users = User::query()
113 | ->whereSqidIn('id', ['use_A3EyoEb2TO'])
114 | ->get();
115 | ```
116 |
117 | This will return all users where the `id` is in the array of decoded Sqids.
118 |
119 | #### Where Sqid not in
120 |
121 | To get all models where the Sqid is not in a given array, you can use the `whereSqidNotIn` method:
122 |
123 | ```php
124 | $users = User::query()
125 | ->whereSqidNotIn('id', ['use_A3EyoEb2TO'])
126 | ->get();
127 | ```
128 |
129 | This will return all users where the `id` is not in the array of decoded Sqids.
130 |
131 | ### Route model binding
132 |
133 | Laravel Sqids supports route model binding out of the box. Simply create a route as you normally would and we'll take
134 | care of the rest:
135 |
136 | ```php
137 | // GET /users/use_A3EyoEb2TO
138 | Route::get('users/{user}', function (User $user) {
139 | return "Hello $user->name";
140 | });
141 | ```
142 |
143 | ### Finding a model from a Sqid
144 |
145 | One of the most powerful features of Laravel Sqids is being able to resolve a model instance from a given Sqid. This
146 | could be incredibly powerful when searching models across your application.
147 |
148 | ```php
149 | use RedExplosion\Sqids\Model;
150 |
151 | $model = Model::find('use_A3EyoEb2TO');
152 | ```
153 |
154 | When we run the following, `$user` will be an instance of the `User` model for the given Sqid. If no model could be
155 | found, then `null` will be returned.
156 |
157 | if you would like to throw an exception instead, you can use the `findOrFail` method which will throw an instance of
158 | the `ModelNotFoundException`:
159 |
160 | ```php
161 | use RedExplosion\Sqids\Model;
162 |
163 | $model = Model::findOrFail('use_A3EyoEb2TO');
164 | ```
165 |
166 | > [!IMPORTANT]
167 | > In order to use this feature, you must use prefixes for your Sqids.
168 |
169 | ## Testing
170 |
171 | ```shell
172 | composer test
173 | ```
174 |
175 | ## Changelog
176 |
177 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
178 |
179 | ## Contributing
180 |
181 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
182 |
183 | ## Security Vulnerabilities
184 |
185 | If you discover a security vulnerability, please send an e-mail to Ben Sherred via ben@redexplosion.co.uk. All security
186 | vulnerabilities will be promptly addressed.
187 |
188 | ## Credits
189 |
190 | - [Ben Sherred](https://github.com/bensherred)
191 | - [All Contributors](../../contributors)
192 |
193 | ## License
194 |
195 | Laravel Sqids is open-sourced software licensed under the [MIT license](LICENSE.md).
196 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "red-explosion/laravel-sqids",
3 | "description": "Easily generate Stripe/YouTube looking IDs for your Laravel models.",
4 | "license": "MIT",
5 | "homepage": "https://github.com/red-explosion/laravel-sqids",
6 | "type": "library",
7 | "keywords": [
8 | "red-explosion",
9 | "laravel",
10 | "laravel-sqids"
11 | ],
12 | "authors": [
13 | {
14 | "name": "Ben Sherred",
15 | "email": "ben@sherred.co.uk"
16 | }
17 | ],
18 | "require": {
19 | "php": "^8.2",
20 | "ext-mbstring": "*",
21 | "illuminate/contracts": "^10.0|^11.0|^12.0",
22 | "illuminate/support": "^10.0|^11.0|^12.0",
23 | "sqids/sqids": "^0.4.1"
24 | },
25 | "require-dev": {
26 | "laravel/pint": "^1.10",
27 | "orchestra/testbench": "^8.0|^9.0|^10.0",
28 | "pestphp/pest": "^2.0|^3.0",
29 | "pestphp/pest-plugin-arch": "^2.0|^3.0",
30 | "phpstan/phpstan": "^1.10",
31 | "red-explosion/pint-config": "^1.1",
32 | "spatie/laravel-ray": "^1.32"
33 | },
34 | "autoload": {
35 | "psr-4": {
36 | "RedExplosion\\Sqids\\": "src/"
37 | }
38 | },
39 | "autoload-dev": {
40 | "psr-4": {
41 | "RedExplosion\\Sqids\\Tests\\": "tests/",
42 | "Workbench\\App\\": "workbench/app/",
43 | "Workbench\\Database\\Factories\\": "workbench/database/factories/",
44 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/"
45 | }
46 | },
47 | "scripts": {
48 | "post-autoload-dump": [
49 | "@clear",
50 | "@prepare"
51 | ],
52 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
53 | "prepare": "@php vendor/bin/testbench package:discover --ansi",
54 | "build": "@php vendor/bin/testbench workbench:build --ansi",
55 | "serve": [
56 | "Composer\\Config::disableProcessTimeout",
57 | "@build",
58 | "@php vendor/bin/testbench serve"
59 | ],
60 | "lint": "./vendor/bin/pint --config vendor/red-explosion/pint-config/pint.json",
61 | "test:lint": "./vendor/bin/pint --config vendor/red-explosion/pint-config/pint.json --test",
62 | "test:types": "./vendor/bin/phpstan analyse --ansi",
63 | "test:unit": "./vendor/bin/pest --compact --colors=always",
64 | "test": [
65 | "@test:lint",
66 | "@test:types",
67 | "@test:unit"
68 | ]
69 | },
70 | "extra": {
71 | "laravel": {
72 | "providers": [
73 | "RedExplosion\\Sqids\\SqidsServiceProvider"
74 | ]
75 | }
76 | },
77 | "config": {
78 | "sort-packages": true,
79 | "allow-plugins": {
80 | "pestphp/pest-plugin": true
81 | }
82 | },
83 | "minimum-stability": "dev",
84 | "prefer-stable": true
85 | }
86 |
--------------------------------------------------------------------------------
/config/sqids.php:
--------------------------------------------------------------------------------
1 | env('APP_KEY'),
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Alphabet
23 | |--------------------------------------------------------------------------
24 | |
25 | | This option controls the default "alphabet" used for generating Sqids.
26 | | The characters and numbers listed below will be included. You must
27 | | have at least 3 unique characters or numbers.
28 | |
29 | */
30 |
31 | 'alphabet' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
32 |
33 | /*
34 | |--------------------------------------------------------------------------
35 | | Length
36 | |--------------------------------------------------------------------------
37 | |
38 | | This option controls the "minimum length" of the generated Sqid
39 | | excluding the prefix and separator. This value must be greater
40 | | than 0.
41 | |
42 | */
43 |
44 | 'min_length' => 10,
45 |
46 | /*
47 | |--------------------------------------------------------------------------
48 | | Blacklist
49 | |--------------------------------------------------------------------------
50 | |
51 | | This option allows you to "blacklist" certain words that shouldn't be
52 | | included in the generated Sqids.
53 | |
54 | */
55 |
56 | 'blacklist' => [],
57 |
58 | /*
59 | |--------------------------------------------------------------------------
60 | | Separator
61 | |--------------------------------------------------------------------------
62 | |
63 | | This option controls the "separator" between the prefix and the
64 | | generated Sqid.
65 | |
66 | */
67 |
68 | 'separator' => '_',
69 |
70 | /*
71 | |--------------------------------------------------------------------------
72 | | Prefix Class
73 | |--------------------------------------------------------------------------
74 | |
75 | | This option controls the class that should be used for generating the
76 | | Sqid prefix. You can use any class that implements the following
77 | | contract: \RedExplosion\Sqids\Contracts\Prefix.
78 | |
79 | */
80 |
81 | 'prefix_class' => RedExplosion\Sqids\Prefixes\ConsonantPrefix::class,
82 |
83 | ];
84 |
--------------------------------------------------------------------------------
/src/Concerns/HasSqids.php:
--------------------------------------------------------------------------------
1 | append(['sqid']);
19 | }
20 |
21 | public function getSqidAttribute(): ?string
22 | {
23 | return Sqids::forModel(model: $this);
24 | }
25 |
26 | public function getSqidPrefix(): ?string
27 | {
28 | return $this->sqidPrefix ?? null;
29 | }
30 |
31 | /**
32 | * Get the route key for the model.
33 | */
34 | public function getRouteKeyName(): string
35 | {
36 | return 'sqid';
37 | }
38 |
39 | /**
40 | * Retrieve the model for a bound value.
41 | *
42 | * @param Model|Relation $query
43 | * @param mixed $value
44 | * @param null $field
45 | */
46 | public function resolveRouteBindingQuery($query, $value, $field = null): Builder|Relation
47 | {
48 | if ($field && $field !== 'sqid') {
49 | return parent::resolveRouteBindingQuery(query: $query, value: $value, field: $field);
50 | }
51 |
52 | if (! $field && $this->getRouteKeyName() !== 'sqid') {
53 | return parent::resolveRouteBindingQuery(query: $query, value: $value, field: $field);
54 | }
55 |
56 | return $query->whereSqid(sqid: $value);
57 | }
58 |
59 | public static function keyFromSqid(string $sqid): ?int
60 | {
61 | $sqid = Str::afterLast(subject: $sqid, search: Config::separator());
62 |
63 | $length = Str::length(value: $sqid);
64 |
65 | if ($length < Config::minLength()) {
66 | return null;
67 | }
68 |
69 | return Sqids::decodeId(model: __CLASS__, id: $sqid)[0] ?? null;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Contracts/Prefix.php:
--------------------------------------------------------------------------------
1 | $model
13 | */
14 | public function prefix(string $model): string;
15 | }
16 |
--------------------------------------------------------------------------------
/src/Mixins/FindBySqidMixin.php:
--------------------------------------------------------------------------------
1 | $this->find(id: $this->getModel()->keyFromSqid(sqid: $sqid), columns: $columns);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Mixins/FindBySqidOrFailMixin.php:
--------------------------------------------------------------------------------
1 | $this->findOrFail(id: $this->getModel()->keyFromSqid(sqid: $sqid), columns: $columns);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Mixins/WhereSqidInMixin.php:
--------------------------------------------------------------------------------
1 | getModel();
19 |
20 | /** @phpstan-ignore-next-line */
21 | $values = array_map(callback: fn (string $sqid) => $this->getModel()->keyFromSqid(sqid: $sqid), array: $sqids);
22 |
23 | return $this->whereIn(column: $column, values: $values, boolean: $boolean, not: $not);
24 | };
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Mixins/WhereSqidMixin.php:
--------------------------------------------------------------------------------
1 | $this->whereKey(id: $this->getModel()->keyFromSqid(sqid: $sqid));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Mixins/WhereSqidNotInMixin.php:
--------------------------------------------------------------------------------
1 | whereSqidIn(column: $column, sqids: $sqids, boolean: $boolean, not: true);
18 | };
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Model.php:
--------------------------------------------------------------------------------
1 | |null $model */
30 | $model = $models[$prefix] ?? null;
31 |
32 | if (! $model) {
33 | return null;
34 | }
35 |
36 | /** @phpstan-ignore-next-line */
37 | return $model::query()->findBySqid(sqid: $sqid);
38 | }
39 |
40 | public static function findOrFail(string $sqid): EloquentModel
41 | {
42 | $models = static::models();
43 | $prefix = Str::beforeLast(subject: $sqid, search: Config::separator());
44 |
45 | /** @var class-string|null $model */
46 | $model = $models[$prefix] ?? null;
47 |
48 | if (! $model) {
49 | throw new ModelNotFoundException();
50 | }
51 |
52 | /** @phpstan-ignore-next-line */
53 | return $model::query()->findBySqidOrFail(sqid: $sqid);
54 | }
55 |
56 | /**
57 | * @return array>
58 | */
59 | protected static function models(): array
60 | {
61 | /** @var array> $models */
62 | $models = collect(static::getFilesRecursively())
63 | ->map(fn (SplFileInfo $file) => self::fullQualifiedClassNameFromFile(file: $file))
64 | ->map(function (string $class): ?ReflectionClass {
65 | try {
66 | /** @phpstan-ignore-next-line */
67 | return new ReflectionClass(objectOrClass: $class);
68 | } catch (Exception|Error) {
69 | return null;
70 | }
71 | })
72 | ->filter()
73 | /** @phpstan-ignore-next-line */
74 | ->filter(fn (ReflectionClass $class): bool => $class->isSubclassOf(class: EloquentModel::class))
75 | /** @phpstan-ignore-next-line */
76 | ->filter(fn (ReflectionClass $class) => ! $class->isAbstract())
77 | /** @phpstan-ignore-next-line */
78 | ->filter(fn (ReflectionClass $class) => in_array(needle: HasSqids::class, haystack: $class->getTraitNames()))
79 | /** @phpstan-ignore-next-line */
80 | ->mapWithKeys(function (ReflectionClass $reflectionClass): array {
81 | /** @var class-string $model */
82 | $model = $reflectionClass->getName();
83 |
84 | return [
85 | Sqids::prefixForModel($model) => $reflectionClass->getName(),
86 | ];
87 | })
88 | ->toArray();
89 |
90 | return $models;
91 | }
92 |
93 | /**
94 | * @return array
95 | */
96 | protected static function namespaces(): array
97 | {
98 | $composer = File::json(path: base_path(path: 'composer.json'));
99 |
100 | /** @var array $namespaces */
101 | $namespaces = Arr::get(array: $composer, key: 'autoload.psr-4', default: []);
102 |
103 | return array_flip($namespaces);
104 | }
105 |
106 | protected static function fullQualifiedClassNameFromFile(SplFileInfo $file): string
107 | {
108 | /** @var Application $application */
109 | $application = app();
110 |
111 | return Str::of(string: $file->getRealPath())
112 | ->replaceFirst(search: static::basePath(), replace: '')
113 | ->replaceLast(search: '.php', replace: '')
114 | ->trim(characters: DIRECTORY_SEPARATOR)
115 | ->replace(
116 | search: array_keys(static::namespaces()),
117 | replace: array_values(static::namespaces())
118 | )
119 | ->ucfirst()
120 | ->replace(
121 | search: [DIRECTORY_SEPARATOR, 'App\\'],
122 | replace: ['\\', $application->getNamespace()],
123 | )
124 | ->toString();
125 | }
126 |
127 | /**
128 | * @return array
129 | */
130 | protected static function getFilesRecursively(): array
131 | {
132 | $files = [];
133 |
134 | $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
135 | directory: static::basePath(),
136 | ));
137 |
138 | /** @var SplFileInfo $file */
139 | foreach ($iterator as $file) {
140 | if ($file->isDir() || str_contains(haystack: $file->getRealPath(), needle: 'vendor')) {
141 | continue;
142 | }
143 |
144 | $files[] = $file;
145 | }
146 |
147 | return $files;
148 | }
149 |
150 | protected static function basePath(): string
151 | {
152 | /** @var Application $application */
153 | $application = app();
154 |
155 | if ($application->runningUnitTests()) {
156 | $basePath = new SplFileInfo(base_path(path: '../../../../'));
157 |
158 | return $basePath->getRealPath();
159 | }
160 |
161 | return base_path();
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/Prefixes/ConsonantPrefix.php:
--------------------------------------------------------------------------------
1 | $model
17 | */
18 | public function prefix(string $model): string
19 | {
20 | $classBasename = class_basename(class: $model);
21 |
22 | $prefix = str_replace(search: ['a', 'e', 'i', 'o', 'u'], replace: '', subject: $classBasename);
23 |
24 | $prefix = rtrim(mb_strimwidth(string: $prefix, start: 0, width: 3, encoding: 'UTF-8'));
25 |
26 | return Str::lower(value: $prefix);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Prefixes/SimplePrefix.php:
--------------------------------------------------------------------------------
1 | $model
17 | */
18 | public function prefix(string $model): string
19 | {
20 | $classBasename = class_basename(class: $model);
21 |
22 | $prefix = rtrim(mb_strimwidth(string: $classBasename, start: 0, width: 3, encoding: 'UTF-8'));
23 |
24 | return Str::lower(value: $prefix);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Sqids.php:
--------------------------------------------------------------------------------
1 | getKey();
17 |
18 | if ($id === null) {
19 | return null;
20 | }
21 |
22 | $prefix = static::prefixForModel(model: $model::class);
23 | $separator = $prefix ? Config::separator() : null;
24 | $sqid = static::encodeId(model: $model::class, id: $id);
25 |
26 | return "{$prefix}{$separator}{$sqid}";
27 | }
28 |
29 | /**
30 | * @param class-string $model
31 | */
32 | public static function prefixForModel(string $model): ?string
33 | {
34 | $prefixClass = Config::prefixClass();
35 |
36 | /** @var string|null $modelPrefix */
37 | /** @phpstan-ignore-next-line */
38 | $modelPrefix = (new $model())->getSqidPrefix();
39 |
40 | if ($modelPrefix) {
41 | return $modelPrefix;
42 | }
43 |
44 | if (! $prefixClass) {
45 | return null;
46 | }
47 |
48 | return $prefixClass->prefix(model: $model);
49 | }
50 |
51 | public static function encodeId(string $model, int $id): string
52 | {
53 | return static::encoder(model: $model)->encode(numbers: [$id]);
54 | }
55 |
56 | /**
57 | * @return array
58 | */
59 | public static function decodeId(string $model, string $id): array
60 | {
61 | return static::encoder(model: $model)->decode(id: $id);
62 | }
63 |
64 | public static function encoder(string $model): SqidsCore
65 | {
66 | $model = mb_strtolower(string: class_basename($model));
67 |
68 | return new SqidsCore(
69 | alphabet: static::alphabetForModel(model: $model),
70 | minLength: Config::minLength(),
71 | blocklist: Config::blacklist(),
72 | );
73 | }
74 |
75 | public static function alphabetForModel(string $model): string
76 | {
77 | $alphabet = Config::alphabet();
78 | $shuffle = $model . Config::shuffleKey();
79 | $shuffleLength = mb_strlen(string: $shuffle);
80 |
81 | if (! $shuffleLength) {
82 | return Config::alphabet();
83 | }
84 |
85 | $alphabetArray = static::multiByteSplit(string: Config::alphabet());
86 | $shuffleArray = static::multiByteSplit(string: $shuffle);
87 |
88 | for ($i = mb_strlen($alphabet) - 1, $v = 0, $p = 0; $i > 0; $i--, $v++) {
89 | $v %= $shuffleLength;
90 | $p += $int = mb_ord($shuffleArray[$v], 'UTF-8');
91 | $j = ($int + $v + $p) % $i;
92 |
93 | $temp = $alphabetArray[$j];
94 | $alphabetArray[$j] = $alphabetArray[$i];
95 | $alphabetArray[$i] = $temp;
96 | }
97 |
98 | return implode(separator: '', array: $alphabetArray);
99 | }
100 |
101 | /**
102 | * @return array
103 | */
104 | protected static function multiByteSplit(string $string): array
105 | {
106 | return preg_split(pattern: '/(?!^)(?=.)/u', subject: $string) ?: [];
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/SqidsServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(path: __DIR__ . '/../config/sqids.php', key: 'sqids');
20 | }
21 |
22 | public function boot(): void
23 | {
24 | if ($this->app->runningInConsole()) {
25 | $this->publishes(
26 | paths: [
27 | __DIR__ . '/../config/sqids.php' => config_path('sqids.php'),
28 | ],
29 | groups: 'sqids-config',
30 | );
31 | }
32 |
33 | $this->bootBuilderMixins();
34 | }
35 |
36 | protected function bootBuilderMixins(): void
37 | {
38 | Builder::mixin(new FindBySqidMixin());
39 | Builder::mixin(new FindBySqidOrFailMixin());
40 | Builder::mixin(new WhereSqidInMixin());
41 | Builder::mixin(new WhereSqidMixin());
42 | Builder::mixin(new WhereSqidNotInMixin());
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Support/Config.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | protected static array $defaultBlacklist = [];
21 |
22 | protected static string $defaultSeparator = '_';
23 |
24 | protected static int $defaultPrefixLength = 3;
25 |
26 | protected static string $defaultPrefixCase = 'lower';
27 |
28 | public static function shuffleKey(): ?string
29 | {
30 | $shuffleKey = config(key: 'sqids.shuffle_key');
31 |
32 | if (! is_string($shuffleKey)) {
33 | return null;
34 | }
35 |
36 | return $shuffleKey;
37 | }
38 |
39 | public static function alphabet(): string
40 | {
41 | $alphabet = config(key: 'sqids.alphabet');
42 |
43 | if (! $alphabet || ! is_string($alphabet)) {
44 | return static::$defaultAlphabet;
45 | }
46 |
47 | return $alphabet;
48 | }
49 |
50 | public static function minLength(): int
51 | {
52 | /** @var int|null $minLength */
53 | $minLength = config(key: 'sqids.min_length', default: static::$defaultMinLength);
54 |
55 | if (! $minLength || ! is_int($minLength)) {
56 | return static::$defaultMinLength;
57 | }
58 |
59 | return $minLength;
60 | }
61 |
62 | /**
63 | * @return array
64 | */
65 | public static function blacklist(): array
66 | {
67 | $blacklist = config(key: 'sqids.blacklist', default: static::$defaultBlacklist);
68 |
69 | if (! is_array($blacklist)) {
70 | return static::$defaultBlacklist;
71 | }
72 |
73 | return $blacklist;
74 | }
75 |
76 | public static function separator(): string
77 | {
78 | $separator = config(key: 'sqids.separator', default: static::$defaultSeparator);
79 |
80 | if (! $separator || ! is_string(value: $separator)) {
81 | return static::$defaultSeparator;
82 | }
83 |
84 | return $separator;
85 | }
86 |
87 | public static function prefixClass(): ?Prefix
88 | {
89 | $prefix = config(key: 'sqids.prefix_class');
90 |
91 | if (! $prefix) {
92 | return null;
93 | }
94 |
95 | try {
96 | $prefix = new $prefix();
97 | } catch (Exception) {
98 | return new SimplePrefix();
99 | }
100 |
101 | if (! $prefix instanceof Prefix) {
102 | return new SimplePrefix();
103 | }
104 |
105 | return new $prefix();
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/testbench.yaml:
--------------------------------------------------------------------------------
1 | providers:
2 | - RedExplosion\Sqids\SqidsServiceProvider
3 |
4 | migrations:
5 | - workbench/database/migrations
6 |
7 | seeders:
8 | - Workbench\Database\Seeders\DatabaseSeeder
9 |
10 | workbench:
11 | start: '/'
12 | install: true
13 | discovers:
14 | web: true
15 | api: false
16 | commands: false
17 | views: false
18 | build: []
19 | assets: []
20 | sync: []
21 |
--------------------------------------------------------------------------------