PHPのsetcookieは配列オプションで書く|expiresに秒数を渡すと即消える落とし穴

setcookie() の配列オプション形式とは、PHP 7.3 以降で使える「setcookie($name, $value, [ ‘expires’ => …, ‘samesite’ => … ])」という書き方のことです。SameSite 属性を指定できるほか、引数の順序ミスも防げます。

古い書き方の setcookie(‘a’, ‘b’, 3600, ‘/’, ”, true, true) は、引数が7つも並んでいて、どれが何だったか毎回忘れます。しかもこの 3600 を「1時間」のつもりで渡すと、Cookie は保存された瞬間に期限切れになります。実務で何度か見かけた、地味に痛いハマりどころです。

expiresは「何秒後」ではなく「いつ」を渡す

expires に渡すのは Unix タイムスタンプ(エポックからの秒数)です。「3600秒間有効」ではなく「1970年1月1日から3600秒後まで有効」と解釈されるので、1970年の1時間後、つまりとっくに過去の時刻になります。ブラウザは期限切れの Cookie をそのまま捨てるため、エラーも警告も出ないまま保存されません。

<?php
// NG: 3600 は1970年の話になり、即座に期限切れ
setcookie('theme', 'dark', 3600);

// OK: 現在時刻に足して「いつまで」を渡す
setcookie('theme', 'dark', time() + 3600);

「相対秒数ではなく絶対時刻」と頭に入れておけば、この事故はまず起きません。ちなみに 0 を渡す(または省略する)と、ブラウザを閉じたら消えるセッションCookieになります。

配列オプションで書くと何が嬉しいのか

配列形式なら、キーで意味が読めるので順序を覚える必要がありません。省略したキーは、従来の引数と同じデフォルト値になります。私は、新規のコードならこちらで統一したほうがいいと思っています。

<?php
setcookie('theme', 'dark', [
    'expires'  => time() + 60 * 60 * 24 * 30, // 30日後
    'path'     => '/',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

使えるキーは expires、path、domain、secure、httponly、samesite の6つです(PHP 8.5 では partitioned も加わりました)。コードを読む人が「この Cookie は JavaScript から見えないんだな」「他サイトからのPOSTには付かないんだな」と一目で分かるのが何よりの利点ですね。

SameSiteはなぜ配列形式でないと指定できないのか

samesite は PHP 7.3 で、この配列形式と一緒に追加されました。従来の7引数の形式には samesite の置き場がありません。古い書き方のまま SameSite を付けたい場合は、path に「/; samesite=Lax」のような文字列を紛れ込ませる小細工が使われることもありましたが、今は配列形式を使えば済みます。

値は None、Lax、Strict のいずれかです。省略すると SameSite 属性そのものが付かず、ブラウザ側の既定値に任されます。ここで注意したいのが None です。公式ドキュメントにも書かれているとおり、SameSite=None を指定するときは secure も有効にしないと、クライアントに Cookie をブロックされます。

<?php
// 外部サイトの iframe 内などから送りたい Cookie
setcookie('widget', 'abc', [
    'expires'  => time() + 3600,
    'path'     => '/',
    'secure'   => true,   // None のときは必須
    'samesite' => 'None',
]);

PHP 側は secure が false でもエラーにしてくれません。開発中のHTTP環境で動かして、本番のHTTPSで初めて挙動が変わる、といった形で気づくことが多いかもしれません。

配列形式でやりがちな失敗

まず、キーの綴り間違いです。PHP 8.0 以降は、存在しないキーを渡すと ValueError が投げられます(PHP 7 系では警告でした)。「httpOnly」と大文字を混ぜたり、「same_site」と書いたりすると落ちるので、動かせばすぐ気づけます。

次に、配列形式と従来の引数を混ぜることです。第3引数に配列を渡したときは、引数は3つちょうどでなければならず、第4引数以降を足すと ArgumentCountError になります。さらに、配列形式は名前付き引数とも併用できません。

<?php
// NG: 配列の後ろに path を足すと ArgumentCountError
setcookie('a', 'b', ['expires' => time() + 3600], '/');

// OK: path も配列の中に入れる
setcookie('a', 'b', ['expires' => time() + 3600, 'path' => '/']);

削除するときは「同じpathとdomain」で過去の時刻を渡す

Cookie を消すには、有効期限を過去にして上書きします。ここで 0 を渡すと「セッションCookie」になるだけで消えないので、time() – 3600 や 1 のような過去の時刻にします。もうひとつ大事なのは、最初にセットしたときと同じ path と domain を指定することです。path が違うと、別の Cookie として扱われて元の Cookie が残ります。

<?php
// セット時に path を '/' にしていた場合
setcookie('theme', '', [
    'expires' => time() - 3600,
    'path'    => '/',
]);

セットした直後の$_COOKIEには反映されない

setcookie() は Set-Cookie ヘッダーをレスポンスに積むだけの関数です。$_COOKIE はブラウザから届いたリクエストのCookieを表すので、同じリクエスト内で setcookie() を呼んでも、$_COOKIE には現れません。次のリクエストで初めて読めます。同じ処理の中で値を使いたいなら、変数に持っておくか、自分で $_COOKIE にも代入するしかありません。

それから、setcookie() はヘッダーを送る関数なので、何かを出力したあとに呼ぶと false が返ります。この「headers already sent」の話は別の記事で書いたので、そちらを見てもらえればと思います。戻り値が true でも、ブラウザが受け入れたかどうかまでは分からない点も、頭の片隅に置いておくといいですね。

まとめ

setcookie() は配列オプション形式で書き、expires には time() 基準の絶対時刻を渡す。SameSite=None のときは secure を忘れない。削除は同じ path と domain で過去の時刻を指定する。この4点を押さえておけば、Cookie まわりの「なぜか保存されない」はだいぶ減ると思います。地味な関数ですが、ログインや同意バナーの保存先として出番が多いので、書き方を一度そろえておく価値はあるという気がしています。

よくある質問

Q. setcookie() の配列オプション形式は、どのPHPバージョンから使えますか?
A. PHP 7.3.0 からです。samesite キーもこのバージョンで使えるようになりました。それより古い環境では、7つの引数を順に渡す従来の形式しか使えません。

Q. expires に 3600 を渡したのにCookieが保存されません。なぜですか?
A. expires は「何秒後」ではなく Unix タイムスタンプなので、3600 は1970年の1時間後という過去の時刻になるためです。time() + 3600 のように現在時刻に足して渡してください。

Q. setcookie() の直後に $_COOKIE を見ても値が入っていないのはバグですか?
A. バグではなく仕様です。$_COOKIE はブラウザから届いたリクエストの内容で、setcookie() はレスポンスのヘッダーを積むだけなので、次のリクエストから読めるようになります。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です