Laravelで業務システム

Laravel関連の記事掲載してます

顧客マスタに削除機能をつけてみる

今回は、前回作成した顧客マスタのメイン画面にデータを削除できる機能をつけてみようと思います。

削除機能動作イメージ


メイン画面の一覧の右端に削除ボタンをつけ、ボタンを押すと、削除しても問題ないか注意を促すモーダルウィンドウを出し、削除ボタンを押すとデータが削除されるイメージを考えています。削除画面に遷移して削除させるという場合もありますが、今回は削除画面はつくらないパターンでいこうと思います。なお、検索した状態は保持することも考慮にいれます。

メイン画面の修正


動作イメージを実現できるように以下のように”mst_customer_list.blade.php”を修正します。長くなるので、削除ボタンを配置した後から記載してます。

                                <td class="col-2">{{ $customer->customer_fax }}</td>
                                <td class="col-1 text-center">
                                    <button type="button" class="btn btn-danger btn-sm text-white" data-toggle="modal" data-target="#deleteModal" onclick="set_delete_key('{{ $customer->id }}')">
                                    削除
                                    </button>
                                </td>
                                </tr>
                            @endforeach
                        @endif
                        </tbody>
                    </table>
                    @if(isset($customers))
                        {{ $customers->withPath('mst_customer_search')->appends(['search_customer_id'=>"$search_customer_id",'search_customer_nm'=>"$search_customer_nm"])->links('vendor\pagination\bootstrap-4') }}
                    @endif
                </div>
            </div>
        </div>
    </div>
    <!-- Modal -->
    <div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
        <div class="modal-dialog modal-dialog-centered" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="deleteModalTitle">確認</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                      <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                本当に削除しますか?
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">キャンセル</button>
                    <a class="btn btn-primary text-white" href="javascript:mst_customer_delete()">削除</a>
                </div>
            </div>
        </div>
    </div>
</div>
<script type="text/javascript">
    let id;

    function set_delete_key(delete_id){id = delete_id;}
    function mst_customer_delete(){
        let url = new URL('/mst_customer_delete',window.location.origin);
        url.searchParams.set('id', id);
        location.href = url.href + '&' + $("form").serialize();
    }
</script>

実質今回の目玉はこのBladeファイルになるのですが、思うところもありますので、簡単ですが解説します。

削除ボタンをクリックするとBooststrapのモーダル画面を表示させるため、モーダル画面を下のほうに配置してます。どうしてもソースが見難くなるので、コンポーネント化したほうがいいかもしれません。

今回、削除ボタンは行ごとに持たせる形にしてますので、その行のキーを保存し、モーダル画面の削除ボタンをクリックすることで、ルートにその保存したキーを渡す仕組みを考えました。

ちょっと微妙なのですが、削除ボタンにonClickつけてキー保存用のjavascript関数を呼び出し、モーダル画面の削除ボタンをクリックすると、保存したキーをGETで、削除のメンバ関数を呼び出すルートに渡すようにしました。

尚、検索状態を保持するため、検索フォームの内容は、jQueryシリアライズして、GETのパラメータに追加しました。

このへんのロジックは普段の開発っぽくなってしまい、Laravelの良さを損なってるかもしれません。

ルートの修正


以下のように"web.php"にルートを追記します。

Route::get('/mst_customer_delete', [App\Http\Controllers\MstCustomerController::class, 'delete'])->name('mst_customer_delete');

コントローラーの修正


"MstCustomerController.php"に以下の関数を追加します。

public function delete(Request $request)
{
    $customer = new Customer();

    $id = $request->input('id');
    $search_customer_id = $request->input('search_customer_id');
    $search_customer_nm = $request->input('search_customer_nm');

    $customer->where('id', $id)->update([ 'status' => 2 ]);

    $customers = $customer->mst_customer_list($search_customer_id,$search_customer_nm);

    return view('mst_customer_list',compact('search_customer_id','search_customer_nm','customers'));
}

上記は削除のロジックになりますが、物理削除でなく論理削除にしてます。

以上で削除機能が実装できました。

まとめ


この程度の機能なのでまだいいのですが、javascriptを活用した機能実装の場合、複雑になるとセキュリティー問題やブラウザ依存の問題をよく考えねばならないため、本当に必要な実装なのかはよく考えたほうがいいかと思います。

今日はここまで。

顧客マスタのメイン画面を作ってみる

顧客マスタの新規入力画面ができたので、メイン画面の一覧をつくっていきます。

Bladeファイルの編集


メイン画面は、顧客IDと顧客名の検索ボックスを配置し、検索するとその下に顧客マスタデータ一覧が出るようにします。また一覧は5行ずつのページネーションを付けます。

メイン画面の"mst_customer_list.blade.php"はまだ、Cardのヘッダーに新規とホームへ戻るボタンしか配置してませんので、上の機能が実現できるようにCardのボディーを以下のように記述します。

<div class="card-body">

    <form class="row" method="POST" action="/mst_customer_search">
        @csrf
        <div class="form-group col-6">
            <label for="search_customer_id">顧客ID</label>
            <input name="search_customer_id" type="number" class="form-control" id="search_customer_id" value="{{ isset($search_customer_id)?$search_customer_id:"" }}">
        </div>
        <div class="form-group col-6">
            <label for="search_customer_nm">顧客名</label>
            <input name="search_customer_nm" type="text" class="form-control" id="search_customer_nm" value="{{ isset($search_customer_nm)?$search_customer_nm:"" }}">
        </div>

        <div class="form-group col-12">
        <button type="submit" class="btn btn-primary btn-sm">検索</button>
        <button type="reset" class="btn btn-primary btn-sm">クリア</button>
        </div>
    </form>
    
    <br/>
    
    <table class="table table-sm table-bordered table-striped">
        <thead>
            <tr>
            <th class="bg-light">顧客ID</th>
            <th class="bg-light">顧客名</th>
            <th class="bg-light">住所</th>
            <th class="bg-light">TEL</th>
            <th class="bg-light">FAX</th>
            </tr>
        </thead>
        <tbody>
        @if(isset($customers))
            @foreach ($customers as $customer)
                <tr>
                <td class="col-1">{{ $customer->customer_id }}</td>
                <td class="col-2"><div class="text-truncate" style="max-width: 150px;">{{ $customer->customer_nm }}</div></td>
                <td class="col-4"><div class="text-truncate" style="max-width: 200px;">{{ $customer->customer_addr }}</div></td>
                <td class="col-2">{{ $customer->customer_tel }}</td>
                <td class="col-2">{{ $customer->customer_fax }}</td>
                </tr>
            @endforeach
        @endif
        </tbody>
    </table>
    @if(isset($customers))
        {{ $customers->withPath('mst_customer_search')->appends(['search_customer_id'=>"$search_customer_id",'search_customer_nm'=>"$search_customer_nm"])->links() }}
    @endif
</div>

検索部分についてはPostリクエストするため、"@csrf"をつけないとLaravelではセキュリティー対策からエラーがでます。

ところどころisset関数つかってますが、これは、最初画面を開いた際、変数が存在していないため、そのエラー対策です。

一覧表示については、"@foreach"ディレクティブ使ってループし表示させる想定です。この辺りは複雑になりがちかつ、よく利用するので、別途ファサードコンポーネント化しておくのもひとつかもしれません。

最後にページネーションつけてます。Laravelを利用するとかなり簡単に実装できます。

尚、Laravel8だと標準ではBootstrapのページネーションが適用されないようです。ページネーションはカスタマイズを今後したいと思っているので、今は初期のままにしときます。

ルート設定


次に、検索ボタンを押したときと、ページネーションのリンクをクリックした時のルートを以下のようにweb.phpに記述します。

Route::post('/mst_customer_search', [App\Http\Controllers\MstCustomerController::class, 'search'])->name('mst_customer_search');
Route::get('/mst_customer_search', [App\Http\Controllers\MstCustomerController::class, 'page'])->name('mst_customer_page');

コントローラの設定


"MstCustomerController.php"に以下ファンクションを記述します。

public function search(Request $request)
{

    $search_customer_id = $request->input('search_customer_id');
    $search_customer_nm = $request->input('search_customer_nm');

    $customer = new Customer();
    $customers = $customer->mst_customer_list($search_customer_id,$search_customer_nm);

    return view('mst_customer_list',compact('search_customer_id','search_customer_nm','customers'));
}

public function page(Request $request)
{
    $search_customer_id = $request->input('search_customer_id');
    $search_customer_nm = $request->input('search_customer_nm');
    $page = $request->input('page');

    $customer = new Customer();
    $customers = $customer->mst_customer_list($search_customer_id,$search_customer_nm);

    return view('mst_customer_list',compact('search_customer_id','search_customer_nm','customers','page'));
}

尚、上の、二つの関数は共通したDBへのアクセスロジックがあるため、"Customer.php"モデルファイルに共通ロジックを切り出ししました。‘

public function mst_customer_list($search_customer_id,$search_customer_nm)
{
    $query = Customer::query();
    $query->where('status', 1);
    if(!empty($search_customer_id)) {
        $query->where('customer_id','like',"$search_customer_id%");
    }
    if(!empty($search_customer_nm)) {
        $query->where('customer_nm','like',"%$search_customer_nm%");
    }
    $query->orderBy('id', 'asc');
    return $query->paginate(5);
}

以上で、メイン画面の完了です。だいぶ開発に慣れてきた?かなという感じですが、今後、この画面に、削除ボタンや編集ボタンを付けたりしていく予定です。

PHPとアロー演算子

今回は少し、アロー演算子について語ってみたいと思います。

大昔、C言語学んでいたころ、なんとなくアロー演算子、カッケーとか思ってた頃があります。

今では当たり前のように使ってますが、当時はすごく新しい記述に見えたんですよね。

さて、PHPでもクラスのメンバへのアクセスはアロー演算子を利用します。

クラスへのアクセスですので、C言語というよりはC++のほうが近いですかね。

ただCから入って、PHPを始めてる人はおそらく違和感を感じるひとつがこのアロー演算子かと思います。

そもそもCの場合のアロー演算子利用はポインタ変数から構造体にアクセスする場合に用い、通常の変数からのアクセスはドット演算子を使います。

ということはPHPでアロー演算子使うからポインタ変数が存在すうのかというと、PHPではポインタ概念がありません。

PHPはクラスメンバへのアクセスにアロー演算子を使うようになってます。

まあ、言語違うし、Cのルールのようにドット演算子使う必要ないですからね。

ただ、PHPではドットは文字列結合で使います。

最初、これ見た時クラスメンバへアクセスしているのかと思いました。

今日はここまで。

顧客マスタ新規登録画面を作ってみる

顧客マスタ用のテーブルもできたので、ようやく画面の開発に移りたいと思います。

まず、顧客マスタのメイン画面のベースを”home.blade.php”をコピーし、"mst_customer_list.blade.php"というファイル名にリネームして作成します。メイン画面は登録した顧客情報の検索及び一覧を表示できるようにすることを想定してますが、今回は、新規登録画面を先に開発を考えており、新規登録画面へのリンクを張るのみにしておきます。

尚、マスタに関しては、一般の画面と区分けしやすいように、Bladeファイル名の頭に"mst"をつけることにします。とはいえ、今のところマスタ画面しか作る予定ないですが。

次に、前に作った、マスタメニューに配置した"顧客マスタ"ボタンから、先ほどのメイン画面に飛ぶように、web.phpに以下のようにルートを記述します。

Route::view('/mst_customer_list', 'mst_customer_list');

次にメイン画面のCardのヘッダに新規登録画面に遷移するボタンを以下のように記述します。 尚、ホームへ戻るボタンも記述しておきます。ボタンがないとホームに戻るには直接URL叩くしかないので。

<div class="card-header d-flex justify-content-between">
    顧客情報検索
    <div>
        <a class="btn btn-primary btn-sm text-white" href="/mst_customer_create">新規</a>
        <a class="btn btn-dark btn-sm text-white" href="/home">Dashboardへ戻る</a>
    </div>
</div>

次に、"home.blade.php"をコピーして”mst_customer_list.blade.php”とリネームし新規登録画面のベースを作成し以下のように画面の内容を記述します。

<div class="card-header d-flex justify-content-between">
    顧客情報登録
    <div>
        <a class="btn btn-dark btn-sm text-white" href="/mst_customer_list">戻る</a>
    </div>
</div>

<div class="card-body">

    <form class="row" method="POST" action="/mst_customer_store">
    @csrf
    <div class="form-group col-12">
        <label for="customer_id">顧客ID</label>
        <input name="customer_id" type="number" class="form-control @error('customer_id') is-invalid @enderror" id="customer_id" placeholder="顧客IDを入力してください" value="{{ old('customer_id') }}" required>
        @error('customer_id')
            <div class="invalid-feedback">{{ $message }}</div>
        @enderror
    </div>
    <div class="form-group col-12">
        <label for="customer_nm">顧客名</label>
        <input name="customer_nm" type="text" class="form-control" id="customer_nm" placeholder="顧客名を入力してください" value="{{ old('customer_nm') }}" required>
    </div>
    <div class="form-group col-12">
        <label for="customer_addr">住所</label>
        <input name="customer_addr" type="text" class="form-control" id="customer_addr" value="{{ old('customer_addr') }}" placeholder="住所を入力してください">
    </div>
    <div class="form-group col-6">
        <label for="customer_tel">TEL</label>
        <input name="customer_tel" type="text" class="form-control" id="customer_tel" value="{{ old('customer_tel') }}" placeholder="TELを入力してください">
    </div>
    <div class="form-group col-6">
        <label for="customer_fax">FAX</label>
        <input name="customer_fax" type="text" class="form-control" id="customer_fax" value="{{ old('customer_fax') }}" placeholder="FAXを入力してください">
    </div>
    <div class="form-group col-12">
        <label for="remarks">備考</label>
        <textarea name="note" class="form-control" rows="5">{{ old('note') }}</textarea>
    </div>
    <div class="form-group col-12">
        <button type="submit" class="btn btn-primary btn-sm">登録</button>
    </div>
    </form> 

</div>

上が新規画面のBladeファイルになり今回の目玉商品ですが、少し補足しておくと、エラー等で、画面を戻す際、一度入力した内容を再セットできるように、入力タグのvalue等にold関数を記述してます。ただし、バリデーションでのエラー想定ですので、他の方法でエラーのロジック組む場合はそれに合わせて変更する必要あります。

一応、顧客IDのところは何かしらのバリデーションをかませる予定にしており、その時のための記述してます。ここではあまり深く考えてません。

次に、メインメニューに配置した新規ボタンから、新規入力画面に飛ぶように、web.phpに以下のようにルートを記述します。

Route::view('/mst_customer_create', 'mst_customer_create');

次にPostしたデータを保存する等の処理を記述するコントローラーファイルを以下Artisanコマンドで作成します。

#php artisan make:controller MstCustomerController --resource

"--resource"をつけると、標準のメンバ関数をデフォルトで記述してくれます。今回は一応つけときます。不要であれば消せばいいだけなので。

作成したコントローラーにPostしたデータをDBに保存できるようにstore関数を以下のように記述します。

public function store(Request $request)
{
    $customer = new Customer();

    $customer->customer_id = $request->customer_id;
    $customer->customer_nm = $request->customer_nm;
    $customer->customer_addr = $request->customer_addr;
    $customer->customer_tel = $request->customer_tel;
    $customer->customer_fax = $request->customer_fax;
    $customer->note = $request->note;
    $customer->status = 1;
    $customer->save();

    return view('mst_customer_list');
}

DB操作関連のロジックは今後、バリデーションをかませる等、いろいろと拡張する予定ですが、今回はシンプルにしておきます。むしろこれだけの記述でDBに保存できることがすごいです。

最後に、新規登録画面の"登録"ボタンをクリックすると、store関数に飛ぶように以下のようにweb.phpにルートを記述します。

Route::post('/mst_customer_store', [App\Http\Controllers\MstCustomerController::class, 'store'])->name('mst_customer_store');

今までの、ただ、viewに遷移するだけのルートと違い、少し、複雑になりますが、Postしたデータをstore関数に渡していることはお分かりいただけるかと思います。

これで、とりあえず、新規登録画面から顧客情報入力し"登録"ボタンをクリックするとDBにデータが反映されます。

備忘録代わりに掲載しているだけとはいえ、小生、本当に文章力ないなと改めて感じる今日この頃ですが、 今回までで、基本的なLaravelの開発の流れは見えてきたかなといった感じです。

今日はここまでにしたいと思います。

PHP一画面一変数失敗談

PHPは型宣言がなく代入するデータによって型が決まるある意味便利で、PHP使えない議論の火種になっているところかと思いますが、その昔、私がPHP始めた頃この変数にまつわるある恐ろしい試みを実施しました。

少しこのことを語りたいと思います。

フレームワークを利用しないPHP開発だと、複雑な画面になると、変数が多くなる傾向があり、特に最初のころはノウハウもなく、行き当たりばったり(型宣言の必要もないため)で、無駄に変数つかっていました。特に、変数の命名規約も設定してなかったのも悪かったとは思います。

そんな中、いまではなんでこんな考えにいたった恐怖でしかないですが、極力つかう変数を少なくすれば、ロジックもきれになるのではないかという考えがよぎり、実際これを行いました。

具体的に言えば、同じファイル内で使う、データベースや数字、文字列の変数を可能なかぎり同じ変数を使うというものです。

PHPは同じ変数つかっても代入する値で型を変更してくれるのでこれが可能なのです。

確かに、変数が少なくなり一見、ロジックがきれいになったような気にはなりましたが、それは一時の喜びにすぎませんでした。

まず、問題点として、ロジック修正するときに、何の変数なのかすぐにわからず、上から順を追って確認しないと、ミスリードしてしまうこと。次に、同じ変数を使いまわすことによって、意図しない挙動(変数の初期化がうまくいかずゴミが残る等)をするときがあったということです。

笑い話にもなりませんが、素でPHPを扱うなら特に型には気を付けましょうね。