CURLOPT_*のデフォルトオプション結合の罠

# 問題

cURL関数を用いる際，`CURLOPT_RETURNTRANSFER`のように使用頻度の高いものは，デフォルトオプションとして省略できるようにラッパー関数を作りたいところ．

```php:これだけでHTMLを取得できるように&#32;curl_init_with()&#32;関数を作りたい
$html = curl_exec(curl_init_with('https://github.com/mpyw'));
```

ところが，データをファイルとして保存したい場合には`CURLOPT_FILE`オプションを使う場合もあるだろう．

```php:これだけでHTMLを保存できるように&#32;curl_init_with()&#32;関数を作りたい
curl_exec(curl_init_with('https://github.com/mpyw', [CURLOPT_FILE => fopen('data.html', 'wb')]));
```

これらの要件をどちらも満たすように関数を作るにはどうすればいいだろうか．ただし言うまでもないが，オプションがデフォルトのものと衝突したときは当然，後から設定した方を優先したい．

以下のうち，正しく動作するものはどれ？ （複数回答可）

```php:A
function curl_init_with($url, array $options = [])
{
    $options += [
        CURLOPT_RETURNTRANSFER => true,
    ];
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

```php:B
function curl_init_with($url, array $options = [])
{
    $options = [
        CURLOPT_RETURNTRANSFER => true,
    ] + $options;
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

```php:C
function curl_init_with($url, array $options = [])
{
    $options = array_merge($options, [
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

```php:D
function curl_init_with($url, array $options = [])
{
    $options = array_merge([
        CURLOPT_RETURNTRANSFER => true,
    ], $options);
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

```php:E
function curl_init_with($url, array $options = [])
{
    $options = array_replace($options, [
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

```php:F
function curl_init_with($url, array $options = [])
{
    $options = array_replace([
        CURLOPT_RETURNTRANSFER => true,
    ], $options);
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

　
　
　
　
　
　
　
　
　
　
　
　
　
　
　
　
　
　　
　
　
　
　
　
　


# 答え

正しく動作するのは **F** のみです！

```php:F
function curl_init_with($url, array $options = [])
{
    $options = array_replace([
        CURLOPT_RETURNTRANSFER => true,
    ], $options);
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

以下では，何故間違っているのかの解説を行います．

```php:C
function curl_init_with($url, array $options = [])
{
    $options = array_merge($options, [
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

```php:D
function curl_init_with($url, array $options = [])
{
    $options = array_merge([
        CURLOPT_RETURNTRANSFER => true,
    ], $options);
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

まず真っ先に [`array_merge`] を使っている C と D は選択肢から外れます．この関数は数値キーに対してはキーを振り直すという動作を取りますが，**`CURLOPT_*`の定数は数値です**．

```php:B
function curl_init_with($url, array $options = [])
{
    $options = [
        CURLOPT_RETURNTRANSFER => true,
    ] + $options;
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

```php:E
function curl_init_with($url, array $options = [])
{
    $options = array_replace($options, [
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

続いて， B と E も選択肢から外れます．キーが衝突したとき，`+`演算子は最初に出現したものを優先して残し，逆に [`array_replace`] 関数は後から出現したもので上書きします． B と E はこれを考慮した際に，デフォルト値のほうが優先されるようになってしまっています．

```php:A
function curl_init_with($url, array $options = [])
{
    $options += [
        CURLOPT_RETURNTRANSFER => true,
    ];
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    return $ch;
}
```

さて，一見こいつは正解に見えるんですが，実際に動かしてみると期待したように動きません． そこで，`$options`の値に着目してみましょう． F との違いはこのようになるはずです．

```php:A&#32;の&#32;$options
[
    CURLOPT_FILE => fopen('data.html', 'wb'),
    CURLOPT_RETURNTRANSFER => true,
]
```

```php:F&#32;の&#32;$options
[
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FILE => fopen('data.html', 'wb'),
]
```

はい，実は順番って大事なんです．**`CURLOPT_RETURNTRANSFER`と`CURLOPT_FILE`は互いに衝突し合うオプションです**．後から出現したほうが前を上書きしてしまうんです．

- `CURLOPT_RETURNTRANSFER` を `true` にしたとき<br>→ 成功時には結果文字列，失敗時には `false` を返す．
- `CURLOPT_RETURNTRANSFER` を `false` にしたとき<br>→ 成功時に`true`，失敗時には `false` を返す．結果は標準出力する．
- `CURLOPT_FILE` を設定したとき<br>→ 成功時に`true`，失敗時には `false` を返す．結果はファイルに書き込む．

しかし， A でも呼び出し方次第で正常に動作させることはできます．以下のように書けば問題はありません．

```php:A&#32;も正しく動かすための記述
curl_exec(curl_init_with('https://github.com/mpyw', [
    CURLOPT_RETURNTRANSFER => false, // true でも false でもどっちでもいいｗ
    CURLOPT_FILE => fopen('data.html', 'w'),
]));
```

でも全然直感的じゃないからできれば避けたいところ…


[`array_merge`]: http://php.net/manual/ja/function.array-merge.php
[`array_replace`]: http://php.net/manual/ja/function.array-replace.php