LaravelのRedisで存在しないキーを取得しようとするとnullが返ってくるお話

`false` が返ってきたり `null` が返ってきたりする楽しい世界です。

# 前提
## 動作環境
- Laradock
  - Laravel 6.12
  - Redis 5.0.3
  - phpredis 5.1.1

## DBの中身
Redis の中には `key1` のみが入っているとします。

```
127.0.0.1:6379> SET key1 value1
OK
127.0.0.1:6379> KEYS *
1) "key1"
```

# 要約
## Redis
Redis で存在しないキーを指定すると、 `nil` が返ってきます。

```
127.0.0.1:6379> GET key1
"value1"
127.0.0.1:6379> GET key2
(nil)
```

## PhpRedis
[PhpRedis](https://github.com/phpredis/phpredis) では、boolean 型の `false` が返ってきます。

```php
$redis = new Redis();
$redis->connect('redis', 6379);
$value = $redis->get('key2');
var_dump($value);
```

```
string(6) "value1"
bool(false)
```

## Laravel
Laravel で Redis に接続するときに PhpRedis を使っている場合、null 型の `null` が返ってきます。

```php
use Illuminate\Support\Facades\Redis;

Redis::set('key1', 'value1');
$value = Redis::get('key1');  //--> "value1"
$value = Redis::get('key2');  //--> null
```

# 解説
## Redis
Redis の `GET` や `MGET` で存在しないキーを取得しようとすると、 `nil` が返ってきます。

`"nil"` という **文字列** を `SET` することはできますが、 `nil` という **特別な値** を `SET` することはできません。[^1]

[^1]: この Redis の動作は明瞭で、あとで見る PHP のゆるゆるな型システムから戻ると、とても気持ちよく感じます（笑）

```
127.0.0.1:6379> GET key2
(nil)
127.0.0.1:6379> SET key3 nil
OK
127.0.0.1:6379> GET key3
"nil"
```

## PhpRedis
PhpRedis の `get` や `mget` で存在しないキーを取得しようとすると、 boolean 型の `false` が返ってきます。

このことは [公式ドキュメント](https://github.com/phpredis/phpredis#get) にも明記されています。

> Return value
>
> String or Bool: If key didn't exist, FALSE is returned. Otherwise, the value related to this key is returned.

しかし、ここでぜひ思い出していただきたいのは、PHP では `"0"` や空文字と `false` は等しく扱われるということです。

```php
// 値として "0" をセットしてみる。
$redis->set('key4', 0);
$val4 = $redis->get('key4');

// キーの存在チェックのつもり...
if (!$val4) {
    echo "key4は存在しない";
}

// 値として空文字をセットしてみる。
$redis->set('key5', "");
$val5 = $redis->get('key5');

// これもキーの存在チェックのつもり...
if (!$val5) {
    echo "key5は存在しない";
}
```

さて、結果はどうでしょうか？

```
key4は存在しない
key5は存在しない
```

これを防ぐには、 `===` 演算子による厳密比較しかありません。

```php
// 正しいキーの存在チェック
if ($val4 === false) {
    echo "key4は存在しない";
}
```

## Laravel
さて、ようやく本題です。

Laravel では、Redis にアクセスする際に `PhpRedis` を使うことができます。

`PhpRedis` を使っているのですから、 `get()` や `mget()` も `false` を返してきそうなものですが、実際には `null` が返ってきます。

その理由は `Illuminate\Redis\Connections\PhpRedisConnection.php` にあります。

```php:PhpRedisConnection.php
public function get($key)
{
    $result = $this->command('get', [$key]);

    return $result !== false ? $result : null;
}

public function mget(array $keys)
{
    return array_map(function ($value) {
        return $value !== false ? $value : null;
    }, $this->command('mget', [$keys]));
}
```

取得した値が `false` かどうかをわざわざチェックして、 `false` だったときには `null` を返すようにしていることが分かります。

なぜこんな面倒なことをしているのか？

かつて PhpRedis が返す `false` をそのまま使った結果、キャッシュ周りで [バグ](https://github.com/laravel/framework/pull/17196) が起きました。そのときに `null` を返すようにしたようです。

# 感想
Laravel で Redis にアクセスするときに PhpRedis を使っているからといって、PhpRedis と同じ結果になるとは限りません。

バグ修正時の [ある開発者のコメント](https://github.com/laravel/framework/issues/17188#issuecomment-271113004) をもじって言うならば、

> 僕らは Laravel を使っているのであって、PhpRedis を直接使っているわけではない

といったところでしょうか。[^2]

[^2]: nullを返すこと自体は別に構わないのですが、ドキュメントに記載されていないのは、少し不親切に感じるところです。フレームワークのソースコードを見れば分かるだろ！と言われれば、それまでですが...

# 参考
- Laravel 公式ドキュメント
  - [Redis](https://readouble.com/laravel/6.x/ja/redis.html)
- PHP 公式ドキュメント
  - [型の比較表](https://www.php.net/manual/ja/types.comparisons.php)
- PhpRedis
  - [Why does GET/MGET return false instead of null for non-existent keys?](https://github.com/phpredis/phpredis/issues/1562)
    - 同様の違和感を感じる人は多いみたいですが、破壊的変更になるため却下されたようです。