Laravel の Route Model Binding で主キーをハッシュ化したものを使う

## はじめに

Laravel でアプリケーションを作っていく際に、何らかの理由で URL に主キーをそのまま表示させたくない場合どうしたらいいか、そこらへんを書いていきます。

例:
`https://example.com/posts/1` ではなく
`https://example.com/posts/Lk6-W7gbKmv ` にしたい場合。

## 環境
Laravel 6.x

## インストールと設定
Laravel のインストール:

```
composer create-project --prefer-dist laravel/laravel foo
```

プロジェクトルートに移動:

```
cd $_
```

主キーをハッシュ化するのに便利なライブラリをインストール:

```
composer require vinkla/hashids
```

Laravel Hashids の設定ファイルを作成:

```
php artisan vendor:publish
```

リストが表示されるので `Vinkla\Hashids\HashidsServiceProvider` の番号を入力。これで config 以下に hashids.php が作成される。

.env ファイルに hashids で使う salt を追加:

```
HASHIDS_SALT=GkQDUgz4c8UmSY2P6nozchOz0fOxg5sk
```

`php artisan tinker` を使って `Str::random(32);` とかで値を生成する。APP_KEY と同様に絶対に公開してはいけない。

hashids の設定:

```php:config/hashids.php
    'connections' => [
        'main' => [
            // ハッシュ化するときの魔法の粉
            'salt' => env('HASHIDS_SALT'),
            // ハッシュ値で使いたい文字列
           'alphabet'=>　'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-',
            // ハッシュ値の長さ
            'length' => 11,
        ],
        // ...
    ],
```

tinker で確認してみる:

```
php artisan tinker
>>> Hashids::encode(1);
=> "Lk6-W7gbKmv"
>>> Hashids::decode('Lk6-W7gbKmv')[0];
=> 1
```

## 実際に使ってみる

MySQL を起動:

```
mysql.server start
```

データベースの作成:

```
mysql --login-path=local
mysql> create database laravel;
```

posts テーブル用のマイグレーションファイルを生成:

```
php artisan make:migration create_posts_table
```

生成されたマイグレーションファイルの編集:

```php:database/migrations/xxx_creates_posts_table.php
class CreatePostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('content');
            $table->timestamps();
        });
    }
}
```

テーブルの作成:

```
php artisan migrate
```

モデルとコントローラの作成:

```
php artisan make:controller PostController --resource --model=Post
```

ルートの登録:

```php:routes/web.php
Route::resource('posts', 'PostController');
```

データを 1 件挿入:

```
php artisan tinker
>>> App\Post::forceCreate(['content' => 'foo']);
```

Route Model Binding に Hashids を適用:

```php:app/Post.php
use Illuminate\Database\Eloquent\Model;
use Vinkla\Hashids\Facades\Hashids;

class Post extends Model
{
    // ...
    public function getRouteKey(): string
    {
        return Hashids::encode($this->getKey());
    }

    public function resolveRouteBinding($value): ?Model
    {
        $value = Hashids::decode($value)[0] ?? null;

        return $this->where($this->getRouteKeyName(), $value)->first();
    }
}
```

tinker で確認してみる:

```
php artisan tinker
>>> route('posts.show', App\Post::find(1));
=> "http://localhost/posts/Lk6-W7gbKmv"
```

これでコントローラーやビューで主キーのエンコードやデコード処理をやらずとも、Route Model Binding を通して良い感じにやってくれる。

## まとめ

Route Model Binding 自体便利だけど、コードの見通しが悪くなるのであまり好きではないが、ここまで簡単にできてしまう感じが「おーなんか Laravel らしいね」と思ってついつい記事にしてみました。

## 備考

上記の実装の場合、各テーブルの主キーのハッシュ値が同じになるので、テーブル毎にハッシュ値を設定したい場合はコネクションを複数用意して使うこともできる:

```php:config/hashdis.php
    'default' => 'foo',
    'connections' => [
        'foo' => [
            'salt' => env('HASHIDS_SALT_FOO'),
            'alphabet' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-',
            'length' => 11,
        ],
        'bar' => [
            'salt' => env('HASHIDS_SALT_BAR'),
            'alphabet' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-',
            'length' => 11,
        ],

    ],
```

```
php artisan tinker
>>> Hashids::connection('foo')->encode(1);
=> "Lk6-W7gbKmv"
>>> Hashids::connection('bar')->encode(1);
=> "BnP-1Ll3wLA"
```

## リンク
[Hashids](https://github.com/vinkla/hashids)
[Laravel Hashids](https://github.com/vinkla/laravel-hashids)
[Route Model Binding - Laravel](https://laravel.com/docs/6.x/routing#route-model-binding)