Slim docsには記載がないっぽいのですが。
まぁ普段「sessionとCookieはよく使うよねぇ」というあたりで、その辺を少し検証。
なおセッションはどうも、公式のSkeletonですら
・自力でsession_start()発行
・sessionっぽい字面のクラスがない
ので、「おとなしく生PHPの機能使え」って感じぽいです。
うんまぁそれはそれであり。
なお「おいちゃんSkeleton」では「セッション系記述用のファイルを1つ作ってそこで"随所に関所"する」予定。
あと、セッションについては。最近いくつかのフレームワークで見かける「flash」をどうすっかなぁ? くらい。
……どうすっかなぁ本当に。Laravelのコードとか軽く読んでみたけど、割と「がっつりラッピングして自由度削ってる」しなぁ。
うん。一端放置しよう(笑
で、一方のCookie。
確認をすると
・クラスはある( ./vendor/slim/slim/Slim/Http/Cookies.php )
・使ってる箇所はないぽい
な感じ。
厳密には。
vendor/slim/slim/Slim/Http/Request.php が使ってるぽいんだけど
$cookies = Cookies::parseHeader($headers->get('Cookie', []));
だけ。ちなみにここを見ると
vendor/slim/slim/Slim/Http/Cookies.php
public static function parseHeader($header) { if (is_array($header) === true) { $header = isset($header[0]) ? $header[0] : ''; } if (is_string($header) === false) { throw new InvalidArgumentException('Cannot parse Cookie data. Header value must be a string.'); } $header = rtrim($header, "\r\n"); $pieces = preg_split('@[;]\s*@', $header); $cookies = []; foreach ($pieces as $cookie) { $cookie = explode('=', $cookie, 2); if (count($cookie) === 2) { $key = urldecode($cookie[0]); $value = urldecode($cookie[1]); if (!isset($cookies[$key])) { $cookies[$key] = $value; } } } return $cookies; }
がっつりと「自力でparseしてる」ので、幾分「ふぅん」という感じ。$_COOKIE使ってるわけじゃないんだ。
んで。
https://github.com/slimphp/Slim/issues/1310
に質問があるんだけど
・ど〜やってCookie使うのん?
に対して
「In the meantime I'd suggest using the standard PHP functions.」とか書いてあって「あぁ標準関数かぁ」とか思うわけでございます。
ちなみに「こんど準備する」って書いてありますが、肝心の https://github.com/slimphp/Slim-HttpCookies が、これを書いている限りでは「3 years ago」以降、変更がございませぬ、のと、ソースコードが一切上がっておりません。
「じゃぁsetcookieでいいぢゃん」とか思わなくもないのですが、他方で、幾分気になるロジックが。
vendor/slim/slim/Slim/Http/Cookies.php
/** * Set response cookie * * @param string $name Cookie name * @param string|array $value Cookie value, or cookie properties */ public function set($name, $value) { if (!is_array($value)) { $value = ['value' => (string)$value]; } $this->responseCookies[$name] = array_replace($this->defaults, $value); }
と、
/** * Convert to `Set-Cookie` headers * * @return string[] */ public function toHeaders() { $headers = []; foreach ($this->responseCookies as $name => $properties) { $headers[] = $this->toHeader($name, $properties); } return $headers; }
/** * Convert to `Set-Cookie` header * * @param string $name Cookie name * @param array $properties Cookie properties * * @return string */ protected function toHeader($name, array $properties) { $result = urlencode($name) . '=' . urlencode($properties['value']); if (isset($properties['domain'])) { $result .= '; domain=' . $properties['domain']; } if (isset($properties['path'])) { $result .= '; path=' . $properties['path']; } if (isset($properties['expires'])) { if (is_string($properties['expires'])) { $timestamp = strtotime($properties['expires']); } else { $timestamp = (int)$properties['expires']; } if ($timestamp !== 0) { $result .= '; expires=' . gmdate('D, d-M-Y H:i:s e', $timestamp); } } if (isset($properties['secure']) && $properties['secure']) { $result .= '; secure'; } if (isset($properties['hostonly']) && $properties['hostonly']) { $result .= '; HostOnly'; } if (isset($properties['httponly']) && $properties['httponly']) { $result .= '; HttpOnly'; } if (isset($properties['samesite']) && in_array(strtolower($properties['samesite']), ['lax', 'strict'], true)) { // While strtolower is needed for correct comparison, the RFC doesn't care about case $result .= '; SameSite=' . $properties['samesite']; } return $result; }
でございます。
つまり
・なんとなく、前提としてのメソッドは一式あるっぽい
んですなぁ。
ということは
・CookieのreadはRequestから
・Cookiesのインスタンスを、container あたりに → Cookieの設定は、ここからsetメソッドで
・どこか(基本は終了する処理あたりがよいなぁ)に、「CookiesのインスタンスからtoHeaders()メソッドでresponseヘッダにSet-Cookieを追加」
とかやると、割と「美しい」んじゃなかろうか? と。
したらまぁ、最悪は「Cookiesのインスタンスのsetメソッドでsetcookie発行すればいいや」とかいう、雑なこともできそうなのでw
別解として https://github.com/dflydev/dflydev-fig-cookies ってのが割とあちこちで出てくるんだけどねぇ。
更新が大体2〜3年前、ってのもあるし、一旦は「できるだけ自力実装」したいので(「他人のを使う」のは、自力実装で状況を理解してからでもできるので)。
一旦は、自力で頑張ってみませう。
ってなわけで
・Cookiesのクラスを軽く解析しつつ
・「Cookieを発行して読み込める」ところまで一式
を、レッツらGo*1。
何はともあれコンストラクタを拝見。
vendor/slim/slim/Slim/Http/Cookies.php
public function __construct(array $cookies = []) { $this->requestCookies = $cookies; }
ふぅん。「requestCookies」となっていて、それを保持する、のか。
見ると
public function get($name, $default = null) { return isset($this->requestCookies[$name]) ? $this->requestCookies[$name] : $default; }
ってのもあるなぁ。
ふむ……
vendor/slim/slim/Slim/Http/Request.php
public function getCookieParam($key, $default = null) { $cookies = $this->getCookieParams(); $result = $default; if (isset($cookies[$key])) { $result = $cookies[$key]; } return $result; }
ってのもあるんで、幾分悩ましいんだけど。
まぁ「Requestに乗っかってくるCookieは、プログラム実行中に変更されない(不変)」のはず、なので。
この辺は「お好み」なのかも、なぁ。
んで。割と重要なのが
/** * Default cookie properties * * @var array */ protected $defaults = [ 'value' => '', 'domain' => null, 'hostonly' => null, 'path' => null, 'expires' => null, 'secure' => false, 'httponly' => false, 'samesite' => null ];
/** * Set default cookie properties * * @param array $settings */ public function setDefaults(array $settings) { $this->defaults = array_replace($this->defaults, $settings); }
この辺。
httponlyと、必要に応じてpathやsecure、expires、くらいは変更したい、かもしれない。samesiteもあるので、気になる御仁もいらっしゃろうかと思うのですが如何でしょうか。
さてこのsetDefaults(っつかここで設定される $this->defaults)、割と多様されるかも。
ってのが、どうもset()、toHeaders()、toHeader()を見ていると「setのタイミングの $this->defaults が、個々のnameごとに保持されて、それをもとにSet-Cookieヘッダを作る」ぽい、から。
うん見えてきたんだけど、まずは「実際にコード書いて動きを確定」させて、そこから次の考察を重ねてみませう。
っつわけで、ざっくりと
・Cookieの読み込み(まだ未設定だからnullのはずだけど)
・2種類の、パラメタの違うCookieを設定
あたりをやる一連の処理を、ざっくりとでっち上げ。
public/index.php
// Cookie用ミドルウェア class cookieMiddleware { private $container; public function __construct($container) { $this->container = $container; } public function __invoke($request, $response, $next) { $response = $next($request, $response); ob_start(); $cookie = $this->container->cookie; var_dump( $cookie->toHeaders() ); $s = ob_get_clean(); return $response->write($s); } } // $app = new \Slim\App; // Cookieインスタンス作成&コンテナに突っ込み $container = $app->getContainer(); $container['cookie'] = function ($c) { // インスタンス作成 $cobj = new \Slim\Http\Cookies($c->get('request')->getCookieParams()); // XXX ここに「デフォ設定」が書いてあるつもり & 突っ込むつもり // $cobj->setDefaults( $c->get('settings')['cookie'] ); // return $cobj; }; // middlewareの設定 $app->add( new cookieMiddleware($app->getContainer()) ); // ルーティング $app->get('/', function(Request $request, Response $response, array $args) { // Cookieの読み込みと表示 ob_start(); $cookie = $this->cookie; var_dump($cookie->get('hoge')); var_dump($cookie->get('foo')); $s = ob_get_clean(); // Cookieの設定その1 $cookie->set('hoge', mt_rand(0, 99)); // Cookieの設定その2 $cookie->setDefaults(['httponly' => true, 'expires' => date(DATE_COOKIE, time() + 3600), 'secure' => true]); $cookie->set('foo', mt_rand(0, 99)); // return $response->write("hoge test\n" . $s); }); $app->run();
hoge test NULL NULL array(2) { [0]=> string(7) "hoge=43" [1]=> string(63) "foo=25; expires=Thu, 01-Jan-1970 01:00:00 UTC; secure; HttpOnly" }
うん概ね予想通り。
さて、では本命の「Set-Cookie込み」。
このヘッダは「追加追加」なので、withHeader()じゃなくてwithAddedHeader()である、ってあたりに気を付けてみませう。
// Cookie用ミドルウェア class cookieMiddleware { private $container; public function __construct($container) { $this->container = $container; } public function __invoke($request, $response, $next) { $response = $next($request, $response); // Cookieの設定 foreach($this->container->cookie->toHeaders() as $cookie_string) { $response = $response->withAddedHeader('Set-Cookie', $cookie_string); } return $response; } } // $app = new \Slim\App; // Cookieインスタンス作成&コンテナに突っ込み $container = $app->getContainer(); $container['cookie'] = function ($c) { // インスタンス作成 $cobj = new \Slim\Http\Cookies($c->get('request')->getCookieParams()); // XXX ここに「デフォ設定」が書いてあるつもり & 突っ込むつもり // $cobj->setDefaults( $c->get('settings')['cookie'] ); // return $cobj; }; // middlewareの設定 $app->add( new cookieMiddleware($app->getContainer()) ); // ルーティング $app->get('/', function(Request $request, Response $response, array $args) { // Cookieの読み込みと表示 ob_start(); $cookie = $this->cookie; var_dump($cookie->get('hoge')); var_dump($cookie->get('foo')); $s = ob_get_clean(); // Cookieの設定その1 $cookie->set('hoge', mt_rand(0, 99)); // Cookieの設定その2 $cookie->setDefaults(['httponly' => true, 'expires' => date(DATE_COOKIE, time() + 3600) ]); $cookie->set('foo', mt_rand(0, 99)); // return $response->write("hoge test\n" . $s); }); $app->run();
hoge test string(2) "48" string(2) "46"
よし、意図通り。
多分、「比較的綺麗に」やると、こんな感じだなぁ。
………ふむ、この手の「ちょっとしたMiddleware」とか、1パッケージにまとめてみるかしらん???
パッケージにまとめるとすると、何となし、Middlewareクラス一か所に処理まとめたいなぁ。
// Cookie用ミドルウェア class cookieMiddleware { private $container; public function __construct($container) { $this->container = $container; } public function __invoke($request, $response, $next) { // 事前処理:インスタンスの生成 $this->container['cookie'] = function ($c) { // インスタンス作成 $cobj = new \Slim\Http\Cookies($this->container->get('request')->getCookieParams()); // XXX ここに「デフォ設定」が書いてあるつもり & 突っ込むつもり $settings = $this->container->get('settings'); if (true === isset($settings['cookie'])) { $cobj->setDefaults( $settings['cookie'] ); } // return $cobj; }; // 本処理 $response = $next($request, $response); // 事後処理:setCookieヘッダ群の出力 foreach($this->container->get('cookie')->toHeaders() as $cookie_string) { $response = $response->withAddedHeader('Set-Cookie', $cookie_string); } return $response; } } // $app = new \Slim\App; // middlewareの設定 $app->add( new cookieMiddleware($app->getContainer()) ); // ルーティング $app->get('/', function(Request $request, Response $response, array $args) { ob_start(); $cookie = $this->get('cookie'); var_dump($cookie->get('hoge')); var_dump($cookie->get('foo')); $s = ob_get_clean(); // $cookie->set('hoge', mt_rand(0, 99)); // $cookie->setDefaults(['httponly' => true, 'expires' => date(DATE_COOKIE, time() + 3600) ]); $cookie->set('foo', mt_rand(0, 99)); // return $response->write("hoge test\n" . $s); });
よしまとまった。これだな。
あとは……setDefaultsのところのtypoとかが気になるなぁ。なんか、チェックロジックとか入れてみるかしらん??
もう一個。
「Cookieを削除する」時は、大雑把には
$cookie->setDefaults(['httponly' => true, 'expires' => date(DATE_COOKIE, 0) ]); $cookie->set('foo', '');
で消せマッスル。……これだけなんかメソッド切っておいたほうが楽なんじゃないか? って気が、せんでもない。
まぁ気をつけないと「後続のCookieが全部消える」とかありそうなので。どっちかってぇと、getDefaults()がある、とか、その辺のほうが安全なような気もするのだが。
………ラッパークラス、書こうかしらん???
最後。
Cookie自体の運用上の注意として。
defaultsの
・どこは毎回さわって
・どこは変更しない
のかを、内々に決めておかないと、危ない、かも。
この辺は「ルール決め」の範疇だねぇ。
……Cookies、継承クラスとか作ってラップメソッドとか書いたほうが、楽、かもしれない。
まぁとりあえず、一通り「Cookieのreadとwrite」が出来たので、よし、としませう。
*1:びっくりするほどに古いwww