Laravel CRUD(API)


## はじめに
前回、Laravelを使ってCRUDを作成したので、今回はWeb API版のCRUDを作っていきます。
vanilla JSで簡単なSPAを作って実際にAPIを使ってみます。
認証やセキュリティ対策などは扱いません。

前回までのあらすじ
[Laravelのインストール、初期設定、GitHubの使用](https://qiita.com/takeshisakuma/items/44055d362e2f3e8da3d9)
[Laravel CRUD](https://qiita.com/takeshisakuma/items/8d33b1f10b273f0050b1)

## テーブル、レコードの作成

### 作成するテーブル

|id|name|telephone|email|created_at|updated_at|
|---|---|---|---|---|---|
|ID|名前|電話番号	|メールアドレス|作成日時	|更新日時|
 					
 			

### テーブルの作成

#### modelとmigrationファイル作成

```:terminal
php artisan make:model Models/Member -m
```

-mオプションをつけて、migrationファイルも一緒に作成します。

下記の2ファイルが作成されます。

- app\Models\Member.php
- database\migrations\xxxx_xx_xx_xxxxxx_create_members_table.php


#### migrationファイルの編集

```:database\migrations\xxxx_xx_xx_xxxxxx_create_members_table.php
public function up()
  {
    Schema::create('members', function (Blueprint $table) {
      $table->id();
      $table->string('name', 20);
      $table->string('telephone', 13)->nullable()->unique();
      $table->string('email', 255)->nullable()->unique();
      $table->timestamps();
    });
  }
     
public function down()
  {
    Schema::dropIfExists('members');
  }
```

#### migrationの実行

```:terminal
php artisan migrate
```

membersテーブルが作成されます。




### レコードの作成


#### seederファイルの作成

```:terminal
php artisan make:seeder MembersTableSeeder
```

database\seeds\MembersTableSeeder.phpが作成されます。


#### seederファイルの編集

```:database\seeds\MembersTableSeeder.php
public function run()
  {
    DB::table('members')->insert(
      [
        [
          'name'=>'山田',
          'telephone'=>'xxxx-xxxxx',
          'email'=>'yamada@example.com',
          'created_at'=>now(),
          'updated_at'=>now(),
        ],
 
        [
          'name'=>'鈴木',
          'telephone'=>'yyyy-yyyy',
          'email'=>'suzuki@example.com',
          'created_at'=>now(),
          'updated_at'=>now(),
        ],

      ]

    );

  }
```

#### 実行するseederファイルの登録

```:database\seeds\DatabaseSeeder.php
public function run()
  {
    //追加
    $this->call(MembersTableSeeder::class);
  }
```

#### seederファイルの実行

```:terminal
php artisan db:seed
```

membersテーブルにレコードが入ります。




## APIの作成



### controllerの作成

#### controllerファイルの作成

```:terminal
php artisan make:controller MemberController --resource
```

app\Http\Controllers\MemberController.phpが作成されます。

#### controllerファイルの編集
createとeditは不要なので削除します。

```:app\Http\Controllers\MemberController.php
//追加
use App\Models\Member;

public function index()
  {
    //追加
    $members = Member::all();
 
    return $members->toArray();
  }
        
public function store(Request $request)
  {
    //追加
    $member=new Member;
     
    $member->name=$request->input('name');
    $member->telephone=$request->input('telephone');
    $member->email=$request->input('email');
     
    $member->save();
  }
        
public function show($id)
  {
    //追加
    $member = Member::find($id);
 
    return $member->toArray();
  }
          
public function update(Request $request, $id)
  {
    //追加
    $member=Member::find($id);
 
    $member->name=$request->input('name');
    $member->telephone=$request->input('telephone');
    $member->email=$request->input('email');
 
    $member->save();
  }
     
public function destroy($id)
  {
    //追加
    $member=Member::find($id);
 
    $member->delete();
  }      
```

### ルーティング
Route::resouceでCRUDに使うアクション7つの設定が行えますが、createとeditは使用しないので、除外します。

```:routes\api.php
//追加
Route::resource('member', 'MemberController')
  ->except([
    'create', 'edit',
  ]);
```

#### ルーティングの確認

```:terminal
php artisan route:list
```

## 動作確認の準備

作成したAPIの動作確認のために簡単なSPAを作ります。

- バリデーションはHTML5の簡易バリデーションのみです。
- ブラウザの履歴操作(進む、戻るなど)対応まではしません。


### viewの作成

#### viewファイルを作成します。

resources\views\spa\spa.blade.phpを新規作成します。

#### viewファイルを編集します。

```:resources\views\spa\spa.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>簡易SPA</title>
  <script src="\js\app.js" async></script>
</head>
<body>
 
</body>
</html>
``` 


### ルーティング
/spaにブラウザでアクセスしたときにresources\views\spa\spa.blade.phpを使うように設定します。

```:routes\web.php
Route::group(['prefix'=>'spa'], function () {
    Route::view('', 'spa/spa');
});
```

#### ルーティングの確認

```:terminal
php artisan route:list
```




### SPAの作成
resources\js\app.jsを編集してindex、create、show、editに相当する画面を作成します。

```:resources\js\app.js 
//操作しているIDを管理する変数currentIDの定義
let currentId = '';
 
 
//ルーティング[一覧表示]
const goIndexAnker = document.createElement('a');
goIndexAnker.innerHTML = '一覧表示に戻る';
goIndexAnker.href = '#';
 
goIndexAnker.addEventListener('click', (e) => {
  e.preventDefault();
  document.body.innerHTML = '';
 
  //操作しているIDの初期化
  currentId = '';
  getMembers();
 
  //URLの表示を/spaに変更
  window.history.pushState(null, "", "/spa");
});
 
 
//ルーティング[詳細表示画面に戻る]
const goDetailAnker = document.createElement('a');
goDetailAnker.innerHTML = '詳細表示に戻る';
goDetailAnker.href = '#';
 
goDetailAnker.addEventListener('click', (e) => {
  e.preventDefault();
  document.body.innerHTML = '';
  getMember();
 
  //URLの表示を/spa/{id}に変更
  window.history.pushState(null, "", "/spa/" + currentId);
});
 
 
//ルーティング[編集画面]
const goEditAnker = document.createElement('a');
goEditAnker.innerHTML = '編集する';
goEditAnker.href = '#';
 
goEditAnker.addEventListener('click', (e) => {
  e.preventDefault();
  document.body.innerHTML = '';
  getEditForm();
 
  //URLの表示を/spa/{id}/editに変更
  window.history.pushState(null, "", "/spa/" + currentId + "/edit");
});
 
 
//ルーティング[新規作成画面]
const goCreateAnker = document.createElement('a');
goCreateAnker.innerHTML = '新規登録';
goCreateAnker.href = '#';
 
goCreateAnker.addEventListener('click', (e) => {
  e.preventDefault();
  document.body.innerHTML = '';
  createMember();
 
  //URLの表示を/spa/createに変更
  window.history.pushState(null, "", "/spa/create");
});
 
 
//一覧表示画面作成
const getMembers = () => {
 
  //値の取得
  fetch('http://127.0.0.1:8000/api/member/')
    .then(response => {
    if (response.ok) {
      return response.text();
      } else {
        return Promise.reject(new Error('エラー'));
      }
   })
     .then(response => {
 
        //要素の作成
        var responseJson = JSON.parse(response);
        const table = document.createElement('table');
        table.createTHead();
        table.tHead.insertRow();
        table.tHead.rows[0].insertCell().innerHTML = '名前';
        table.tHead.rows[0].insertCell().innerHTML = '電話番号';
        table.tHead.rows[0].insertCell().innerHTML = 'メールアドレス';
        table.tHead.rows[0].insertCell().innerHTML = '詳細';
        table.createTBody();
 
        Object.keys(responseJson).forEach(function (key, index) {
          table.tBodies[0].insertRow();
          table.tBodies[0].rows[index].insertCell().innerHTML = responseJson[key]['name'];
          table.tBodies[0].rows[index].insertCell().innerHTML = responseJson[key]['telephone'];
          table.tBodies[0].rows[index].insertCell().innerHTML = responseJson[key]['email'];
 
          //ルーティング[詳細表示画面に進む]
          const goDetailAnker = document.createElement('a');
          goDetailAnker.href = '#';
          goDetailAnker.innerHTML = '詳細';
          goDetailAnker.id = responseJson[key]['id'];
 
          table.tBodies[0].rows[index].insertCell().appendChild(goDetailAnker);
 
          goDetailAnker.addEventListener('click', (e) => {
            e.preventDefault();
            currentId = e.target.id;
            document.body.innerHTML = '';
            getMember();
 
            //URLの表示を/spa/{id}に変更
            window.history.pushState(null, "", "/spa/" + currentId);
          })
 
        });
 
        //テーブルを配置
        document.body.appendChild(table);
 
      });
 
  document.body.appendChild(goCreateAnker);
 
};
 
 
//新規作成画面描画
const createMember = () => {
 
  //一覧表示への切り替え
  document.body.appendChild(goIndexAnker);
 
  //form作成
  const createForm = document.createElement('form');
  createForm.id = 'creatForm';
  document.body.appendChild(createForm);
 
  const nameLabel = document.createElement('label');
  nameLabel.innerHTML = '名前';
  nameLabel.htmlFor = 'nameInput';
  createForm.appendChild(nameLabel);
 
 
  const nameInput = document.createElement('input');
  nameInput.name = 'name';
  nameInput.type = 'text';
  nameInput.maxLength = 20;
  nameInput.id = 'nameInput';
  nameInput.required = 'required';
  createForm.appendChild(nameInput);
 
 
  const telephoneLabel = document.createElement('label');
  telephoneLabel.innerHTML = '電話番号';
  telephoneLabel.htmlFor = 'telephoneInput';
  createForm.appendChild(telephoneLabel);
 
  const telephoneInput = document.createElement('input');
  telephoneInput.name = 'telephone';
  telephoneInput.type = 'tel';
  telephoneInput.maxLength = 13;
  telephoneInput.id = 'telephoneInput';
  createForm.appendChild(telephoneInput);
 
 
  const emailLabel = document.createElement('label');
  emailLabel.innerHTML = 'メールアドレス';
  emailLabel.htmlFor = 'emailInput';
  createForm.appendChild(emailLabel);
 
  const emailInput = document.createElement('input');
  emailInput.name = 'email';
  emailInput.type = 'email';
  emailInput.maxLength = 255;
  emailInput.id = 'emailInput';
  createForm.appendChild(emailInput);
 
  //登録ボタン作成
  const storeInput = document.createElement('input');
  storeInput.value = '登録';
  storeInput.type = 'submit';
  createForm.appendChild(storeInput);
 
  //フォーム送信時
  createForm.addEventListener('submit', () => {
 
    //登録処理(store)に入力データを送る
    let formData = new FormData(document.getElementById('creatForm'));
 
    fetch('/api/member', {
      method: 'POST',
      body: formData,
    })
      .then((response) => {
        return response.text();
      })
 
    //一覧表示への切り替え処理
    document.body.innerHTML = '';
    getMembers();
 
    //URLの表示を/spaに変更
    window.history.pushState(null, "", "/spa");
 
  }, false);
 
};
 

//詳細表示画面描画メソッド
const getMember = () => {
 
  //一覧表示への切り替えリンク配置
  document.body.appendChild(goIndexAnker);
 
  //変更フォームへの切り替えリンク配置
  document.body.appendChild(goEditAnker);
 
  //値の取得
  fetch('http://127.0.0.1:8000/api/member/' + currentId)
    .then(response => {
      if (response.ok) {
        return response.text();
      } else {
        return Promise.reject(new Error('エラー'));
      }
    })
    .then(response => {
      const responseJson = JSON.parse(response);
 
      //要素の作成
      const displayName = document.createElement('p');
      displayName.innerHTML = '名前：' + responseJson['name'];
      document.body.appendChild(displayName);
 
      const displayTelephone = document.createElement('p');
      displayTelephone.innerHTML = '電話番号：' + responseJson['telephone'];
      document.body.appendChild(displayTelephone);
 
      const displayEmail = document.createElement('p');
      displayEmail.innerHTML = 'メールアドレス：' + responseJson['email'];
      document.body.appendChild(displayEmail);
 
      //削除フォーム作成
      const destroyForm = document.createElement('form');
      destroyForm.id = 'destroyForm';
      document.body.appendChild(destroyForm);
 
      //削除ボタン作成
      const destroyInput = document.createElement('input');
      destroyInput.value = '削除';
      destroyInput.type = 'submit';
      destroyForm.appendChild(destroyInput);
 
      //フォーム送信時の処理
      destroyInput.addEventListener('click', () => {
 
        fetch('/api/member/' + currentId, {
          method: 'DELETE',
          body: {},
        })
          .then((response) => {
            return response.text();
          });
 
        //一覧表示への切り替え
        document.body.innerHTML = '';
        getMembers();
 
      }, false);
    });
};
 
 
//更新画面表示
const getEditForm = () => {
  
  //一覧表示への切り替えリンク配置
  document.body.appendChild(goIndexAnker);
 
  //詳細表示への切り替えリンク配置
  document.body.appendChild(goDetailAnker);
 
  //値の取得
  fetch('http://127.0.0.1:8000/api/member/' + currentId)
    .then(response => {
      if (response.ok) {
        return response.text();
      } else {
        return Promise.reject(new Error('エラー'));
      }
    })
      .then(response => {
        const responseJson = JSON.parse(response);
 
        const editForm = document.createElement('form');
        editForm.id = 'editForm';
        document.body.appendChild(editForm);
 
 
        const nameLabel = document.createElement('label');
        nameLabel.innerHTML = '名前';
        nameLabel.htmlFor = 'nameInput';
        editForm.appendChild(nameLabel);
 
 
        const nameInput = document.createElement('input');
        nameInput.name = 'name';
        nameInput.type = 'text';
        nameInput.maxLength = 20;
        nameInput.id = 'nameInput';
        nameInput.required = 'required';
        nameInput.value = responseJson['name'];
        editForm.appendChild(nameInput);
 
        const telephoneLabel = document.createElement('label');
        telephoneLabel.innerHTML = '電話番号';
        telephoneLabel.htmlFor = 'telephoneInput';
        editForm.appendChild(telephoneLabel);
  
        const telephoneInput = document.createElement('input');
        telephoneInput.name = 'telephone';
        telephoneInput.type = 'tel';
        telephoneInput.maxLength = 13;
        telephoneInput.id = 'telephoneInput';
        telephoneInput.value = responseJson['telephone'];
        editForm.appendChild(telephoneInput);
 
        const emailLabel = document.createElement('label');
        emailLabel.innerHTML = 'メールアドレス';
        emailLabel.htmlFor = 'emailInput';
        editForm.appendChild(emailLabel);
 
        const emailInput = document.createElement('input');
        emailInput.name = 'email';
        emailInput.type = 'email';
        emailInput.maxLength = 255;
        emailInput.id = 'emailInput';
        emailInput.value = responseJson['email'];
        editForm.appendChild(emailInput);
 
 
        //更新ボタン作成
        const updateInput = document.createElement('input');
        updateInput.value = '更新';
        updateInput.type = 'submit';
        editForm.appendChild(updateInput);
 
        //フォーム送信時の処理
        editForm.addEventListener('submit', (e) => {
 
          let formData = new FormData(document.getElementById('editForm'));
 
          fetch('/api/member/' + currentId, {
            headers: { "X-HTTP-Method-Override": "PATCH" },
            method: 'POST',
            body: formData,
          })
            .then((response) => {
              return response.text();
            })
 
          //画面を一覧表示に切り替え
          document.body.innerHTML = '';
          getMembers();
 
          //URLの表示を/spaに変更
          window.history.pushState(null, "", "/spa");
        });
 
      }, false);
 
};
 

//一覧表示を初期表示にします。
getMembers();
``` 


## 動作確認

### JavaScriptのコンパイル
resources\js\app.jsをコンパイルします。

```:terminal
 npm run dev
```

### 簡易サーバーの起動

```:terminal
php artisan serve
```

ブラウザで/spaにアクセスして、動作確認をします。
Ctrl+Cで簡易サーバーを終了します。