【Laravel】モデルを扱う　-写真投稿-

# 内容
「写真を投稿する機能を作る」ということを題材にモデルの扱い方を説明してみます。
おしながきは以下の通りです。

- テーブルの作成
- モデルの作成
- コントローラーの作成

# 前提

- Laravel7
- マイグレーション済み(usersテーブル作成済み)
- public/storageからstorage/app/public/へシンボリックリンクを張っておき、picturesディレクトリを作っておく

# テーブルの作成
まずは、モデルで扱うテーブルをデータベースに用意します。

```zsh
% php artisan make:migration create-pictures-table
```

ファイル名からpicturesというテーブルを用意してくれますが、明示的に--tabelオプションで指定できます。

```zsh
% php artisan make:migration create-pictures-table --table=pictures
```

作成されたファイルを編集します。
今回はidとtimestamps以外に、外部キーのuser_id、タイトル、写真の保存場所のパス、コメントをカラムとして作成します。

```php:database/migrations/20XX_00_00_000000_create-pictures-table.php
class CreatePicturesTable extends Migration
{
    public function up()
    {
        Schema::create('pictures', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id');
            $table->string('title', 20);
            $table->string('path', 30)->unique();
            $table->string('comment', 100)->nullable();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('pictures');
    }
}
```

マイグレーションを忘れずに。

```zsh
% php artisan migrate
```

# モデルの作成

```zsh
% php artisan make:model Picture
```

テーブル名が複数形に対し、モデル名は単数形にするという命名規則があります。
これは絶対というわけではなく、\$tableをオーバーライドすることで結びつけるテーブルを指定することができます。

```php:app/Picture.php
    protected $table = 'pictures';
```

次に、\$fillableとバリデーションルールの設定です。
fill()やupdate()など、複数代入する際に、ホワイトリストとして働く\$fillableと、ブラックリストとして働く\$guardedがあります。この二つのうち一方しか利用できません。
今回は$fillableを使います。

```php:app/Picture.php
    protected $fillable = [
        'user_id', 'title', 'path', 'comment',
    ];

    public static $rules = [
        'title' => ['string', 'max:20'],
        'picture' => ['image', 'max:2048'],
        'title' => ['string', 'max:100'],
    ];
```

バリデーションでpathではなくpictureを設定しているのは、送られてくる画像ファイルをバリデーションするためです。保存する際は、pictureのファイルをは別で保存して、pathにコントローラーで作るファイル名を保存します。

# コントローラーの作成

```zsh
% php artisan make:controller PictureController --resource --model=Picture
```

## index、show

```php:app/Http/Controllers/PictureController.php
    public function index()
    {
        $pictures = Picture::all();
        return view('picture.index', ['pictures' => $pictures]);
    }

    public function show($id)
    {
        $picture = Picture::find($id);
        return view('picture.show', ['picture' => $picture]);
    }
```

表示する画像の分を$pictureに格納してビューに渡しています。

## create、store

```php:app/Http/Controllers/PictureController.php
    public function create()
    {
        $user = Auth::user();
        return view('picture.create', ['user' => $user]);
    }

    public function store(Request $request)
    {
        $this->validate($request, Picture::$rules);

        $form = $request->all();

        $pictureFile = $request->file('picture');
        $extension = $pictureFile->getClientOriginalExtension();
        do {
            $file_name = Str::random(20).'.'.$extension;
        } while (Picture::where('path', $file_name)->first() != null);
        $this->savePicture($pictureFile, $file_name);

        $form['path'] = $file_name;
        unset($form['_token']);
        unset($form['_method']);
        unset($form['picture']);
        $picture = Picture::create($form);
        return redirect(route('picture.show', ['picture' => $picture->id]));
    }

    private function savePicture($image, $file_name) {
        // get instance
        $img = \Image::make($image);
        // resize
        $img->fit(512, null, function($constraint){
            $constraint->upsize(); 
        });
        // save
        $save_path = 'storage/pictures/'.$file_name;
        $img->save($save_path);
    }
```

POSTで渡されるデータは、\_token、\_method、user_id、title、picture(ファイル)、commentです。
バリデーション後、do~while分で重複しないファイル名を生成し、savePictureでpicture(ファイル)をstorage以下に保存しています。
createで複数の項目をセットするのに必要なpathをセットし、不必要な項目をunsetしています。

## edit、update

```php:app/Http/Controllers/PictureController.php
    public function edit($id)
    {
        $picture = Picture::find($id);
        return view('picture.edit', ['picture' => $picture]);
    }

    public function update($id, Request $request)
    {
        $this->validate($request, [
            'title' => ['required', 'string', 'max:20'],
            'comment' => ['max:100'],
        ]);

        $picture = Picture::find($id);
        $form = $request->all();
        unset($form['_token']);
        unset($form['_method']);
        $picture->fill($form)->save();
        return redirect(route('picture.show', ['picture' => $picture->id]));
    }
```

画像以外編集可能という仕様にしています。よって、updateのバリデーションは、titleとcommentだけにしています。他はstoreとほぼ一緒です。

## destroy

```php:app/Http/Controllers/PictureController.php
    public function destroy($id)
    {
        $picture = Picture::find($id);

        if (Storage::exists('public/pictures/'.$picture->path)) {
            Storage::delete('public/pictures/'.$picture->path);
        }
        $picture->delete();
        return redirect('/home');
    }
```

Storageを利用して、削除するpicture（ファイル）が存在しているか確認して、削除しています。

# ダウンロード

アップロードを実装したらダウンロードも実装したくなりますよね。
ダウンロードはコントローラーで以下のように
    return response()->download(パス);
と記述すればいいです。
これに対応するルーティングも忘れずにセットしてください。

```php:app/Http/Controllers/PictureController.php
    public function download($id) {
        $picture = Picture::find($id);
        return response()->download('storage/pictures/'.$picture->path);
    }
```

# おわり
モデルの扱いを説明するつもりが、画像のアップロード・ダウンロードを説明している感じになってしまいましたね。savePicture()で画像を取得したりリサイズしたりするのにIntervention Imageを使いました。[こちら](https://qiita.com/KagYeah/items/138ebceb29e7e370d1e3)と一緒に読んでみるといいかもしれません。