Webアプリ開発　記事の更新、削除編

# 初めに
webアプリを開発したので、開発中に考えたことをまとめます
今回は作成した記事の更新、削除機能についてです。

# 開発環境
macOS Sonoma 14.4.1
CentoOS Stream X_86_64
Apache/2.4.57
PHP 8.3.6 
mysql  Ver 8.0.36 
phpMyAdmin 5.2.1
composer version 2.7.2
Laravel Installer 5.7.1
Laravel Framework 11.0.5

# ソースコード

https://github.com/opatu07/portfolio

# 考えたこと
## 記事の更新
### 実装事例
ダッシュボード画面から「記事を編集する」を押してもらうと

![dashboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3682713/5468d24c-19c3-d869-301b-7e516c675e91.png)

記事の編集画面が出てくるのでここで記事を編集していきます

![updateArticle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3682713/e2010752-c145-6301-ce6d-70277bdb025b.png)

### ビュー

```edit.blade.php
<x-layout>
    <x-setting :heading="'記事を編集する: ' . $post->title">
        <form method="POST" action="/admin/posts/{{ $post->id }}" enctype="multipart/form-data">
            @csrf
            @method('PATCH')

            <div class="py-2">
                <x-form.label name="タイトル"/>  
                    <input class="border border-gray-200 p-2 w-full rounded"
                        name="title"
                        id="title"
                        value="{{ old('title', $post->title) }}"
                    >
                <x-form.error name="title"/>
            </div>

            <div class="py-2">
                <x-form.label name="スラッグ"/>  
                    <input class="border border-gray-200 p-2 w-full rounded"
                        name="slug"
                        id="slug"
                        value="{{ old('slug', $post->slug) }}"
                    >
                <x-form.error name="slug"/>
            </div>

            <div class="flex mt-6">
                <div class="flex-1">
                    <x-form.label name="サムネイル"/>  
                        <input class="border border-gray-200 p-2 w-full rounded"
                            name="thumbnail"
                            id="thumbnail"
                            type="file"
                            value="{{ old('thumbnail', $post->thumbnail) }}"
                        >
                    <x-form.error name="thumbnail"/>
                </div>
                <img src="{{ asset('storage/' . $post->thumbnail) }}" alt="" class="rounded-xl ml-6" width="100">
            </div>

            <div class="py-2">
                <x-form.label name="要約"/>  
                    <textarea class="border border-gray-200 p-2 w-full rounded"
                        name="excerpt"
                        id="excerpt"
                    >{{ old('excerpt', $post->excerpt) }}</textarea>
                <x-form.error name="excerpt"/>
            </div>

            <div class="py-2">
                <x-form.label name="内容"/>  
                    <textarea class="border border-gray-200 p-2 w-full rounded"
                        name="body"
                        id="body"
                    >{{ old('body', $post->body) }}</textarea>
                <x-form.error name="body"/>
            </div>

            <x-form.field>
                <x-form.label name="カテゴリー"/>

                <select name="category_id" id="category_id" required>
                    @foreach (\App\Models\Category::all() as $category)
                        <option
                            value="{{ $category->id }}"
                            {{ old('category_id', $post->category_id) == $category->id ? 'selected' : '' }}
                        >{{ ucwords($category->name) }}</option>
                    @endforeach
                </select>

                <x-form.error name="category"/>
            </x-form.field>

            <x-form.button>更新</x-form.button>
        </form>
    </x-setting>
</x-layout>
```
コンポーネントファイルのsettingは左上のダッシュボードを表示するファイルです。
ここも共通化することで修正しやすくしました。
各フォームの名前やエラーは同様に共通化して修正しやすくしました。
編集する際に前回の記事の内容が欲しかったのでvalue属性を置きました。

### コントローラ
```AdminPostController.php
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;


class AdminPostController extends Controller
{

    public function edit(Post $post)
    {
        return view('admin/posts/edit', ['post' => $post]);
    }

    public function update(Post $post)
    {
        $attributes = $this->validatePost($post);

        if ($attributes['thumbnail'] ?? false) {
            $attributes['thumbnail'] = request()->file('thumbnail')->store('thumbnails');
        }

        $post->update($attributes);

        return redirect('/')->with('success', '記事が更新されました!');
    }

    protected function validatePost(?Post $post = null): array
    {
        $post ??= new Post();

        return request()->validate([
            'title' => 'required|max:20',
            'thumbnail' => $post->exists ? ['image'] : ['image'],
            'slug' => ['required', Rule::unique('posts', 'slug')->ignore($post), 'alpha_dash' , 'lowercase', 'max:10'],
            'excerpt' => 'required|max:40',
            'body' => 'required|max:500',
            'category_id' => ['required', Rule::exists('categories', 'id')]
        ]);

    }
```
記事の更新に関するメソッドはedit、update、validatePostの3つがあります。
editメソッドは記事の編集画面を表示します
updateメソッドは記事の更新処理をします
validatePostメソッドはバリデーションの処理を行います
validatePostメソッドは記事の作成と同じです

### ルート
```web.php
Route::middleware('can:admin')->group(function () {
    Route::resource('admin/posts', AdminPostController::class)->except('show','destroy');
});
```
記事の更新のルートはグループ化させました

## 記事の削除
### 実装事例


![dashboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3682713/b1d2b5e5-856b-490c-31bb-c715763179d1.png)

ダッシュボード画面で「記事を削除」を押してもらうと
![deleteConfirm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3682713/d5efb5c0-d817-d348-aa5b-00dc43a5a388.png)
削除の確認がでてくるので「OK」を押してもらうと
![deleteFlash.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3682713/ebec3440-55f7-22d0-d92b-2519d8a5ee2c.png)
削除のフラッシュが出てきて削除ができました


### ビュー
```index.blade.php
<x-layout>
    <x-setting heading="ダッシュボード">

        <div class="flex flex-col">
            <div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
                <div class="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
                    @if ($posts->count())
                    <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg ">
                        <table class="min-w-full divide-y divide-gray-200">
                            <tbody class="bg-white divide-y divide-gray-200">

                                @foreach ($posts as $post)
                                <tr>
                                    <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium text-gray-400">
                                        <form class="id">
                                            <input data-post_id="{{$post->id}}" type="submit" class="btn btn-danger btn-dell" value="記事を削除する">
                                        </form>
                                    </td>
                                </tr>
                                @endforeach

                            </tbody>
                        </table>
                        <script type="text/javascript">
                            $.ajaxSetup({
                                headers: {
                                    'X-CSRF-TOKEN': '{{ csrf_token() }}'
                                }
                            });

                            $(function() {
                                $('.btn-danger').on('click', function() {
                                    var deleteConfirm = confirm('削除してよろしいでしょうか？');

                                    if (deleteConfirm == true) {
                                        var clickEle = $(this)
                                        var postID = clickEle.attr('data-post_id');

                                        $.ajax({
                                            type: 'POST',
                                            url: '/admin/posts/' + postID, //userID にはレコードのIDが代入されています
                                            dataType: 'json',
                                            data: {
                                                'id': postID
                                            },
                                        })
                                        //”削除しても良いですか”のメッセージで”いいえ”を選択すると次に進み処理がキャンセルされます
                                    } else {
                                        (function(e) {
                                            e.preventDefault()
                                        });
                                    };
                                });
                            });
                        </script>
                    </div>
                </div>
            </div>
        </div>
    </x-setting>
</x-layout>
```
削除処理の部分だけ抜粋しました。
このwebアプリで苦労した部分の1つの削除機能のajax処理化です。
jqueryとlaravelの連携がとても難しかったです。
でもこれで削除の確認が出せるようになったので、とても使いやすくなりました。



### コントローラ
```AdminPostController.php
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;


class AdminPostController extends Controller
{

    public function destroy(Request $request ,Post $post)
    {   
        $post = Post::findOrFail($request->id);

        $post->delete();

        return back()->with('success', '記事を削除しました。');
    }

```

destoryメソッドで削除処理を行なっています
findOrFailメソッドを使うことで万が一記事が存在しないときに404エラーを出すようにしました。

### ルート
```web.php
Route::post('admin/posts/{id}', [AdminPostController::class, 'destroy'])->middleware('can:admin')->name('posts.destroy');
```
記事関連をまとめたルートとは別で削除処理専用のルートを作成しました。
名前をつけて管理がしやすくしました。

# 改善したいこと
## ビューのinput classやtextarea classを共通化

```edit.blade.php

<input class="border border-gray-200 p-2 w-full rounded"
    name="title"
    id="title"
    value="{{ old('title', $post->title) }}"
>

<textarea class="border border-gray-200 p-2 w-full rounded"
    name="excerpt"
    id="excerpt"
>{{ old('excerpt', $post->excerpt) }}</textarea>

```
他の記事でも書いてますが同じ処理を繰り返し書いているのでこの部分を共通化したいと思います。

## AdminPostController.phpの無駄なコードを削除

```AdminPostController.php

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

class AdminPostController extends Controller
{

    public function update(Post $post)
    {
        $attributes = $this->validatePost($post);

        if ($attributes['thumbnail'] ?? false) {
            $attributes['thumbnail'] = request()->file('thumbnail')->store('thumbnails');
        }

        $post->update($attributes);

        return redirect('/')->with('success', '記事が更新されました!');
    }

    protected function validatePost(?Post $post = null): array
    {
        $post ??= new Post();

        return request()->validate([
            'title' => 'required|max:20',
            'thumbnail' => $post->exists ? ['image'] : ['image'],
            'slug' => ['required', Rule::unique('posts', 'slug')->ignore($post), 'alpha_dash' , 'lowercase', 'max:10'],
            'excerpt' => 'required|max:40',
            'body' => 'required|max:500',
            'category_id' => ['required', Rule::exists('categories', 'id')]
        ]);

    }

    
}
```
updateメソッドのifの処理がいらないと思いました。
「thumbnail」があったときだけ保存しなくてもupdateメソッドで登録しているのでifの処理を削除したいと思います。

## updateをsaveに変更
updateメソッドで更新処理にupdate()を使っていましたが、save()の方が差分だけ更新できることを発見しました。
save()の方が効率的と考えるのでこっちに変更したいと思います。
## 「記事を削除する」を赤色に変更
見た目の問題で「記事を削除する」を赤色にした方がより危険性を感じられるので変更したいと思います。
## 削除がうまく機能しない
ローカルだと「記事を削除する」を押すとすぐ記事が削除されますが、本番環境では2回押さないと記事が削除されないので重い動作を改善したいです。

# 最後に
改善点がいっぱいあったのでこれから改善していきます!