URL の /foo/view/1 を /foo/-ZRLSS-4vl_ みたいなランダムなIDにする方法

## はじめに

ウェブアプリなどで、なんらかの理由で主キーの auto_increment な ID を URL に付けたくない場合、どうしたらいいか、いろいろ調べてみた。

http://example.com/foo/view/1 みたいなやつを
http://example.com/foo/-ZRLSS-4vl_ みたいにしたい。

## 例として使ったもの

PHP 7.0.x
MySQL 5.7.x
Yii Framework 2.0.x

## やり方はいろいろある

**CHAR(11) みたいなものを主キーとする方法**

* そこにランダムな文字列をデータ挿入時に生成して、一緒に追加する
* 大文字小文字を含む場合 COLLATE は utf8_bin にする必要がある
* フレームワーク側で、ランダムな文字列を生成させ、それがユニークであることを保証する必要がある
* MySQL の主キーは INT 型が良いと聞くけど ...
* 主キーから追加した順番が失われる (数値が 1 ずつ増えていくわけではないので)

**ランダムな文字列を挿入するカラムを追加する方法**

* これです。[Rails等でURLにランダムな文字列を使いたい時のアレ](http://qiita.com/ainame/items/eb9dcf7862630c44d5e9)
* auto_increment な ID は残しておいて、slug カラムみたいなものを追加する必要がある
* フレームワーク側で、ランダムな文字列を生成させ、それがユニークであることを保証する必要がある
* データを 1 件取得する場合、主キーではなく、ランダムな文字列が入ったカラムに where をかける

**auto_increment な主キーの値をエンコード、デコードして使う方法**

* テーブルに何かしら手をかけなくてもよく auto_increment な ID のままでいい
* データを 1 件取得するリンクに ID をエンコードした値を使う
* データを 1 件取得するクエリを発行する前にエンコードした ID をデコードする必要がある
* フレームワーク側でそれらを実装する必要がある

他にもあったら教えてください :)

## auto_increment な主キーの値をエンコード、デコードして使う方法の例

例として [Yii](http://www.yiiframework.com/) + [Hashids](http://hashids.org/) という組み合わせでできるみたいなのでやってみます。PHP だと 各フレームワーク用のライブラリもけっこう揃っているみたいです。

Hashids のインストール:

```
composer require hashids/hashids
```

Hashids コンポーネントを作る:

```php:components/Hashids.php
<?php

namespace app\components;

use Hashids\Hashids as HashidsBase;
use yii\base\Object;

/**
 * Yii 2 wrapper for the Hashids.
 * @link https://github.com/lichunqiang/hashids
 */
class Hashids extends Object
{
    /**
     * @var string
     */
    public $salt;

    /**
     * @var int
     */
    public $minHashLength;

    /**
     * @var string
     */
    public $alphabet;

    private $hashids;

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        $this->hashids = new HashidsBase(
            $this->salt,
            $this->minHashLength,
            $this->alphabet
        );
    }

    /**
     * @inheritdoc
     */
    public function __call($name, $params)
    {
        return method_exists($this->hashids, $name)
            ? call_user_func_array([$this->hashids, $name], $params)
            : parent::__call($name, $params);
    }

    /**
     * @param string $id hashed id
     * @return int|array decoded id
     */
    public function decode($id)
    {
        $id = $this->hashids->decode($id);
        return 1 === count($id) ? $id[0] : $id;
    }
}
```

設定して、使えるようにする:

```php:config/main.php
'components' => [
    'hashids' => [
        'class' => app\components\Hashids::class,
        'salt' => 'b1a9ab1e5d8bf4129934b799bef469e88ca31e91',
        'minHashLength' => 11,
        'alphabet' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-',
    ],
```

使う:

```php
$id = 777;
$encodedId = Yii::$app->hashids->encode($id); // string 'w3m5_XG2pbk' (length=11)
$decodedId = Yii::$app->hashids->decode($encodedId); // int 777
```

encode メソッドは可変長な引数です。引数がひとつでも、 decode すると配列になるので、そこは上手くライブラリ側で工夫しておく必要があるかと思います。

ビューの例:

```php:index.php
<?= Html::a(Html::encode($model->title), ['view', 'id' => Yii::$app->hashids->encode($model->id)]) ?>
```

コントローラの例:

```php:ExampleController.php
/**
 * @param string $id hashed id
 * @return mixed
 */
public function actionView($id)
{
    return $this->render('view', [
        'model' => $this->findModel(Yii::$app->hashids->decode($id)),
    ]);
}
```

urlManager の rules の例:

```php:config/web.php
'components' => [
    'urlManager' => [
        // ...
        'rules' => [
            // ...
            '<controller:(controller1|controller2)>/<id:[\w-]+>' => '<controller>/view',
            // ...
        ],
    ],
```

Yii 2 だとこんな感じでしょうか。

ちなみに Hashids の salt の値がバレると id の値も簡単にわかってしまうので、オープンソースな形で GitHub などに公開する場合は、`getenv('HASHIDS_SALT')` とかにして、値を直接指定しないようにしましょう。

## リンク

* [Hashids](http://hashids.org/)
* [Github: PHP: hashids/hashids](https://github.com/ivanakimov/hashids.php)
* [Github: yii2-ensure-unique-behavior](https://github.com/jamband/yii2-ensure-unique-behavior) （おまけ。あまりおすすめできない ...）