Blazor から Laravel の Web API 呼び出しの組み合わせ

実はもうひとつ梃入れしないといけないのが「PHP&Laravelで作るWeb API開発入門」https://amzn.asia/d/05D3tq44 だったりします。

基本 .NET な人が何故に PHP/Laravel なのか?ってのもありますが、新人研修で Vue/Nuxt と PHP/Laravel を使っていてですね、サーバーサイドの Web API 実装本がないので、教科書代わりに書いた。というのが真相です。サーバーサイドだから Web API のサンプルとして ASP.NET Core でもいいのではないか、という気もしますし、暫くはサーバーサイドの実験は .NET 系でやっていました(新人教育では、Vue/Nuxt のフロントエンドでおしまいになることが多いので)、ただ、HTTP プロトコルの細かいところやら認証やセキュリティまわりの解説となると実際の PHP/Laravel の実装が欲しかったのです。

実は、同時期に「API Platformを活用したPHPによる本格的なWeb API開発」 https://amzn.asia/d/09ZdjoNj が出版されたので、被らなくてよかった…と思いつつ、フレームワークが API Platform と Laravel と異なるので、実装の違いが良く分かるようになる…かもしれません。
Web API の本は、設計やテストの本はあるのですが、実際のフレームワーク前提の実装本がないのでちょっと苦労していたのですが(Laravel 本だとフロントエンドの話が多いので、Web API のバックエンドの話がちょっぴりしかないので)…果たして。

そんな訳で実装例が github https://github.com/moonmile/webapi-sample にあるので、活用してくださいという形で置いてあります。書籍を買う買わないは別として、laravel-webapi として「回転寿司注文サイト」を作るための web api を揃えてあります。

クライアントのほうは、React/React Native などで作ってありますが、他のプログラム言語/フレームワークでも十分可能です。といいますか、ひとまず、Docker で laravel-webapi を動かしておいて、別途クライアントを自由に作る、という開発環境になっています。docker-compose.yml を用意してあるので、windows の Docker でも動作が可能です。

クライアントサンプルとして order-tablet を動作させると、こんな画面がでます。

これは、Next.js で作っているのですが、当然のことながら Web API さえ使えればクライアントの種類は問いません。そのあたりは、ローカルPCにサーバーサイドの Docker を動かして、自由にクライアントを作ってみようという趣旨なわけですが…まあ、ちょっと amaqzon の評がなくて苦戦中ですね。いや、同時期の API Platform 本のほうも苦戦中?っぽいので、Web API 限定というのが地味なのかもしれません。

私としては MVC パターンとか、SSR(サーバーサイドレンダリング)なんかは、フロントエンドとバックエンドと同じプログラム言語で書けるので楽だと思うのですが、フロントエンドとしての JavaScript/TypeScript 一強な感じを見るとですね、じゃあ、バックエンドも Node.js で TypeScript で書くのがベストなのか?とも考えてしまうわけです。ただし、実運用となると、Java, C#, PHP などなどとバックエンドの選択肢があるのが事実なので。まあ、住み分けでもいいかもしれません。

じゃあ、Blazor 版でクライアントを作ってみる

サーバーサイドの Web API が用意されているのだから、クライアントは何でもいいですよね、というのが実情です。それこそ OpenAPI 仕様で決まっていれば、言語やフレームワークを問いません。

トップページ

React や Vue の場合はデザイナを使って、きれいな画面をデザインできるのですが、果たして Blazor の場合はどうなのか? これは、Blazor のプロジェクトでは Bootstrap を使うようになっているので、AI エージェントを使って Bootstrap 形式で書いて貰うと綺麗なものができます。

App.razor の中で bootstrap を指定しているので、この部分を切り替えれば Tailwind のほうも組みこめるはずです。

@page "/top"
@rendermode InteractiveServer
@inject NavigationManager Navigation

<PageTitle>お寿司注文システム</PageTitle>

<div class="min-vh-100 bg-light d-flex align-items-center justify-content-center p-4">
    <div class="bg-white rounded-4 shadow-lg p-5 text-center" style="max-width: 28rem; width: 100%;">
        <div class="mb-4">
            <h1 class="display-4 text-danger mb-2">🍣</h1>
            <h2 class="fs-4 fw-bold mb-2">お寿司注文システム</h2>
            <p class="text-secondary">ようこそ!ご着席ありがとうございます</p>
        </div>

        <div class="mb-4">
            <label class="form-label fw-semibold fs-5 mb-3">
                ご利用人数を選択してください
            </label>
            <div class="d-flex align-items-center justify-content-center gap-3">
                <button class="btn btn-outline-secondary btn-lg" @onclick="Decrement">
                    −
                </button>
                <div class="bg-light rounded d-flex align-items-center justify-content-center"
                     style="width: 5rem; height: 4rem;">
                    <span class="fs-2 fw-bold">@guests</span>
                </div>
                <button class="btn btn-outline-secondary btn-lg" @onclick="Increment">
                    +
                </button>
            </div>
            <p class="text-muted small mt-2">最大8名様まで</p>
        </div>

        <button class="btn btn-primary btn-lg w-100" @onclick="startOrder">
            注文開始
        </button>

        <div class="mt-4 small text-muted">
            <p>テーブル番号: <span class="fw-semibold">T-001</span></p>
        </div>
    </div>
</div>

@code {
    private int guests = 2;

    private void Increment()
    {
        System.Diagnostics.Debug.WriteLine("click Increment");
        if (guests < 8) guests++;
    }

    private void Decrement()
    {
        System.Diagnostics.Debug.WriteLine("click Decrement");
        if (guests > 1) guests--;
    }

    private void startOrder()
    {
        System.Diagnostics.Debug.WriteLine("click startOrder");
        Navigation.NavigateTo("/categories");
    }
}

これを動作させると、トップ画面を表示できます。

左側のナビゲーションをトップに持っていくのは後にして、テーブル人数を指定して「注文開始」ボタンをクリックすると、カテゴリ一覧が表示されるところまで可能です。

デザインが一緒なのは、React で作った Tailwind のデザインを Claude Code を使って Bootstrap 形式に直してもらっているからです。このあたり、UI デザインを AI エージェントを使って模索する https://www.moonmile.net/blog/archives/12182 で諸々試していたのですが、部品だけ用意してデザインを AI エージェントに直して貰う、という方法が「デザインセンスのない私」は最適な気がします。いやぁ、ちまちまと Bootstrap のクラスを指定するのも大変なので、最初のひな形だけ用意してくれるのは助かります。もっとも、Bootstrap だとどうしても業務アプリちっくになるのが難点ですが、まあ、そこは良しとします。社内アプリなんかはこれで十分です。

カテゴリ一覧を表示する

Laravel の Web API を利用してカテゴリ一覧を表示させてみましょう。
ひとまず、div タグでカテゴリを表示をさせるところまで。

@page "/categories"
@rendermode InteractiveServer
@inject NavigationManager Navigation
@inject HttpClient Http
@inject IJSRuntime JSRuntime

<PageTitle>カテゴリ一覧</PageTitle>
<div>
    カテゴリを表示
    
    @foreach(  var it in categories) {
        <div>
            @it.Id : @it.Name
        </div>
    }
</div>

@code {
    class Category {
        public int Id { get; set; }
        public string Name {get; set; } = "";
        public string Description { get; set; } = "";
        public int ProductsCount { get; set; }
        public DateTime CreatedAt {get; set; }
        public DateTime UpdatedAt {get; set; }
    }
    class CategoriesResponse {
        public Category[] Data { get; set; } = default! ;
    }

    private List<Category> categories = new List<Category>();
    protected override async Task OnInitializedAsync()
    {
        await onSearch();
    }

    /// <summary>
    /// 検索
    /// </summary>
    private async Task onSearch()
    {
        var res = await Http.GetFromJsonAsync<CategoriesResponse>(
            "api/categories");
        if (res == null) return;
        this.categories = res.Data.ToList();

        System.Diagnostics.Debug.WriteLine( this.categories );

        foreach( var it in this.categories  ) {
            System.Diagnostics.Debug.WriteLine( it.Name );
        }
    }
}

この状態から、カテゴリの表示を bootstrap 形式で AI エージェントに化粧してもらいます。

<div class="container py-4">
    <div class="mb-4">
        <h2 class="fw-bold text-danger">🍣 カテゴリ一覧</h2>
        <p class="text-secondary">カテゴリを選択してください</p>
    </div>

    @if (isLoading)
    {
        <div class="d-flex justify-content-center py-5">
            <div class="spinner-border text-danger" role="status">
                <span class="visually-hidden">読み込み中...</span>
            </div>
        </div>
    }
    else if (categories.Count == 0)
    {
        <div class="alert alert-warning">カテゴリが見つかりませんでした。</div>
    }
    else
    {
        <div class="row row-cols-1 row-cols-md-3 g-4">
            @foreach (var it in categories)
            {
                <div class="col">
                    <div class="card h-100 shadow-sm border-0" style="cursor: pointer;"
                         @onclick="() => SelectCategory(it)">
                        <div class="card-body">
                            <h5 class="card-title fw-bold">@it.Name</h5>
                            <p class="card-text text-secondary">@it.Description</p>
                        </div>
                        <div class="card-footer bg-white border-0 d-flex justify-content-between align-items-center">
                            <span class="badge bg-danger rounded-pill">@it.products_count 品</span>
                            <small class="text-muted">→</small>
                        </div>
                    </div>
                </div>
            }
        </div>
    }
</div>

コード部分

class Category
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public string Description { get; set; } = "";
    public int products_count { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
}

class CategoriesResponse
{
    public Category[] Data { get; set; } = default!;
}

private List<Category> categories = new List<Category>();
private bool isLoading = true;

protected override async Task OnInitializedAsync()
{
    await onSearch();
}

private async Task onSearch()
{
    isLoading = true;
    var res = await Http.GetFromJsonAsync<CategoriesResponse>("api/categories");
    if (res != null)
        categories = res.Data.ToList();
    isLoading = false;
}

private void SelectCategory(Category category)
{
    System.Diagnostics.Debug.WriteLine($"Selected: {category.Name}");
    Navigation.NavigateTo($"/products/{category.Id}");
}

多分、Blazor でバイブコーディングしてもこれぐらいは一気に行けると思うのですが、クラス化したり Web API の動きを確認したりしながらやる場合は、最初に Web API 呼び出しの C# コードだけ作って、後から HTML 部分で AI に化粧してもらうのが安全そうです。
Category と CategoriesResponse クラスは後で Models のほうに放り込みます。

商品一覧を表示して、カートに入れる機能のほうは後日。

カテゴリー: 開発 パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

*