【VSCode】Laravelアプリ 「TwitterModoki」手動開発

Laravel

1. 雛形作成とローカル起動

 コマンドプロンプトやターミナルを立ち上げ、Webサーバーの公開ディレクトリ(htdocs など)へ移動してからプロジェクトを作成し、VS Codeで開いてローカル開発環境を立ち上げる。

BAT (Batchfile)
# ① XAMPP等のWebサーバー公開ディレクトリ「htdocs」に移動
cd C:\xampp\htdocs
# (※環境に応じて /var/www/html や、自身の作業用ディレクトリのパスに置き換える)

# ② Composerを使用し、最新のLaravelプロジェクト「TwitterModoki」を新規作成
C:\xampp\htdocs> composer create-project "laravel/laravel=12.*" TwitterModoki

# ③ 作成されたプロジェクトのフォルダ内へ移動
C:\xampp\htdocs> cd TwitterModoki

# ④ 現在のフォルダを対象にしてVS Codeを起動(コマンドからエディタを開く)
C:\xampp\htdocs\TwitterModoki> code .

# ⑤ PHPバージョンエラー対策
C:\xampp\htdocs\laravel-app> composer config platform.php 8.2.12
C:\xampp\htdocs\laravel-app> composer --no-plugins update --no-scripts

※以降は、VSCode内のターミナル画面として説明
  • 確認事項: 確認事項:
     XAMPPのコントロールパネルから「Apache(およびMySQL)」を起動する。
     その後、ブラウザで http://localhost/TwitterModoki/public にアクセスし、Laravelの標準Welcome画面が表示されれば正常。

2. データベースの準備とMVCコンポーネントの一括生成

1.phpMyAdminでデータベースtwitter_modokiを作成

2.1で作成したデータベースにアクセスする為に.envファイル編集

Plaintext
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=twitter_modoki
DB_USERNAME=root
DB_PASSWORD=

3.データを扱う「Model」、テーブルを定義する「Migration」、制御ロジックを担う「Controller」を一挙に作成する。

BAT (Batchfile)
# Model、Migration、Controllerをワンコマンドで同時生成
C:\xampp\htdocs\TwitterModoki> php artisan make:model Post -mc
  • 生成されるファイルとパス:
    • Model: app/Models/Post.php
    • Migration: database/migrations/xxxx_xx_xx_xxxxxx_create_posts_table.php
    • Controller: app/Http/Controllers/PostController.php

💾 マイグレーション設定(テーブル定義)

生成された database/migrations/xxxx_xx_xx_xxxxxx_create_posts_table.php を開き、ツイートの本文を保存するためのカラム(列)を定義する。

PHP
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();                  // 自動連番のプライマリキー(id)
            $table->string('content', 140);// 💡 追加:ツイート本文(VARCHAR型、最大140文字制限)
            $table->timestamps();          // created_at(作成日時)と updated_at(更新日時)の自動生成
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

ファイルの編集・保存が完了したら、以下のコマンドを実行してデータベースに反映する。

PowerShell
# マイグレーションを実行し、データベースへ実際にテーブルを構築
C:\xampp\htdocs\TwitterModoki> php artisan migrate

💾 モデル設定(大量代入・一括保存の許可)

Laravelの安全機能(Mass Assignment 保護)に対応するため、コントローラーから Post::create() を使って一括で値を書き込めるよう、セーフリスト(保存を許可するカラム)を指定する。

app/Models/Post.php

PHP
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    // 💡 外部からのデータ書き込み・一括保存を許可するカラムを明示
    protected $fillable = ['content'];
}
  • 注意: これを設定し忘れると、画面からフォームを送信した際にエラーは出ないが、データベースの content カラムが黙って空(または弾かれる)状態になる。

3. ルーティングの定義

「特定のURLに特定の形式(GETやPOST)でアクセスが来たら、コントローラーのどのメソッドを動かすか」というルート対応表を定義する。

routes/web.php

PHP
<?php

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

// ① タイムライン表示(トップページへのGETアクセス)
Route::get('/', [PostController::class, 'index'])->name('posts.index');

// ② ツイート送信(/posts へのフォームPOSTリクエスト)
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
  • 補足: 末尾に ->name('名前') をつけておくことで、ビュー側から route('posts.index') のように名前でURLを逆引きできるようになり、将来的にURLを変更してもコードの書き換えが不要になる。

4. コントローラーの実装(ビジネスロジック)

データの「取得」と「バリデーション・保存」を行う具体的な中身(ロジック)を記述する。

app/Http/Controllers/PostController.php

PHP
<?php

namespace App\Http\Controllers;

use App\Models\Post; // 💡 データベース操作のためにPostモデルをインポート
use Illuminate\Http\Request; // 💡 フォームデータを受け取るためにRequestクラスをインポート

class PostController extends Controller
{
    /**
     * タイムライン(一覧表示)
     */
    public function index()
    {
        // データベースから最新の投稿順(created_atの降順)で全件取得
        $posts = Post::latest()->get();
        
        // resources/views/posts/index.blade.php を呼び出し、取得した変数 $posts を渡す
        return view('posts.index', compact('posts'));
    }

    /**
     * ツイートの保存処理
     */
    public function store(Request $request)
    {
        // ① 入力データのバリデーション(チェック)
        // 条件(必須入力、かつ最大140文字)を満たさない場合は、自動でエラーメッセージを持ち前の画面に戻る
        $request->validate([
            'content' => 'required|max:140',
        ]);

        // ② データの保存を実行
        Post::create([
            'content' => $request->content, // フォームの name="content" から文字列を抽出して格納
        ]);

        // ③ 処理完了後、タイムライン画面(トップページ)へリダイレクト
        return redirect()->route('posts.index');
    }
}

5. ビュー(UI)の作成と配置

画面(ビュー)ファイルを作成・配置し、HTMLとLaravel専用の構文(Blade)を使って画面を構築する。

  1. 専用フォルダの作成resources/views/ の直下に、posts という名前の新しいフォルダを手動で作成する。
    • パス: resources/views/posts/
  2. ファイルの作成 新しく作った posts フォルダの中に、index.blade.php という名前のファイルを新規作成する。
    • パス: resources/views/posts/index.blade.php
    • 注意: Laravelで動的な命令(ループやエラー表示など)を使うため、拡張子は必ず .blade.php とすること。

💾 ビューのコード実装

作成した resources/views/posts/index.blade.php に以下のコードを記述する。

HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>TwitterModoki</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 font-sans">
    <div class="max-w-xl mx-auto my-10 bg-white shadow-md rounded-lg p-6">
        <h1 class="text-2xl font-bold text-blue-500 mb-6">TwitterModoki</h1>

        <form action="{{ route('posts.store') }}" method="POST" class="mb-8">
            @csrf
            
            <textarea name="content" rows="3" class="w-full p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="いまどうしてる?"></textarea>
            
            @error('content')
                <p class="text-red-500 text-sm mt-1">{{ $message }}</p>
            @enderror
            
            <div class="text-right mt-2">
                <button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full">ツイートする</button>
            </div>
        </form>

        <div class="space-y-4 border-t pt-6">
            @foreach ($posts as $post)
                <div class="p-4 bg-gray-50 rounded-lg border">
                    <p class="text-gray-800">{{ $post->content }}</p>
                    <span class="text-xs text-gray-400 block mt-2">{{ $post->created_at->format('Y/m/d H:i') }}</span>
                </div>
            @endforeach
        </div>
    </div>
</body>
</html>
  • 紐付けのルール: コントローラー側で指定した return view('posts.index'); という記述は、resources/views/ の中にある posts フォルダの index.blade.php を読み込む」 という命令になる。Laravelではフォルダの区切りを / ではなく .(ドット)で表現する。

Nodeモジュールのインストール

1. パッケージのインストール

ターミナルで以下のコマンドを実行し、package.json に記載されている依存パッケージ(Viteを含む)をすべてインストールします。

PowerShell
PS C:\xampp\htdocs\TwitterModoki> npm install

※完了までに数十秒〜数分かかることがあります。

2.アセットをビルド

デザインにCDN経由の Tailwind CSS(<script src="https://cdn.tailwindcss.com"></script>)を使用されていためビルドします。

PowerShell
PS C:\xampp\htdocs\TwitterModoki> npm run build

3.自動テスト(Featureテスト)の実装と環境整備

機能が正常に動くか、バリデーションが正しく効くかを検証する自動テスト環境を構築する。XAMPP環境(MySQL)でテスト用DBを分離して安全に実行する手順。

📁 1. テスト専用データベースの作成(初回のみ)

テスト実行時に開発用データを破壊しないよう、テスト専用の空データベースを用意する。

  1. ブラウザで http://localhost/phpmyadmin/ を開く。
  2. 新しいデータベースとして twitter_modoki_test を作成する。

📁 2. テスト環境設定の修正(MySQL用)

プロジェクトルート直下にある phpunit.xml を開き、<php> タグ内を以下のように修正してテスト時の接続先を固定する。

XML
<php>
    <env name="DB_CONNECTION" value="mysql"/>
    <env name="DB_DATABASE" value="twitter_modoki_test"/>
    <env name="DB_USERNAME" value="root"/>
    <env name="DB_PASSWORD" value=""/>
</php>

⚠️ 注意: 修正後、設定が反映されない場合は必ず php artisan config:clear を実行してキャッシュを飛ばすこと。

📁 3. テストファイルの生成とインポートの修正

VS Codeのターミナルで以下のArtisanコマンドを実行し、テストファイルを生成する。

PowerShell
PS C:\xampp\htdocs\TwitterModoki> php artisan make:test PostTest

生成された tests/Feature/PostTest.php を開き、以下のように実装する。

  • use App\Models\Post; のインポート漏れに注意すること。
  • username カラムのNOT NULL制約(エラーコード1364対策)を回避するため、テストデータ生成時およびPOST送信時には必ず username も含めること。
PHP
<?php

namespace Tests\Feature;

use App\Models\Post; // 💡 必須:モデルのインポート漏れ注意
use Illuminate\Foundation\Testing\RefreshDatabase; // 💡 各テスト実行時にDBをリセットする設定
use Tests\TestCase;

class PostTest extends TestCase
{
    use RefreshDatabase; // テストごとにマイグレーションを実行し、データをクリーンにする

    /**
     * 1. タイムライン画面が正常に表示されるかテスト
     */
    public function test_timeline_page_displays_correctly(): void
    {
        // 💡 username を含めてダミーデータをDBに1件作成
        Post::create([
            'content' => 'テスト投稿です。',
            'username' => 'test_user'
        ]);

        // トップページにGETリクエストを送信
        $response = $this->get(route('posts.index'));

        // ステータスコードが200(成功)であることを確認
        $response->assertStatus(200);

        // 画面内に指定の文字列が含まれているか確認
        $response->assertSee('TwitterModoki');
        $response->assertSee('テスト投稿です。');
    }

    /**
     * 2. 正当なツイートが投稿(保存)できるかテスト
     */
    public function test_user_can_post_a_valid_tweet(): void
    {
        // 💡 送信データに username を含めてPOSTリクエストを送信
        $response = $this->post(route('posts.store'), [
            'content' => 'Hello Laravel Test!',
            'username' => 'test_user'
        ]);

        // データベースに該当データが保存されているか検証
        $this->assertDatabaseHas('posts', [
            'content' => 'Hello Laravel Test!'
        ]);

        // 処理後にトップページへリダイレクトされるか検証
        $response->assertRedirect(route('posts.index'));
    }

    /**
     * 3. 空白投稿のバリデーションが効くかテスト
     */
    public function test_content_is_required_to_post(): void
    {
        // contentを空の状態で送信
        $response = $this->post(route('posts.store'), [
            'content' => ''
        ]);

        // セッションで'content'カラムにエラーがあるか検証
        $response->assertSessionHasErrors(['content']);

        // DBにデータが増えていない(レコード数が0件)ことを確認
        $this->assertCount(0, Post::all());
    }

    /**
     * 4. 140文字超えの投稿のバリデーションが効くかテスト
     */
    public function test_content_cannot_exceed_140_characters(): void
    {
        // 141文字の文字列を生成して送信
        $longString = str_repeat('', 141);

        $response = $this->post(route('posts.store'), [
            'content' => $longString
        ]);

        $response->assertSessionHasErrors(['content']);
        $this->assertCount(0, Post::all());
    }
}

📁 4. 既存の ExampleTest.php の修正

初期状態から存在する tests/Feature/ExampleTest.php は、トップページ(/)への疎通確認テストである。
本編でトップページがDB(postsテーブル)を参照するように変更したため、こちらもDB初期化用のトレイトを追加しないと500エラーで共倒れする。

tests/Feature/ExampleTest.php を開き、以下のように修正する。

PHP
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase; // 💡 追加
use Tests\TestCase;

class ExampleTest extends TestCase
{
    use RefreshDatabase; // 💡 追加(これでこのテスト時もテーブルが自動生成される)

    public function test_the_application_returns_a_successful_response(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

🏃‍♂️ 5. キャッシュクリアとテストの実行

設定ファイルの変更を確実に適用させ、すべてのテストを一斉に実行する。

PowerShell
# ① 設定キャッシュを一回クリアする
PS C:\xampp\htdocs\TwitterModoki> php artisan config:clear

# ② テストを実行
PS C:\xampp\htdocs\TwitterModoki> php artisan test

PASS  Tests\Unit\ExampleTest
✓ that true is true

PASS  Tests\Feature\ExampleTest
✓ the application returns a successful response  0.46s  

PASS  Tests\Feature\PostTest
✓ timeline page displays correctly     0.01s  
✓ user can post a valid tweet          0.04s  
✓ content is required to post          0.01s  
✓ content cannot exceed 140 characters 0.03s  

Tests:    6 passed (14 assertions)
Duration: 0.66s
  • 成功時の表示: PASS が並び、最終行に Tests: 6 passed緑色の文字で出力されれば、テスト環境の分離とロジックの担保がすべて完了。

4.動作確認

次のURLにアクセスする。http://localhost/TwitterModoki/public/

何か記入してツイートする


📌 備忘録:開発時セルフチェック要点

本番サーバー(Alias運用)への対応:
 自宅サーバーの環境のように、ポートフォリオ用の同一サブドメイン配下で /twitter などの別ディレクトリとして動かす場合は、本番デプロイ時に public/.htaccessRewriteEngine On の直下に RewriteBase /twitter/ を追記することを忘れない。

419 Page Expired エラー:
 フォームを送信してこの画面が出たら、Bladeの <form> 内に @csrf が書き漏れていないかを即座に確認する。

保存されない問題:
 投稿ボタンを押してもDBに保存されない場合は、Post.phpprotected $fillable = ['content']; の綴りや記述を真っ先に確認する。

コメント