ITの隊長のブログ

ITの隊長のブログです。Rubyを使って仕事しています。最近も色々やっているお(^ω^ = ^ω^)

【CakePHP3.x】AuthがかかったController::Actionをテストしようとしたらハマった

ログ残し。

qiita.com

ここを参考にテストを実行する。

<?php
//...
    private function setAuthSession()
    {
        $this->session([
            'Auth' => [
                'id' => 1,
                'email' => 'xxx@gmail.com',
                'password' => 'Lorem ipsum dolor sit amet',
                'created' => '2017-01-31 14:43:15',
                'modified' => '2017-01-31 14:43:15'
            ]
        ]);
    }

    public function testLoginRequiredPageNotAccess()
    {
        $this->get('/contact/login-required');
        $this->assertRedirect(['controller' => 'users', 'action' => 'login']);
    }

    /**
     * Test contact pre register
     *
     * @return void
     */
    public function testLoginRequiredPageAccess()
    {
        $this->setAuthSession();
        $postData = ['test'];
        $this->post('/contact/login-required', $postData);
        $response = json_decode($this->_response->body(), true);
    }
// ...

よーし、これを実行すれば。。。と思ったら失敗。testLoginReequiredPageNotAccess()は失敗してもいいけど、その下のメソッドは失敗しちゃ困る。なぁーぜぇー?

これだけで1時間程潰してしまったが、AuthComponentを読んでみたらわかった。

<?php
// ...
class AuthComponent extends Component

    //...
    public function authCheck(Event $event)
    {
        if ($this->_config['checkAuthIn'] !== $event->name()) {
            return null;
        }

        /* @var \Cake\Controller\Controller $controller */
        $controller = $event->subject();

        // ...

        $isLoginAction = $this->_isLoginAction($controller); // ここがfalse

        if (!$this->_getUser()) { // ここがfalse
            if ($isLoginAction) { // ここもfalse
                return null; // 未ログインで返り値
            }
            $result = $this->_unauthenticated($controller);
            if ($result instanceof Response) {
                $event->stopPropagation();
            }

            return $result;
        }
        // ...
    }
// ...

省略しているが、$isLoginActionは「ログインページへのアクセスなら~」をチェックしているので、今回は関係ない(ログインページのテストではないので)。

ということは、$this->_getUser()でfalseが返ってくるのが困るってことだ。こいつの中を覗く。

<?php
// ...
class AuthComponent extends Component
{
    // ...
    public function user($key = null)
    {
        $user = $this->storage()->read();
        if (!$user) {
            return null;
        }

        if ($key === null) {
            return $user;
        }

        return Hash::get($user, $key);
    }
    // ...

this->storage()->read()のところで、セッションから値を取得しているようだ。要するにログインしているとAuthの名前をkeyとしてセッションに保存しているはずなので、それが取れていないということである。

うーん? 何故?

(°ω°・・・・( ゚д゚)ハッ!

思い出しました。

このプロジェクトではログインフォームを2つ用意していて、それぞれ管理者用とユーザー用で分けていました。

そのとき、セッションのkeyの値が被るとおかしくなるとCakePHP2.xから知っていたので、Auth keyをそれぞれAuth.AdminAuth.Userとしていました。

ということは

<?php
    // ...
    private function setAuthSession()
    {
        $this->session([
            'Auth.User' => [ // ここを変更
                'id' => 1,
                'email' => 'xxx@gmail.com',
                'password' => 'Lorem ipsum dolor sit amet',
                'created' => '2017-01-31 14:43:15',
                'modified' => '2017-01-31 14:43:15'
            ]
        ]);
    }
    // ...

セッションのkeyを変更してみて、テストを実行してみると。

うまくいきましたぁー!ヾ(´∀`)ノキャッキャ

はぁ...先週の1時間。

Behaviorの名前空間を変更しようとしたら、できませんでした。

Controllerも階層深くできるし、いけるっしょと考えていました。

試す

とあるTableクラス

<?php
// ...
  $this->addBehavior('Register', [
      'className' => 'App\Model\Behavior\Users\Register'
  ]);
// ...

ディレクトリとnamespaceを変更したBehavior

<?php
namespace App\Model\Behavior\Users; // <- ①
// ...
class RegisterBehavior extends Behavior
{
  // ...

これでいけると思った。

しかし、色々不具合が。

PHPStormで開発していますが、①のnamespaceのところを定義すると、Declaration of referenced constant is not found in built-in library and project files. ていうエラーが波線が表示されます。

よくわかりませんが、補完で定義すると、治りました。イミフ。

また、動作でもエラーが発生しました。

Missing Behavior

わかりやすいですね。見つからない。なんでや!

色々コアのソースを覗いてみたけど、namespaceをパスとして認識しないようになっているので、ロードするディレクトリを増やさないとダメなんだろうけど、よくわからん。

つーか、ドキュメントに明記されていた

https://book.cakephp.org/3.0/ja/orm/behaviors.html

ビヘイビアクラスは App\Model\Behavior 名前空間または MyPlugin\Model\Behavior 名前空間に存在する必要がある。

( ゚д゚)・・・

なるほど。

おわり

転職すると母に伝えたら、すごく怒られた

今度転職します。ということが、最近決まりました。

全く通ると思っていなかったから、びびったけど、興味がある、というか、是非いきたい!という業種だったので、自分としてはすごくうれしい。

また、まだちゃんと分からないが貰える額も良いらしい。いいじゃないか。興味があることをやって、お金が貰える。最高じゃないか。

彼女には事前に話ししていた。仲の良い同僚にも事前に話をしていた。寂しそうな人もいるが、みんな喜んでくれている。みんないい人だ。

今いる会社は嫌いとか不満とかそういうのはあまりなかった。じゃあ何故転職したかったの? 開発のスキル。または、開発経験をもっと積みたかったから。さらに言えば、もっとプログラマーが溢れる場所で仕事がしたかった。それに尽きる。絶対伸びると信じてるからだ。

給与が下がるのが怖かったでもあるが、もともとは13万で東京で暮らしていた頃もある。まぁ、貯金ができている今は大丈夫だろう。と、考えている。

さて、タイトルにもある。彼女、同僚。と、続けて大丈夫だった。なので、そのノリで母に話をしたら、すごく怒られた。

あれー? いまより良くなるかもしれないよー? うれしくないの? と思っていた。

私は実家で住んでいる。なので、こういう生活の話は言わずともすぐにバレるので、先に話すことが吉と思ったからだ。(前、黙っていたらものすごく怒られた)

「どうしてそんな年数を立たずに転職するのか?」「安定してきたのに、わざわざ手放すのか?」「転職先に騙されていないか?」など、ガンガン言われ続ける。

確かに今の場所は1年くらいしか立っていない、IT業界ではそんな状況ざらにあると思うけど。。。

まぁでも、その前は実力なしフリーランスだったので、お金はそんなに入ってこなかった。また、もっと前を話せば、4年ほど務めた会社は日々頑張る割には給与が少なかったので、まぁ母からしたらすごく心配なんだと思う。

前の会社のときは、たしかにひどかったと思う。あのときは、会社も悪いところはあったと思うが、今思い返して見れば、自分に落ち度もあったと思っている。営業の仕方、プログラムの仕方、人の育て方。自分だけ、自分だけでやろうとして、人に頼るということを考えていなかった。全部自分でできると信じていた時期があった。

その時、本当に本当に勝手に自分だけでいっぱいいっぱいになって、携帯を自分で折った(ガラケーです)ことがあった。社長から怒りの電話を取ることが本当に怖かったのだ。

パソコン開いて、今日は休みます。的なメールを送信して、布団にくるまって。意味もなく時間だけが過ぎるのをまって眠ったふりをしていた。仕事をサボっていた。

その時、母が部屋を開けた。恐らく仕事をサボっているのはバレていたと思うが、何も言わず、頭に手をおいてやさしくなでてくれた。仕事の話は一切しなかったので、なんにもわからないだろうが、心配だったのだろう。部屋から母がでた後、すごく泣いた覚えがある。

ということで、私なりにはわかってもらうために話さないといけないな。と、思っていたのですが、母の心配を再発させた感じになりました。。。あー、もうちょういIT業界のことをちゃんと話せばよかった。

まあ、ほとんどの人はそうだと思うが、いまだにどうやって稼いでいるのかわからないらしい。HP(ホームページ)ってなに? なんでそんなに高いん? Googleって検索サイトのことでしょ? そんな会社存在するの? こんな感じの人。心配されるのもしょうがないかな。

次はレベル高いと思うので、自分は必死になると思うけど、うまくいったら、家族をもう少し安心させきれるように、遊びの旅行代を全額出すとか、少しの親孝行でもできたらなぁと思う。

こういうのが(うまくいけば)すぐできるIT業界ってやっぱりいいなぁと思った。

※ちなみに、親父は何もわかっていないと思うが、昔から反対はほとんどしないので、「いいんじゃない?」といってくれました。他からしたら、「いい親父さんや・・・!」と思うかもしれませんが、家族代表から言わせてもらえば、何も考えていないと思いますorz 親父らしい。

【CakePHP3.x】PhpStormにてPHPUnitをデバッグしたときのエラー

環境

$ sw_vers 
ProductName:  Mac OS X
ProductVersion: 10.11.6
BuildVersion: 15G1212

'TMP'が定義されていない

Use of undefined constant TMP - assumed 'TMP'

なんぞこれ?

コードを追っていくと、下記コードでエラーが発生していた。

    // ...
    protected static function _defaultConfig($name)
    {
            $defaults = [
            'php' => [
                'cookie' => 'CAKEPHP',
                'ini' => [
                    'session.use_trans_sid' => 0,
                ]
            ],
            'cake' => [
                'cookie' => 'CAKEPHP',
                'ini' => [
                    'session.use_trans_sid' => 0,
                    'session.serialize_handler' => 'php',
                    'session.use_cookies' => 1,
                    'session.save_path' => TMP . 'sessions', // <- ここ
                    'session.save_handler' => 'files'
                ]
            ],
            // ...
    }
    // ...

要は、「"TMP"って定数が宣言されていないぞコラァー!」ってことだと思う。

過去こういうのでハマんなかったかな? ってことで思い出した。

www.aipacommander.com

そそ! これこれ。

$ ~/vendor/bin/phpunit --configuration ~/phpunit.xml.dist

./config/bootstra.php が見つからない

Warning Error: require_once(./config/bootstrap.php): failed to open stream: No such file or directory in ...

次はなにー?

<?php
    //...
    public function bootstrap()
    {
        require_once $this->configDir . '/bootstrap.php';
    }
    // ...

ここで発生している。

予備元を順に追っていきます。

<?php
    // ...
    public function run(ServerRequestInterface $request = null, ResponseInterface $response = null)
    {
        $this->app->bootstrap();
        // ...
<?php
    // ...
    public function execute($request)
    {
        try {
            $reflect = new ReflectionClass($this->_class);
            $app = $reflect->newInstanceArgs($this->_constructorArgs); // (´・ω・`)
        } catch (ReflectionException $e) {
            throw new LogicException(sprintf(
                'Cannot load "%s" for use in integration testing.',
                $this->_class
            ));
        }

        // Spy on the controller using the initialize hook instead
        // of the dispatcher hooks as those will be going away one day.
        EventManager::instance()->on(
            'Controller.initialize',
            [$this->_test, 'controllerSpy']
        );

        $server = new Server($app);
        $psrRequest = $this->_createRequest($request);
        $response = $server->run($psrRequest); // ここの中でbootstrap()を呼んでエラーが発生する

        return ResponseTransformer::toCake($response);
    }
    // ...

(´・ω・`)のところで、渡している値$this->_constructorArgsの中が、./configとなっているがダメ。

PhpStormのデバッグの設定では相対パスで設定すると何故かエラーになるので、絶対に変更しなければならない。

どうにかこいつの値を変更できないものか。と、調べていたら

<?php
    public function __construct($test, $class = null, $constructorArgs = null)
    {
        $this->_test = $test;
        $this->_class = $class ?: Configure::read('App.namespace') . '\Application';
        $this->_constructorArgs = $constructorArgs ?: ['./config']; // ここ
    }

__constructで、設定されていた。三項演算子なので、$constructorArgsの値がnullなのがダメ。

じゃあ、このクラスをオブジェクト化しているのはどこか。

<?php
    // ...
    protected function _makeDispatcher()
    {
        if ($this->_useHttpServer) {
            return new MiddlewareDispatcher($this, $this->_appClass, $this->_appArgs); // ここ
        }

        return new LegacyRequestDispatcher($this);
    }
    // ...

ふむう。じゃあ、この、$this->_appArgsってのはどこで設定されているのかなー。

<?php
    // ...
    public function configApplication($class, $constructorArgs)
    {
        $this->_appClass = $class;
        $this->_appArgs = $constructorArgs;
    }
    // ...

みつけたぁあああ!!

ってことは、このメソッドがあるクラスを継承しているテストクラスでそのまま呼べんじゃん!

ってことで、試す。

  • 自分のテストクラス
<?php
    // ...
    public function testIndex()
    {
        $path = '<絶対パス>';
        $this->configApplication(null, [$path]); // 配列で渡す
        $this->post('/schedule-api/get', $postData);
        // ...
    }

うまくいったぁああああああああ!!!!

うまくいったけど

って、ちょいまち。

そもそも、コマンドの引数でbootstrap.phpを指定できないのか? と、疑問に思った瞬間、記憶が戻ってきた。

www.aipacommander.com

$ ~/vendor/bin/phpunit --bootstrap ~/tests/bootstrap.php ~/tests/

(°ω°; マサカ...!?

・・・できませんでした。( ´ー`)フゥー...

いや、よくないけどね。

なんででしょー?

うーん。そもそも、設定するのが、configApplication()のメソッドなら、こいつをコールしている箇所を探せばいいはず。

$ grep 'configApplication' -r ~/vendor/
~/vendor/cakephp/cakephp/src/TestSuite/IntegrationTestCase.php:    public function configApplication($class, $constructorArgs)

Oh...

どこも呼んでないやん。

よくわからんが、とりあえずconfigApplication()setup()で呼んで設定することにしました。

<?php
    // ...
    public function setUp()
    {
        $this->configApplication(null, [ROOT . 'config']);
        parent::setUp();
    }
    // ...