
# Cookie 存储

默认情况下，Phasis 的 `fetch()` 会忽略响应中的 `Set-Cookie`，并且请求不会发送 `Cookie` 标头。这是服务端嵌入的合适默认设置——通常你不希望某个用户的响应影响另一个用户的请求。

如果确实需要 Cookie——例如运行依赖会话状态的流程、抓取登录后才能访问的内容，或在测试中模拟浏览器行为——可以用 `$engine->setCookieJar($jar)` 挂载存储。

## 接口

存储可以是任何实现以下两种方法的对象：

```
get(url: string): string         // returns a "Cookie:" header value, e.g. "sid=abc; pref=dark"
set(url: string, header: string): void   // receives one Set-Cookie line
```

PHP 对象和具有匹配成员的 JS 端 `JsObject` 都可以使用。传入 `null` 会清空存储。

每个请求跳转时，Phasis 都会调用一次 `get(url)`（因此重定向到其他主机时会获取新的 Cookie）；响应中的每个 `Set-Cookie` 标头也会调用一次 `set(url, line)`。Cookie *策略*——域名匹配、路径范围、过期时间、`SameSite`、`Secure`——由存储负责，而不是由 Phasis 负责。

## 内存中的存储（测试）

```php
final class MemoryJar
{
    /** @var array<string, array<string, string>> host → name → "name=value" */
    private array $store = [];

    public function get(string $url): string
    {
        $host = parse_url($url, PHP_URL_HOST) ?? '';
        return implode('; ', $this->store[$host] ?? []);
    }

    public function set(string $url, string $header): void
    {
        $host = parse_url($url, PHP_URL_HOST) ?? '';
        [$nv] = explode(';', $header, 2);
        [$name, $value] = explode('=', trim($nv), 2) + ['', ''];
        if ($name !== '') {
            $this->store[$host][$name] = "$name=$value";
        }
    }
}

$engine->setCookieJar(new MemoryJar());
```

这足以满足基本的成功路径测试，但没有处理 `Domain=`、`Path=`、`Expires=`，也没有处理 `Secure`/`HttpOnly`/`SameSite` 属性。不要在跨越信任边界的场景中使用它。

## Symfony BrowserKit

如果已安装 Symfony 的 HTTP 库，`BrowserKit\CookieJar` 会妥善处理策略。

```php
use Symfony\Component\BrowserKit\CookieJar as BrowserCookieJar;
use Symfony\Component\BrowserKit\Cookie;

final class SymfonyJarAdapter
{
    public function __construct(private BrowserCookieJar $jar) {}

    public function get(string $url): string
    {
        $cookies = $this->jar->allValues($url);
        return implode('; ', array_map(
            fn ($n, $v) => "$n=$v",
            array_keys($cookies),
            array_values($cookies),
        ));
    }

    public function set(string $url, string $header): void
    {
        $cookie = Cookie::fromString($header, $url);
        $this->jar->set($cookie);
    }
}

$engine->setCookieJar(new SymfonyJarAdapter(new BrowserCookieJar()));
```

## JS 端存储

如果存储位于 JS 端——例如你想从 JavaScript 检查或填充它——可以通过 JS 对象绑定：

```js
const jar = new Map();
$jar.get = (url) => Array.from(jar.entries()).map(([k, v]) => `${k}=${v}`).join('; ');
$jar.set = (url, header) => {
  const [nv] = header.split(';');
  const [name, value] = nv.split('=');
  jar.set(name.trim(), value.trim());
};
```

```php
$engine->eval($script);
$engine->setCookieJar($engine->eval('$jar'));
```

接收方无论是 PHP 对象还是 `JsObject`，都会调用相同的 `get`/`set` 方法。

## 每个请求的作用域

存储属于当前运行环境——它会在每个 `fetch()` 之间持续存在，直到通过 `setCookieJar(null)` 清除。若要将存储限定在单个请求流程中，请显式处理：

```php
$engine->setCookieJar($jar);
$engine->eval('await runLoginFlow()');
$engine->setCookieJar(null);
```

## 从 PHP 检查

`$engine->getCookieJar()` 会返回传入的内容——适合在测试中进行断言。

```php
$jar = new MemoryJar();
$engine->setCookieJar($jar);
$engine->eval('await fetch("https://api.example.com/login", { method: "POST" })');

assertNotEmpty($engine->getCookieJar()->get('https://api.example.com/'));
```

## 另请参阅

- [Fetch 传输](https://phasis.inline0.dev/zh/docs/interop/fetch-transport) — Cookie 存储在 Phasis 的重定向循环**内部**运行，并叠加在已安装的传输之上。
- [Web API (WPT)](https://phasis.inline0.dev/zh/docs/compatibility/web-apis) — Fetch Pack 中覆盖 Cookie 行为的测试。
