- 投稿日:2019-10-08T22:02:24+09:00
LaravelでCRUD
どこにでもある記事だけどね。基本は押さえておかないと。
前提
自分で勝手に作ったadminテーブルに対してCRUDを作成する。
テーブルは下記の記事で作ったやつ。
LarabelでDBにテストデータを仕込むここまでに作ったものをまとめると、
App/Admin.php
database/Factories/AdminFactory.php
database/migrations/2019_09_21_104722_create_admin_table.php
database/seeds/AdminTablesSeeder.phpテストデータを作るところだけだね。
このテーブルをCRUDで一式やれるようにしてみるよ。何はともあれやること
CRUDの何を作るにしても、Controllerを作らないことには話にならない。
例によってartisanでControllerを作ってみる。$php artisan make:controller AdminController --resource--resourceをつけると、CRUDに必要なfunctionをあらかじめ入れた形でソースを生成してくれる。
AdminController.php<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class AdminController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { // } /** * Show the form for creating a new resource. * * @return \Illuminate\Http\Response */ public function create() { // } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { // } /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($id) { // } /** * Show the form for editing the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function edit($id) { // } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $id) { // } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($id) { // } }それぞれのfunctionについてはlaravelマニュアル見たほうがわかりやすいかも。
URI Action route 内容 GET /admin index admin.index 一覧 GET /admin/create create admin.create 追加 POST /admin store admin.store 追加情報の保存 GET /admin/{id} show admin.show 表示 GET /admin/{id}/edit edit admin.edit 編集 PUT/PATCH /admin/{id} update admin.update 編集情報の更新 DELETE /admin/{id} destroy admin.destroy 削除 一覧表示してみる
まずは一覧表示から。
作らなくちゃいけないものは、以下の通り。
・routes/web.phpにルートを追加する。
・一覧表示画面を作る(/admin/index.blade.php)。
・AdminController.phpのfunction indexに処理を追加する。
では、順番に。ルートを追加
/admin/indexへのルートを追加する。
--resourceをつけて生成したControllerのルートは、1行で全部設定できる。web.phpRoute::resource('admin', 'adminController');こうしたときは、画面からジャンプ先を指定するrouteの設定内容は、前に書いた表の「Route」の内容になります。
indexに飛ばいしたいときは、「admin.index」を指定します。書き方例<a href="{{ route('admin.index') }}">Admin</a>一覧画面を作る
とりあえず画面デザインをそろえるため、Login画面を基にして一覧画面を作ってみた。
Login用の項目などを削除して、代わりに一覧表示用のテーブルを追加しています。index.blade.php@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Admin Index</div> <div class="card-body"> @if (session('status')) <div class="alert alert-success" role="alert"> {{ session('status') }} </div> @endif {{--成功時のメッセージ--}} @if (session('success')) <div class="alert alert-success">{{ session('success') }}</div> @endif {{-- エラーメッセージ --}} @if ($errors->any()) <div class="alert alert-danger"> <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif // この辺から追加 <div class="table-resopnsive"> <table class="table table-striped"> <thead> <tr> <th>{{__('ID')}}</th> <th>{{__('admin_code')}}</th> <th>{{__('name')}}</th> <th>{{__('role')}}</th> </tr> </thead> <tbody> @if(isset($admins)) // $adminデータ存在チェック @foreach ($admins as $admin) // テーブル作成 <tr> <td>{{ $admin->id }}</td> <td>{{ $admin->admin_code }}</td> <td>{{ $admin->name }}</td> <td>{{ $admin->role}}</td> </tr> @endforeach @endif </tbody> </table> // この辺まで追加 </div> </div> </div> </div> </div> </div> @endsectionとりあえず単に一覧を表示するだけの画面です。
@ifで$adminsに値が入っているかチェックしています。
@foreachで取得したデータをすべてテーブルに設定しています。indexに処理を追加する
今度はadmincontroller.phpのindexにデータ取得処理を追加します。
admincontroller.phppublic function index() { $admins = \App\Admin::all(); return view('admin/index', compact('admins')); }adminテーブルはEloquentを使っているので、allで全件抽出することができる。
全件取得したインスタンスをviewに渡してあげれば、bladeの中で一覧表を作ってくれる。とりあえず実行
上記までの処理を実装すると、こんな画面が表示される。はず。
ちなみにデータはシーダーで作りました。
次はcreate…の前に
今回のadminに関する処理は、一覧画面から各機能へ遷移するようにします。今決めました。
追加→「追加」ボタンクリック→admin.create→admin.store
詳細→IDをクリック→admin.show
変更→詳細画面で「変更」ボタンをクリック→admin.edit→admin.update
削除→詳細画面で「削除」ボタンをクリック→admin.destroy
なので、createの処理を作る前に、一覧画面に「追加」ボタンを追加します。menu.blade.php@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Admin Index</div> <div class="card-body"> @if (session('status')) <div class="alert alert-success" role="alert"> {{ session('status') }} </div> @endif {{--成功時のメッセージ--}} @if (session('success')) <div class="alert alert-success">{{ session('success') }}</div> @endif {{-- エラーメッセージ --}} @if ($errors->any()) <div class="alert alert-danger"> <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif // ここから追加 <button type="button" class="btn btn-primary" onclick="location.href='{{ route('admin.create') }}'"> {{ __('追加') }} </button> // ここまで追加 <div class="table-resopnsive"> <table class="table table-striped"> <thead> <tr> <th>{{__('ID')}}</th> <th>{{__('admin_code')}}</th> <th>{{__('name')}}</th> <th>{{__('role')}}</th> </tr> </thead> <tbody> @if(isset($admins)) @foreach ($admins as $admin) <tr> <td>{{ $admin->id }}</td> <td>{{ $admin->admin_code }}</td> <td>{{ $admin->name }}</td> <td>{{ $admin->role}}</td> </tr> @endforeach @endif </tbody> </table> </div> </div> </div> </div> </div> </div> @endsectionとりあえず追加ボタンだけ追加。
createとstoreを作る
createはGET、storeはPOSTで送信される。てことは、createは参照だけ、storeで更新ということになります。
controllerのcreateに処理を追加する
createで行うのは登録画面の表示だけなので、controllerには画面表示の処理だけ記述します。
AdminController.php// createのところだけ抜粋 public function create() { return view('admin.create'); }createの画面を作ってみる
表示する画面は下記のように作ってみた。基本/auth/register.blade.phpをぱくって作ってます。
create.blade.php@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Admin Add</div> <div class="card-body"> @if (session('status')) <div class="alert alert-success" role="alert"> {{ session('status') }} </div> @endif {{--成功時のメッセージ--}} @if (session('success')) <div class="alert alert-success">{{ session('success') }}</div> @endif {{-- エラーメッセージ --}} @if ($errors->any()) <div class="alert alert-danger"> <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif <form action="{{ route('admin.store') }}" method="POST"> @csrf <div class="form-group row"> <label for="admin_code" class="col-md-4 col-form-label text-md-right">{{ __('Admin Code') }}</label> <div class="col-md-6"> <input id="admin_code" type="text" class="form-control" name="admin_code" value="{{ old('admin_code') }}"> </div> </div> <div class="form-group row"> <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label> <div class="col-md-6"> <input id="name" type="text" class="form-control" name="name" value="{{ old('name') }}"> </div> </div> <div class="form-group row"> <label for="role" class="col-md-4 col-form-label text-md-right">{{ __('Role') }}</label> <div class="col-md-6"> <input id="role" type="text" class="form-control" name="role" value="{{ old('role') }}"> </div> </div> <div class="form-group row"> <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label> <div class="col-md-6"> <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password"> @error('password') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label> <div class="col-md-6"> <input id="password-confirm" type="password" class="form-control" name="password_confirmation"> </div> </div> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <button type="submit" class="btn btn-primary" name='action' value='add'> {{ __('追加') }} </button> <button type="submit" class="btn btn-primary" name='action' value='back'> {{ __('戻る') }} </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsectionエラーメッセージが出せる仕組みになっているが、今はまだvalidationしてないので何の役にも立ってません。そのうち何とかするから、待っとけ。
画面はこんな感じで表示される。
追加ボタンを押したら入力データを追加、戻るボタンを押したら一覧画面に戻るようにする。
これは、Formタグのaction属性でadmin.storeに遷移するよう設定している。そのため追加を押そうが戻るを押そうがすべてAdminControllerのstoreに飛ばされる。controllerのstoreに処理を追加する
createで入力されたものに対する処理を行うのがstoreという位置づけのようであります。
なのでvalidateしたり確認画面出したりテーブル更新したり、という処理は基本的にすべてstoreで行うことになります。
今回はとりあえずテーブルへのinsertだけしてみる。AdminController.php// storeのところだけ抜粋 public function store(Request $request) { if($request->action === 'back') { return redirect()->route('admin.index'); } else { $admin = new Admin; $admin->admin_code = $request->admin_code; $admin->name = $request->name; $admin->role = $request->role; $admin->password = Hash::make($request->password); $admin->save(); return redirect()->route('admin.index'); } }最初に押されたボタンの判定を行い、「戻る」ボタンの場合はindexにリダイレクトしています。
「戻る」ボタンが押されたのではない場合、画面に入力された内容をテーブルにセットしてinsertします。
テーブルにデータを追加したら、indexにリダイレクトします。詳細を表示する
一覧画面のIDにリンクを張って、IDをクリックしたらそのIDの内容を詳細画面に表示します。
詳細画面に「変更」ボタンを追加して、「変更」ボタンが押されたら編集画面を開くようにします。
まずは詳細画面を表示させます。一覧画面にリンクを追加する
IDにリンクを追加する。
index.blade.php//データ表示部分を抜粋 <tbody> @if(isset($admins)) @foreach ($admins as $admin) <tr> <td><a href="/admin/{{ $admin->id }}">{{ $admin->id }}</a></td> <td>{{ $admin->admin_code }}</td> <td>{{ $admin->name }}</td> <td>{{ $admin->role}}</td> </tr> @endforeach @endif </tbody>controllerのshowに処理を追加する
一覧でクリックしたIDを受け取ってadminテーブルを検索し、その内容を表示する。
書くのはこれだけ。AdminController.phppublic function show($id) { $admins = \App\Admin::find($id); return view('admin.show', compact('admins')); }画面はこんな感じで。
show.blade.php@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Admin Add</div> <div class="card-body"> @csrf <div class="form-group row"> <label for="id" class="col-md-4 col-form-label text-md-right">{{ __('ID') }}</label> <div class="col-md-6 input-group-text"> {{ $admins->id }} </div> </div> <div class="form-group row"> <label for="admin_code" class="col-md-4 col-form-label text-md-right">{{ __('Admin Code') }}</label> <div class="col-md-6 input-group-text"> {{ $admins->admin_code }} </div> </div> <div class="form-group row"> <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label> <div class="col-md-6 input-group-text"> {{ $admins->name }} </div> </div> <div class="form-group row"> <label for="role" class="col-md-4 col-form-label text-md-right">{{ __('Role') }}</label> <div class="col-md-6 input-group-text"> {{ $admins->role }} </div> </div> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <button type="button" class="btn btn-primary" onclick="location.href='{{ route('admin.edit', $admins->id) }}'"> {{ __('変更') }} </button> <button type="button" class="btn btn-primary" onclick="history.back()"> {{ __('戻る') }} </button> </div> </div> </div> </div> </div> </div> </div> @endsection一覧から詳細画面を出すのはこんな感じ。
IDをクリックすると。
詳細画面が表示される。
editとupdateを作る
editとupdateはcreateとstoreの関係と同じで、editで変更用の画面を表示して、updateで更新をする。
変更画面は、さっき作った詳細画面にこっそり仕込んだ変更ボタンをクリックして遷移することにしていた。formのactionにeditへのリンクを書いておいたので、遡って見つけてくれ。Controllerのeditに処理を追加する
今回の処理では詳細画面から変更画面へ遷移するので、直前の処理でテーブル検索しているから、それを持ってきてもいいんだけれど、resourceで作成したひな形では$idを引数にしているので、そのままIDを渡すことにする。
なので、渡されたIDで再度テーブルを検索して、その内容を変更画面に表示させる。AdminController.phppublic function edit($id) { $admins = \App\Admin::find($id); return view('admin.edit', compact('admins')); }画面はこんな風に。
resourceでrouteを作成すると、updateはmethodがPUTになっているが、HTMLではPUTなんて指定できないので、FormのmethodではPOSTを指定しておき、Formの下に「@method('PUT')」と書いてhiddenの_methodにPUTを設定する。edit.blade.php@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Admin Edit</div> <div class="card-body"> @if (session('status')) <div class="alert alert-success" role="alert"> {{ session('status') }} </div> @endif {{--成功時のメッセージ--}} @if (session('success')) <div class="alert alert-success">{{ session('success') }}</div> @endif {{-- エラーメッセージ --}} @if ($errors->any()) <div class="alert alert-danger"> <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif <form action="/admin/{{ $admins->id }}" method="POST"> @csrf @method('PUT') <div class="form-group row"> <label for="id" class="col-md-4 col-form-label text-md-right">{{ __('ID') }}</label> <div class="col-md-6"> <input id="id" class="input-group-text text-md-left" type="text" name="id" value="{{ old('$admins->id', $admins->id) }}"> </div> </div> <div class="form-group row"> <label for="admin_code" class="col-md-4 col-form-label text-md-right">{{ __('Admin Code') }}</label> <div class="col-md-6"> <input id="admin_code" type="text" class="form-control" name="admin_code" value="{{ old('admin_code',$admins->admin_code) }}"> </div> </div> <div class="form-group row"> <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label> <div class="col-md-6"> <input id="name" type="text" class="form-control" name="name" value="{{ old('name',$admins->name) }}"> </div> </div> <div class="form-group row"> <label for="role" class="col-md-4 col-form-label text-md-right">{{ __('Role') }}</label> <div class="col-md-6"> <input id="role" type="text" class="form-control" name="role" value="{{ old('role',$admins->role) }}"> </div> </div> <div class="form-group row"> <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label> <div class="col-md-6"> <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password"> @error('password') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label> <div class="col-md-6"> <input id="password-confirm" type="password" class="form-control" name="password_confirmation"> </div> </div> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <button type="submit" class="btn btn-primary" name='action' value='edit'> {{ __('変更') }} </button> <button type="submit" class="btn btn-primary" name='action' value='back'> {{ __('戻る') }} </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection実際の画面はこんな風に表示される。
cssとかデフォルトのままなので、結構苦し紛れだ。
Controllerのupdateに処理を追加する
中身はstoreに似ているが、追加ではなく更新なので、またテーブル読んじまった。
ここら辺はなんかできそうなので、各自宿題で考えておくように。
更新はsaveでできる。キーがなければinsert、存在したらupdateになる。
あと、passwordはHash化して保存しています。AdminController.phppublic function update(Request $request, $id) { if($request->action === 'back') { return redirect()->route('admin.index'); } else { $admin = \App\Admin::find($id); $admin->admin_code = $request->admin_code; $admin->name = $request->name; $admin->role = $request->role; $admin->password = Hash::make($request->password); $admin->save(); return redirect()->route('admin.index'); } }最後にDestroy
最後に削除を追加する。
Controllerのdestroyに処理を追加する
AdminController.phppublic function destroy($id) { $admins = \App\Admin::find($id); $admins->delete(); return redirect()->route('admin.index'); }削除ボタンは詳細画面に追加する。
DestroyはmethodにDELETEを指定しなくてはならないので、formでPOST指定した後、@methodでDELETEを指定する。
こんな感じ。show.blade.php@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Admin Add</div> <div class="card-body"> @csrf <div class="form-group row"> <label for="id" class="col-md-4 col-form-label text-md-right">{{ __('ID') }}</label> <div class="col-md-6 input-group-text"> {{ $admins->id }} </div> </div> <div class="form-group row"> <label for="admin_code" class="col-md-4 col-form-label text-md-right">{{ __('Admin Code') }}</label> <div class="col-md-6 input-group-text"> {{ $admins->admin_code }} </div> </div> <div class="form-group row"> <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label> <div class="col-md-6 input-group-text"> {{ $admins->name }} </div> </div> <div class="form-group row"> <label for="role" class="col-md-4 col-form-label text-md-right">{{ __('Role') }}</label> <div class="col-md-6 input-group-text"> {{ $admins->role }} </div> </div> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <button type="button" class="btn btn-primary" onclick="location.href='{{ route('admin.edit', $admins->id) }}'"> {{ __('変更') }} </button> <form style="display:inline" action="{{ route('admin.destroy', $admins->id) }}" method="post"> @csrf @method('DELETE') <button type="submit" class="btn btn-danger"> {{ __('削除') }} </button> </form> <button type="button" class="btn btn-primary" onclick="history.back()"> {{ __('戻る') }} </button> </div> </div> </div> </div> </div> </div> </div> @endsectionこれでとりあえずCRUDが実装できました。
validationが未実装とか、確認画面出してないとか、まだまだ更新するところはいろいろありますが、まずは一段落。
今日のところはここまで。アディオス、アミーゴ!
- 投稿日:2019-10-08T18:44:50+09:00
PHPでデータベース接続
1行で実行する場合
$dbh = new PDO ('mysql:dbname=データベース名;host=ホスト名またはIPアドレス;charset=utf8;',ユーザ,パスワード,PDO属性);・PDOクラス(基底クラス)のインスタンスを作成することで接続が確立します。
基底クラスは、ベースクラス、スーパークラス、親クラスとも言い、元々用意されているクラスのことです。2つのファイルで実行する場合
file1.php
// file2.phpを読み込む require ('file2.php'); // file2.php内の関数を使用して接続を確立する $dbh = 関数();file2.php
function 関数(){ $dsn = 'mysql:dbname=データベース名;host=ホスト名またはIPアドレス;charset=utf8'; $user = 'ユーザ'; $pass = 'パスワード'; $options = 'PDO属性' $dbh = new PDO($dsn, $user, $pass, $options); return $dbh; }
・$dsnのDSNはData Souse Nameの略であり、PHPではデータベース接続指定書式のことです。
・関数内でreturnすると、その関数の実行を停止し、引数を関数の値として返します。
この値をfile1.phpが受け取り、使用することで接続を確立します。
・returnは関数ではなく言語構造なので引数の指定に()は不要です。
PDO属性は以下のように指定します。
$options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,属性2,属性3,); または $options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,属性2,属性3,];■参考
・PHPマニュアル(PDO::setAttribute)
PDO属性の詳細が確認できます。
https://www.php.net/manual/ja/pdo.setattribute.php
- 投稿日:2019-10-08T18:32:05+09:00
Laravelで楽観的ロックをつくってみる
Laravelには悲観的ロックの機能は備わってるけど、楽観的ロックの機能が用意されていないっぽいのでつくってみました。
そもそも悲観的ロック・楽観的ロックとは??
書きたい事から外れるので、ざっくりというと、、
- 悲観的ロック:自分がデータを取ってきた時点でロックをかけて、他の人にはそのデータを取ってこれないようにする。
- 楽観的ロック:誰でもデータを取ってこれるけど、先に更新した人が勝ちで、更新前のデータを後から更新しようとしてもダメ。
わかりやすく丁寧に説明してくれている記事はこちら↓
ということで、今回は「楽観的ロック」を作ってみます。
たまに見かける「他の人が変更してるので、画面更新して最新の情報を表示してね」的なメッセージが出るアレです。今回は管理画面で「タイトル」と「本文」だけがある記事を更新するケースを考えてみます。
シナリオケース
仕組み
ざっくりとした仕組みはこんな感じ。
- 編集画面を表示した時にで最終更新日時を隠し持っておく[^1]
- 更新処理の直前に現在のレコードをselectして最終更新日時を取得する
- 「hiddenの値 < 現在のレコードの値」だった場合は他の人が先に更新してるのでエラーにする
※処理1:最終更新日時ではなくレコードのバージョンを保存するカラムを作って、更新のたびにインクリメントしていく方法もあります。
※処理2〜3:レコードを更新する時の条件としてhiddenの値を指定して、更新結果が0件の場合はエラーとする方法もあるかと思います。
update blog set title = ?, -- 入力値 body = ?, -- 入力 edited_at = ? -- CURRENT_TIMESTAMP() where id = ? and edited_at = ? -- hiddenの日時環境
- php7.3
- laravel5.8
- mysql5.7
構成
テーブル
CREATE TABLE `blog` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(30) DEFAULT NULL, `body` text, `edited_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;ディレクトリ構成
※必要なものだけ記述しています。
├── app │ ├── Http │ │ └── Controllers │ ├── Models │ │ └── Blog.php │ ├── Observers │ │ └── OptimisticLockObserver.php │ ├── Services │ │ └── BlogService.php │ ├── Traits │ │ └── OptimisticLockObserverTrait.php ├── resources │ └── views │ ├── input.blade.phpではでは、具体的な処理へ。
1. 編集画面を表示した時にで最終更新日時を隠し持っておく
/resources/views/input.blade.php<form action="/save" method="post"> @csrf <input type="hidden" name="edited_at" value="{{old('edited_at', $blog->edited_at??null)}}" /> <input type="text" name="title" value="{{old('title', $blog->title??null)}}"> <textarea name="body">{{old('body', $blog->body??null)}}</textarea> </form>これは単純にhiddenを作りましたというだけ。
2. 更新処理の直前に現在のレコードをselectして最終更新日時を取得する
更新処理
/app/Services/BlogService.php<?php namespace App\Services; use App\Models\Blog; class BlogService{ /** * 保存 * * @param array $values * @param Blog|null $blog */ public function save(array $values, Blog $blog=null) { $input = collect($values); $model = $blog ?: new Blog(); $model->title = $input->get('title'); $model->body = $input->get('body'); $model->setEditedAt($input->get('edited_at')); $model->save(); } }
$model->setEditedAt($input->get('edited_at'));
で画面のhiddenに保持していた更新日時をモデルに設定しています。楽観的ロック制御のための仕組み
/app/Models/Blog.php<?php namespace App\Models; use App\Traits\OptimisticLockObserverTrait; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Blog extends Model { use SoftDeletes; use OptimisticLockObserverTrait; /** * 日付へキャストする属性 * * @var array */ protected $dates = [ 'created_at', 'updated_at', 'edited_at', 'deleted_at', ]; }
use OptimisticLockObserverTrait;
でトレイトを使ってBlogモデルに2つのfunctionを定義しています。/app/Traits/OptimisticLockObserverTrait.php<?php namespace App\Traits; use App\Observers\OptimisticLockObserver; use Carbon\Carbon; trait OptimisticLockObserverTrait { protected static function bootOptimisticLockObserverTrait() { self::observe(OptimisticLockObserver::class); } public function setEditedAt($editedAt){ $this->{OptimisticLockObserver::OPTIMISTIC_LOCK_CHECK_COLUMN} = $editedAt? Carbon::parse($editedAt): null; } }setEditedAt()
前述の
BlogService#save
で呼び出していて、これを呼び出すとモデルオブジェクトに楽観的ロックチェック用の一時プロパティを追加して最終更新日を設定しています。
(更新処理でこのfunctionを呼び出さなければ楽観的ロックは行わないう様になります。)bootOptimisticLockObserverTrait()
このfunctionですが、
- 修飾子が
static
であること- function名が「boot【+クラス名】」であること
というのがポイントで、このルールに則って定義していると、Modelクラスのコンストラクタでこのfunctionを実行してくれるというものです。
そして、functionの中身の
self::observe(OptimisticLockObserver::class);
は、Modelクラスが読み込んでるConcerns\HasEvents
トレイトのfunctionに定義されていてい、コメントにRegister observers with the model.
と書いてあるので、オブザーバをサービスプロバイダではなく、モデルで登録する機能のようです。
そしてこの機能を利用して登録しているオブザーバクラスはこちらです。
3. 「hiddenの値 < 現在のレコードの値」だった場合は他の人が先に更新してるのでエラーにする
<?php namespace App\Observers; use App\Exceptions\ExclusionException; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; class OptimisticLockObserver { /** * 楽観的ロックチェック用の一時プロパティ */ const OPTIMISTIC_LOCK_CHECK_COLUMN = 'edited_at_optimistic_lock_check_column'; /** * INSERT前 * * @param Model $model */ public function creating(Model $model){ if (!$this->checkPropExists($model)) return; $this->unsetOptimisticLockColumn($model); $model->edited_at = Carbon::now(); } /** * UPDATE前 * * @param Model $model */ public function updating(Model $model){ if (!$this->checkPropExists($model)) return; $this->check($model, 'update'); $model->edited_at = Carbon::now(); } /** * 論理削除前 * * @param Model $model */ public function deleting(Model $model){ if (!$this->checkPropExists($model)) return; $this->check($model, 'delete'); $model->edited_at = Carbon::now(); } /** * 物理削除前 * * @param Model $model */ public function restoring(Model $model){ if (!$this->checkPropExists($model)) return; $this->check($model, 'delete'); $model->edited_at = Carbon::now(); } /** * 楽観的ロックチェック * * @param Model $model */ private function check(Model $model, $msgType){ //楽観的ロックチェック用の一時プロパティがあった時だけチェックする if (!$this->checkPropExists($model)) return; //現時点でのDBのデータを取得 $currentMe = $model->withTrashed()->find($model->id); $currentEditedAt = $currentMe->edited_at; //更新されているかチェック if($model->{self::OPTIMISTIC_LOCK_CHECK_COLUMN} != $currentEditedAt){ //楽観ロック throw new ExclusionException(trans('error_message.exclusion.'.$msgType)); } $this->unsetOptimisticLockColumn($model); } /** * 楽観的ロック用の一時プロパティを削除する * これをしないと存在しないカラムに値を登録しようとしてsave()時にエラーになる。 * * @param Model $model */ private function unsetOptimisticLockColumn(Model $model){ unset($model->{self::OPTIMISTIC_LOCK_CHECK_COLUMN}); } /** * 楽観的ロック用の一時プロパティがあるかチェックする * * @param Model $model * @return bool true:ある */ private function checkPropExists(Model $model){ return array_key_exists(self::OPTIMISTIC_LOCK_CHECK_COLUMN, $model->getAttributes()); } }Eloquentモデルのイベント処理を記述しています。レコードの登録・更新・削除前に実行されます。
処理内容は、
- モデルオブジェクトに楽観的ロックチェック用のプロパティが存在したら
- 対象レコードの
edited_at
カラムの値をselectする- 「プロパティの値 <
edited_at
カラム」の場合はExclusionException
(自作のException)をスローする- 全部OKなら、モデルオブジェクトの
edited_at
に現在日時を追加するとなっています。あとはEloquentの流れに乗って勝手に更新される。
ここら辺のオブザーバ周りは「Laravelのeloquentのeventでcreated_byとかupdated_byとか更新するobserverとtrait」をまるっと参考にさせてもらいました。
参考
最後にもう一度、参考にさせて頂いた記事をまとめておきます。
- 投稿日:2019-10-08T15:53:52+09:00
【コピペ用】laravelで都道府県のバリデーション
都道府県のバリデーション
作っているうちにどこかでコピペできたら便利だな、と思って記載しました。
Laravelで都道府県を入力するフォームで用いるための
バリデーションロジックです。
バリデーション自体の説明は割愛しますが、
特定の値のみ受け付ける場合にはin
を用います。県あり
$request->validate([ 'prefecture' => 'required|in:北海道,青森県,岩手県,宮城県,秋田県,山形県,福島県,茨城県,栃木県,群馬県,埼玉県,千葉県,東京都,神奈川県,新潟県,富山県,石川県,福井県,山梨県,長野県,岐阜県,静岡県,愛知県,三重県,滋賀県,京都府,大阪府,兵庫県,奈良県,和歌山県,鳥取県,島根県,岡山県,広島県,山口県,徳島県,香川県,愛媛県,高知県,福岡県,佐賀県,長崎県,熊本県,大分県,宮崎県,鹿児島県,沖縄県', ]);県なし
$request->validate([ 'prefecture' => 'required|in:北海道,青森,岩手,宮城,秋田,山形,福島,茨城,栃木,群馬,埼玉,千葉,東京,神奈川,新潟,富山,石川,福井,山梨,長野,岐阜,静岡,愛知,三重,滋賀,京都,大阪,兵庫,奈良,和歌山,鳥取,島根,岡山,広島,山口,徳島,香川,愛媛,高知,福岡,佐賀,長崎,熊本,大分,宮崎,鹿児島,沖縄', ]);参考
バリデーションについて公式ドキュメント:
https://laravel.com/docs/5.8/validation
- 投稿日:2019-10-08T14:02:03+09:00
webプログラミング演習ノート 3回目
webプログラミング演習 2019/10/8
復習から
/lesson3/form.phpにアクセスして formをつくり数字をいれて結果をresult.phpに出力する。/
- 3の倍数の場合 Foo
- 5の倍数の場合 Bar
- 15の倍数の場合FooBar
と出力する。
わからない人は モジュル演算 phpとかで調べるといいよと言われました。
やり方がわからなかったのでリサーチして参考にしたのは何故かJSの記事草 ここ
result.phpにはこんな感じで書きました。
result.php<? $val = ['value'] if( $val % 3 == 0 ){ echo "Foo<br />"; } if( $val % 5 == 0 ){ echo "Bar<br />"; } if( $val % 15 == 0 ){ echo "FooBar<br />"; } ?>こっちはform.php↓
form.php<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>webプログラミングむずいよ</title> </head> <body> <form action="/lesson3/result.php" method="POST"> <input type="text" name="value1" value="0" /> <button value='foobar'>ボタン</button> </form> </body> </html>絶対違うけどこれで動いてしまった、そして先生の模範解答は・・・
まずはform.phpの方から
form.php<!DOCTYPE html> <html><body> <form action="/lesson3/result.php" method="POST"></form> <input type="text" name="number"> <button>ボタン</button> </body> </html>そしてこっちがresult.php
result.php<? $num = $_POST['number']; $msg = ''; if($num %3 == 0){ $msg = 'Foo'; } if($num %5 == 0){ $msg = 'Bar'; } if($num %15 == 0){ $msg = 'FooBar'; } ?>いきなり「はい、15分でやってくださーい」と言われて焦ったけどもなんとかそれっぽくできた!
まだ舞える
- 投稿日:2019-10-08T14:02:03+09:00
webプログラミング演習ノート 3回目(3限)
webプログラミング演習 2019/10/8
復習から
/lesson3/form.phpにアクセスして formをつくり数字をいれて結果をresult.phpに出力する。/
- 3の倍数の場合 Foo
- 5の倍数の場合 Bar
- 15の倍数の場合FooBar
と出力する。
わからない人は モジュル演算 phpとかで調べるといいよと言われました。
やり方がわからなかったのでリサーチして参考にしたのは何故かJSの記事草 ここ
result.phpにはこんな感じで書きました。
result.php<? $val = ['value'] if( $val % 3 == 0 ){ echo "Foo<br />"; } if( $val % 5 == 0 ){ echo "Bar<br />"; } if( $val % 15 == 0 ){ echo "FooBar<br />"; } ?>こっちはform.php↓
form.php<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>webプログラミングむずいよ</title> </head> <body> <form action="/lesson3/result.php" method="POST"> <input type="text" name="value1" value="0" /> <button value='foobar'>ボタン</button> </form> </body> </html>絶対違うけどこれで動いてしまった、そして先生の模範解答は・・・
まずはform.phpの方から
form.php<!DOCTYPE html> <html><body> <form action="/lesson3/result.php" method="POST"></form> <input type="text" name="number"> <button>ボタン</button> </body> </html>そしてこっちがresult.php
result.php<? $num = $_POST['number']; $msg = ''; if($num %3 == 0){ $msg = 'Foo'; } if($num %5 == 0){ $msg = 'Bar'; } if($num %15 == 0){ $msg = 'FooBar'; } ?>いきなり「はい、15分でやってくださーい」と言われて焦ったけどもなんとかそれっぽくできた!
まだ舞える
- 投稿日:2019-10-08T10:05:23+09:00
Laravel 6 で管理者だけがユーザーを追加する系の仕組みに認証をカスタマイズする
歳とともにスポーツの上達がガンガン鈍っていることに絶賛絶望中のまついです。
プログラミングはある程度順調に成長していることがせめてもの救いです。
さて、タイトルにもある通り、Laravel の認証をカスタマイズして、管理者だけがユーザーを追加する系のウェブアプリを作る方法をまとめてみました。
なるべくシンプルに、かつ Laravel 6 に対応した方法を考えました。
要件
管理者だけがユーザーを追加できるという状況を作るために、標準状態では困る部分に絞ってリストアップ。
- ユーザー登録ページには管理者だけがアクセス可能
- 管理者もユーザーのマイページ的なものを見る必要がある
- ユーザー作成後に自動ログインされるのは困る
- ログイン中のユーザーを適切にリダイレクトしたい
こんなところでしょうか。
コード
というわけでコードを先に見ていただきましょう。
web.phpRoute::get('/', function () { return view('welcome'); }); Route::view('login', 'auth.login')->name('login'); Route::group(['middleware' => ['auth', 'can:admin']], function () { Route::get('admin', 'GateController@admin'); Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register'); Route::post('register', 'Auth\RegisterController@register')->name('register'); }); Route::group(['middleware' => ['auth', 'can:user']], function () { Route::redirect('/user', '/login', 301); Route::get('user/{user}', 'GateController@user'); }); Auth::routes(['register' => false]);まずはルーティング用のファイルです。
アクセス制限の方法としてはごく普通のコードですね。
ポイントは
Auth::routes
の引数でユーザー登録のルーティング無効化。登録時のルーティングをカスタマイズするのでこれだけ記入する必要があります。次に、コントローラーを見ていきましょう。
web.php に書いたとおり、GateController と RegisterController を呼び出しています。
GateController は新規に作成します。
GateController.phpnamespace App\Http\Controllers; use Illuminate\Http\Request; class GateController extends Controller { /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('auth'); } /** * Show the application dashboard. * * @return \Illuminate\Contracts\Support\Renderable */ public function admin() { return view('home'); } public function user() { return view('home'); } }認証後の動きをここで定義しますが、今回は管理者、ユーザーともに home.blade.php を表示させています。
このあたりは要件によって変えてください。
次に、RegisterController を見てみましょう。
Auth/RegisterController.phpprotected $redirectTo = '/login'; protected function create(array $data) { return User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), 'role' => $data['role'] ?? 5, ]); } protected function register(Request $request, $user) { $this->validator($request->all())->validate(); event(new Registered($user = $this->create($request->all()))); // $this->guard()->login($user); return $this->registered($request, $user) ?: redirect($this->redirectPath()); }全部載せると長いので一部抜粋。
まずは
$redirectTo = '/login'
でユーザー作成後のリダイレクト先を指定します。リダイレクト先をログインページにしている理由は、後ほど掲載するログインページで認証済みの場合の処理に関係してくるのです。次に、
create
メソッドに Role を追加。今回はデフォルト値を設定してあります。そして、
register()
メソッド。これは Illuminate/Foundation/Auth/RegistersUsers.php のregister()
メソッドを上書き。RegisterUsers.php には
registered()
という空のメソッドがあって、いかにも上書きしろと誘ってきますが、今回はスルー。内容的にはメソッドをコピペして、
$this->guard()->login($user);
をコメントアウトしています。動き的にはこのログイン処理、
registered()
にあるべきな気がするんですが、どうなんでしょうね。。。続きましては、AuthServiceProvider.php です。
Providers\AuthServiceProvider.phpconst ADMIN_ROLE = 10; const USER_ROLE = 5; public function register() { // } public function boot() { $this->registerPolicies(); Gate::define('admin', function ($user) { return $user->role == self::ADMIN_ROLE; }); Gate::define('user', function ($user) { return $user->role == self::USER_ROLE; }); }最初にRoleの値を決めて、定数に入れます。
今回、管理者を10、ユーザーを5にしていますが、このへんはお好きなようにどうぞ。
そして、Gateの定義をするのですが、今回は管理者とユーザーの2種類あるのでそれぞれ定義。
ここは特にややこしいこともないですね。
さあ、次で最後になります。Middleware にある RedirectIfAuthenticated.php。こいつを見ていきましょう。
Middleware/RedirectIfAuthenticated.phpconst ADMIN_ROLE = 10; public function handle($request, Closure $next, $guard = null) { if (Auth::guard($guard)->check()) { $redirect = Auth::user()->role == ADMIN_ROLE ? 'admin' : 'user/'.Auth::user()->name; return redirect('/'.$redirect); } return $next($request); }訪問者が認証済みの場合の動きを定義しています。管理者の場合は /admin それ以外は /user/{username} にリダイレクトといった感じ。
Roleの値はいくつかの場所で使うので、コンフィグかなにかに書いておいたほうがいいですね。
そんなわけで、ネットで探してもドンピシャのものが見つからなかったので自分用メモとして書いてみました。
こういう構成は多分、それほど需要が無いか、開発者が独自で実装していて表に情報が出ていないかということなんでしょうかね。
Laravelで似たようなことをしたい人のお役に立てたら嬉しいです。
では。
- 投稿日:2019-10-08T08:07:05+09:00
GoogleのAPIでサービスアカウントのJSONファイルを設定値として使いたいがGit管理はしたくないとき、Google Analytics Reporting API V4とgoogleapis/google-api-php-clientの例。
コードとか
# サービスアカウントのjsonファイル cat foo-bar-123456-123456789012.json { "type": "service_account", "project_id": "foo-bar-hoge", "private_key_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "private_key": "-----BEGIN PRIVATE KEY-----\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxx\n-----END PRIVATE KEY-----\n", "client_email": "foo-bar@-xxxxxx.iam.gserviceaccount.com", "client_id": "xxxxxxxxxxxxxxxxxxxxx", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.iam.gserviceaccount.com" } # エンコードする cat foo-bar-123456-123456789012.json | jq -r .private_key | base64 LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCnh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHgKeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eAp4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4Cnh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHgKeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eAp4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4Cnh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHgKeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eAp4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4Cnh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHgKeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eAp4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4Cnh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHgKeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eAp4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4Cnh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHgKeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eAp4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4Cnh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHgKeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eAp4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4Cnh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHgKeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eAp4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4Cnh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHgKeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg== # エンコード出力された値を環境変数や.envなどに設定する vi .envprivate function getClient() { $client = new \Google_Client(); $client->setConfig('use_application_default_credentials', true); $client->setConfig('client_id', env('ANALYTICS_SERVICE_ACCOUNT_ID')); $client->setConfig('client_email', env('ANALYTICS_SERVICE_ACCOUNT_EMAIL')); // ココでデコードして使う $client->setConfig('signing_key', base64_decode(env('ANALYTICS_KEY_BASE64_ENCODED'))); $client->setConfig('signing_algorithm', 'HS256'); $client->setScopes(['https://www.googleapis.com/auth/analytics.readonly']); return new \Google_Service_AnalyticsReporting($client); }まとめ
- Googleの各種APIを使うためにサービスアカウントのJSONファイルを持っている。が
- そのJSONファイルはGit管理したくないことがある。12Factor的に「設定を環境変数に格納しよう」のケース
- キーをエンコードした文字列を環境変数に設定して保持しておき、仕様時にはデコードして使う
- エンコード/デコードは暗号化/復号じゃないですよ
- いいねとかストックとかリアクションがあればもうすこし書きますよ
リンク
- 投稿日:2019-10-08T05:13:47+09:00
Laravel 6で一覧ページに検索窓とページ行数指定を付ける
Laravel 6でindex()コントローラとビュー(Blade)を使ってレコード一覧を作るときに、Laravel標準ページネーションを使いつつ、キーワードによる絞り込み、1ページの表示数を指定できるようにする方法を紹介します。
指定した条件は、URLのクエリーパラメータに以下のような形で明示されるようにします。これにより、ユーザーがリンクをほかのユーザーに共有した時に、そのユーザーも同じ画面が見られます。
/customers?keyword=a&perpage=10&page=3今回は
Customer
モデルの一覧画面という設定で見ていきましょう。コントローラ
まずはコントローラ。
CustomerController.phppublic function index(Request $request) { $query = Customer::query(); $keyword = $request->input('keyword'); if (!empty($keyword)) { $query->where('name', 'like', '%' . $keyword . '%'); } $perpage = $request->input('perpage', 10); //paginate $customers = $query->paginate($perpage); return view('customers.index', ['customers' => $customers->appends($request->except('page')), 'request'=>$request->except('page')]); }順に見ていきます。
検索キーワードと表示行数の入力をコントローラで受け取るため、
Request
オブジェクトをパラメータに追加します。public function index(Request $request)次に検索条件を追加できるように
Customer
モデルからクエリービルダーを取得します。$query = Customer::query();検索キーワードをGetパラメータから取得し、指定があれば
where
句を追加します。$keyword = $request->input('keyword'); if (!empty($keyword)) { $query->where('name', 'like', '%' . $keyword . '%'); }また、表示行数もGetパラメータから取得し、指定があればその数値、なければ10行とします。
Request
オブジェクトのinput
メソッドに第2パラメータを指定すると、規定値として処理してくれます。$perpage = $request->input('perpage', 10);指定条件で絞って、さらにページ処理(Pagination)を施した結果の
Customer
一覧を取得します。$customers = $query->paginate($perpage);最後に
customers.index
というblade
(ビュー)に、絞り込んだ$customersを渡します。return view('customers.index', ['customers' => $customers->appends($request->except('page')), 'request'=>$request->except('page')]);このとき、
customer
のPaginationがpage以外のGetパラメータを適切に取り扱えるよう、以下のようにパラメータを渡しています。'customers' => $customers->appends($request->except('page'))さらにbladeに、手動でGetパラメータ全体(Page以外)を渡しています。
'request'=>$request->except('page')]ビュー
まずPaginationリンク。これは標準のまま。
index.blade.php{{ $customers->links() }}次に検索ボックス。
index.blade.php<form method="get"> @foreach ($request as $key=>$value) @if ($key!="keyword") <input type="hidden" name="{{$key}}" value="{{$value}}" /> @endif @endforeach <input type="text" class="form-control" name="keyword" placeholder="Name" value="{{$request["keyword"] ?? ""}}"> </form>自分のパラメータである
keyword
以外についてはhidden
で保持します。ただしpage
はコントローラ側で排除したので、ここに保持されません。つまり、キーワード検索を実施すると必ず1ページ目に戻ることになります。
さらに、input
ボックスの値は{{$request["keyword"] ?? ""}}
とし、キーワードが既に指定されていればそれ、なければ空文字列とします。なお、??
はPHP7以降で導入されたnull合体演算子という便利な三項演算子です。同じように1ページの行数指定ドロップダウンを作ります。
index.blade.php<form method="get"> @foreach ($request as $key=>$value) @if ($key!="perpage") <input type="hidden" name="{{$key}}" value="{{$value}}" /> @endif @endforeach <select class="custom-select" name="perpage" onchange="this.form.submit()"> <option value="10" {{($request["perpage"] ?? "")==10?"selected":""}}>10</option> <option value="20" {{($request["perpage"] ?? "")==20?"selected":""}}>20</option> <option value="50" {{($request["perpage"] ?? "")==50?"selected":""}}>50</option> <option value="100" {{($request["perpage"] ?? "")==100?"selected":""}}>100</option> </select> </form>形は違いますがやってることは上と同じです。
本来なら行数指定がまだの場合、デフォルトの選択が何になるか明示すべきですが、デフォルトの選択肢が一番上に来ているため、初期状態の"selected"指定はなし=一番上になる、ということにして手抜きしています。なぜ
customers
に格納されているパラメータを使わないのかコントローラからビューに渡す
$customers
には、実はクエリーパラメータが格納されています。しかし、このパラメータは仕様上protected
になっていて、blade内で読むことはできません。そのため、自前で$requestをbladeに渡しているのです。
- 投稿日:2019-10-08T00:51:56+09:00
未経験からweb系プログラマーになるための独学履歴~遅延静的束縛と名前空間~
このコーナーについて
私は根っからの文系なのでプログラミングや情報技術についてはITパスポート程度の知識しかなく、現在は全くの独学でコードや言語等のスキルの勉強はチュートリアル等でコードを書いて成果物を作りながら学んでいけるが、それらの過程で出てくる用語や技術についてはイマイチ学びきれない。
なので、折に触れて書籍及びQlita記事等のネットサーフィンで学んだ座学的知識をアウトプットすることがこのコーナーの目的である。今回
パーフェクトPHPから万部遅延静的束縛と名前空間
遅延静的束縛
遅延静的束縛<?php class Foo { public function helloGateway() { // self::hello(); static::hello(); // 遅延静的束縛を行う。 } public static function hello() { echo __CLASS__, 'hello!', PHP_EOL; } } class Bar extends Foo { public static function hello() { echo __CLASS__, 'hello!', PHP_EOL; } } $bar = new Bar(); $bar->helloGateway(); ?>出力結果Barhello!
self::
を使った場合、self::
が使われたメソッドが存在するクラス名を参照しそのクラス名を明示する。しかし、これは静的にメソッドやプロパティを参照するので、例えば今回のように継承した子クラスで親クラスのhelloGateway
メソッドを呼び出すと、self
が参照するクラス名は子クラスのBar
ではなく、親クラスFoo
のままになってしまう。したがって、このままでは子クラスでオーバーライドしたメソッドを正しく親クラスから呼び出すことができていない。
よって、ここでコールされたhello
メソッドは親クラスであるFoo
のものとなってしまう。そこで、遅延静的束縛を行う。
static
は非転送コールによって呼ばれたメソッド・クラス名を保持する機能を持つのでself
の部分をstatic
にすることで、子クラスから親クラスのメソッドを参照しても、子クラス名を参照しながらhelloメソッドを呼び出してくれる。よって、コールされるhello
メソッドは子クラスであるBar
のものになる。転送・非転送コールとは
非転送コールはクラス名またはオブジェクト名を明示した呼び出し方である。
例$bar->helloGateway(); $Foo::hello();これに対して転送コールはメソッド内でクラス名またはオブジェクト名を代名詞的に呼び出すやり方である。
例self:: // 必ず記載されたクラス名を参照し、そのクラスのメソッド及びプロパティをコールする。 parent:: // 記載されたクラスの親クラスを参照し、そのクラスのメソッド及びプロパティをコールする。 static:: // 非転送コールによって呼ばれたメソッド及びクラス名を保持し、保持したクラスのメソッド及びプロパティをコールする。 これらはメソッド内で使用され、各々クラス名を参照し代名詞のように記載することできる。名前空間
名前空間はディレクトリの考え方を利用し、関数名やクラス名の衝突を防ぎ、機能の参照をわかりやすくするためにクラスや関数の使える名前の集合を限定する機能である。
例えばチームで開発する場合、メンバー各々がコードを書いてそれぞれコードを読み込んだ時に、関数やクラスの名前が衝突することでエラーが出てしまう。
そういったことを解決するための機能。名前空間がディレクトリでいうフォルダと考え、その中に名前空間を定義したファイルが存在し、さらにそのファイルの中関数やクラスなどが存在すると考えればいい、実際似たような書き方で表す。
一つのファイルに複数の名前空間を定義することは不可能ではないがリーダブルではないので、原則として、一つのファイルにつき一つの名前空間の関係性を保つことが望ましい。
複数のファイルで同じ名前空間を使うことはできるが、同じ名前空間で同名の関数諸々は使えない。
ちなみに名前空間の影響を受けるのはクラス・関数・constで定義した定数の3つである。
以下簡単な例。tanaka.php<?php namespace tanaka;// 名前空間の宣言、必ず最初に行い、命名はファイル名と同じが望ましい function getIntroduce() { return '私は田中です。'; } ?>yamada.php<?php namespace yamada; function getIntroduce() { return '私は山田です'; } ?>call.php<?php require_once 'tanaka.php'; require_once 'yamada.php'; echo getIntroduce(); //そのまま関数を呼び出そうとすると、同名の関数が存在してしまうためエラーとなる。またファイル側に名前空間の定義がないと同様にエラーとなる。 echo sato\getIntroduce(); // 名前空間に属する関数を呼び出すときの書き方。satoという名前空間のgetIntroduce関数が呼び出される ?>このように書くと同じメソッド名を用いてもエラーにはならない。
名前空間を定義した場合、名前空間の外に命令文を記述することはできない。例<?php namespace yamada { function getIntroduce() { return "私は山田です。"; } } echo "Hello"; ?>Fatal error: No code may exist outside of namespace {}サブ名前空間
名前空間をさらに階層化させる手法である。
例を見ると以下の通り。例(kaneda.php)<?php namespace japan\tokyo\shibuya\kaneda; function getIntroduce() { return "私は金田です。"; } ?>記述は
名前空間\サブ名前空間\サブ名前空間
という形になる。
ディレクトリで言えばjapan/ ┣ tokyo/ ┣ shibuya/ ┣ kaneda/ ┣ kaneda.phpということになる。
当然呼び出し行う関数も
call.php<?php require_once 'kaneda.php'; echo japan\tokyo\shibuya\kaneda\getIntroduce(); ?>と記述することになる。
ちなみに呼び出しを行う側の名前空間の記述はルート相対(完全修飾形式)及びドキュメント相対(修飾形式)パスで記述することになる。
同一階層の名前空間の参照は後者、別階層の名前空間は前者の記述を行う。
また、名前空間を定義しないもしくはnamespace
宣言時に名前空間名を記載しなかった場合はグローバル扱いとなる。useキーワード
useキーワードを使用すると、ある名前空間を短くするか、全くの同名のものを作ることができる。
前者がエイリアス、後者がインポートと呼ばれている。
useを使う場合名前空間の記述は必ず完全修飾形式で記述をする。名前空間のエイリアスの作成use 名前空間のすべて、または一部 as 別名;例えば以下のコードを元に考える。
kaneda.php<?php namespace japan\tokyo\shibuya\kaneda; class introduce { public function getIntroduce() { return "私は金田です。"; } } ?>これをコールする関数をエイリアスを使って書いてみる。
call.php<?php require_once "kaneda.php" use japan\tokyo\shibuya as area; $a = new area\kaneda\introduce; echo $a->getIntroduce(); ?>useキーワードを使った際に、名前空間の一部を
area
と置き換え、インスタンス化の時にそれを使って名前空間を記述する。つまり。area=japan\tokyo\shibuya
。クラスのインポートuse 完全修飾形式のクラス名;kaneda.php<?php namespace japan\tokyo\shibuya\kaneda; class introduce { public function getIntroduce() { return "私は金田です。"; } } ?>call.php<?php require_once "kaneda.php"; use japan\tokyo\shibuya\kaneda\introduce; $b = new introduce(); echo $b->getIntroduce(); ?>useをインポートで使うと、先程までのように関数を呼び出すときに逐一名前空間の記述をしなくても良くなっているのがわかる。
つまり、インポートの場合はuseの時点で名前空間の定義は済んでいて、別ファイルで定義した関数をcall.phpで読み込んでいるということになる。
よって、エイリアスとは違い普通にインスタンス化を行う。
また、インポートはまとめて行うこともできるクラスをまとめてインポートする例use 完全修飾形式のクラス名, 完全修飾形式のクラス名, ......; use 完全修飾形式 {クラス名,クラス名,......}; // PHP7.0.0よりクラスをまとめてインポートする際の記述例<?php use europa\uk\england\ { sport,bird,flower }; ?> // 上記をさらに細かくインポートして見てみる。 <?php use europa\uk\england\ { london\sport, // london.phpのsportクラス birmingham\bird, // birmingham.phpのbirdクラス glasgow\flower // glasgow.phpのflowerクラス }; ?>インポートはクラスだけではなく、関数や定数もインポートできる。
関数をインポートするuse function 完全修飾形式の関数名;関数をインポートする際の記述例(otyanomizu.php)<?php namespace japan\tokyo\otyanomizu; function getUniversityName() { return "Meiji University"; } ?>call.php<?php require_once "otyanomizu.php"; use function japan\tokyo\otyanomizu\getUniversityName; echo getUniversityName(); ?>定数をインポートするuse const 完全修飾形式の関数名;関数をインポートする際の記述例(ueno.php)<?php namespace japan\tokyo\ueno; const STATION = "Ueno Station"; ?>call.php<?php require_once "ueno.php"; use const japan\tokyo\ueno\STATION; echo STATION; ?>インポート・エイリアスの有効範囲
同じファイルにおいて別の名前空間でインポートないしエイリアスを作成し、別の名前空間で利用することができるのか?
例(kanagawa.php)<?php namespace asia\japan\kanto\kanagawa; class city { public function getCity() { return 'yokohama'; } } class bird { public function getBird() { return 'kamome'; } } ?> ```php:call.php <?php namespace a { require_once 'kangawa.php'; use asia\japan\kanto; use asia\japan\kanto\kanagawa\bird as tori; } namespace b { require_once "kanagawa.php"; $a = new kanto\kanagawa\city; echo $a=>getCity(); $b = new tori; echo $b->getBird(); } ?>上記の処理結果はエラーとなる。
つまり、同じファイルにおいても別の名前空間で定義したインポートないしエイリアスは、別の名前空間では有効ではないということになる。では今度は同じ名前空間を別ファイルで利用した場合、インポート・エイリアスは利用することはできるのか?
検証するために、コールする関数は先程と同じにして、コール関数を2つ書いてみる。call.php<?php // コールする関数が定義してあるファイルとは、別の名前空間を作り、そこにコールする関数で定義した名前空間のエイリアスを作成とインポートを行う。 namespace call; require_once "kanagawa.php"; use asia\japan\kanto; use asia\japan\kanto\kanagwa\bird as tori; ?>call2.php<?php // call.phpで作った名前空間を利用して、クラスをインポートしてみる。 namespace call; require_once "kanagawa.php"; $a-> new kanto\kanagawa\city; echo $a->getCity(); $b-> new tori; echo $b->getBird(); ?>結果は、エラーとなる。
つまり、複数のファイルで同じ名前空間を使えても、別ファイルで定義したエイリアスやインポートは有効ではないということになる。同様に今度は別ファイルかつさらに別の名前空間で作成したエイリアスとインポートは有効なのだろうかということを確認してみる。
call2.phpを以下のように変更し、検証する。call2.php<?php // call.phpとは別の名前空間をさらに作る。 namespace call2; // call.phpでエイリアスとインポートの定義をしているのでそれをコールする関数のファイルとともにrequire_onceでロードしてみる。 require_once "kanagawa.php"; require_once "call.php"; $a = new kanto\kanagawa\city; echo $a->getCity(); $b = new tori; echo $b->getBird(); ?>結果はエラーとなる。
つまり、インポートとエイリアスは同じファイルかつ同じ名前空間でしか有効ではないということになる。
名前空間が複数のファイルで同じ名前空間を利用できるのとは違ってくるので忘れないようにしよう。現在の名前空間を表示する
call.php<?php namespace call; echo __NAMAESPACE__; ?>名前空間の階層が複雑になってしまい、今弄っているファイルが名前空間のどの位置にあるのかということを確認したい場合に使う。
参考
パーフェクトPHP