ITの隊長のブログ

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

【CakePHP3.x】FlashComponentのメッセージが表示されない

タイトルだけではよくわかんないね。

<?php
// ...
    public function cakeAction($id = null)
    {
        $function = function ($post) {
            // ...
        };
        $this->_something($id, $function);
        $this->render('edit');
    }

    private function _something($id, $function)
    {
        // ...
        $this->Flash->success(__('success!'));
        // ...
    }
// ...

とあるControllerの中に↑のようなアクションとメソッドがあるとする。

/controller/cake-action/1へアクセスすると、cakeAction($id = null)が呼ばれて、その中の処理で、_something($id, $function)メソッドが呼ばれるような感じです。

_something($id, $function)は、処理の途中で$this->Flash->success(__('success!'))を実行しています。これでPHPでよく見る$_SESSIONにメッセージが書き込まれたはずです。

このメッセージを取り出して使うのはView(template)のほうで、この場合だと、edit.ctpなんてテンプレートを呼び出してブラウザでレンダリングするようになっています。

が、何度検証しても、何故かメッセージがでてこない。

色々調べた所、わかりました。

理由としては、セッションに書き込んだ後、$this->render()を呼び出すと、中の処理でセッションがクリアされてることがわかりました。何故ー!?

  1. Cake\Controller\Controller->render()
  2. Cake\View\View->render()
  3. Cake\View\View->renderLayout()
  4. Cake\View\View->_render()
  5. Cake\View\View->_evaluate()

↑の5番目の箇所でセッションがクリアされていました。

ただ、根本な理由はわかりませんでした。。。(´・ω・`)

なんか、何度も実行されるんだよね、このCake\View\View->_evaluate()ってメソッド。

呼ばれたセッションが消えるわけではなくて、その何度も実行されているときに何かのタイミングで削除されてるん感じ。なのでよくわからん。

とりあえず、このアクションはレンダリングするテンプレートファイルは決まっているので、処理の最初で決めるように修正しました。

<?php
// ...
    public function cakeAction($id = null)
    {
        $this->render('edit');
        $function = function ($post) {
            // ...
        };
        $this->_something($id, $function);
    }

    private function _something($id, $function)
    {
        // ...
        $this->Flash->success(__('success!'));
        // ...
    }
// ...

これでよし。

しかし、処理の結果でテンプレートファイルをswitchさせたい場合はメッセージをどうすればいいんだろ?

・・・・

きになったので、やっぱりもう少し調べてみることにした。

<?php

// ...
    /**
     * Sandbox method to evaluate a template / view script in.
     *
     * @param string $viewFile Filename of the view
     * @param array $dataForView Data to include in rendered view.
     *    If empty the current View::$viewVars will be used.
     * @return string Rendered output
     */
    protected function _evaluate($viewFile, $dataForView)
    {
        $this->__viewFile = $viewFile;
        extract($dataForView);
        ob_start();

        include $this->__viewFile;

        unset($this->__viewFile);

        return ob_get_clean();
    }
// ...

_evaluate()メソッドの中身の全部です。ob_get_clean()が怪しいと思っています。

これが呼ばれたら、$_SESSIONの中身が空になるのか調べてみました。

<?php

require_once('./a.php');
require_once('./b.php');

ob_start();

include './index.php';

ob_get_clean();

require_once('./b.php');

echo $_SESSION['test'] . PHP_EOL;
<?php
@session_start();
$_SESSION['test'] = 'test';
<?php
@session_start();
if (isset($_SESSION['test'])) {
  echo $_SESSION['test'].PHP_EOL;
} else {
  echo 'testは見つかりませんでした'.PHP_EOL;
}

a.phpでセッションをセットして、b.phpを最初に呼ぶ。

んで、次にob_start()してob_get_clean()を実行する。

そのあとに、b.phpを呼び出して、セッションから値が読めるかチェックします。

はたして結果は…

$ php ob_get_clean-session-delete.php
test # 最初のrequire_onceでよんだやつ
test # ob_get_clean()のあとによんだやつ

ダメでした。うーん。これが原因じゃないのかな。

【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 名前空間に存在する必要がある。

( ゚д゚)・・・

なるほど。

おわり

【CakePHP3.x】Unable to emit headers. Headers sent in file=... line=xxx

今回jsonを返すapiを用意した。前にこのブログでも記事を書いたことがある。

www.aipacommander.com

<?php
...
echo json_encode([]);
return; // returnはいらないけど、強制でactionの処理を終了したいときにあわせて使う

これでいいと思い実装していたのだが、↓のエラーがでてきた。

このエラーと

Unable to emit headers. Headers sent in file=... line=xxx

このエラーである。

Cannot modify header information - headers already sent by ${something php file name}.

発生源はここ

  • ~/vendor/cakephp/cakephp/src/Http/ResponseEmitter.php:42 line
<?php
// ...
    public function emit(ResponseInterface $response, $maxBufferLength = 8192)
    {
        $file = $line = null;
        if (headers_sent($file, $line)) {
            $message = "Unable to emit headers. Headers sent in file=$file line=$line";
            if (Configure::read('debug')) {
                trigger_error($message, E_USER_WARNING);
            } else {
                Log::warn($message);
            }
        }
      // ...
    }
// ...

ふむ?すでにheaderに登録されているとな? よくわからん。

んで、最初jsonの形式がエラーなんだろ?と思っていたけど、どうやらjson_encode()に渡すデータ量が増えるとそのエラーが発生するっぽい。

何故データの量が増えたら、headerに影響するかわからないが、検証したらそうだったので、なんか関係あるのでしょう。(・ω・`)

で、ずっとググっていたが、phpではよく発生しているっぽいけど、cakephpの記事すくなすぎー。

stackoverflow.com

↑の記事のコードをみて、試してみたらビンゴだったので解決策を書く。

responseのオブジェクトにあるbody()へ渡せばよい。

<?php
$this->response->body(json_encode($data));

これでおk。エラーは綺麗サッパリなくなりましたとさ。

【CakePHP3.x】query builderを使った複数条件のwhere

便利になったんだろうけど、最初の壁は乗り越えづらい。

ちとハマったので、昔みたいに軽くメモ。

cakephp3.xはquery builderが一新されている。色々方法があるとは思うが、情報探すの大変。

一番はリファレンスを見ることがだが、基礎なので、応用を考えなければいけない。

「これって、こうだから、こんなこともいけるんじゃね?」みたいな発想(?)と試してみるべし。

話戻るけど、下記条件のqueryを実行したかった。

既存のデータから、とある日付 and とあるユーザー and (between 開始時刻 ユーザーが入力した情報1 and ユーザーが入力した情報2 or between 終了時刻 ユーザーが入力した情報1 and ユーザーが入力した情報2)

みたいなquery(わかりづらいな・・・)

1つの条件ならwhereを使えばすぐできそうだが、複雑になるとどうやるかわからなかった。

とりあえず、リファンレスに書いてあることを、試し試しでやっていたらできたのでメモ。@

<?php
// ...
    public static function existsDatetime($date, $userId, $start, $end) {
        $tableName = TableRegistry::get('table_name');
        $query = $tableName->find('all')
            ->where(function ($exp) use ($date, $start, $end, $userId) {
                // (between or between)を作っている
                $orConditions = $exp->or_(function ($or) use ($start, $end) {
                    return $or->between('start_time', $start, $end)
                        ->between('end_time', $start, $end);
                });
                // orは`$exp->or_()`で作らないといけないが
                // andはメソッドチェーンすればandとなる
                return $exp->add($orConditions)
                    ->eq('my_date', $date)
                    ->eq('user_id', $userId);
            });
        $count = $query->count();
        return !(boolean)$count;
    }
// ...

↑のコードはバリデーションクラスで使ったので、バリデーションのこともまたいつか書こうと思う。

無名関数を使えば、色々な表現方法を使ってqueryを生成できそうだ。便利。

これでできた。( ´ー`)フゥー...