JavaScriptでPHPのsetcookie相当の関数を作成

まずPHPのsetcookie関数の仕様を確認。
[setcookie - PHPマニュアル](https://www.php.net/manual/ja/function.setcookie.php)


パラメータは
**name**、**value**、**expires**、**path**、**domain**、**secure**、**httponly**、**options**

**options**は指定が特殊で、第8引数ということではなく**expires**、**path**、**domain**、**secure**、**httponly**、**samesite**のうち必要分を連想配列として第3引数に渡すということのよう。

パラメータの内容についてはこちらも合わせて参照。
[HTTP Cookie - MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies)
[Document.cookie - MDN](https://developer.mozilla.org/ja/docs/Web/API/Document/cookie)

##挙動確認##

以下、PHPの**setcookie**関数を実行しながらレスポンスヘッダに実際に出力されるSet-Cookie:フィールドの内容を確認。

####パラメータ全省略####
~~~php
setcookie();
~~~
> Warning: setcookie() expects at least 1 parameter, 0 given

####nameのみ####
~~~php
setcookie('name');
~~~
> Set-Cookie: name=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0

valueには'deleted'が付加され、expiresに'Thu, 01-Jan-1970 00:00:01 GMT'、Max-Ageに'0'が固定で指定された。
実質的に、Cookie名 'test' の削除動作。

####value####
~~~php
setcookie('name', 'value');
~~~
> Set-Cookie: name=value

expires、Max-Ageは付かない。
セッションクッキー(ブラウザを閉じた時点で破棄)扱い。

~~~php
setcookie('name', '');
setcookie('name', NULL);
~~~
空文字列やNULLはsetcookie('name'); の時と同じ

~~~php
setcookie('name', []);
~~~
> Warning: setcookie() expects parameter 2 to be string, array given

####expires####
~~~php
setcookie('name', 'value', time());
~~~
> Set-Cookie: name=value; expires=Wed, 27-May-2020 06:33:16 GMT; Max-Age=0

~~~php
setcookie('name', 'value', time() - 86400);
~~~
> Set-Cookie: name=value; expires=Tue, 26-May-2020 06:35:14 GMT; Max-Age=0

expiresに実行日時以前の値を指定すると、Max-Ageも0になる。
削除と同等。

~~~php
setcookie('name', 'value', time() + 86400);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 06:36:54 GMT; Max-Age=86400

expiresに実行時以降の値を指定すると、expiresとMax-Age両方で有効期限が設定される。
expiresは有効期限日時、Max-Ageは保持秒数。

####path####
~~~php
setcookie('name', 'value', time() + 86400, '');
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 06:51:43 GMT; Max-Age=86400

~~~php
setcookie('name', 'value', time() + 86400, '/');
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 06:52:26 GMT; Max-Age=86400; path=/

~~~php
setcookie('name', 'value', time() + 86400, '/test/test');
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 06:53:45 GMT; Max-Age=86400; path=/test/test

~~~php
setcookie('name', 'value', time() + 86400, 0);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 06:54:25 GMT; Max-Age=86400; path=0

pathパラメータに指定した値が空文字列以外であればそのままSet-Cookieフィールドにpathパラメータとして出力される様子。

####domain####
~~~php
setcookie('name', 'value', time() + 86400, '', '');
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 06:58:37 GMT; Max-Age=86400

~~~php
setcookie('name', 'value', time() + 86400, '', 'testdomain.com');
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 06:59:28 GMT; Max-Age=86400; domain=testdomain.com

~~~php
setcookie('name', 'value', time() + 86400, '', 'localhost');
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:00:10 GMT; Max-Age=86400; domain=localhost

~~~php
setcookie('name', 'value', time() + 86400, '', '127.0.0.1');
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:00:54 GMT; Max-Age=86400; domain=127.0.0.1

空文字列以外であればそのままdomainパラメータとして出力。

####secure####
~~~php
setcookie('name', 'value', time() + 86400, '', '', '');
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:05:54 GMT; Max-Age=86400

~~~php
setcookie('name', 'value', time() + 86400, '', '', 0);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:06:13 GMT; Max-Age=86400

~~~php
setcookie('name', 'value', time() + 86400, '', '', 1);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:06:42 GMT; Max-Age=86400; secure

~~~php
setcookie('name', 'value', time() + 86400, '', '', false);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:08:27 GMT; Max-Age=86400

~~~php
setcookie('name', 'value', time() + 86400, '', '', true);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:08:02 GMT; Max-Age=86400; secure

パラメータがtrueであればsecureが付く。

####httponly####
~~~php
setcookie('name', 'value', time() + 86400, '', '', '', '');
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:11:39 GMT; Max-Age=86400

~~~php
setcookie('name', 'value', time() + 86400, '', '', '', 0);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:11:54 GMT; Max-Age=86400

~~~php
setcookie('name', 'value', time() + 86400, '', '', '', 1);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:12:19 GMT; Max-Age=86400; HttpOnly

~~~php
setcookie('name', 'value', time() + 86400, '', '', '', false);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:12:43 GMT; Max-Age=86400

~~~php
setcookie('name', 'value', time() + 86400, '', '', '', true);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:13:04 GMT; Max-Age=86400; HttpOnly

パラメータがtrueであればHttpOnlyが付く。
HttpOnlyはレスポンスヘッダのSet-Cookie:フィールドで保存指示を受けた場合のみ有効とする指定なのでJavaScriptのdocument.cookieで指定しても無効になるかと思いますが、一応確認。

####options####
~~~php
setcookie('name', 'value', []);
~~~
> Set-Cookie: name=value

~~~php
setcookie('name', 'value', ['expires' => time() + 86400]);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:17:50 GMT; Max-Age=86400

~~~php
setcookie('name', 'value', ['Expires' => time() + 86400]);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:18:23 GMT; Max-Age=86400

~~~php
setcookie('name', 'value', ['expires-' => time() + 86400]);
~~~
> Warning: setcookie(): Unrecognized key 'expires-' found in the options array
> Warning: setcookie(): No valid options were found in the given array

キー名の大文字小文字は区別しない様子。
無効なキー名を渡すとWarningが出る。

~~~php
setcookie('name', 'value', ['expires' => time() + 86400, 'name' => 'name2']);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:22:59 GMT; Max-Age=86400
> Warning: setcookie(): Unrecognized key 'name' found in the options array

optionsパラメータでnameパラメータの上書きはできなかった。

~~~php
setcookie('name', 'value', ['expires' => time() + 86400, 'max-age' => 3600]);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:25:08 GMT; Max-Age=86400
> Warning: setcookie(): Unrecognized key 'max-age' found in the options array

optionsパラメータでもmax-ageの指定はできなかった。
setcookie関数においては常にexpiresでの指定になり、内部的には同じ意味になるmax-ageパラメータを付加する動作みたい。

~~~php
setcookie('name', 'value', ['expires' => time() + 86400, 'path' => '/']);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:26:59 GMT; Max-Age=86400; path=/

~~~php
setcookie('name', 'value', ['path' => '/', 'expires' => time() + 86400]);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 07:27:31 GMT; Max-Age=86400; path=/

optionsパラメータ内の項目の順序を変えてもSet-Cookie:フィールド上の項目の並びはとくに変わらなかった。

~~~php
setcookie('name', 'value', ['expires' => time() + 86400], '/');
~~~
> Warning: setcookie(): Cannot pass arguments after the options array

第3引数にoptionsとして連想配列を渡した時はそれ以降の引数を渡すとWarningが出る。

~~~php
setcookie('name', 'value', [
    'expires' => time() + 86400,
    'samesite' => 'Lax',
]);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 12:11:05 GMT; Max-Age=86400; SameSite=Lax

~~~php
setcookie('name', 'value', [
    'expires' => time() + 86400,
    'samesite' => 'aaaaaaaa',
]);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 12:12:00 GMT; Max-Age=86400; SameSite=aaaaaaaa

~~~php
setcookie('name', 'value', [
    'expires' => time() + 86400,
    'samesite' => '',
]);
~~~
> Set-Cookie: name=value; expires=Thu, 28-May-2020 12:12:41 GMT; Max-Age=86400

~~~php
setcookie('name', 'マルチバイト文字', [
    'expires' => time() + 86400,
    'samesite' => 'マルチバイト文字',
]);
~~~
> Set-Cookie: name=%E3%83%9E%E3%83%AB%E3%83%81%E3%83%90%E3%82%A4%E3%83%88%E6%96%87%E5%AD%97; expires=Thu, 28-May-2020 12:32:12 GMT; Max-Age=86400; SameSite=マルチバイト文字

samesiteオプションはとりあえず有効な値(None,Lax,Strict)でなくても何か指定されていればそのまま出力する様子。

##JavaScriptで作成した互換関数##

####setcookie()####

~~~js:setcookie.js
/**
 * PHPのsetcookie()関数相当のCookie発行処理
 * サーバ側からのレスポンスヘッダのSet-Cookie:での発行指示とは経路が異なるため
 * 反映タイミングなど差異が出る可能性もあり
 */
function setcookie(name, value, expires, path, domain, secure, httpOnly) {

    let sameSite = undefined;

    // name未指定なら終了
    if(name === undefined) return;
    // URIエンコードはせず無効な文字が含まれる場合は終了
    if(name.match(/[=,;\0-\x20]/)) return;

    // 第3引数が連想配列だった場合はoptions指定扱いとする
    if(typeof expires === 'object') {
        // path以降の引数が指定されていたら終了
        for(let tmp of [path, domain, secure, httpOnly]) {
            if(typeof tmp !== 'undefined') return;
        }
        let options = expires;
        expires = undefined;
        // optionsの設定を各パラメータの変数へ代入
        for(let tmp in options) {
            switch(tmp.toUpperCase()) {
                case 'EXPIRES':
                    expires = options[tmp];
                    break;
                case 'PATH':
                    path = options[tmp];
                    break;
                case 'DOMAIN':
                    domain = options[tmp];
                    break;
                case 'SECURE':
                    secure = options[tmp];
                    break;
                case 'HTTPONLY':
                    httpOnly = options[tmp];
                    break;
                case 'SAMESITE':
                    sameSite = options[tmp];
                    break;
                default:
                    // 想定外のキー名の場合はとりあえず何もしない
                    break;
            }
        }
    }

    for(let tmp of [name, value, expires, path, domain, secure, httpOnly]) {
        if(typeof tmp === 'object') return;
    }

    // name=valueのペア作成 valueはURIエンコードが必要
    let cookie = [
        name + '=' + (value === undefined ? 'deleted' : encodeURIComponent(value)),
    ];

    // expires, max-age
    let maxAge = 0;
    if(expires !== undefined) {
        maxAge = expires - Math.floor(Date.now() / 1000);
    }
    if(value === undefined || (expires !== undefined && expires > 0)) {
        cookie.push(
            'expires=' + new Date(value === undefined ? 1000 : Date.now() + 1000 * maxAge).toUTCString(),
            'Max-Age=' + (maxAge > 0 ? maxAge : '0')
        );
    }

    // その他のパラメータは指定されていればほぼそのまま渡す
    if(path !== undefined && path !== '') cookie.push('path=' + path);
    if(domain !== undefined && domain !== '') cookie.push('domain=' + domain);
    if(secure !== undefined && Boolean(secure) === true) cookie.push('secure');
    if(httpOnly !== undefined && Boolean(httpOnly) === true) cookie.push('HttpOnly');
    if(sameSite !== undefined && sameSite !== '') cookie.push('SameSite=' + sameSite);

    // 各項目をセパレータで繋いでdocument.cookieへ渡す
    cookie = cookie.join('; ');
    document.cookie = cookie;
}
~~~

**使用例**

~~~js
// PHPのtime()相当
const time = () => Math.floor(Date.now() / 1000);

// PHPのsetcookie()相当
setcookie('test1', 'value1', time() + 3600, '', '', true);
setcookie('test2', 'value2', {'expires':time() + 3600, 'path':'/'});

let options = [];
options['expires'] = time() + 3600;
options['samesite'] = 'Lax';
setcookie('test3', 'value3', options);
~~~

####getcookie()####

~~~js:getcookie.js
/**
 * Cookie取得
 * PHPの$_COOKIE変数相当の連想配列としてパース
 */
function getcookie() {
    let r = {};
    for(let cookie of document.cookie.split(/;\s+?/)) {
        if(cookie === '') continue;
        let tmp = cookie.split('=');
        if(tmp[1] !== undefined) tmp[1] = decodeURIComponent(tmp[1]);
        if(r[tmp[0]] === undefined && tmp[1] !== undefined) r[tmp[0]] = tmp[1];
    }
    return r;
}
~~~

**使用例**

~~~js
let $_COOKIE = getcookie();
console.log($_COOKIE);
~~~