Friction River Software

  • お問い合わせ

CakePHP5入門【CakePHP5応用編⑪】さまざまな改良

A子

掲示板に投稿した画像をクリックすると、別ウィンドウ上にオリジナルサイズで表示できるようにしてるじゃん

C菜

ですね~

A子

画像がでかい場合、全体像が見えないのがやっぱり不満でさー
ウインドウサイズに合わせて縮小表示するのをデフォルトにして、ワンクリックでオリジナルサイズの表示に切り替えられるようにしたいわけよ

どうだろう、できるかな?

C菜

そういえばCSSフレームワークである「Bulma」には、タブのデザインがありましたよ~

タブで「縮小表示あり」と「縮小表示なし」を切り替えられるようにしたらどうでしょう~?

A子

お、良いねぇ
その案、採用!

B美

ちょっと口を挟むけどさ

「Bulma」のタブ(tabsクラス)ってあくまでもデザインであって、マウスクリックによる切り替え機能は無いわよ

C菜

え?
そうなんですか~?

A子

ヒント!
ヒントをクレメンス

B美

JavaScriptで制御すれば良いのよ

手順は「ChatGPT」が知ってるわ(苦笑)

A子

OK
聞いてみるよ

・・・

まず、「bbsapp/templates/Posts」の中にある「imageview.php」だけどさ
こんな感じに書き換えたよ

<section class="section">
    <div class="container">
        <a class="buttons" href="" onclick="window.close(); return false;">
        <button class="button is-danger is-rounded" style="color: white;">このウィンドウを閉じる</button>
        </a>
        <br />
        <div class="tabs is-toggle is-toggle-rounded">
            <ul id="tab-menu">
                <li class="is-active" data-target="tab1"><a>ウィンドウ幅に合わせて縮小</a></li>
                <li data-target="tab2"><a>オリジナルサイズ</a></li>
            </ul>
        </div>
        <div class="tab-content">
            <div id="tab1" class="content is-active">
                <div class="viewpanel2">
                    <img src="<?= $this->Url->build(ltrim($filepath, '.'), ['fullBase' => true]) ?>" />
                </div>
            </div>
            <div id="tab2" class="content">
                <div class="viewpanel">
                    <img src="<?= $this->Url->build(ltrim($filepath, '.'), ['fullBase' => true]) ?>" />
                </div>
            </div>
        </div>
    </div>
</section>

C菜

以前あった「style」タグが無くなってます~

A子

うん、それはレイアウトファイルのほうへ移動した
(「bbsapp/templates/layout」の中にある「image_layout.php」のことね)

あ、赤字で示したクラスが追加したやつよ

<style>
    .content {
        display: none;
    }

    .content.is-active {
        display: block;
    }

    .viewpanel {
        width: 90vw;
        height: 90vh;
        overflow: auto;
        resize: both;
        display: block;
    }

    .viewpanel img {
        width: auto!important;
        height: auto!important;
        max-width: none!important;
        max-height: none!important;
        display: block;
    }

    .viewpanel2 {
        width: 90vw;
        height: 90vh;
    }

    .viewpanel2 img {
        height: auto!important;
        max-width: 100%!important;
    }
</style>

さらに同じレイアウトファイル(image_layout.php)の末尾に「script」タグを追加したよ

<script>
document.addEventListener("DOMContentLoaded", function () {
    const tabs = document.querySelectorAll("#tab-menu li");
    const contents = document.querySelectorAll(".tab-content .content");

    tabs.forEach(tab => {
        tab.addEventListener("click", function () {
            // すべてのタブとコンテンツの `is-active` クラスを削除
            tabs.forEach(t => t.classList.remove("is-active"));
            contents.forEach(c => c.classList.remove("is-active"));

            // クリックされたタブと対応するコンテンツに `is-active` を追加
            tab.classList.add("is-active");
            const target = document.getElementById(tab.dataset.target);
            target.classList.add("is-active");
        });
    });
});
</script>

まぁ、ほとんどはChatGPTに教えてもらった通りなんだけどさ(苦笑)


C菜

大きな画像ファイル(2,816×2,112ドット)を投稿したあと、それをクリックして別ウィンドウ表示をやってみました~

B美

ふむ、良い感じね

それじゃ「オリジナルサイズ」のほうをクリックすると…

C菜

大きすぎて、片方の耳しか見えてませんよ~(笑)

ちなみに、掲示板上での縮小表示はこうです~

A子

どうやらうまくいったみたいね

どうよ(ドヤ顔)

B美

はい、はい(苦笑)

C菜

あとですね~

掲示板の発言内容に対して検索できる機能を付けたいです~

A子

それは簡単じゃない?

すぐにできそう(笑)

B美

ほほう、言ったわね

吐いた唾を飲むんじゃないわよ(苦笑)

C菜

あ、やばいです~
B美部長の不敵な笑み…

これって多分、ハマるパターンですよ~

A子

え?そんなことは…
いや、だって単なるデータベース検索だし…

と、とにかく「bbsapp/templates/Posts」の中にある「index.php」に検索ワード入力欄を設けよう

<?= $this->Form->create(null, ['id' => 'search_form', 'url' => ['controller' => 'Posts', 'action' => 'search']]) ?>
    検索ワード:<br />
    <?= $this->Form->text('search_word', ['id' => 'search_word', 'label' => false, 'div' => false, 'class' => 'input is-info is-normal', 'style' => 'width: 50%;']) ?>
    <?= $this->Form->button(__('検索'), ['type' => 'submit', 'id' => 'submit_btn', 'class' => 'button is-warning']) ?>
<?= $this->Form->end() ?>
<br /><br />

あ、もともとあったstyleタグはレイアウトファイル(default.php)のほうへ移動したよ
(headタグの内側ね)


C菜

ブラウザで確認してみましたけど、なかなか良い感じです~

A子

んで、結果を表示するための「search.php」だけどさ

「index.php」の上のほうをちょこちょこっと書き換えて、「search.php」というファイル名で保存しただけ…
(ほとんどは「index.php」と同じってこと)

C菜

あとは「bbsapp/src/Controller」の中にある「PostsController.php」ですね~

A子

うん、色々と調べながらだけど、こんな感じでコーディングしてみたよ

public function search()
{
    $condition = ['delete_flag' => 0];
    $search_word = '';

    if ($this->request->is('post')) {
        $search_word = $this->request->getData('search_word');
        if ($search_word != '') {
            //何らかの検索ワードが指定されている場合、検索条件を追加
            $condition += ['body like' => '%'.$search_word.'%'];

        }
    }

    $query = $this->Posts->find()->where($condition);
    $posts = $this->paginate($query, ['limit' => SEARCH_MAX_PAGE, 'order' => ['id' => 'desc']]);

    $this->set(compact('posts'));
    $this->set(compact('search_word'));
}

ポイントは、$conditionという連想配列に検索条件を追加しているところだね

あと、SEARCH_MAX_PAGEという定数を「const.php」の中に追記した
(ついでにバージョンも「1.3.0」に変更)


B美

ふむふむ

なかなかやるじゃないの
(一見、問題なさそうに見えるわね)

あ、そうだ!
開発環境だけは「bbsapp/config」の中にある「app_local.php」のdebugパラメータをtrueにしておきなさい
(良い機会だから、DebugKitを使ってみましょう)

C菜

とりあえずテストしてみるです~

検索ワードには「テスト」って入れて、「検索」ボタンを押してみますね~

A子

おぉー、バッチリじゃん

我ながらすごいわね
(自画自賛(笑))

B美

うんうん、もしもページネーションを行わないのであれば完璧ね

でも、「次へ」を押してページを切り替えたらどうなるかしら?

A子

あ、あれ?

全ての投稿が表示されてる?
(検索ワードが空欄じゃん)

B美

「一覧へ戻る」をクリックして、もう一度同じことをやってみて

A子

さっきと同じ結果だね

B美

んじゃ、右下にある赤いケーキのアイコンをクリックします

B美

最下段に赤い帯が出るから、その中にある「Sql Log」をクリックしてね

C菜

うわぁ~
これってSQL文ですね~

ちゃんとwhere句の中に検索条件が追加されてます~

B美

LIKEというのは「あいまい検索」で使うキーワードで、「%」はワイルドカード、すなわち「0文字以上の任意の文字列」のことよ

これにより、本文(body)の中に検索ワード(この例では「テスト」)が含まれているレコードを取り出す…って意味になるわ

A子

へぇー
便利じゃん

あ、この画面を閉じるときは?

B美

右上にある「×」をクリックしてちょうだい

C菜

それじゃ~
「次へ」を押してページを切り替えてから、「Sql Log」をもう一度確認してみますね~

A子

LIKEの検索条件が消えてるよ

なんでだー?

B美

ここからは、とても重要な話

Webページって、複数のページをリンクによって移動できるようになってるわよね?
(そういう文書のことを「ハイパーテキスト」と呼びます)

A子

うん、めっちゃ便利よね

B美

実はリンクによってページを移動する場合、前のページから次のページへパラメータを受け渡すことは「基本的にできない」の
(言い換えれば、各ページは独立しているってこと)

C菜

え?
でも色々な通販サイトでは、買い物かご(カート)に入れた商品を決済ページまで持ち続けることができますよね~?

あれはページ間を移動する際、パラメータ(購入したい商品)を受け渡してるってことになるのでは~?

B美

さすがはC菜

そう、あれはそれぞれのWebページから同じ買い物かごを見てるってイメージなんだけど、ページをまたいでパラメータを受け渡しているってことにもなるわね

A子

ん?
そもそも会員サイトへのログインなんかでも、ログイン情報をページをまたいで受け渡してるんじゃないの?

B美

その通り!

さて、それじゃその仕組みを取り入れれば、ページネーションによるページ切り替えを行っても、次のページへ「検索ワード」を受け渡せるってことになるわよね

A子

ググるためのキーワードを教えてよ

B美

「CakePHP」では「Session」クラスになるわね
ただ、割と難しいから基本的なところだけは教えておいてあげるわ

1.コントローラークラスの中にprivateフィールドとして「$session」を宣言します

private $session;

2.同じファイルの「initialize」メソッド内に

$this->session = $this->request->getSession();

を記述します
(これによりセッションを使う準備は完了よ)

3.セッションへの書き込みは「write」メソッド

$this->session->write(['キー' => 値]);

あ、このメソッドの引数は連想配列なんだけど、複数のパラメータを「一度に」渡す必要はないわよ

$this->session->write(['キー1' => 値1]);
$this->session->write(['キー2' => 値2]);
$this->session->write(['キー3' => 値3]);
・・・

という感じで、複数のパラメータを順に指定できます
(あとからwriteしたもので前のやつが上書きされることはないってこと)

要するに、複数回のwriteは「上書き」じゃなくて「追加」ってわけ…
あ、「同じキー」を指定した場合は、当然「上書き」になるけどね

4.セッションから読み出すには「read」メソッド

変数 = $this->session->read('キー');

5.セッション内にそのキーが存在するかのチェックは「check」メソッド

$this->session->check('キー')
(戻り値はtrueまたはfalse)

6.セッションキーの削除は「delete」メソッド

$this->session->delete('キー');

なお、感覚的には「連想配列」と同じよ
Webページをまたいで、どこからでもアクセス可能な連想配列(みたいなもの)ってイメージかな

C菜

これだけわかれば何とかなりそうです~

B美

あ、あともう一つヒント

フォーム送信のメソッドは「POST」だけど、リンクをたどったり普通にアクセスする際のメソッドは「GET」よ

A子

えーっと、まずはPOST送信なら検索ワードを取得するのは同じで、そのときにセッションへの書き込みを行えば良いんだよね

んで、POSTじゃないとき(つまり、GETのとき)はセッションから検索ワードを読み出して、それを使って検索条件を追加すると…

public function search()
{
    $condition = ['delete_flag' => 0];
    $search_word = '';

    if ($this->request->is('post')) {
        $search_word = $this->request->getData('search_word');
        if ($search_word != '') {
            //何らかの検索ワードが指定されている場合、検索条件を追加
            $condition += ['body like' => '%'.$search_word.'%'];
        }

        //セッションへ書き込み
        $this->session->write(['search_word' => $search_word]);
    } else {
        //セッションから読み出し
        if ($this->session->check('search_word')) {
            $search_word = $this->session->read('search_word');

            if ($search_word != '') {
                $condition += ['body like' => '%'.$search_word.'%'];
            }
        }

    }

    $query = $this->Posts->find()->where($condition);
    $posts = $this->paginate($query, ['limit' => SEARCH_MAX_PAGE, 'order' => ['id' => 'desc']]);

    $this->set(compact('posts'));
    $this->set(compact('search_word'));
}

赤字部分が追加した箇所よ


C菜

テスト結果は完璧です~

B美

さて、それじゃ次ね

Google検索みたいに、複数の単語を空白文字で区切って絞り込み検索できるようにしてみなさい
なお、空白文字については半角でも全角でも同じように処理できるようにすること

A子

ChatGPTに聞いてみたよ

$search_word_array = preg_split('/\s+/u', $search_word, -1, PREG_SPLIT_NO_EMPTY);

で、空白文字(半角または全角)で区切られた複数単語の文字列から配列を作れるらしい
(「正規表現」って言うみたい…よく知らんけど)

で、その配列をforeachで回して、$conditionに対して検索条件を追加していったのがこれ

$condition = [];
$condition[] = ['delete_flag' => 0];
・・・
foreach ($search_word_array as $word) {
    $condition[] = ['body LIKE' => '%'.$word.'%'];
}

ポイントは「$condition += [ ]」ではなく、「$condition[] = [ ]」としている点ね
これによって同じキー('body LIKE')を複数混在できるようになる(らしい…知らんけど)

C菜

うまくいきました~

一応、コピペする人のためにsearchメソッドの最終版をまとめておきますね~

public function search()
{
    $condition = [];
    $condition[] = ['delete_flag' => 0];


    $search_word = '';

    if ($this->request->is('post')) {
        $search_word = $this->request->getData('search_word');
        if ($search_word != '') {
            //空白文字(半角または全角)で区切られた複数の単語を配列化
            $search_word_array = preg_split('/\s+/u', $search_word, -1, PREG_SPLIT_NO_EMPTY);

            //検索ワードの数だけ条件を追加
            foreach ($search_word_array as $word) {
                $condition[] = ['body LIKE' => '%'.$word.'%'];
            }

        }

        //セッションへ書き込み
        $this->session->write(['search_word' => $search_word]);
    } else {
        //セッションから読み出し
        if ($this->session->check('search_word')) {
            $search_word = $this->session->read('search_word');

            if ($search_word != '') {
                $search_word_array = preg_split('/\s+/u', $search_word, -1, PREG_SPLIT_NO_EMPTY);
                foreach ($search_word_array as $word) {
                    $condition[] = ['body LIKE' => '%'.$word.'%'];
                }

            }
        }
    }

    $query = $this->Posts->find()->where($condition);
    $posts = $this->paginate($query, ['limit' => SEARCH_MAX_PAGE, 'order' => ['id' => 'desc']]);

    $this->set(compact('posts'));
    $this->set(compact('search_word'));
}

赤字の箇所が追加・変更したところです~


B美

A子…
今度こそあなたを見直したわ

実戦によって経験値を獲得した結果、レベルが上がったみたいね

A子

いや、ロールプレイングゲームかよ!(笑)

てか、値の受け渡しって、セッション以外のやり方ってないの?

B美

もう一つあるわよ

それが「GETパラメータ」と呼ばれる、URLの中に値を埋め込む方法ね
(Google検索がまさにこの方法です)

C菜

そういえば投稿の削除って、URLに「bbsapp/posts/delete/(投稿番号)」を指定するわけですが、この(投稿番号)がパラメータとも言えますよね~

B美

その通り

まぁ、本来の「GETパラメータ」というのは、URLの末尾に「?キー1=値1&キー2=値2&・・・」という形の文字列を連結するやり方だけどね

A子

なるほどねぇ

たしかにGoogleで検索したときって、URLの部分がめっちゃ複雑になるよね

B美

GoogleのURLが複雑に見えるのは、検索ワード以外の値が長いからであって、検索ワード自体は「q=」のあとに続く文字列

ただ、検索ワードとして指定する文字の種類によっては「URLエンコード」が必要だったり、ちょっと面倒なの
(セッションを利用するほうが多分簡単よ)

C菜

なるほど、よくわかりました~

あと、URLがごちゃごちゃしてると見栄えが悪いですしね~

A子

たしかに…(苦笑)