setcookie()っていつ使うの？

今回はわりと強引。

## 結論

(使う場面は)ないです。

### [setcookie()](http://php.net/manual/ja/function.setcookie.php)とは

Cookieに値を登録する関数。

```php:foo.localhost/a.html
	setcookie('hoge', 'fuga', 0); // Set-Cookie:hoge=fuga
```

とても簡単にCookieを発行することができます。

### Cookieとは

何らかの情報をブラウザに保存したい場合に使います。
Cookieに値を登録すると、次に同じドメインのURLにアクセスした場合、登録してあるデータを自動的に送信してくれます。
上記の例であれば、次に`foo.localhost`ドメイン上にあるURLにアクセスすると、自動的に`hoge=fuga`という値がブラウザから送信され、PHPでは$_COOKIEで受け取ることが可能です。
送信するしないの判断はドメイン単位で行われるため、`foo.localhost/a.html`には送信されますが、ドメインの異なる`bar.localhost/a.html`には送信されません。

一見便利ですが、このCookie、使える量に制限があり、[RFC6265](https://triple-underscore.github.io/RFC6265-ja.html)ではドメイン毎に50個、1クッキーの容量が4096バイト以上と定義されています。
実際はもっとたくさん使えるブラウザが多いですが、それでもほいほい値を突っ込んでいるとすぐに尽きてしまう程度です。
あと中身がユーザから丸見えなため、特にセキュリティ的なところで色々な問題があります。
ということでPHPではセッションという仕組みでこの問題を解決しています。

### セッションとは

PHPでセッションを使った場合、裏では`Set-Cookie:PHPSESSID=54r0kqh4s67952b8i9qu4nu4jtnpcd6l`といった形のCookieが発行されます。
この54r0kqh…というのは`$_SESSION`の登録内容とは全く無関係な、ランダムな文字列です。
そして`$_SESSION`の中身自体は、サーバのどこかに`sess_54r0kqh4s67952b8i9qu4nu4jtnpcd6l`のようなファイル名で保存されています。
で、PHPでは`PHPSESSID=54r0kqh4s67952b8i9qu4nu4jtnpcd6l`というCookieが送られてきたら、同名のファイルを探して中身を`$_SESSION`に展開してくれます。
この仕組みにより、Cookieをひとつ使うだけで、大量のデータを保持できるようになっています。

PHPでは[session_start()](http://php.net/manual/ja/function.session-start.php)って一言書くだけで、このあたりを全て裏側で自動的にやってくれるので、良くも悪くも、仕組みを知らずに簡単に動かすことができてしまいます。

あらゆるデータはとりあえず全て`$_SESSION`に突っ込んでおけばいいのですが、あえてデータをCookieに置かなければならない場面というのは存在するのでしょうか。


## setcookieが使えそうなところ

### 同一ドメイン上に複数のユーザがいる

最近はめっきり減りましたが、URLのドメイン部分が同じで、ディレクトリを切って複数のユーザにスペースを貸し出しているようなサーバが昔はよくありました。
例としては以下のようなURL。
http://hoge.localhost/~user1/
http://hoge.localhost/~user2/

ここでユーザuser1が何も考えずに

```php:hoge.localhost/~user1/a.html
	session_start();
	$_SESSION['password'] = $password;
```

とか書くと、
ユーザuser2が自分のところで

```php:hoge.localhost/~user2/a.html
	session_start();
	var_dump($_SESSION);
```

としたらパスワードが丸見えになってしまいます。
上で書いたようにCookieはドメイン単位で発行されるので、どちらも`hoge.localhost`で同じものと見なされるからです。

そんな場合に、setcookieのオプションでディレクトリ制限をかけて、他者から見えないようにすることが可能です。

```php:hoge.localhost/~user1/a.html
	setcookie('_SESSION', $session, 0, '/~user1/');
```

ここでセットしたCookieは、/~user1/ディレクトリでは見れますが、他のユーザからは見えなくなります。
<del>[これでセキュリティ問題は解決です。めでたし。](http://qiita.com/rana_kualu/items/ec1a965cec895a87699a)</del>

もちろん[セッションでもディレクトリ制限が可能](http://php.net/manual/ja/function.session-set-cookie-params.php)なので、あえてsetcookie()を使う理由はありません。

```php:hoge.localhost/~user1/a.html
	session_set_cookie_params(0, '/~user1/');
	session_start();
```

### サブドメインにCookieを渡したい

ディレクトリ方式は色々と問題があるため、最近では以下のようにサブドメインを切る方法が主流です。
http://user1.localhost/
http://user2.localhost/

基本的にCookieはドメインが完全一致しないとデータを送信しません。
ユーザ毎にCookieを分けたいという目的には便利ですが、今度は逆に、機能によってサブドメインを分けているサービスなどで問題が発生します。
例えばYahooは、ニュースが`news.yahoo.co.jp`、ヤフオクは`auctions.yahoo.co.jp`などとサブドメインが分かれていますが、デフォルトのままだとnewsのCookieはauctionsに送ってくれないため、セッション情報が途切れてしまいます。
ドメインを移動するたびにログインが必要になってしまい、とても不便です。

こちらについても、setcookieの設定で動作を変更可能です。

```php:news.yahoo.co.jp/index.html
	setcookie('_SESSION', $session, 0, null, 'yahoo.co.jp');
	session_start();
```

Cookieを発行したドメインは`news.yahoo.co.jp`ですが、発行されたCookieのドメイン指定は`yahoo.co.jp`になります。
このCookieは`auctions.yahoo.co.jp`や`www.yahoo.co.jp`など、`yahoo.co.jp`以下であればどこでも送ってくれるので認証を維持できるようになります。

当然ですがセッションでも指定可能です。

```php:news.yahoo.co.jp/index.php
	session_set_cookie_params(0, null, 'yahoo.co.jp');
	session_start();
```

ちなみに不用意にドメイン指定すると[Cookie Monster Bug](https://www.google.co.jp/search?q=Cookie+Monster+Bug)ってのがあるので気をつけましょう。

### 値をJavaScriptと共有する

PHPで作った値をJavaScriptに渡したい。

```php
<?php
	setcookie('hoge', 'fuga');
?>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<script type="text/javascript">
	console.log($.cookie("hoge"));
</script>
```

いや、普通に渡せばええやん。

```php
<?php
	$hoge = 'fuga';
?>
<script type="text/javascript">
	console.log(<?=json_encode($hoge, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);?>);
</script>
```

JavaScriptでCookieの取り扱いは面倒なので、`jquery.cookie`等を使わないとやってられません。
さらに実際は受け渡す変数がひとつだけなんてことはなく、たくさんの値を渡すのが普通でしょうから、その点でも配列で突っ込めばいいだけの後者の方が楽です。

### セッションクッキーを削除する

http://d.hatena.ne.jp/Kappuccino/20080726/1217049706
> これは「会員カードを破棄する」に対応する。

`$_SESSION = [];`でセッションファイルの中身を空っぽにする(スポーツクラブが所有する会員情報シートの項目を消しゴムで消す)、`session_destroy()`はセッションファイル自体を削除(スポーツクラブが所有する会員情報シートそのものを破棄)、そしてセッションクッキー削除はブラウザに保存されている`PHPSSID=**`を削除します(ユーザが自分の会員カードを破棄)。
ということでそれぞれ動作は違うのですが、どうせ前2者のどちらかを行った時点で会員カードは使えなくなるので、会員カード自体は別に破棄する必要はありません。
どうせブラウザを閉じれば勝手に破棄されますしね。

どうしても今すぐ会員カードを破棄したいんだ、という場合には、

```php
	setcookie(session_name(), '', 0, '/');
```

とすればすぐにCookieからも消滅します。
これについてはセッション関数では対応できませんが、そもそも対応する必要がないです。

### セッションが使えない

セッションが使えないけどデータは保持したい場合、さすがにこれはセッションではどうにもならないので何かしら別の方法を考える必要があります。
データはURLで持ち回すか、Cookieに登録するくらいしかないでしょう。

URLでのデータ持ち回しはリファラなどから簡単にデータを抜かれてしまうため、現在では禁忌です。
従って保存先はCookie一択です。[^1]

Cookieにデータを入れる場合、上記Cookieの量的制限がかかってくるため、情報量が多くなるとデータの保持が困難になります。
また、中身がユーザに丸見えで、簡単に中身を書き換えられてしまいます。
これが商品点数などであればまだいいですが、かつて価格をCookieに入れていたため書き換えられてただ同然で買われたなどという[アレな事例](http://itpro.nikkeibp.co.jp/members/SI/Security/20050719/164839/)も存在します。
クリティカルな情報は、Cookieなどのユーザから見える場所に置くべきではありません。

というか、この場合そもそもセッションが使えないようなサーバを使っていること自体が問題なので、環境の方をどうにかすべきです。

### 有効期限の違う値を使う

```php
	setcookie('name_long', '有効期限長い値', time() + 31536000);
	setcookie('name_short', '有効期限短い値', time() + 1);
	setcookie('name_close', 'ブラウザ閉じるまで', 0);
```

これはセッションで扱うのは難しいですね。
セッションの有効期限は全て同一なので、デフォルトを一番長い値に合わせておいて、短い値は有効期限を別途設定して扱うといった方法になるでしょう。

```php
	/**
	* @param String キー
	* @param mixed  値
	* @param int 有効期限time()
	* @return null
	*/
	function setSessionData(string $name, $value, int $expire = 0){
		if($expire > 0){
			$_SESSION[$name]['expire'] = $expire;
		}elseif(isset($_SESSION[$name]['expire'])){
			unset($_SESSION[$name]['expire']);
		}
		$_SESSION[$name]['value'] = $value;
	}
	
	/**
	* @param String キー
	* @return mixed $value
	*/
	function getSessionData(string $name){
		if(!isset($_SESSION[$name])){ return null; }
		if(!isset($_SESSION[$name]['expire']) || $_SESSION[$name]['expire'] > time()){
			return $_SESSION[$name]['value'];
		}
		unset($_SESSION[$name]);
		return null;
	}
```

うーん微妙。
とはいえこのような機能、そうそう出番のあるものでもないだろうし、使うときだけ個別に設定すればいいんじゃないかな、

### 自動ログイン

上記の異なる有効期限を用いる用途の発展系で、唯一`setcookie()`を必ず使わなければならないところです。
では何故`setcookie()`を使う場面なんて無いと主張するかというと、要するに自動ログインなんて実装するんじゃねえってこった。

```php
	if(自動ログイン機能を使う){
		$token = bin2hex(random_bytes(32));
		// Cookieにtokenを発行
		setcookie('id', ユーザID, time() + 31536000);
		setcookie('token', $token, time() + 31536000);
		// DBに保存
		($pdo->prepare('INSERT INTO auto_login (id, token, expire) VALUES(:id, :value, NOW()+INTERVAL 1 YEAR )'))
			->execute( ['id'=>ユーザID,'token'=>password_hash($token)] );
	}
	
	if(自動ログインする)){
		$stmt = $pdo->prepare('SELECT * FROM auto_login WHERE id=:id AND expire > NOW()');
		$stmt->execute( ['id']=>$_COOKIE['id'] );
		$user = stmt->fetch();
		if($user && password_verify($_COOKIE['token'], $user['token'])){
			// ログイン処理、CookieとDBの更新、session_regenerate_id(true)等
		}
	}
```

Cookieにはアカウント情報は含まれていないため、万一Cookieが盗まれてしまってもパスワードは漏洩しません。
ただアカウント情報を知らなくてもログインができるのが自動ログイン機能なので、盗まれないにこしたことはありません。
セッションはブラウザを閉じれば消え去る[^2]のに対し、トークンは有効期限までずっと残ってしまうため、相対的に危険度が高くなります。
不用意に危険性を上げるような機能は実装するべきではありません。

なお、『<a href="https://www.google.co.jp/search?q=PHP+自動ログイン">PHP 自動ログイン</a>』でググって<a href="https://www.websec-room.com/2015/02/22/2055">最初にでてくるページ</a>は、2015年の記事にも関わらずMDB2などという文字が出てきたりする。

### その他

何かありますかね。

## まとめ

あえてsetcookie()を使うところは無い。[^3]


[^1]: localStorage？知らない子ですね。
[^2]: デフォルトでは。
[^3]: 正直かなり無理矢理感ある。