【12日目】QiitaのDTM版を作りたい【作曲の補助ツールを作るまでの日記】

プログラミング

2023年7月27日~2023年8月2日

とりあえず、昨日簡易的な物は作れたのでQiitaっぽいサイトを目指し作っていく。
作る過程を全部乗っけると途方もない長さになることがわかったので、重要そうなところだけ記述していく。

  1. そもそもQiitaとは?
  2. 何故作りたいか?
    1. 課題
    2. 求める機能
  3. 初期設定でやること
  4. laravel-adminとは
    1. ユーザーについて
      1. ユーザーという概念
      2. 管理者ユーザーは何故必要なのか
    2. laravel-adminを入れてみる
  5. CKEditor
    1. CKEditorのテスト
  6. タグ機能
  7. ユーザー機能の追加
  8. Viteを使ったCSS適用が出来ない問題の解決まで
    1. 前提知識
    2. なぜViteが必要なのか?
    3. ブリッジ接続とは何か?
    4. そもそも
    5. ブリッジ接続はどんな接続方式か
    6. 結論
  9. VMのネットワーク設定を変える
  10. スナップショットは絶対取っておこう
  11. テーブルの作成
    1. 記事一覧とそれに関係するタグを取得するコントローラ
  12. 記事作成ページの作成
  13. Bootstrap VS Vite
  14. storeコントローラ
  15. 保存・表示は出来たけど、書式が適用されない
  16. CSSをviteで上書き
  17. Tailwind CSSについて
    1. コンポーネントとユーティリティクラスって何?
      1. コンポーネント
      2. ユーティリティクラス
    2. Tailwindはユーティリティクラスしか使えないのか?
    3. @tailwind base;/@tailwind components;/@tailwind utilities;とは何か
  18. CSSを調整したい
  19. マスアサインメントエラー
    1. マスアサインメントとは
    2. なぜこれが脆弱性につながるのか
    3. なぜ$tag = Tag::firstOrCreate([“name”=>$tagName]);がマスアサインメントなのか
  20. タグの指定、表示までできた
  21. GithubCopilotが優秀すぎて勉強にならない
  22. 検索機能の完成
  23. いいね機能を追加する
    1. マイグレーションファイル
    2. 各モデルへ関係の記述
    3. いいねボタンをshow viewに追加してみる
  24. 合計いいね数の表示
    1. 実装方法
    2. 編集画面を作成する
    3. editメソッドは以下の通り
    4. updateメソッドは以下の通り
  25. 削除機能の追加
  26. おわりに

そもそもQiitaとは?

簡単に言えば、エンジニアがプログラミング関連の記事を個人が投稿できるサイト。

個人的におすすめな記事は桃太郎電鉄の「いけるかな」のアルゴリズムを考察する記事

人生ゲーム系のマス探索って一見単純そうに見えるんだけど、様々な要素が重なることで計算量が指数関数的に増えていくんだよね。
それをアルゴリズムで解決しようというもの。

こんな感じで個人がプログラミング関連の好きな話題についての記事を投稿できる。

一応、どんな記事を投稿してよいかについてはこちらを。

何故作りたいか?

  • そもそも、DTMってめっちゃむずくて奥が深いのに情報が少ないし、情報の総量が少ないので良質な記事も少ない。
  • だから、個人がDTMについての記事を気軽に発信できるようにしたい。
  • Qiitaみたいに専門のサイトがあれば、記事の発見、拡散もしやすいよね。

課題

  • プログラミングは文字だからいいけど、音楽は文字じゃない
  • だから、情報伝え方が難しいし、音を発信しようとするとデータ量とか、そもそも記事である必要性みたいな話になりそう。
  • 音楽には正解がない

求める機能

求めるものは、Qiitaに音要素を加えたものなんだけど、最初からそんなの目指しても数年かかってしまいそうなのでとりあえず最初は最低限の機能を目指す。

  • ユーザー登録機能
    普通にやってみたいので付ける。記事を投稿する以上、ユーザーは判別出来た方がいいよね。
  • 記事投稿機能
    ここがちょっと難しそうで、Qiitaはマークダウン式で記事を作って投稿をするんだよね。
    ユーザーがエンジニアだからこれでもいいんだけど、DTMerにマークダウン式でってきつよなぁ。

    なので、最初はただの文字だけを投稿できるようにするのを目標にしようかいな。
    調べたらWYSIWYG(ウィジウィグ)エディタというのもあるらしいので、ここら辺も視野に入れつつ作っていく。
  • いいね機能
    単純に、いいねが出来る機能。大事。

とりあえず、最低限この3つの機能と絶対に必要になる閲覧画面とかメイン画面を作っていければいいかなと思う。

とりあえず、昨日と同様にプロジェクトの作成からDBの作成、各画面の作成まで行う。

初期設定でやること

  1. “laravel new example-app”でプロジェクト作成
  2. MySQLで“create database DB名”でDBの作成。
  3. .envでDBの指定
  4. config/app.phpのタイムゾーンを’timezone’ => ‘Asia/Tokyo’,に。また、”locale”=”ja”,に。
  5. “php artisan config:clear”でコンフィグのキャッシュ削除。
  6. git initからホストPCでクローン

laravel-adminとは

laravel-adminというのを入れるとそのLaravelプロジェクトの管理画面を簡単に作ることが出来て、そこではGUIでDBのCRUD操作やユーザー管理などが出来る。

ここで、私が引っかかったのがユーザー管理というところで、そもそもユーザーという概念があんまり理解できてなかった。

ユーザーについて

ユーザーという概念

そもそも、ここで言うユーザーはLinuxとかMySQLとかのユーザーではなく、Laravelアプリケーションのユーザー。YoutubeとかTwitterとかオンラインゲームにあるユーザーと思ってもらって問題ないと思う。

このユーザーには個人を識別し、その人に合ったデータを提供するという意味ぐらいしかないと思ってたんだけど、それとは別に権限の付与という意味があるみたい。

つまり、

  • ゲストユーザー(ログインしていない状態)だと閲覧のみが可能
  • 一般ユーザーだと自分の作成した記事の追加・編集が可能
  • 管理者ユーザーだと全ての記事の削除やユーザーの削除が可能

みたいな。

管理者ユーザーは何故必要なのか

ここで引っかかるのが、わざわざ管理者ユーザーを作る意味があるのか、というところ。
そんなスーパー権限を持たせた管理者ユーザーを作って、もしログインされてしまったら好き放題されてしまう。リスクでしかないと思っちゃう。

だから、そもそもユーザー自体に凄い権限は与えずに、サーバー側でしか重要なことはできないようにすればいいのでは? と思うんだけど、効率を考えるとそうでもないみたい。

  • アクセスや時間効率の問題
    管理者ユーザーを作ることでサーバーにアクセスしなくても変更が出来るようになるから、効率が上がる。
  • 操作性の問題
    例えば、設定を弄りたい時ってサーバー側だとvimで設定ファイルを弄ると思うんだけど、この世の中にvimを使いこなせる人がどれだけいるの? っていう。
    サイト内のGUIを使って編集ができるようになってれば誰でも出来るから、同じ会社内の技術を知らない人にも任せられるよねみたいな話らしい。

こんなメリットがあるから、管理者ユーザーは存在する。
確かにオンラインゲームとかも運営アカウントあったりするよね。

その上で、管理者ユーザーはただのIDとPWだけじゃなく2段階認証とか、異常ログイン検知機能をしっかりと付けて管理しないといけない的な話みたい。

したがって、ユーザーは必要でその管理を簡単にしてくれるのがlaravel-adminというわけだ。

laravel-adminを入れてみる

導入はQiita記事を参考に。公式ドキュメントもあるけど、情報がLaravel5の時の物なので、コマンドが若干違ったりする。

とりあえず入れた。

正直使い方が訳わかんないので、とりあえず入れるだけ入れて放置することにした。申し訳ない。
なんかコントローラを追加して管理画面のカスタマイズをするっぽいんだけど、ちょっと私にはまだ早い。

CKEditor

CKEditorというWYSIWYGエディタを導入してみる。
WYSIWYG(ウィジウィグ)は「What You See Is What You Get(見たまんま)」という意味らしく、Googleドキュメントとかwordみたいに、書きながら簡単に文字に編集(リッチテキスト化)をできるのが特徴。

マークダウン式だとマークダウンの知識とか、リアルタイム変換とかが必要だから、そういうところの壁が無くなるね。

WYSIWYGのイメージ

このCKEditorはHTMLで保存される。最初はHTMLってDBに入んのかいと思ったんだけど、あれ改行入れなかったら1行の文字列になるし、全然入るね。

写真データとか動画データもDBに入れるときは文字列にするってことなのかな。かなり面倒そうだけど、どうなんだろうか。

とりあえず、テストviweを作成してそこにCKEditorを導入。

導入といっても、チュートリアルにあるHTML文をコピペしただけ

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>CKEditor 5 – Classic editor</title>
    <script src="https://cdn.ckeditor.com/ckeditor5/38.1.1/classic/ckeditor.js"></script>
</head>
<body>
    <h1>Classic editor</h1>
    <div id="editor">
        <p>This is some sample content.</p>
    </div>
    <script>
        ClassicEditor
            .create( document.querySelector( '#editor' ) )
            .catch( error => {
                console.error( error );
            } );
    </script>
</body>
</html>

CKEditorのテスト

テストしてみよう!

おー。結構簡単に実装できたし、使い勝手も普通に良い。

ここに音源をアップロードしたり、画像をアップロードしたりとなるとちょっとひと手間かかるっぽいんだけど、文字だけならそのままPOSTしちゃえばいいっぽいね。

タグ機能

とりあえず、メイン画面等の土台から作って後からユーザー機能を入れればいいんじゃないかなと思う。

作る上で、Qiita画面を見直したんだけど…

やっぱタグ機能も欲しいなぁ。

タグ機能を付けようと思ったんだけど、DBの設計が思ったより難しい。
結論だけ言えば、記事テーブルとタグテーブルを作って、その関係を管理するテーブルを作ることで大体のことはできるっぽい。
記事テーブルにタグカラムを入れてもいいと思ったんだけど、文字数の制限とか複数のタグの追加削除が難しくなるみたい。

ユーザー機能の追加

認証機能を追加するために、Paizaの講義通り最初はmake:authをしようとしたんだけど、Laravel6で廃止されていた。

周り回ってlaravel breezeというのが良いのではとなり、laravel breezeを入れた。
入れてみたはいいものの、フロントエンド知識が無く、npmらへんが全然わからない。

最終的に、Node.jsを入れてnpm run devして/login画面に行ってみたんだけど

こんな感じで恐らくCSSが適用されていない。

Viteを使ったCSS適用が出来ない問題の解決まで

breezeを入れる所まではいいんだけど、CSSが適用されていなかった。

デベロッパーツールで見たところ、net::ERR_CONNECTION_REFUSEDとなっているのがわかる。

情報、知識共に無かったのでChatGPTと共に解決したんだけど、結論から言えばVMのブリッジ接続バーチャルホストの問題
特に、私がVite(ヴィート)の仕組みを理解できていれば、すぐ問題は解決できたように思う。

前提知識

前提知識として、Viteはフロントエンド向けの開発環境を提供するもの。だから、フロントエンドの知識が少し必要になる。私はjavascriptとかを一切触ったことが無いので、まじで何もわかんなかったし、未だによくわからないところもある。

なぜViteが必要なのか?

今回の場合は、BreezeがViteを使うからというのが答えになるんだけど、一般的になぜViteが使われるのかという話。

詳しいことはViteの公式サイトに行って欲しいんだけど、Viteには恐らく3つの大きな役割がある。

  1. フロントエンドファイルの素早い反映
    ViteのHMRという機能を使うことでフロントエンドファイルの変更を素早く再読み込みなしでブラウザに反映できる。
    フロントエンドならではの有難さがあるよね
  2. ビルドやコンパイルの実施
    ビルドとかコンパイル系、なんなら古いブラウザでも表示できるようにとかもしてるみたいだね。
  3. 依存関係の解消
    javascriptとか色んな依存関係を自動解決してくれるみたい。

これらの役割を果たしたうえで、Viteは滅茶苦茶速いのが売りみたい。Viteはフランス語で素早いの意。


普通は沢山あるjavascriptを1つにまとめるバンドルという作業を行うらしいんだけど、それを行わないから滅茶苦茶速いみたい。

そして、これら上の条件を満たすために、Viteはファイルの変更がある度に動的に動作をしなきゃいけないよね。
そのために、Viteは開発用のサーバーを建てる。←ここが大事で、VMのブリッジ接続している際は考慮しなきゃいけない。
つまり、Viteはサーバーを建て、CSSとそのURLを生成し、Laravelはそれを取得しようとする

ブリッジ接続とは何か?

6日目の自分の記事を見るとわかるんだけど、ブリッジ接続についてあまり理解せずに進めている。今回、ブリッジ接続が大事になるので、ここら辺を理解する。

因みにここで出てくるIPは全てローカルIPのこと。

そもそも

そもそも、VM周りの接続方向を分類すると

  • ホストPC→VM
  • VM→ホストPC
  • VM→外部ネットワーク
  • 外部ネットワーク→VM

に分けられる。

練習用のサーバーを考えると、必要になってくるのは外部ネットワーク→VM以外の接続。むしろ、外部ネットワーク→VMが出来るとセキュリティ的にあまりよろしくない。
※正し、外部から接続されるのが完全に危険というわけではない。通常のサーバーは外部からアクセスされるものだし、適切なセキュリティがなされていれば問題は無い。

ブリッジ接続はどんな接続方式か

VMがホストPCの接続しているアダプタに直接接続し、ネットワークに接続する方法。
だから、上に挙げた接続方法全てのことができる。むしろ、外部ネットワーク→VM接続も出来てしまうので、適切なセキュリティをしていないと危ない。

ここで、アダプタとかの概念が出てきて分かんなくなると思うんだけど、絵で解説してくれている人がいるので、そちらがおすすめ

そんで、ブリッジ接続の場合、ホストPCがVMに接続する際はローカルネットワークを介して接続する。マインクラフトでLANに公開っていうの見たことない? 他にも同じWiFiに接続してたらマルチプレイ出来たりしたことってあるよね。あれと一緒。LAN=LocalAreaNetwork

つまり、VMとホストPCに直接的なネットワークの関係は無く、それぞれ独立している。ということは、VMにはIPアドレスが割り振られており、ホストPCがVMに接続する際はそのIPアドレスを元に接続する必要がある。

結論

これまでを踏まえた上で、つまりホストPCからVMにアクセスするにはVMのIPアドレスを元にローカルネットワークで接続する必要がある。

Viteはサーバーを建てて開発環境を整えるツールだった。LaravelはこのViteのサーバーからCSSやJSを取得する。しかし、ホストPCからVM内にあるViteに接続するには勿論IPアドレスが必要。
しかし、Viteが提示するURLは以下の通り127.0.0.1=localhost=自分自身を表すIPアドレスを含むURLだった。

だから、ホストPCはVM内のViteサーバーにアクセス出来ず、CSSを取得できなかったと。

つまり、サーバーをlocalhostでなく、VMに割り振られているIPアドレスで建てればいい。

これは、”アプリ名/vite.config.js”で出来る。

例えば私の場合はこうなる。

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
            ],
            refresh: true,
        }),
    ],
    server: {
        host: 'qtm.test',
        port: 8000,
      }
});

ここで、更にややこしいのがバーチャルホストという概念。
上のhostにVMのIPを入れてくれればいいんだけど、私はバーチャルホストというので同じIPでも複数のサーバーを建てられるようにしているので、qtm.testという名前になっている。
バーチャルホストは名前解決(DNS)をVM側でもする必要があった。

また、portは1025以上で被ってなければ良いみたい。1024以下は管理者権限が必要。
更に、指定したポートのファイアウォール開放が必要。

ここまでやってnpm run devを実行。
やっとCSSが適用できた。

VMのネットワーク設定を変える

VMのネットワークについて調べたらブリッジ接続があまり安全でないことが分かったので、違う接続方法に今のうちに変えておく。

具体的には、NATホストオンリーネットワークを利用し接続する。
方法はこちらのサイト様を参考にする。

これを更に説明すると、NATを使い、ホストPCを経由したネットワーク接続を可能にし、ホストオンリーアダプターを使い、ホストPCとの相互通信を可能にするというイメージ。
更に更に説明すると、NATだけではVMからネットワークに接続出来なく、ポートフォワーディングというリダイレクトの技術を使い接続を確立させる。

こうすると、外部からVMの接続は出来なくなり、VM⇔ホストPCと、VM→外部ネットワークの接続は出来るようになる。ホストPCからであればSSH接続もできる。

私の場合、”sudo nmtui”をしてもenp0s3しかなかったので、”nmcli con add type ethernet con-name enp0s8 ifname enp0s8”というコマンドで作成してから”sudo nmtui”で編集することで問題なく行けた。

よし、SSH接続できた。
ping google.comでpingも返ってきたので外部ネットワーク接続も問題なし。

私の場合はバーチャルホストの設定も更新しないとね。

“npm install && npm run dev”も実行して…

よっし! めっちゃ大変だったけど、ネットワーク設定もヨシ!

今まで外でVM関係弄るの控えてたけど、これでスマホのデザリングくらいだったら問題無くなったのでは!? 外でも作業できるのはかなり大きい。

スナップショットは絶対取っておこう

最近取ってなかったんだけど、絶対セーブはしておこう。
ただそれだけ。

テーブルの作成

今更感はあるけど、ようやく再開できる。

今、作るテーブルは以下の通り

  1. Usersテーブル
    初期からあるユーザーテーブルをそのまま使う。
  2. Articlesテーブル
    記事を管理するテーブル
  3. Tagsテーブル
    タグを管理するテーブル
  4. Article_Tagテーブル
    記事とタグの関係を保存するテーブル

一応、他にも初期からあるパスワードリセットテーブルとか、認証テーブルとかもあるんだけど、一旦放置。

Usersテーブル

public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name',32)->unique();
            $table->string('email',255)->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password',255);
            $table->rememberToken();
            $table->timestamps();
        });
    }
    public function down()
    {
        Schema::dropIfExists('users');
    }

stringとかtimestampは型みたいなもん。stringの場合は255文字の文字型に制限される。
string(“name”,32)の32は文字数制限。stringの時点で255文字に制限されるので、emailとpasswordの制限は正直要らないと思うけど、明示的にしとく意味も込めて。

->unique()はそれが一意であることを保証しようとするもの。同じ値が入ろうとするとエラーを出力する。

downはマイグレーションファイルのupで行ったことを巻き戻す役割。
今回の場合、0からテーブルを作るので、その逆であるテーブルの削除を記述。
正直、一人でやってるならdownは別に書かなくて良い感ある。

Articlesテーブル

    

    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger("user_id");
            $table->string('title');
            $table->longText('body');
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }
    public function down()
    {
        Schema::dropIfExists('articles');
    }

unsignedBigIntegerはidを保存するときと同じ型で、符号のない大きな数を保存できる。だから、外のテーブルIDを入れるときはこれを使う。
longTextは2^32くらいの膨大な量のテキストを保存できる型。ここにHTMLを保存する予定。

Textが2^16=65,536くらいなのに対して、longTextが2^32=42億。HTMLだと65000文字超えることもありそうだと思ったので一応longText。

Tagsテーブル

    public function up()
    {
        Schema::create('article_tag', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger("tag_id");
            $table->unsignedBigInteger("article_id");
            $table->timestamps();

            $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
            $table->foreign('article_id')->references('id')->on('articles')->onDelete('cascade');

            $table->unique(['article_id', 'tag_id']);
        });
    }
    public function down()
    {
        Schema::dropIfExists('article_tag');
    }

こちらはタグを保存するテーブル。
nameは一意であってほしいので、uniqueを付けている。

Article_Tagテーブル

    public function up()
    {
        Schema::create('likes', function (Blueprint $table) {
            $table->id();
            $table->foreignId("user_id")->constrained()->onDelete("cascade");
            $table->foreignId("article_id")->constrained()->onDelete("cascade");
            $table->timestamps();
        });
    }
    public function down()
    {
        Schema::dropIfExists('likes');
    }

tag_idとarticle_idの関係を保存する感じ。こうすれば、tagで記事検索もできるし、記事でtag検索もできる。
いわゆる、中間テーブルというもので、単数形_単数形の形で左の単数形が辞書順で早いみたいな名づけ制約があるっぽい。
この命名規則に従わないと、Laravelが自動的に関係を推測してくれなかったりする。

多対多のリレーションについてこちらの和訳ドキュメントを。

“migrate:fresh”という全てのテーブルを削除して、1からマイグレーションを行うコマンドを打って完了。adminのseeder作ってないと、アドミン系が面倒になるので注意。

現在のテーブルはこんな感じ。

コントローラ、ビューらへんを作っていく。

記事一覧とそれに関係するタグを取得するコントローラ

public function index()
    {
        $article = Article::with("tags")->get();
        return view("article.index",["article"=>$article]);
    }

これはArticleControllerのindexメソッド。
Articleモデルで

{
    public function tags()
    {
        return $this->belongsToMany(Tag::class, "article_tag", "article_id", "tag_id");
    }
}

みたいなbelongToMany関係を指定しているので、withメソッドで1回で取得できる。

これにはメリットがあって、1回のDBアクセスで良くなるので負担が減るみたい。

記事作成ページの作成

なんか公式チュートリアルのコードをそのまま持ってきたら新しい記事作成ページは作れた。正しく動作するか、正しく保存できるかは別。

Bootstrap VS Vite

ログインしているかどうかが大事なので、Breezeのナビゲーションバーを付けたんだけど、BootstrapのCSSとViteのCSSが衝突してスゴイ見にくい。

白地に白文字になっちゃってる。

認証機能のCSSをviteがやってくれるので、Bootstrapからviteに乗り換えかなぁという感じではある。
でも、フロントエンドの知識が必要になってきそうなんだよね。まじでどうしよう。

storeコントローラ

 

public function store(Request $request)
    {
        $article=new Article();
        $user = \Auth::user();
        $article->title = $request->title;
        $article->body = $request->body;
        $article->user_id = $user->id;
        $article->save();
        return redirect()->route("articles.index");
    }

保存・表示は出来たけど、書式が適用されない

見出しとかはいけてるっぽい? 箇条書きが消えてる。
知識ないからわかんないんだけど、恐らくCSSの問題な気がする。

一回viteを切ってみる。

うん。認証系のCSSは崩れたけど、書式は適用されている。

つまり、アカウント認証のナビゲーションバーのCSSでviteは欲しいんだけど、viteを付けるとこの書式とBootstrapと喧嘩してしまうと……むずいな。
いっそフロントエンドも若干学んで、viteを弄っちゃうのが一番良い気がする。

因みに、HTMLデータは以下のように保存されている。

だから、最悪この<li>系のCSSだけ強制的に変えることが出来れば問題はない。

CSSをviteで上書き

ChatGPTに聞いたところ、以下のようにresources/css/app.cssを編集すれば、上書きで適用できるよ! と言われた。

@tailwind base;
@tailwind components;

/* li要素に対するスタイル */
li {
    list-style-position: inside; /* リストマーカーを内側に表示 */
}

/* ol要素に対するスタイル */
ol {
    list-style-type: decimal; /* 数字でリストを表示 */
    padding-left: 1em; /* 左側にパディングを追加 */
}

/* ul要素に対するスタイル */
ul {
    list-style-type: disc; /* 黒丸でリストを表示 */
    padding-left: 1em; /* 左側にパディングを追加 */
}

@tailwind utilities;

とりあえず従ってみると。

これ、出来てますねぇ。

なるほど、こんな感じで自分でCSSの上書きもできるのね。

Tailwind CSSについて

このviteの中で動いている? CSSがTailwindというもので、フロントエンドの知識になってくるんだけど、知ってると良さげなので理解していく。

コンポーネントとユーティリティクラスって何?

調べてると出てくるのがコンポーネントとユーティリティクラスという単語。
どっちも知らないので理解する。

コンポーネント

こっちはBootstrap等のCSSフレームワークで使われる方式。

例えば、ボタンであればボタンのclassにclass=”btn”とすることで、事前にBootstrapで用意しているボタン系のCSSのかたまりが適用される。という感じ。

<button class=”btn”>ボタン</button>

ユーティリティクラス

こっちはTailwind CSS等で使われる方式。


こっちもclass=で色々指定するんだけど、更に細かい要素で何個も指定するイメージ。
例えば、ボタンなら
class=”bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded”
のようにすることで、
ボタンの背景色、ホバーしたときの色、テキスト色、太字、上下空白、左右空白、角丸
のように細かく指定できる。

<button class=”bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded”> ボタン</button>

Tailwindはユーティリティクラスしか使えないのか?

となると、毎回class=であの長ったらしいのを書かなきゃいけないように思えちゃうんだけど、そんなことはなくてユーティリティクラスを使ったコンポーネントを作って使いまわすこともできるらしい。

便利やね。

@tailwind base;/@tailwind components;/@tailwind utilities;とは何か

とは何か、といってもここまで来るとほぼほぼ理解できる。

@tailwind components;はコンポーネントを使えるようにするもので、@tailwind utilities;はユーティリティクラスを使えるようにするもの。

@tailwind base;はちょっとフロントエンドの知識がもう少し必要っぽくて、ブラウザ毎のCSSの癖? ブラウザデフォルトのCSS? を無効化したり色々してるみたい。とりあえず付けとく。

CSSを調整したい

今までのを理解した上で、私はどうすればいいかというと、Bootstrapを卒業して、Tailwindの方でCSSを少し書けばいいね。

でも、先に要素を作ってからCSSの調整をした方が後々も楽だと思うので、とりあえずタグ機能とか、いいね機能を作る。

マスアサインメントエラー

タグを保存する段階でマスアサインメントエラーというのに遭遇したので。

具体的には、以下のようなStoreメソッドで発生。

    

public function store(Request $request)
    {
        $article=new Article();
        $user = \Auth::user();
        $article->title = $request->title;
        $article->body = $request->body;
        $article->user_id = $user->id;
        $article->save();
        // 個数制限や同一タグの重複を考慮していない
        $tagNames = explode(",", $request->tags);
        foreach($tagNames as $tagName){
            $tagName = trim($tagName);
            $tag = Tag::firstOrCreate(["name"=>$tagName]);  //ここで発生
            $article->tags()->attach($tag);
        }
        return redirect()->route("articles.index");
    }

マスアサインメントとは

そもそもmass assignmentってなんだぁって感じ。
詳しくは公式ドキュメントを読んでいただきたいが、自分の理解の為にも簡易的に説明する。

マスアサインメントとは、DBにデータを一括で適用、保存すること。

普通、項目ごとに$user->name = $request->name;みたいな感じで一つずつ保存するけど、そうじゃなく、一括で保存する感じ。

これが脆弱性につながるので、初期状態ではできないようになっている。

なぜこれが脆弱性につながるのか

例えば、以下のようなUserテーブルがあるとする。

  • ユーザー名
  • メールアドレス
  • 権限の種類

これら3つの項目があって、ユーザー登録画面ではユーザー名とメールアドレスだけが設定でき、権限の種類は何も登録してなければデフォルトで一般ユーザー、adminと追加すればアドミンユーザーになるとする。

ここで、マスアサインメント方式でデータを保存すると以下のようなコードになる

$user = new User();
$user->fill($request->all());
$user->save();

逆に、通常であれば

$user = new User()
$user->name = $request->name;
$user->email = $request->email;
$user->save();

どちらも問題なさそうに見えるけど、この$userには権限を扱う要素authも裏に存在する。
ここで、悪意を持ったユーザーが開発者ツールなどを利用し、ユーザー登録フォームの送信にname=”auth” value=”admin”を追加したらどうだろうか。

通常の一つずつ指定する方式であればnameとemailしか適用されないが、マスアサインメント方式だと、auth=adminで保存されてしまう。

これがマスアサインメントの脆弱性らしい。

なぜ$tag = Tag::firstOrCreate([“name”=>$tagName]);がマスアサインメントなのか

理由は、firstOrCreateメソッドが内部でfill()コマンドを利用しているから。
今回の場合、保存するのは”name”属性1つだけだからややこしいんだけど、firstOrCreate自体、内部でfillを使っているので個数関係なくマスアサインメント判定になる。

だから、該当する要素を

class Tag extends Model {
    protected $fillable = [“name”];
}

のように一括代入を許可すれば問題ない。
でも、さっきの脆弱性の通り、auth等の意図せず弄られたらまずいものや、そもそもマスアサインメントを使う必要が無さそうなら使わないのが一番なのかなと思う。

タグの指定、表示までできた

一番下の「タグテスト」が成功したやつ。

こんな感じでタグを指定した。

GithubCopilotが優秀すぎて勉強にならない

別に今始まったことではないんだけど、AIの予測が需要に合ったもの過ぎて勉強にならない。

今は、記事の検索窓のコントローラを作っているところ。具体的には、検索キーワードからタグとタイトルで引っかかったものだけを表示させたい。

以下の予測コードをみてほしい。

凄くないかこれ。
まだ私はこのコードを理解できていないんだけど、恐らく合っている。

今まで、仕組みの理解→実装だったのが、AIによる実装→仕組みの理解という順序になることも増えてきたように思う。
本当に現在進行形で時代が変わっているんだなと感じる。

未だにこのAIに頼る勉強法が良いのか悪いのかはわからない。

過去に試しに理解しないでChatGPTに任せっぱなしでiOSアプリを作ったことがあるんだけど、途中でエラーが取り除けなくなって詰んだことがある。

それ以降、コードは出来る限り全て理解するようにしたり、自分でコードを書いてアウトプットするようにしてるんだけど、やっぱ頼ってしまう。

検索機能の完成

検索ワードを元に、タイトルまたはタグに一致するものが出てくる。

「テスト」で調べると以下の感じ

いいね機能を追加する

これはよほど大きいサイトにならない限りはユーザーidと記事idを結ぶlikesテーブルを作れば問題は無さそう。

マイグレーションファイル

マイグレーションファイルは以下の通りにしてみた

    public function up()
    {
        Schema::create('likes', function (Blueprint $table) {
            $table->id();
            $table->foreignId("user_id")->constrained()->onDelete("cascade");
            $table->foreignId("article_id")->constrained()->onDelete("cascade");
            $table->timestamps();
        });
    }
    public function down()
    {
        Schema::dropIfExists('likes');
    }

特に変わった点は無いと思うんだけど、唯一あるなら外部キーとの関係の書き方をforeignIDの方式に変えた。こっちの方が短く書けるのでいいかなと思い。

各モデルへ関係の記述

これはArticleとUserの中間テーブルとなるので、その関係を記述する。

Articleモデルの方であるArticle.phpは以下の通り

    public function likedUsers()
    {
        return $this->belongsToMany(User::class,”likes”);
    }

UserモデルのUser.phpは以下の通り

    public function likedArticles()
    {
        return $this->belongsToMany(Article::class,”likes”);
    }

いいねボタンをshow viewに追加してみる

これ、問題が1つあって、いいねボタン押したときに連動してボタンの見た目を変えることができない。
JSとかフロントエンド系の知識が無いとそこらへんを作れないんだよね。

だから、今回はいいねボタンを押すと画面が強制リロードされる仕組みでごり押す。

超地味だけど、いいね機能搭載できた。

いいねを押すといいね済みになって、いいね済みを押すといいねになる。
わかりにくいなこれ。

中ではユーザーの識別をしていて、ユーザー登録した人しか押せないし、1記事に1回しか押せないようになっている。

合計いいね数の表示

合計いいね数の表示もできた。

実装方法

今回はArtcileコントローラにいいねを集めるコードを入れて、そのままViewに送る方式にした。

いいねを集めるコードは以下のようなもの

$article->likedUsers->count();

このlikedUserメソッドは以下のようなもので、belongsToManyのリレーションを設定している。

    public function likedUsers()
    {
        return $this->belongsToMany(User::class,”likes”);
    }

count()はクエリビルダといわれるもので、LaravelでSQLのよく使う動きとかを再現できるというものらしい。
詳しくは日本語ドキュメント

この場合は取り出したDBの要素の数を数えてくれる。likedUsersではその記事にいいねしている人の要素を取り出してくれるものだから=いいね数となる。
いいね数0の時はcount()はしっかり0を返してくれるみたい。

これを以下のようにarticleのlikeCountという要素に入れる。

$article->likeCount = $article->likedUsers->count();

これは、ArticleにlikeCountカラムを追加したりしてるわけじゃなく、一時的に追加してるみたいな感じ。
こうすればViewにarticlesを渡してもlikesCountが個々のarticleと紐づいて取得できる。

編集画面を作成する

最後に、記事の削除、編集、タグの追加・削除を出来るようにする。

これ、記事の編集が出来ちゃうと、いいね数を集めてから全然別の記事にしちゃうみたいな問題があるんだけど、今回は知らないふりをして作る。

とりあえず、articles/edit/{id}みたいなルーティングを作って、editとupdateメソッドにルーティングさせる。

editメソッドは以下の通り

public function edit($id)
    {
        $article = Article::find($id);
        $tagsNameString = $article->tags->pluck('name')->implode(',');
        return view("article.edit",["article"=>$article,"tagsNameString"=>$tagsNameString]);
    }

該当のid記事の取得と、該当の記事に紐づいているタグをStringのカンマ区切りで取得している。

tagsはarticleモデルのメソッド

    public function tags()
    {
        return $this->belongsToMany(Tag::class, “article_tag”, “article_id”, “tag_id”);
    }

そんで、pluck(“name”)を指定することで、nameだけのコレクションを受け取れる。
implodeでそのコレクションを,区切りで文字列化してくれる。

コレクションというのはLaravelでDBからデータを取得する際の型で、こんな感じでいろんなメソッドがある。

これでeditViewにデータを送って、editViewからはupdateメソッドに新しくなったデータを送ってもらう。

updateメソッドは以下の通り

public function update(Request $request, $id)
    {
        DB::transaction(function () use ($request, $id) {
            // articleDBの処理
            $article = Article::find($id);
            $article->title = $request->title;
            $article->body = $request->body;
            $article->save();
            // tagDBの処理
            $article->tags()->detach();
            $tagNames = explode(",",$request->tags);
            foreach ($tagNames as $tagName) {
                $tagName = trim($tagName);
                $tag = Tag::firstOrCreate(["name"=>$tagName]);
                $article->tags()->attach($tag);
            }
        });
    return redirect()->route("articles.index");
    }

Articleはデータの上書きをしてしまえばいいんだけど、Tagは関係を保存することで処理しているので上書きができない。
だから、一回全ての関係を削除してから新しい関係を付けるという処理をしている。

また、detachで関係を削除してからエラーが起きて処理が中断したりするとtagのデータが無くなってしまうので、トランザクション処理をしている。DB::transactionがそれ。

削除機能の追加

Route::delete(“/article/{id}”, [ArticleController::class, “destroy”])->name(“article.destroy”);

みたいなルーティングをして、destroyメソッドを書く。

public function destroy($id)
    {
        $article = Article::find($id);
        $article->delete();
        return redirect("/articles");
    }

そして、formでDELETEを送ると

<form method="post" action="{{ route('article.destroy',$article->id) }}">
        @csrf
        @method('DELETE')
        <button type="submit" class='btn btn-outline-primary'>記事の削除</button>
</form>

削除機能の完成!

おわりに

ちょっと終わるタイミングを完全に見失っちゃって、めっちゃ長くなった。
とりあえず6日? くらいで基本的なCRUDと画面の基盤まで作成した。

あとやりたいのはこんな感じ
上から優先度高

  • ユーザー認証を行い、その人だけ編集、削除できるように
  • POST系のバリデーション
  • しっかりモデルでビジネスロジックを動かしているか
  • コードの見やすさ(ルートをまとめたり)
  • トランザクション処理
  • CSSやJSなどの見た目系

更に先を言えば、AWSとかと契約して誰でも使えるようにとかしてみたいんだけど、更に一週間くらいかかりそう。

それと、今月のWebアプリ制作の精進時間が100時間を超えた。
PHPをしっかり学び始めたのが7月7日なので、大体1ヵ月くらい経った。

これからもしっかり精進していきたいところ。

とりあえずおやすみなさい。