Laravelのセッションに関するテストでSession name cannot be emptyが出て落ちる場合の対応

軽い気持ちでcomposer updateしたらテストが落ちて、その調査と暫定対応をしたのでメモ。

エラーログ

PHPUnit\Framework\Exception: [2017-12-04 00:44:41] testing.ERROR: Session name cannot be empty, did you forget to call "parent::open()" in "Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler"?.

原因

AbstractSessionHandlerの2ヶ月前の変更による。

[HttpFoundation] Make sessions secure and lazy · symfony/http-foundation@55ca8d8 · GitHub

かつ、もう一つ。

テストケースにphpunit@runInSeparateProcessを付けていると落ちる。
ちゃんと原因を追っていないですが、別プロセスゆえにparent::open()parent::destory()がバラバラになっているのかな。分かりませんが。

なんでこのアノテーションを付けているかというと、依存ライブラリのStaticファサードをモックにしたかったから。
PHPUnitで静的(static)メソッドのモック - Qiita

なお StaticMethods をモッククラスとして定義するので、別のテストで StaticMethods を呼ぶと意図しない結果になります。例えば、次のようなテストケースだと test1 の実行時に StaticMethods がモックとして定義されるので test2 のテストは通りません。 @runInSeparateProcess アノテーションを使えば test1 は別プロセスになるので回避できます。

前も@runInSeparateProcessの挙動でハマったから、これ使いたくない。
その場合はStaticファサードをラップするクラスを自分で用意する必要がある。

対応

ちゃんとやっている余裕はないので暫定対応。
上記のコードを読むとsession.use_cookiesを使っていることが分かるので一時的にoffっている。

# これをテストコードの適当なところに書く
ini_set('session.use_cookies', false);

結論

テスト大事。
とはいえ、今回は「テストでしか落ちない」ケースではあるんだけど。

TRCとopenBDを利用した新刊一覧を表示するサイトを作った

こちらのニュースを今日知りました。

TRC新刊図書オープンデータを公開しました!!

数日前にアナウンスがあったようですが、見落としていました。
せっかくなので、無料かつ飽きない程度の実装で「新刊一覧」サイトを作りました。

サイトはこちらです。 https://trc-opendata-viewer.herokuapp.com/
よろしければご覧ください。

f:id:kanno_kanno:20170830234716p:plain

そんなアクセスされないと思いますが、無料枠なのでアクセスが多いとダメかもしれません。

サイトの概要

上記サイトにある「TRC新刊図書オープンデータ」を利用しています。
zipのtsvに書かれているISBNの一覧を元に表示しているだけです。数件ISBNがないデータがありますが、それは表示されません。

書影にはopenBDを使わせてもらっています。
よって、openBDに書影がない本は書影部分が表示されません。
(残念なことにファーストビューの本は全部ない)

検索等、凝った仕組みはありません。現状、追加開発する予定もありません。
それらを作り込もうとするとTRCのデータだけではダメで色々面倒だからです。
タイトルや書籍名の絞込実装ぐらいは出来ますが、個人的にその必要性をまだ感じていません。
(具体的な名前が分かっていればググるので、一覧でどうこうする必要がない)

zipの更新

zipは毎週更新されるようですが、現状はまだ1つしかないので自動更新は考えていません。
(ローカルのzipを読んでいます)

この辺りはどうしようか考え中です。
こちらの非公式アーカイブを利用させて頂こうかなと思っていますが、ひとまず様子見です。
https://github.com/takahashim/trc_opendata

現状の使い勝手

正直、自分でも使っていくかどうか微妙。
なんとなく書影を眺めているのは楽しいけど、リピーターになるにはもっとUIが洗練されないといけないと思う。
利用者としてページングが面倒なのだけど、インスタみたいなローディング方式にするのも微妙かなあと思ったり。どうなんだろう。

PHPのnull[0]はエラーにならないしnoticeも出ない

背景

null変数に添字アクセスするところで、落ちると思っていたら落ちなくてびっくりした。

環境

OSX 10.11.5

PHP 7.0.14 (cli) (built: Apr 1 2017 23:41:23) ( NTS )
Copyright © 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright © 1998-2016 Zend Technologies

再現

まずは普通の配列に対して、存在しない添字アクセスをするとどうなるか。

<?php
$a = [];
var_dump($a[1]);

上記コードを実行すると、以下のようにnoticeを出力する。

Notice: Undefined offset: 1 in /Users/kanno/workspace/sandbox/piece/2017/06/25-1036.php on line 4
PHP Notice:  Undefined offset: 1 in /Users/kanno/workspace/sandbox/piece/2017/06/25-1036.php on line 4
NULL

ではnullに対して添字アクセスしてみる。

<?php
var_dump(null[0]);
var_dump(null[1]);

上記コードを実行するとnoticeが出ない。というかエラーにすらならない。

NULL
NULL

わお…。

ちなみに勝手に配列になったりしないかどうかも確認したが、さすがにそれは大丈夫だった。

<?php
$a = null;
var_dump($a[1]);
var_dump($a);
NULL
NULL

ちなみにforeachもいけちゃうのかと思いきや、これはwarningだった。

<?php
foreach (null as $x) {
}
Warning: Invalid argument supplied for foreach() in /Users/kanno/workspace/sandbox/piece/2017/06/25-1036.php on line 2
PHP Warning:  Invalid argument supplied for foreach() in /Users/kanno/workspace/sandbox/piece/2017/06/25-1036.php on line 2

count(null)は0だから普通のfor文ならいけちゃうけど。

<?php
for ($i=0; $i < count(null); $i++) {
}

Gaucheで指定文字を指定回数繰り返す書き方

背景

Gaucheで指定した文字を指定した回数だけ繰り返したい。
Rubyでいう"a" * 5PHPでいうstr_repeat('a', 5)のようなこと。

irb(main):002:0> "a" * 5
=> "aaaaa"

目的は受け入れテストにて「x文字以上ならエラー」というケースでx文字を簡単に用意したかったから。
(Vimですぐ出来るけど、練習のためにGaucheのやり方を調べた)

結論

1文字の繰り返しでいいならmake-string

gosh> (make-string 5 #\a)
"aaaaa"

文字列で繰り返したいならリストを作ってからapplyする方法っぽい。

gosh> (apply string-append (make-list 5 "Vim"))
"VimVimVimVimVim"

ちなみにClojureでも似たような感じっぽい。

user=> (apply str (repeat 3 "str"))

追記: 他の書き方も教えて頂いた。

調べ方

まずはGaucheのドキュメントをrepeatとかcycleとかのキーワードで探したが辿り着けず。
GaucheというよりSchemeでの書き方が分かればいいはず、ということでググっていたら以下に辿り着いた。

色々な言語での実装例があって面白かった。 Repeat a string

jQueryのonでハンドラを複数登録するのとaddEventListenerで複数登録するのは微妙に挙動が違う

背景

jQueryのonにハンドラを複数登録するのと、addEventListenerで複数登録するのは微妙に挙動が違う。
それについて調べる必要があったのでメモ。

環境

Chrome バージョン 58.0.3029.110 (64-bit)

再現

formのsubmitを押した時のハンドラを複数登録する。

  • 1つ目: alert出すだけ
  • 2つ目: 例外を発生
  • 3つ目: submit本来の挙動を停止(結果として遷移しなくなる)

サンプルコード - addEventListener版

<!DOCTYPE html>
<html lang="ja">
  <head></head>
  <body>
    <form action="" id="form">
      <button type="submit">submit</button>
    </form>
    <script type="text/javascript">
      var form = document.getElementById('form');
      form.addEventListener('submit', function() {
        alert(1);
      });
      form.addEventListener('submit', function() {
        alert(2);
        throw new Error("doooon");
      });
      form.addEventListener('submit', function(e) {
        alert(3);
        e.preventDefault();
        e.stopPropagation();
      });
    </script>
  </body>
</html>

実行結果は以下のようになる。

  1. alert(1)が表示
  2. alert(2)が表示
  3. 例外が発生し、Webコンソールに出力される
  4. alert(3)が表示
  5. 何も遷移しない

つまり各ハンドラは独立して実行される。

サンプルコード - jQuery

バージョンは3.2.1と2.2.4で確認した。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
  </head>
  <body>
    <form action="" id="form">
      <button type="submit">submit</button>
    </form>
    <script type="text/javascript">
      $("#form")
        .on("submit", function () {
          alert(1);
        })
        .on("submit", function () {
          alert(2);
          throw new Error("doooon");
        })
        // このハンドラは呼ばれない
        .on("submit", function (e) {
          alert(3);
          // 本来はreturn falseだけでいいけどテストとして一応書いておく
          e.preventDefault();
          e.stopPropagation();
          return false;
        });
    </script>
  </body>
</html>

実行結果は以下のようになる。

  1. alert(1)が表示
  2. alert(2)が表示
  3. 例外が発生しているが、Webコンソールには何も表示されていない(というか見えない)
  4. 遷移する

つまり3つ目のハンドラは呼ばれていない。

原因

jQueryonで登録するハンドラはそのままaddEventListenerに渡しているわけではなく、内部的にtype毎に1つの関数でラップしている。
このラップした関数が実際のイベント発火時に処理されるわけだが、ここでは登録されたハンドラをループして処理している。
そのため、登録したハンドラが途中で落ちると後続のハンドラは動かない。実際のハンドラ呼び出しでは例外をキャッチしていない。

またハンドラの呼び出しは以下のようになっていて、falseを返すとpreventDefault()stopPropagation()を呼ぶ。

  ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
    handleObj.handler ).apply( matched.elem, args );

  if ( ret !== undefined ) {
    if ( ( event.result = ret ) === false ) {
      event.preventDefault();
      event.stopPropagation();
    }
  }

例外が発生すると当然preventDefaultなどには到達しないので、デフォルトの挙動で動く。よって、上記のサンプルではsubmitがデフォルトの通り遷移した。

ちゃんと動作確認をしないと、期待通りに動いているのか、途中で例外が起きたけどデフォルトの挙動で上手くいっているように見えたか分からない。

おわりに

ChromeのWebコンソールでブレークポイント貼ってCall Stack辿れるの便利。