【Laravel】初めてのCRUD機能

Laravelでの単純なCRUD機能作成時の備忘録。

# 最終的なブラウザ表示
- 一覧表示ページ と 詳細表示ページ  
<img src="https://gyazo.com/fa1b9a5b40ad18aa5d2b15f56a9fa512/raw" width=40%>  <img src="https://gyazo.com/23884d7acd271514f0711565ab674d58/raw" width=40%>
- 新規投稿ページ と 編集ページ  
<img src="https://gyazo.com/6c2b3c4c6b8f9a57ec822c8706fab2c0/raw" width=41%> <img src="https://gyazo.com/0497b6170d80c2469b5167b6352849b2/raw" width=50%>

# 最終的なコード
## ■ ルーティング

|実装機能|HTTPメソッド|メソッド|
|:--:|:--|:--|
|一覧表示|get|index|
|新規作成|get → post|create → store|
|詳細表示|get|show|
|編集|get → post|edit → update|
|削除|delete|destroy|

```php:web.php
Route::get('/', function () {
    return redirect('/articles');
});
Route::get('/articles', [App\Http\Controllers\ArticleController::class, 'index'])->name('article.list');                 // 一覧表示ページ
Route::get('/article/new', [App\Http\Controllers\ArticleController::class, 'create'])->name('article.new');              // 新規投稿ページ
Route::post('/article', [App\Http\Controllers\ArticleController::class, 'store'])->name('article.store');                // 新規保存
Route::get('/article/{id}', [App\Http\Controllers\ArticleController::class, 'show'])->name('article.show');              // 詳細表示ページ
Route::delete('/article/{id}', [App\Http\Controllers\ArticleController::class, 'destroy'])->name('article.delete');      // 削除
Route::get('/article/edit/{id}', [App\Http\Controllers\ArticleController::class, 'edit'])->name('article.edit');         // 編集ページ表示
Route::post('/article/update/{id}', [App\Http\Controllers\ArticleController::class, 'update'])->name('article.update');  // 更新
```

## ■ コントローラー

```php:Articleコントローラー
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller{
  // 一覧表示
  public function index(Request $request){
    if ($request->filled('keyword')) {
      $keyword = $request->input('keyword');
      $message = '検索キーワード: '.$keyword;
      $articles = Article::where('content', 'like', '%'.$keyword.'%')->get();
    }else{
      $message = "検索キーワードを入力してください。";
      $articles = Article::all();
    }
    return view('index', ['message' => $message, 'articles' => $articles]);
  }
  // 新規投稿ページ表示
  public function create(Request $request){
    $message = '投稿フォーム： ';
    return view('new', ['message'=>$message]);
  }
  // 新規保存
  public function store(Request $request){
    $article = new Article();
    $article->content = $request->content;
    $article->user_name = $request->user_name;
    $article->save();
    return redirect()->route('article.show', ['id' => $article->id]);
  }
  // 詳細表示
  public function show(Request $request, $id, Article $article){
    $message = '記事の内容： ';
    $article = Article::find($id);
    return view('show', ['message' => $message, 'article' => $article]);
  }
  // 編集ページ表示
  public function edit(Request $request, $id, Article $article){
    $message = '記事の編集： '.$id;
    $article = Article::find($id);
    return view('edit', ['message'=>$message, 'article'=>$article]);
  }
  // 更新
  public function update(Request $request, $id, Article $article){
    $article = Article::find($id);
    $article->content = $request->content;
    $article->user_name = $request->user_name;
    $article->save();
    return redirect()->route('article.show', ['id' => $article->id]);
  }
  // 削除
  public function destroy(Request $request, $id, Article $article){
    $article = Article::find($id);
    $article -> delete();
    return redirect("/articles");
  }
}
```

## ■ ビュー
- Bootstrapを使用。

### - 共通部分

```html:layout.blade.php
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset='utf-8'>
    <title>Laravel_sample</title>
    <style>body {padding: 10px;}</style>
    @include('style-sheet')      <!-- Bootstrapの読み込み -->
  </head>

  <body>
    @include('nav')      <!-- ナビゲーションバー -->
    <div class='container'>
      @yield('content')
    </div>
  </body>
</html>
```
- ナビゲーションバー

```html:nav.blade.php
<nav class='navbar navbar-expand-md navbar-dark bg-dark fixed-top'>
  <a class='navbar-brand' href='{{ route("article.list") }}'>My Articles📗</a>
</nav>
```
```html:style-sheet.blade.php
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<style>body {padding-top: 80px;}</style>  <!-- 設置位置の調節  -->
```

### - 一覧表示ページ（root）

```html:一覧表示ページ（index.blade.php）
@extends('layout')
@section('content')
  <h1>一覧表示ページ</h1>
  <p>{{ $message }}</p>
  @include('search')     <!-- 検索フォーム -->
  <table class="table table-striped table-hover">
    @foreach ($articles as $article)
      <tr>
        <td>
          <a href='{{ route("article.show", ["id" => $article->id]) }}' >
            {{ $article -> content }}
          </a>
        </td>
        <td>{{ $article -> user_name }}</td>
        <td>{{ $article -> created_at }}</td>
      </tr>
    @endforeach
  </tabel>
  <div>
    <a href = '{{ route("article.new") }}' class="btn btn-primary">投稿</a>
  </div>
@endsection
```


### - 新規投稿ページ

```html:新規投稿ページ（new.blade.php）
@extends('layout')
@section('content')
  <h1>新規投稿ページ</h1>
  <p>{{ $message }}</p>

  {{ Form::open(['route'=>'article.store']) }}
    <div class='form-group'>
      {{ Form::label('content', '内容：') }}
      {{ Form::text('content', null, ['placeholder'=>'内容を入力']) }}
    </div>

    <div class='form-group'>
      {{ Form::label('user_name', '投稿者名： ') }}
      {{ Form::text('user_name', null, ['placeholder'=>'投稿者名を入力']) }}
    </div>

    <div class='form-group'>
      {{ Form::submit('投稿', ['class' => 'btn btn-primary']) }}  <!-- 投稿ボタン -->
      <a href='{{ route("article.list") }}'>もどる</a>  <!-- indexページへのリンク -->
    </div>
  {{ Form::close() }}
@endsection
```

### - 詳細表示ページ

```html:詳細表示ページ（show.blade.php）
@extends('layout')
@section('content')
  <h1>詳細表示ページ</h1>
  <p>{{ $message }}</p>
  <p>{{ $article -> content }}</p>
  <p>{{ $article -> user_name }}</p>
  <p>{{ $article -> created_at }}</p>
  <p>
    <a href='{{ route("article.edit", ["id"=> $article->id]) }}' class="btn btn-secondary">編集</a>
    <a href = '{{ route("article.list") }}' class="btn btn-outline-secondary">戻る</a>
  </p>
  <div>
    {{ Form::open(['method' => 'delete', 'route' => ['article.delete', $article -> id]]) }}
      {{ form::submit("削除ボタン") }}
    {{ Form::close() }}
  </div>
@endsection
```

### - 編集ページ

```html:編集ページ（edit.blade.php）
@extends('layout')
@section('content')
  <h1>編集ページ</h1>
  <p>{{ $message }}</p>

  {{ Form::model($article, ['route'=> ['article.update', $article->id] ]) }}
    <div class='form-group'>
      {{ Form::label('content', '内容：') }}
      {{ Form::text('content', null) }}
    </div>

    <div class='form-group'>
      {{ Form::label('user_name', '投稿者名： ') }}
      {{ Form::text('user_name', null) }}
    </div>

    <div class='form-group'>
      {{ Form::submit('保存', ['class' => 'btn btn-primary']) }}
      <a href='{{ route("article.show", ["id"=> $article->id]) }}'>もどる</a>
    </div>
  {{ Form::close() }}
@endsection
```

### - 検索フォーム
- 部分テンプレートで作成し、indexページに設置。

```html:検索フォーム（search.blade.php）
{{ Form::open(['method' => 'get']) }}
  {{ csrf_field() }}

  <div class='form-group'>
    {{ Form::label('keyword', 'キーワード：') }}
    {{ Form::text('keyword', null, ['class' => 'form-control'], ['placeholder'=>'キーワードから記事を検索できます']) }}
  </div>

  <div class='form-group'>
    {{ Form::submit('検索', ['class' => 'btn btn-outline-primary']) }}
    <a href='{{ route("article.list") }}'>クリア</a>
  </div>
{{ Form::close() }}
```






# 各論
## 前提
- MySQLに、Articlesテーブル（content、user_nameカラム）作成済みの状態とする。
- 動作確認のために、rootページにコントローラーからデータを送れるか？、表示できるか？ 程度は確認しとく。

```php:参）コントローラー作成コマンド
% php artisan make:controller articleController
```
```php:動作確認（ルーティング→コントローラー→indexビュー）
// コントローラー
public function index(){
  $message = '動作確認用';
  return view('index', ['message' => $message]);    // indexビューに、message を渡してみる
}

// ルーティング
Route::get('/', function () {
  return redirect('/articles');
});

// ビュー（index.brade.php で、messageを取得してみる）
<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8'>
    <title>Laravel_sample</title>
    <style>body {padding: 10px;}</style>
  </head>
  <body>
    <p>{{ $message }}</p>
  </body>
</html>
```


## CRUD機能 の実装
### ■ 一覧表示（index）
- コントローラー（モデルから全データを取得し、ビューに送る） → ビュー（一覧表示） の流れ。

```php:/app/Http/Controllers/ArticleController.php
public function index(){
  $message = 'Hello world';
  $articles = Article::all();        // モデルから全データ取得
  return view('index', ['message' => $message, 'articles' => $articles]);  // ビューに渡す
}
```
```php:/resources/views/index.blade.php
@extends('layout')      // 部分テンプレートを呼び出す
@section('content')
  <h1>一覧表示ページ</h1>
  <p>{{ $message }}</p>               // コントローラーから $message 取得
  @foreach ($articles as $article)    // コントローラーから $article をループで取得
    <p>{{ $article->content }}</p>
  @endforeach
@endsection
```




### ■ 詳細表示（show）
- コントローラー（モデルから1つの指定データ取得し、ビューに送る） → ビュー（詳細表示） の流れ。

```php:/app/Http/Controllers/ArticleController.php
public function show(Request $request, $id, Article $article){            // id、記事情報が必要
  $message = '詳細表示: '.$id;
  $article = Article::find($id);       // モデルから指定データ取得
  return view('show', ['message' => $message, 'article' => $article]);   // ビューに渡す
}
```
- 詳細表示ページを作成。

```php:/resources/views/show.blade.php
<h1>詳細表示ページ</h1>
<p>{{ $message }}</p>
<p>{{ $article->content }}</p>
<p>
  <a href='{{ route("article.list") }}'>戻る</a>      // 一覧表示ページへのリンク （例:ルーティング名「article.list」と名付けてる）
</p>
```
- 一覧表示ページに、詳細表示ページへのリンクを貼る。

```php:/resources/views/index.blade.php
@foreach ($articles as $article)
  <p>
    <a href='{{ route("article.show", ["id" =>  $article->id]) }}'>
      {{ $article->content }}
      {{ $article->user_name }}
      {{ $article->created_at }}
    </a>
  </p>
@endforeach
```

### ■ 新規投稿（create、store）
- 投稿ページへのルーティングを設定。
  - **記述順序に注意**!! newよりshowを先に記述するとエラー! `/article/{id}`を探しに行っちゃうから。

```php:/routes/web.php
Route::get('/articles', [App\Http\Controllers\ArticleController::class, 'index'])->name('article.list');       // 一覧表示ページ
Route::get('/article/new', [App\Http\Controllers\ArticleController::class, 'create'])->name('article.new');    // 新規投稿ページ
Route::get('/article/{id}', [App\Http\Controllers\ArticleController::class, 'show'])->name('article.show');    // 詳細表示ページ
```
- コントローラーに、投稿ページ表示のためのメソッド（create）を追加。

```php:/app/Http/Controllers/ArticleController.php
public function create(Request $request){   // createでは、取得したい情報はない
  // 確認用：フォームを使わずに、固定テキストや日時を格納する場合 （※ storeメソッドは不要）
  $article = new Article();       // インスタンス作成
  $article->content = '投稿内容';
  $article->user_name = '投稿者';
  $article->save();
  return redirect('/articles');   // 保存したら、indexページにリダイレクト

  // フォームから、投稿する場合
  $message = '投稿フォーム： ';
  return view('new', ['message'=>$message]);   // newページへ送る
}
```
- 続いて、保存のためのメソッド（store）も追加。

```php:app/Http/Controllers/ArticleController.php
public function store(Request $request){
  $article = new Article();                   // インスタンス作成
  $article->content = $request->content;      // 投稿内容
  $article->user_name = $request->user_name;  // 投稿者名
  $article->save();                           // 保存
  // レコード保存後に、showページへデータを渡してリダイレクト
  return redirect('article.show', ['id' => $article->id]);
}
```
- indexページに投稿ページへのリンクを設置。
  - 上で、新規投稿ページへのルーティング名は、「article.new」と名付けたので、それを使用。

```php:/resources/views/index.blade.php
<div>
  <a href='{{ route("article.new") }}'>新規投稿</a>   // 新規投稿ページへのリンク
</div> 
```
- 最後に、投稿フォームを作成する。 
  - 投稿フォームを部分テンプレートとして作成。
  - Laravelでのフォーム設置には、[laravelcollective/html ライブラリ](https://laravelcollective.com/docs) のインストールが必要。  
**注意**）インストールを忘れると、`Class 'Form' not found` エラー！！

```php:ターミナルでフォーム使用時に必要なライブラリをインストール
% composer require laravelcollective/html
// メモリ制限エラー（ Allowed memory size of 1610612736 bytes exhausted）の場合は、メモリを上げてインストールしてみる
% php -d memory_limit=-1 /usr/local/bin/composer require laravelcollective/html

% composer info      // インストール済みか確認
```

```html:resources/views/new.blade.php
@extends('layout')
@section('content')
  <h1>投稿フォーム</h1>
  <p>{{ $message }}</p>

  {{ Form::open(['route' => 'article.store']) }}  <!-- 投稿フォーム  -->
    <div class='form-group'>                      <!-- 投稿内容の記述エリア -->
      {{ Form::label('content', '内容:') }}
      {{ Form::text('content', null, ['placeholder'=>'内容を入力']) }}
    </div>

    <div class='form-group'>                      <!-- 投稿者名の記述エリア -->
      {{ Form::label('user_name', '投稿者:') }}
      {{ Form::text('user_name', null, ['placeholder'=>'投稿者を入力']) }}
    </div>

    <div class="form-group">                      <!-- ボタン設置 -->
      {{ Form::submit('投稿ボタン', ['class' => 'btn btn-primary']) }}
      <a href='{{ route("article.list") }}'>一覧に戻る</a>
    </div>
  {{ Form::close() }}
@endsection
```

### ■ 編集 （edit、 update）
- 編集ページ表示機能を実装（editメソッド）。

```php:app/Http/Controllers/ArticleController.php
public function edit(Request $request, $id, Article $article){  // 編集には、id情報 と 記事データが必要
  $message = '記事の編集： '.$id;    // 表示用
  $article = Article::find($id);  // 編集するレコードをid情報から取得
  return view('edit', ['message'=>$message, 'article'=>$article]);  // 編集ページに渡す
}
```
- 編集フォームを作成する。

```html:resources/views/edit.blade.php
@extends('layout')
@section('content')
  <h1>編集フォーム</h1>
  <p>{{ $message }}</p>
  <!-- Form::model ： 編集内容を表示してくれる -->
  {{ Form::model($article, ['route' => ['article.update', $article->id]]) }}
    <div class='form-group'>
      {{ Form::label('content', '内容:') }}
      {{ Form::text('content', null) }}
    </div>

    <div class='form-group'>
      {{ Form::label('user_name', '投稿者:') }}
      {{ Form::text('user_name', null) }}
    </div>

    <div class="form-group">
      {{ Form::submit('保存ボタン', ['class' => 'btn btn-primary']) }}
      <a href='{{ route("article.show", ["id" =>  $article->id]) }}'>もどる</a>
    </div>
  {{ Form::close() }}
@endsection
```
- showページに、編集ボタンを設置。

```html:resources/views/show.blade.php
<a href='{{ route("article.edit", ["id" => $article->id]) }}' class='btn btn-outline-primary'>編集ボタン</a>
```
- 編集内容を保存する更新機能を実装（updateメソッド）。

```php:app/Http/Controllers/ArticleController.php
public function update(Request $request, $id, Article $article){
  $article = Article::find($id);
  $article->content = $request->content;
  $article->user_name = $request->user_name;
  $article->save();
  return redirect()->route('article.show', ['id' => $article->id]);
}
```


### ■ 削除（delete）
- Laravelでは、ブラウザとの通信にHTTPメソッドを指定するが、ブラウザが deleteメソッドをサポートしてないので、**POSTで隠しメソッドを送って**、コントローラーメソッドの**destroy**を実行してもらうイメージ。

- まず、削除用のルーティングを設定。

```php:/routes/web.php
Route::delete('/article/{id}', [App\Http\Controllers\ArticleController::class, 'destroy'])->name('article.delete');
```
- コントローラーにアクション（destroy）を定義。

```php:/app/Http/Controllers/ArticleController.php
public function destroy(Request $request, $id, Article $article){   // 削除するレコードのid、記事データ が必要
  $article = Article::find($id);   // id情報から、レコードを取得
  $article->delete();              // 削除
  return redirect('/articles');    // 削除後に、一覧表示ページへリダイレクト
}
```
- 詳細表示ページに、削除ボタンを設置。

```php:/resources/views/show.blade.php
<div>
  {{ Form::open(['method' => 'delete', 'route' => ['article.delete', $article->id]]) }}
    {{ Form::submit('削除ボタン') }}
  {{ Form::close() }}
</div>
```

### ■ 検索
- 削除と同じく、Laravelでフォームを使うには、「laravelcollective/html」ライブラリが必要。

```php:参）laravelcollective/htmlのインストール
% composer require laravelcollective/html
% composer info      // インストール済みかの確認
```
- Laravelでのフォームは、`{{ Form:: ... }}`に記述する（**Formファサード**という）。  
（※ ファサード(facade）とは、建物の入り口という意味。）
- 1. まず、検索フォームの部分テンプレートを作成する。
  - GETメソッドによって、text型で、キー：「keyword」に値を格納してコントローラーにデータを送る。

```html:resources/views/search.blade.php
{{ Form::open(['method' => 'get']) }}
  {{ csrf_field() }}           <!-- CSRFトークン -->
  <div class='form-group'>
    {{ Form::label('keyword', 'キーワード：') }}
    {{ Form::text('keyword', null, ['class' => 'form-control'], ['placeholder'=>'キーワードから記事を検索できます']) }}
  </div>
  <div class='form-group'>
    {{ Form::submit('検索', ['class' => 'btn btn-outline-primary']) }}  <!-- 検索ボタン -->
    <a href='{{ route("article.list") }}'>クリア</a>                     <!-- indexページへのリンク -->
  </div>
{{ Form::close() }}    <!-- フォーム終了タグ -->
```
- 一覧表示ページに、部分テンプレートを読み込む。

```html:resources/views/index.blade.php
<p>{{ $message }}</p>
@section('content')
  @include('search')   <!-- ココで、検索フォームを読み込み -->
@endsection
```
- 2. 次に、検索機能の実装。
  - コントローラーで、フォームから送られたデータを取得し、該当する投稿をビューに渡して表示させる。

```php:app/Http/Controllers/ArticleController.php
public function index(Request $request){      // GETメソッドで受け取ったデータは、引数に指定した変数$requestで取得できる
  if ($request->filled('keyword')) {          // データの有無で条件分岐
    $keyword = $request->input('keyword');    // $request（フォームで送られたデータ） の中から、「keyword」を取得して、変数に定義
    $message = '検索結果: '.$keyword;          // ビュー表示のため、変数に格納
    $articles = モデル名::where('検索するカラム名', 'like', '%'.$keyword.'%')->get();
  }else{     // 未入力の場合
    $message = "検索キーワードを入力してください。";
    $articles = モデル名::all();              // 未入力なら、全データ表示
  }
  return view('検索結果を送りたいビュー名', ['message' => $message, 'articles' => $articles]);  // ビューに渡す
}
```

#### ✔️ モデル名::where()
- 指定条件で、配列を絞り込む。

```php:検索条件の指定（where）
use Illuminate\Support\Arr;
$array = [100, '200', 300, '400', 500];
$filtered = Arr::where($array, function($value, $key) {
  return is_string($value);    // string型のみ取得。 [1=>'200', 3=>'400']
});
```

#### ✔️ filled( )
- 値が空でないか？の判定。
- **blank メソッドの逆**の動作。

```php:空でないか？の判定（filled関数）
hoge1 = 'value';
$request->filled(hoge1);   // true ： キーが存在、値が空でない
$request->filled(hoge2);   // false ： キーが存在しない or 空
```






# 参） artisan tinker （対話型コンソール）
- **artisan tinker**：対話型コンソール
- 手っ取り早く、コンソールからDBにデータを登録したい時などに便利。

```php:対話型コンソールの使い方
% php artisan tinker     // 対話型コンソールの起動
>>> Article::all()       // 全データ取得

>>> $article = Article::find(1)   // 指定idのレコードを取得
>>> $article->content             // カラムを出力
>>> $article                      // レコード出力
>>> $article->delete()            // レコード削除

// レコード追加
>>> $article = new Article()      // インスタンス作成
>>> $article->content = 'テスト'    // 変数に格納
>>> $article->save()              // データ保存

>>> exit       // CTRL + C でもok
```










# （おまけ） 関連づけたテーブルからのデータ取得

```php:例）articlesテーブル、categoriesテーブルのデータを表示したい
// モデル にリレーションを定義 （例： Articleモデルは、Categoryモデルに対して、1対多の場合）
public function category() {
  return $this->belongsTo('App\Models\Category');
}

// ビュー（ ※変数はコントローラーで定義 ）
{{ $article -> name }}               // articlesテーブルのnameカラムを表示
{{ $article -> category -> name }}   // articlesテーブルに関連付けたcategoriesテーブルのnameカラムを表示
```


# （おまけ） 関連づけたテーブルへのデータ保存

```php:例）articlesコントローラーから、categoriesテーブルにデータ保存したい
// コントローラー
use App\Models\Category;   // Categoryモデルの使用を宣言
         :
public function create(){
  $categories = Category::all()->pluck('name', 'id');   // Categoryモデルから、nameとidカラムだけ取得 
  return view('new', ['article' => $article, 'categories'=> $categories] );
}
public function store(Request $request){
  $article = new Article();
  $article -> content = request('content');
  $article -> user_name = request('user_name');
  $article -> category_id = request('category_id');
  $article -> save();
  return redirect() -> route('article.detail', ['article' => $article->id] );
}

// ビュー
{{ Form::open(['route' => 'shop.store']) }}
  {{ Form::text('content', null) }}               // articlesテーブルへのデータ保存
  {{ Form::select('category_id', $categories) }}  // categoriesテーブルのデータ選択
  {{ Form::submit('登録') }}
{{ Form::close() }}
```