はじめに
前回、Laravelを使ってCRUDを作成したので、今回はWeb API版のCRUDを作っていきます。
vanilla JSで簡単なSPAを作って実際にAPIを使ってみます。
認証やセキュリティ対策などは扱いません。
前回までのあらすじ
Laravelのインストール、初期設定、GitHubの使用
Laravel CRUD
テーブル、レコードの作成
作成するテーブル
id | name | telephone | created_at | updated_at | |
---|---|---|---|---|---|
ID | 名前 | 電話番号 | メールアドレス | 作成日時 | 更新日時 |
テーブルの作成
modelとmigrationファイル作成
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ファイルの編集
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の実行
php artisan migrate
membersテーブルが作成されます。
レコードの作成
seederファイルの作成
php artisan make:seeder MembersTableSeeder
database\seeds\MembersTableSeeder.phpが作成されます。
seederファイルの編集
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ファイルの登録
public function run()
{
//追加
$this->call(MembersTableSeeder::class);
}
seederファイルの実行
php artisan db:seed
membersテーブルにレコードが入ります。
APIの作成
controllerの作成
controllerファイルの作成
php artisan make:controller MemberController --resource
app\Http\Controllers\MemberController.phpが作成されます。
controllerファイルの編集
createとeditは不要なので削除します。
//追加
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は使用しないので、除外します。
//追加
Route::resource('member', 'MemberController')
->except([
'create', 'edit',
]);
ルーティングの確認
php artisan route:list
動作確認の準備
作成したAPIの動作確認のために簡単なSPAを作ります。
- バリデーションはHTML5の簡易バリデーションのみです。
- ブラウザの履歴操作(進む、戻るなど)対応まではしません。
viewの作成
viewファイルを作成します。
resources\views\spa\spa.blade.phpを新規作成します。
viewファイルを編集します。
<!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を使うように設定します。
Route::group(['prefix'=>'spa'], function () {
Route::view('', 'spa/spa');
});
ルーティングの確認
php artisan route:list
SPAの作成
resources\js\app.jsを編集してindex、create、show、editに相当する画面を作成します。
//操作している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をコンパイルします。
npm run dev
簡易サーバーの起動
php artisan serve
ブラウザで/spaにアクセスして、動作確認をします。
Ctrl+Cで簡易サーバーを終了します。