PHPでUUIDv4を作成するサンプルコード

<?php

function generateUUIDv4() {
    $data = random_bytes(16);

    // バージョンを設定 (4はUUIDv4を表す)
    $data[6] = chr(ord($data[6]) & 0x0f | 0x40);

    // RFC 4122 バリアントを設定
    $data[8] = chr(ord($data[8]) & 0x3f | 0x80);

    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}

var_dump(generateUUIDv4()); // string(36) "2fe46dc9-cdce-4c94-a2c8-2096bd317b51"
var_dump(generateUUIDv4()); // string(36) "e99e26d4-ef7b-4fe7-93a4-b463e5ad6dff"
var_dump(generateUUIDv4()); // string(36) "1fa22622-5270-4adf-ad57-7faf4ef607ff"

PHPでUUIDv4を生成するためには、上記のサンプルコードで作成できます。

このコードは、PHPのrandom_bytes関数を使用してランダムなバイト列を生成し、それをUUIDv4の仕様に従って整形しています。生成されたUUIDv4は文字列として返されます。

UUIDv4は仕様上ではランダムに生成されるものになりますが、UUIDに共通するバージョン情報の指定とバリアントの指定をすることが必要になるのでその分の指定だけを行なっています。

UUIDとは

UUIDUniversally Unique Identifier)とは、ソフトウェア上でオブジェクトを一意に識別するための識別子である。UUIDは128ビットの数値だが、16進法による550e8400-e29b-41d4-a716-446655440000というような文字列による表現が使われることが多い。

https://ja.wikipedia.org/wiki/UUID

つまり、「(8文字)-(4文字)-(4文字)-(4文字)-(12文字)」形式の16進法の32文字の文字列のことを言います。

ちなみに、大文字と小文字は同一と判定されます。

UUIDv4とは

バージョン4のUUIDは、乱数により生成される。他のUUIDと同様、バージョン4であることを示すために4ビットが使われ、バリアント(バリアント1と2に対して、それぞれ102または1102)を示すために2または3ビットが使われる。
~~~略~~~
16進表記では、RRRRRRRR-RRRR-4RRR-rRRR-RRRRRRRRRRRRとなる。

https://ja.wikipedia.org/wiki/UUID#バージョン4

バリアントとは

UUIDには歴史的経緯から数種類のバリアント(変種)があり、現行の規格で定められているのはそのうちの1つである。16進表記をした場合にxxxxxxxx-xxxx-xxxx-Nxxx-xxxxxxxxxxxxのNの桁の上位ビットがバリアントを示す。現行の規格では上位2ビットが102であることが定められているので、16進表記では8、9、A、Bのいずれかとなる。

https://ja.wikipedia.org/wiki/UUID#バリアント

現行の規格、と表現されている部分には「RFC4122」が採用されているのが一般的な様子です。

つまりはUUIDv4である事は

xxxxxxxx-xxxx-4xxx-{8,9,a,b}xxx-xxxxxxxxxxxx

で表記されている物が相当します。

冒頭で紹介したサンプルコードを実行しまくった結果が以下です

 $ php sample.php
string(36) "2fe46dc9-cdce-4c94-a2c8-2096bd317b51"
string(36) "e99e26d4-ef7b-4fe7-93a4-b463e5ad6dff"
string(36) "1fa22622-5270-4adf-ad57-7faf4ef607ff"
 $ php sample.php
string(36) "0a47e8d0-fe99-4e24-ac74-02d9f4aaa396"
string(36) "7f9df017-182f-4412-9d29-e833936ed553"
string(36) "d1ba8635-2fc5-4a9a-a99d-6cf301baa486"
 $ php sample.php
string(36) "a1c50510-e9f9-40c4-b117-f9819d79b72e"
string(36) "547e3e45-a86c-4ee9-b972-61d0d6eb9861"
string(36) "dd5448d5-9521-40e5-afbc-6dab8d595c02"
 $ php sample.php
string(36) "5aeb1850-cd6d-47be-a641-92d224d160a6"
string(36) "5e6cb333-6b01-4406-8690-c4f77bd17bca"
string(36) "361495f5-557e-4809-8ae2-d26b4b4be00d"
 $ php sample.php
string(36) "e44ac1f5-7d70-4720-b5cc-b9a81868609b"
string(36) "8ae16f4a-e3ea-4aee-9d7c-4ea7afb704f9"
string(36) "f5e36476-a66d-45a6-9e3a-edb2e109f22c"
 $ php sample.php
string(36) "4f3ad84b-3a33-457a-bd13-08f4358a1934"
string(36) "c68d8326-4f60-438d-a92b-e40549e2c88b"
string(36) "029ead16-10e6-4a43-8879-c9b5ce859169"

ハイフン(-)で区切った3つ目のブロックでは常に「4」が表示され、4つ目のブロックは「8、9、a、b」しか表示されていないことがわかると思います。

LaravelのUUIDv4実装コードと比較

PHPでUUIDv4を生成するコードとしては身近なものとしてはLaravelの Str::uuid() が存在します。こちらの実装方法についても確認してみます。

    /**
     * Generate a UUID (version 4).
     *
     * @return \Ramsey\Uuid\UuidInterface
     */
    public static function uuid()
    {
        return static::$uuidFactory
                    ? call_user_func(static::$uuidFactory)
                    : Uuid::uuid4();
    }
    /**
     * Version 4 (random) UUID
     *
     * @link https://tools.ietf.org/html/rfc4122#section-4.1.3 RFC 4122, § 4.1.3: Version
     */
    public const UUID_TYPE_RANDOM = 4;


    /**
     * Returns a version 4 (random) UUID
     *
     * @return UuidInterface A UuidInterface instance that represents a
     *     version 4 UUID
     */
    public static function uuid4(): UuidInterface
    {
        return self::getFactory()->uuid4();
    }
    public function uuid4(): UuidInterface
    {
        $bytes = $this->randomGenerator->generate(16);

        return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_RANDOM);
    }

    /**
     * Returns an RFC 4122 variant Uuid, created from the provided bytes and version
     *
     * @param string $bytes The byte string to convert to a UUID
     * @param int $version The RFC 4122 version to apply to the UUID
     *
     * @return UuidInterface An instance of UuidInterface, created from the
     *     byte string and version
     *
     * @psalm-pure
     */
    private function uuidFromBytesAndVersion(string $bytes, int $version): UuidInterface
    {
        /** @var array $unpackedTime */
        $unpackedTime = unpack('n*', substr($bytes, 6, 2));
        $timeHi = (int) $unpackedTime[1];
        $timeHiAndVersion = pack('n*', BinaryUtils::applyVersion($timeHi, $version));

        /** @var array $unpackedClockSeq */
        $unpackedClockSeq = unpack('n*', substr($bytes, 8, 2));
        $clockSeqHi = (int) $unpackedClockSeq[1];
        $clockSeqHiAndReserved = pack('n*', BinaryUtils::applyVariant($clockSeqHi));

        $bytes = substr_replace($bytes, $timeHiAndVersion, 6, 2);
        $bytes = substr_replace($bytes, $clockSeqHiAndReserved, 8, 2);

        if ($this->isDefaultFeatureSet) {
            return LazyUuidFromString::fromBytes($bytes);
        }

        /** @psalm-suppress ImpureVariable */
        return $this->uuid($bytes);
    }

と流れを追っていくと

$this->randomGenerator->generate(16) で16バイトのランダムなバイト列を生成し、$this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_RANDOM) を呼び出している事がわかります。

uuidFromBytesAndVersion関数のコメントでは「Returns an RFC 4122 variant Uuid, created from the provided bytes and version」とあり、RFC4122のバリアントを指定していることがわかります。

処理の中では

        $bytes = substr_replace($bytes, $timeHiAndVersion, 6, 2);
        $bytes = substr_replace($bytes, $clockSeqHiAndReserved, 8, 2);

と作成したバイト列の6と8の置換処理を行っており、各種変数ではバージョン指定とバリアントの値が入っているため、冒頭で紹介したサンプルコードと同様に置換を行なっています。

実際に実行した場合はこちら


 $ php artisan --version
Laravel Framework 10.20.0
 $ php artisan tinker
Psy Shell v0.11.20 (PHP 8.1.12 — cli) by Justin Hileman
> Str::uuid();
= Ramsey\Uuid\Lazy\LazyUuidFromString {#8082
    uuid: "43e2cec4-cc87-4bc2-8b4a-20efa64514e9",
  }

> Str::uuid();
= Ramsey\Uuid\Lazy\LazyUuidFromString {#8105
    uuid: "2f5686c6-cf9b-495c-931a-e5eaa2c1cc6c",
  }

> 

UUIDv4の値が出力されます。

つまり、Laravelに実装されているUUIDの出力内容と大差無い実装であると言えるかと感じています。

終わりに

以上、PHPでUUIDv4を作成するサンプルコードについて紹介しました。

LaravelなどのフレームワークやPHPライブラリを導入すれば早く解決するかとは思いますが、UUIDの仕様はそこまで複雑では無いので独自実装として組み込むことも可能かと感じています。

将来的な保守を考えるとフレームワークに実装されている物を使用した方がいいかとは思いますが、実装の方法や仕様を紐解いていくのは楽しい作業であり、自分自身のいい経験になりました。

何か間違いがありましたらコメントにて報告いただけますと大変助かります。

UUIDと類似しているユニークIDを生成するためのuniqid()についての記事も書きました。こちらご興味あればご覧になってください。

今回もお疲れ様でした。