- 投稿日:2021-03-04T23:25:27+09:00
LaravelのEloquentとCollection
備忘録
主題の通り実務に入ってヒイヒイ言ってるクソ雑魚エンジニアの備忘録です。
では、Eloquent
Laravelに備わっているデータベースを簡単に扱える機能。
これがあることで生のSQLを書かなくてもデータを追加したり参照したり
様々なことが可能になる。すごい。Collection
Collectionとはリスト形式でデータを格納できるラッパー
のことらしい。
配列と何が違うの!と思うところ。
このCollectionめっちゃ便利Collectionで使えるメソッド↓
https://readouble.com/laravel/6.x/ja/collections.htmlCollectionは最初からwhereやsortBy,groupByなど様々なメソッドが標準で利用できて、
ソート、絞り込み、グルーピング、繰り返し、もうほんとになんでもできる本題
EloquentのメソッドとCollectionのメソッド紛らわしくて混乱するわ!!
転職して初めて基幹システムの開発に放り込まれて詰まったところ。ほんこれ。
- get()
- where()
- groupBy()
- orderBy()
- all()
- first()
上のはほんの一部だがこれ全部EloquentにもCollectionにもある。メソッドの意味もほとんど変わらないんだけど
微妙に扱いが変わってくるから注意が必要。結論
EloquentとCollectionの使い方は記事が腐るほどあってすぐ慣れたので
要点をかいつまむとモデルからgetやallで複数件データを取り出すと取り出したデータはCollection型となって返る
あらかじめ必要なレコードがわかっている場合はfirst()
まとめてデータを取得してそれぞれに処理を行いたい場合は
getやallで取得してforeachなどで都度取り出してデータ処理をかけばいい。/ DBインスタンス
Collection ー DBインスタンス
\ DBインスタンス↑こんなイメージ
もうこれは使いまくって慣れるのが一番楽です。(脳筋)
それではまた。
- 投稿日:2021-03-04T22:50:42+09:00
laravel实现批量更新多条记录的方法示例
用代码实现凭借成下列sql语句即可同时批量更新多条数据
UPDATE table_name SETcolumn
= CASE 'id' WHEN 1 THEN ? WHEN 2 THEN ? ELSEcolumn
END WHEREid
IN (1,2)
- 投稿日:2021-03-04T22:45:49+09:00
laravelで特殊な符号が含めているTABLEを取り扱う方法
table-1じゃsqlの操作ができない
ここは back quoteを付けて処理する
table-1->table-1
- 投稿日:2021-03-04T21:27:56+09:00
LaravelでCloudWatchへログ送信
はじめに
LaravelのログをCloudWatchへ送信する方法は、CloudWatchエージェントを使う方法やaws firelensを使う方法などがあります。しかし、これらの方法はAWSのリソースを作らなくてはならず、設定などが少し手間です。
今回はLaravelのソース(とIAM Userの発行)だけで完結する方法を紹介します。方法
まずは、ライブラリのインストールを行います。使用するライブラリはこちらです。
https://github.com/maxbanton/cwh$ composer require maxbanton/cwh
インストールしたライブラリに含まれるLogハンドラーをコンテナに登録します。ポイントは
CredentialProvider::defaultProvider()
としているところで、このようにすることで、ローカル環境でも本番環境でもよしなに認証情報を取得してくれます。app/Providers/AppServiceProvider.php<?php namespace App\Providers; use Aws\CloudWatchLogs\CloudWatchLogsClient; use Aws\Credentials\CredentialProvider; use Illuminate\Support\ServiceProvider; use Maxbanton\Cwh\Handler\CloudWatch; class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { $this->app->bind(CloudWatchLogsClient::class, function ($app) { return new CloudWatchLogsClient([ 'region' => config('aws.default_region'), 'version' => 'latest', 'credentials' => CredentialProvider::defaultProvider(), ]); }); $this->app->bind(CloudWatch::class, function ($app) { return new CloudWatch( $app->make(CloudWatchLogsClient::class), config('aws.cloudwatch.log.group_name'), config('aws.cloudwatch.log.stream_name'), config('aws.cloudwatch.log.retention'), 10000 ); }); } /** * Bootstrap any application services. * * @return void */ public function boot() { // } }続いて、configファイルの設定を行います。
config/aws.php
というファイルを作り、環境変数から読み取った値をセットします。config/aws.php<?php return [ 'cloudwatch' => [ 'log' => [ 'group_name' => env('AWS_CLOUDWATCH_LOG_GROUP_NAME'), 'stream_name' => env('AWS_CLOUDWATCH_LOG_STREAM_NAME'), 'retention' => env('AWS_CLOUDWATCH_LOG_RETENTION'), ] ], 'default_region' => env('AWS_DEFAULT_REGION'), ];そして、
config/logging.php
にCloudWatchへログを送信するためのチャネルを追加します。handlerには先ほどコンテナへ登録したハンドラのクラスを設定します。json形式でログを出力したい場合はformatterのコメントを解除してください。config/logging.php.... 'emergency' => [ 'path' => storage_path('logs/laravel.log'), ], 'cloudwatch' => [ 'driver' => 'monolog', 'handler' => Maxbanton\Cwh\Handler\CloudWatch::class, // 'formatter' => Monolog\Formatter\JsonFormatter::class, json形式でログを送信する時はコメント解除する ], ], ];最後に、.envファイルへ環境変数の登録をします。
LOG_CHANNELには先ほどconfig/logging.php
に追加したログチャネルの名前を設定します。
あとは、AWSの認証情報とロググループ、ログストリーム、ログの保存期間などの設定値を入れます。この時登録するIAM USERには対象のロググループとログストリームへのアクセス権が必要なので注意してください!.env... LOG_CHANNEL=cloudwatch ... AWS_ACCESS_KEY_ID=XXXX AWS_SECRET_ACCESS_KEY=XXXX AWS_DEFAULT_REGION=ap-northeast-1 AWS_CLOUDWATCH_LOG_GROUP_NAME=laravel_cloudwatch_group AWS_CLOUDWATCH_LOG_STREAM_NAME=laravel_cloudwatch_stream AWS_CLOUDWATCH_LOG_RETENTION=1実行
routes/web.php
にログの出力を仕込んでアクセスします。routes/web.php... Route::get('/', function () { \Illuminate\Support\Facades\Log::info('ok'); return view('welcome'); });AWSコンソールからCloudWatchログを確認すると、ログが飛んできてるのがわかると思います。
最後に
今回の方法は、ログのフォーマット変換から送信まで全てPHPのレイヤーで行っています。お手軽な一方で、PHPの処理には負担がかかっていることが想定できます。大量のログを飛ばすような場合は、やはり一度標準出力に出してからCloudWatchエージェントなりaws firelensなりでCloudWatchへ飛ばすのが良いのかなと思います。
実際に計測したわけじゃないので、どれくらい負荷がかかっているのかは分かりませんが。。。
- 投稿日:2021-03-04T20:36:47+09:00
[419エラー]ECSでセッションが保存されていない問題
概要
Laravel sanctumを利用してセッションベースのログイン認証を実装しており、ローカルではログインできていたのにAWS環境を使ったら419エラーが出てしまいました。
あまり情報もなく(というかAWS知識がなくてググり力が足りない)、解決するのに手間取ってしまったのでメモします。
AWS環境
ALB経由でECS(EC2)につなげている環境
419エラーとは
LaravelのPostする際のCSRF漏れエラーです。
解決策
今回はEC2インスタンスにつけているターゲットグループのスティッキーセッションが無効になっているのが原因でした。
ターゲットグループ>group detailsタブのAttributesのEditをクリック
sticknessをdisabledからenabledに変更(画像は変更後です)。まとめ
この項目をenabledにしないとセッションが保存されないみたい?なので、今後も注意したいです。
- 投稿日:2021-03-04T17:03:18+09:00
外部API呼び出しするクラスをモックする
この記事について
バックエンドから外部の API を呼び出すことがありますが、テストではそれをモックで差し替えたいシチュエーションで、Laravel + PHPUnit でどうやるといいか、備忘録です。
はじめに
環境
- Laravel: 8.30.1
- PHPUnit: 9.5.2
- PHP: 8.0.2
概要
架空の API に ping というエンドポイントがあり、テスト時はそいつのスタブをつくって処理を上書きしつつ、想定される引数が渡されるか、とかのチェックをするようにします。
times で回数を指定して、その回数分
pong
をカンマ区切りで返すとします。curl -X POST -d times=2 https://api.some-service.example/ping // pong,pong実装
プロダクションコード
API 呼び出しを担当するクラスを ExternalApi として作成します。基本的にはエンドポイントにつき1つ公開メソッドを持たせるといいでしょう。
<?php namespace App\Services\Api; use GuzzleHttp\Client; class ExternalApi { private Client $http; public function __construct() { $this->http = new Client([ 'base_uri' => 'https://api.some-service.example', ]); } public function ping(?int $times = 1): string { assert($times > 0 && $times < 256); // 架空のサービスなのでローカルで完結するようにしておくが、イメージは下記のような感じ // $this->http->request('POST', 'ping', ['times' => $times]); return collect()->times($times)->map(fn() => 'pong')->join(','); } }呼び出しのコントローラーのイメージはこんな感じ。
Route::get('/ping', function (Request $request) { $user = $request->user(); $api = app(\App\Services\Api\ExternalApi::class); $response = $api->ping($user->ping_count); return ['message' => $response]; });テストコード
ExternalApi のモックをつくる処理をトレイトにまとめます。
<?php namespace Tests\Concerns\Mock; use App\Services\Api\ExternalApi; use Mockery\MockInterface; trait CreatesExternalApiMock { public function createExternalApiMock(string $method, ...$args): MockInterface { $stubMethod = "{$method}Stub"; if (!method_exists($this, $stubMethod)) { throw new \InvalidArgumentException('method not found.'); } return $this->$stubMethod($args); } private function pingStub(array $args): MockInterface { $state = $args[0] ?? 'default'; $method = "pingStub_${state}"; $options = $args[1] ?? []; if (!method_exists($this, $method)) { throw new \InvalidArgumentException('state not found.'); } return $this->$method($options); } private function pingStub_default(array $options): MockInterface { return $this->mock(ExternalApi::class, function (MockInterface $mock) { $mock->shouldReceive('ping')->with(null)->once()->andReturn('pong'); }); } private function pingStub_multiple(array $options): MockInterface { return $this->mock(ExternalApi::class, function (MockInterface $mock) use ($options) { $mock->shouldReceive('ping')->with($options['times'])->once()->andReturn(implode(',', array_fill(0, $options['times'], 'ping'))); }); } }パターンごとにスタブを用意してやります。
使い方のイメージはこんな感じ。
<?php namespace Tests\Feature\Http\Controllers; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\Concerns\Mock\CreatesExternalApiMock; use Tests\TestCase; class PingControllerTest extends TestCase { use RefreshDatabase; use CreatesExternalApiMock; /** * @param string $state * @param array $args * @param string $expected * @return void * @dataProvider dataPing */ public function testPing(string $state, array $args, string $expected) { $user = factory(User::class)->create(['ping_count' => $args['times'] ?? null]); $this->createExternalApiMock('ping', $state, $args); $response = $this ->actingAs($user) ->get('/'); $response ->assertOk() ->assertJson(['message' => $expected]); } public function dataPing(): array { return [ '1' => [ 'state' => 'default', 'args' => [], 'expected' => 'pong', ], '2' => [ 'state' => 'multiple', 'args' => ['times' => 2], 'expected' => 'pong,pong', ], ]; } }個人的には入出力のパターンのテストはフィーチャーテストではなくユニットテストでやりたいですが、今回は例なのでフィーチャーテストに書きました。
おわりに
いくつかのプロダクトでこの方法を導入していますが、いまのところ便利に使えています。他にも取り入れられそうなアイデアがあれば、コメント欄にて教えていただけると助かります
- 投稿日:2021-03-04T17:03:18+09:00
Laravelで外部API呼び出しするクラスをモックする
この記事について
バックエンドから外部の API を呼び出すことがありますが、テストではそれをモックで差し替えたいシチュエーションで、Laravel + PHPUnit でどうやるといいか、備忘録です。
はじめに
環境
- Laravel: 8.30.1
- PHPUnit: 9.5.2
- PHP: 8.0.2
概要
架空の API に ping というエンドポイントがあり、テスト時はそいつのスタブをつくって処理を上書きしつつ、想定される引数が渡されるか、とかのチェックをするようにします。
times で回数を指定して、その回数分
pong
をカンマ区切りで返すとします。curl -X POST -d times=2 https://api.some-service.example/ping // pong,pong実装
プロダクションコード
API 呼び出しを担当するクラスを SomeServiceApi として作成します。基本的にはエンドポイントにつき1つ公開メソッドを持たせるといいでしょう。
<?php namespace App\Services\Api; use GuzzleHttp\Client; class SomeServiceApi { private Client $http; public function __construct() { $this->http = new Client([ 'base_uri' => 'https://api.some-service.example', ]); } public function ping(?int $times = 1): string { assert($times > 0 && $times < 256); // 架空のサービスなのでローカルで完結するようにしておくが、イメージは下記のような感じ // $this->http->request('POST', 'ping', ['times' => $times]); return collect()->times($times)->map(fn() => 'pong')->join(','); } }呼び出しのコントローラーのイメージはこんな感じ。
Route::get('/ping', function (Request $request) { $user = $request->user(); $api = app(\App\Services\Api\SomeService::class); $response = $api->ping($user->ping_count); return ['message' => $response]; });テストコード
SomeServiceApi のモックをつくる処理をトレイトにまとめます。
<?php namespace Tests\Concerns\Mock; use App\Services\Api\SomeServiceApi; use Mockery\MockInterface; trait CreatesExternalApiMock { public function createExternalApiMock(string $method, ...$args): MockInterface { $stubMethod = "{$method}Stub"; if (!method_exists($this, $stubMethod)) { throw new \InvalidArgumentException('method not found.'); } return $this->$stubMethod($args); } private function pingStub(array $args): MockInterface { $state = $args[0] ?? 'default'; $method = "pingStub_${state}"; $options = $args[1] ?? []; if (!method_exists($this, $method)) { throw new \InvalidArgumentException('state not found.'); } return $this->$method($options); } private function pingStub_default(array $options): MockInterface { return $this->mock(ExternalApi::class, function (MockInterface $mock) { $mock->shouldReceive('ping')->with(null)->once()->andReturn('pong'); }); } private function pingStub_multiple(array $options): MockInterface { return $this->mock(ExternalApi::class, function (MockInterface $mock) use ($options) { $mock->shouldReceive('ping')->with($options['times'])->once()->andReturn(implode(',', array_fill(0, $options['times'], 'ping'))); }); } }パターンごとにスタブを用意してやります。
使い方のイメージはこんな感じ。
<?php namespace Tests\Feature\Http\Controllers; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\Concerns\Mock\CreatesExternalApiMock; use Tests\TestCase; class PingControllerTest extends TestCase { use RefreshDatabase; use CreatesExternalApiMock; /** * @param string $state * @param array $args * @param string $expected * @return void * @dataProvider dataPing */ public function testPing(string $state, array $args, string $expected) { $user = factory(User::class)->create(['ping_count' => $args['times'] ?? null]); $this->createExternalApiMock('ping', $state, $args); $response = $this ->actingAs($user) ->get('/'); $response ->assertOk() ->assertJson(['message' => $expected]); } public function dataPing(): array { return [ '1' => [ 'state' => 'default', 'args' => [], 'expected' => 'pong', ], '2' => [ 'state' => 'multiple', 'args' => ['times' => 2], 'expected' => 'pong,pong', ], ]; } }個人的には入出力のパターンのテストはフィーチャーテストではなくユニットテストでやりたいですが、今回は例なのでフィーチャーテストに書きました。
おわりに
いくつかのプロダクトでこの方法を導入していますが、いまのところ便利に使えています。他にも取り入れられそうなアイデアがあれば、コメント欄にて教えていただけると助かります
- 投稿日:2021-03-04T14:43:29+09:00
LaravelとVue.jsを使った見積作成アプリ その3
前回の復習
前回はVue.jsを使用し見積編集ページのテンプレートを作成しました。今回はdompdfを利用したpdf表示ページとユーザー認証関係のページを作成していきます。
dompdfの導入
larave-dompdfパッケージのインストールはcomposerを使用して行います。
$ composer require barryvdh/laravel-dompdfconfig/app.phpを以下のように編集します。
providersに下記を追記します。app.phpBarryvdh\DomPDF\ServiceProvider::class,aliasesに下記を追記します。
app.php'PDF' => Barryvdh\DomPDF\Facade::class,コントローラーの作成
PDFの作成を行うため、コマンドを使ってコントローラーを作成します。
$ php artisan make:controller PDFController作成されたコントローラーに記述していきます。
PDFController.php<?php namespace App\Http\Controllers; use App\Estimate; use App\Item; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use PDF; class PDFController extends Controller { public function index(Request $request){ $user = Auth::user(); $estimate_id = $request->input('estimate'); $estimate = Estimate::find($estimate_id); $estimated_at = $estimate->estimated_at; $items = Item::where('estimate_id', $estimate_id)->orderBy('id')->get(); $item_price = Item::where('estimate_id', $estimate_id)->get(['quantity', 'unit_price']); $sum_price = 0; if($user->id != $estimate->user_id) { return redirect()->route('estimates.index'); } for($i=0; $i<count($item_price); $i++){ $calculation_price = $item_price[$i]['quantity'] * $item_price[$i]['unit_price']; $sum_price += $calculation_price; } $pdf = PDF::loadView('pdf/generate_pdf', [ 'user' =>$user, 'estimate' => $estimate, 'estimated_at' => date('Y年m月d日', strtotime($estimated_at)), 'items' => $items, 'sum_price' => $sum_price, ]); return $pdf->stream(); } }loadView() を利用することで、通常のビューを用意するのと同じ手順で、PDFに出力したい内容をHTMLで記述することができるようになります。変数を渡すことも可能です。
ルーティングの作成
web.phpRoute::get('pdf','PDFController@index');これでテンプレートを作成すればPDFが表示されます。
日本語化
PDFの作成は可能になりましたが、このままだと日本語が文字化けしてしまいます。日本語のPDFが作成できるように設定を行います。
fontsディレクトリの作成
strageディレクトリの下にfontsディレクトリを作成します。
$ mkdir fontsIPAフォントのダウンロード
以下のサイトからIPAフォントをダウンロードします。
https://moji.or.jp/ipafont/zipファイルとしてダウンロードされるので、解凍後、その中にあるファイルを先ほど作成したstorage/fonts/ディレクトリの下にコピーしてください。
これで無事日本語に対応しましたのでテンプレートを作成していきます。
PDFテンプレートの作成
views/pdf/generate_pdf.blade.phpを作成し記述していきます。
generate_pdf.blade.php<!doctype html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <style type="text/css"> @font-face { font-family: ipam; font-style: normal; font-weight: normal; src: url('{{ storage_path('fonts/ipam.ttf') }}') format('truetype'); } @font-face { font-family: ipam; font-style: bold; font-weight: bold; src: url('{{ storage_path('fonts/ipam.ttf') }}') format('truetype'); } body { font-family: ipam !important; } .box { width : 50px; height : 50px; background : white; border : medium solid #000000; position: absolute; } </style> </head> <body> <h1 style="text-align:center;">御見積書</h1> <div style="float:left"> <h2>{{ $estimate['customer'] }}様</h2> <p>下記の通り御見積申し上げます</p> <p>件名:{{ $estimate['title'] }}</p> <p>納入期限:{{ $estimate['deadline_at'] }}</p> <p>納入場所:{{ $estimate['location'] }}</p> <p>取引方法:{{ $estimate['transaction'] }}</p> <p>有効期限:{{ $estimate['effectiveness'] }}</p> <u><h2>御見積合計金額 ¥{{ number_format($sum_price * 1.1) }}-</h2></u> </div> <div style="float:right"> <p>見積日 {{ $estimated_at }}</p> <p style="padding-top:25px">〒{{ $user['postal_code'] }}</p> <p>{{ $user['address'] }}</p> <p>{{ $user['address2'] }}</p> <p>{{ $user['company'] }}</p> <p>TEL: {{ $user['phone_number']}} FAX: {{ $user['fax_number'] }}</p> <p>担当者: {{ $user['name'] }}</p> </div> <p class="box" style="margin-top:375px;margin-left: 547px;"></p> <a class="box" style="margin-top:375px;margin-left: 597px;"></a> <a class="box" style="margin-top:375px;margin-left: 647px;"></a> <div style="margin-top:360px"> <table border="1" width="100%" cellspacing="0" cellpadding="0" style="table-layout: auto"> <tr> <th>商品名</th> <th>単位</th> <th>数量</th> <th>単価</th> <th>金額</th> <th>備考</th> </tr> @foreach ($items as $item) <tr> <td> {{ $item['name'] }} </td> <td style="text-align:center"> {{ $item['unit'] }} </td> <td style="text-align:right"> {{ $item['quantity'] }} </td> <td style="text-align:right"> {{ number_format($item['unit_price']) }} </td> <td style="text-align:right"> {{ number_format($item['quantity'] * $item['unit_price']) }} </td> <td style="font-size:12px"> {{ $item['other'] }} </td> </tr> @endforeach <tr> <td style="text-align:right"><税抜合計金額></td> <td></td> <td></td> <td></td> <td style="text-align:right">{{ number_format($sum_price) }}</td> <td></td> </tr> <tr> <td style="text-align:right"><消費税></td> <td></td> <td></td> <td></td> <td style="text-align:right">{{ number_format($sum_price * 0.1) }}</td> <td></td> </tr> <tr> <td>毎度ありがとうございます。</td> <td></td> <td></td> <td style="text-align:center">合計</td> <td style="text-align:right">{{ number_format($sum_price * 1.1) }}</td> <td></td> </tr> </table> </div> </body> </html>headタグの@font-faceでは使用するフォントの設定を行っています。storage_pathにはIPAフォントを保存したstorage/fontsを指定します。
font-style:normalだけでは、h1タグのようなfont-weightがboldに設定されているフォントに文字化けが発生します。上記のようにfont-faceを複数設定することで対応できます。これでPDF表示ページが完成しました。
認証機能
ユーザーと見積を紐付ける
認証機能の追加にともなって、データ構造としてはまずユーザーが存在して、ユーザーごとに自分の見積もりを作っていく形にしたいと思います。
マイグレーション
$ php artisan make:migration add_user_id_to_estimates --table=estimates作成されたファイルを編集していきます。
add_user_id_to_estimates.php<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class AddUserIdToEstimates extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('estimates', function (Blueprint $table) { $table->integer('user_id')->unsigned(); $table->foreign('user_id')->references('id')->on('users'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('estimates', function (Blueprint $table) { $table->dropColumn('user_id'); }); } }upメソッドではuser_idカラムを追加して外部キーを設定する処理を記述しています。downメソッドでは逆にuser_idカラムを削除する処理を記述しています。
マイグレーションを実行しましょう。$ php artisan migrate:fresh次にユーザーと見積の関係性をモデルに記述していきます。
User.phpclass User extends Authenticatable { // 中略 public function folders() { return $this->hasMany('App\Folder'); } }シーダーの作成
ユーザーのシーダーを作成しましょう。
$ php artisan make:seeder UsersTableSeederdatabase/seeds/UsersTableSeeder.php が作成されるので、以下の内容で編集します。
UserTableSeeder.php<?php use Carbon\Carbon; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; class UsersTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { DB::table('users')->insert([ 'name' => 'test', 'email' => 'dummy@email.com', 'password' => bcrypt('test1234'), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); } }データベース上、ユーザーのパスワードは必ず暗号化してデータベースに保存します。平文では保存しません。bcrypt関数は与えられた文字列の暗号化を行います。
次に見積テーブル用のシーダーを編集します。星マークが追加した行です。
EstimateTableSeeder.php<?php use Carbon\Carbon; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; class EstimateTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $user = DB::table('users')->first(); // ★ $titles = ['2021年おめでとうセール', '商品見積の件', 'サンプル見積の件']; $customers = ['株式会社XXX', '株式会社YYY', '株式会社ZZZ']; foreach (array_map(NULL, $titles, $customers) as [ $title, $customer ]) { DB::table('estimates')->insert([ 'title' => $title, 'user_id' => $user->id, // ★ 'customer' => $customer, 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); } } }会員登録機能
Laravelには認証機能が最初から搭載されています。認証機能を受け持つコントローラーはapp/Http/Controllers/Authディレクトリにすでに用意されています。ルーティングについても認証用の設定を吐き出すメソッドが用意されているので、基本的にはテンプレートを作成するだけでアプリケーションに認証機能を追加することができます。
ルーティング
web.phpAuth::routes();この1行を記述するだけで、会員登録・ログイン・ログアウト・パスワード再設定の各機能で必要なルーティング設定をすべて定義します。
テンプレート
以下の内容で resources/views/auth/register.blade.phpを作成します。
register.blade.php@extends('layout') @section('content') <div class="container"> <div class="row"> <div class="col col-md-offset-3 col-md-6"> <nav class="panel panel-default"> <h2 class="panel-heading" style="padding-top:25px">会員登録</h2> <div class="panel-body"> @if($errors->any()) <div class="alert alert-danger"> @foreach($errors->all() as $message) <p>{{ $message }}</p> @endforeach </div> @endif <form action="{{ route('register') }}" method="POST"> @csrf <div class="form-group"> <label for="email">メールアドレス</label> <input type="text" class="form-control" id="email" name="email" value="{{ old('email') }}" /> </div> <div class="form-group"> <label for="name">ユーザー名</label> <input type="text" class="form-control" id="name" name="name" value="{{ old('name') }}" /> </div> <div class="form-group"> <label for="password">パスワード</label> <input type="password" class="form-control" id="password" name="password"> </div> <div class="form-group"> <label for="password-confirm">パスワード(確認)</label> <input type="password" class="form-control" id="password-confirm" name="password_confirmation"> </div> <div class="text-right"> <button type="submit" class="btn btn-dark">送信</button> </div> </form> <div class="text-center" style="padding-top:25px"> <a href="{{ route('login.guest') }}"><button class="btn btn-dark">ゲストユーザーとしてログイン</button></a> </div> </div> </nav> </div> </div> </div> @endsection次にログイン機能を実装します。
resources/views/auth/login.blade.phpを以下の内容で作成します。
login.blade.php@extends('layout') @section('content') <div class="container"> <div class="row"> <div class="col col-md-offset-3 col-md-6"> <nav class="panel panel-default"> <h2 class="panel-heading" style="padding-top:25px">ログイン</h2> <div class="panel-body"> @if($errors->any()) <div class="alert alert-danger"> @foreach($errors->all() as $message) <p>{{ $message }}</p> @endforeach </div> @endif <form action="{{ route('login') }}" method="POST"> @csrf <div class="form-group"> <label for="email">メールアドレス</label> <input type="text" class="form-control" id="email" name="email" value="{{ old('email') }}" /> </div> <div class="form-group"> <label for="password">パスワード</label> <input type="password" class="form-control" id="password" name="password" /> </div> <div class="text-right"> <button type="submit" class="btn btn-dark">ログイン</button> </div> </form> </div> </nav> <div class="text-center" style="padding-top:25px"> <a href="{{ route('login.guest') }}"><button class="btn btn-dark">ゲストユーザーとしてログイン</button></a> </div> </div> </div> </div> @endsectionこれでログイン機能の実装は完了です。
プロフィール編集ページ
最後に見積に表示するためのプロフィール編集ページを作成します。
コントローラーの作成
プロフィール編集ページを作成するためのユーザーコントローラーを作成します。
$ php artisan make:controller ItemControllerUserController.php<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class UserController extends Controller { public function showEditForm() { $auth = Auth::user(); return view('auth/edit',[ 'auth' => $auth, ]); } public function edit(Request $request) { $current_user = Auth::user(); $current_user->name = $request->name; $current_user->postal_code = $request->postal_code; $current_user->address = $request->address; $current_user->address2 = $request->address2; $current_user->company = $request->company; $current_user->phone_number = $request->phone_number; $current_user->fax_number = $request->fax_number; $current_user->save(); return redirect()->route('estimates.index'); } }Auth::user()でユーザーの情報を取得しユーザー情報を表示・編集します。
ルーティングの追加
web.phpRoute::group(['middleware' => 'api'], function(){ Route::get('/user/edit', 'UserController@showEditForm')->name('user.edit'); Route::post('/user/edit', 'UserController@edit'); });あとはテンプレートを作成するだけです。
ユーザー編集ページのテンプレートを作成
@extends('layout') @section('content') <div class="container"> <h2 class="panel-heading" style="padding-top:25px">ユーザー情報編集</h2> <form action="{{ route('user.edit')}}" method="post"> @csrf <div class="row"> <div class="col-sm-2">ユーザー名</div> <div class="col-sm-10" style="padding: 3px;"> <input type="text" name="name" value="{{$auth->name}}"> </div> </div> <div class="row"> <div class="col-sm-2">郵便番号</div> <div class="col-sm-10" style="padding: 3px;"> <input type="text" name="postal_code" value="{{$auth->postal_code}}"> </div> </div> <div class="row"> <div class="col-sm-2">住所</div> <div class="col-sm-10" style="padding: 3px;"> <textarea name="address" value="{{$auth->address}}">{{$auth->address}}</textarea> </div> </div> <div class="row"> <div class="col-sm-2">ビル・マンション名</div> <div class="col-sm-10" style="padding: 3px;"> <textarea name="address2">{{$auth->address2}}</textarea> </div> </div> <div class="row"> <div class="col-sm-2">会社名</div> <div class="col-sm-10" style="padding: 3px;"> <textarea name="company">{{$auth->company}}</textarea> </div> </div> <div class="row"> <div class="col-sm-2">電話番号</div> <div class="col-sm-10" style="padding: 3px;"> <input type="text" name="phone_number" value="{{$auth->phone_number}}"> </div> </div> <div class="row"> <div class="col-sm-2">FAX番号</div> <div class="col-sm-10" style="padding: 3px;"> <input type="text" name="fax_number" value="{{$auth->fax_number}}"> </div> </div> <div style="padding-top:25px;"> <button type="submit" class="btn btn-dark">保存</button> </div> </form> </div> @endsectionこれで予定していた全ての機能が完成しました。
LaravelとVue.js共にまだまだ使いこなせてない機能がたくさんあるので勉強と開発を進めていこうと思います。
- 投稿日:2021-03-04T14:07:22+09:00
LaravelでぐるなびAPIを叩く方法(Guzzle)
Laravel5.8を使用している。
手順
guzzleをインストール
composer require guzzlehttp/guzzleコントローラーに処理を書く
今回はぐるなびAPIでレストラン検索をするから、
RestaurantControllerを作成し、その中にget_restaurantアクションを作成した。RestaurantControllerpublic function get_restaurant() { $client = new \GuzzleHttp\Client(); $response = $client->request( 'GET', 'https://api.gnavi.co.jp/RestSearchAPI/v3/', ['query' => [ 'keyid' => '123123123123123', 'pref' => 'PREF39', 'freeword' => '餃子', 'hit_per_page' => 30 ]] ); $data = [ 'res' => json_decode($response->getBody(), true) ]; return view('restaurant.index', $data);まず、Clientのインスタンスを作成し、その中に
メソッド
URL
クエリ
を入れて、リクエストとして送っている。getBody()メソッドで中身を取得することはできるが、これはオブジェクトが返されるため、
json_decodeでjson形式に変換している。ビューを編集
restaurant/index.blade.php@foreach ($res['rest'] as $r) <div class="col-md-8"> <div class="card"> <div class="card-header">おすすめ</div> <div class="card-body"> <h4>{{ $r['name'] }}</h4> <br> <p>{{ $r['pr']['pr_long'] }}</p> </div> </div> </div> @endforeachぐるなびAPIのレストラン検索機能では、
['rest']の中に複数のお店の情報が格納されており、さらに、その一つ一つのお店に
店名やその他詳細がぞれぞれ格納されているから、rest-0-name -url -pr -1-name -url -pr -2-name -url -prこのような順番で格納されている
- 投稿日:2021-03-04T11:02:50+09:00
Laravel Authのユーザ登録とログインのそれぞれのカスタマイズのやり方
ユーザ登録は
name
,age
,tel
,address
,password
に変更し、
ログイン画面はtel
,password
に変更する。ユーザ登録のカスタマイズ
マイグレーションの作成
デフォルト状態から、カラムを変更する。
create_users_table.php$table->bigIncrements('id'); $table->string('name'); $table->integer('age'); $table->unsignedBigInteger('tel'); $table->string('address'); $table->string('password'); $table->rememberToken(); $table->timestamps();モデルの編集
$fillableのカラムを変更する
User.phpprotected $fillable = [ 'name', 'age', 'tel', 'address', 'password', ];コントローラーの編集
バリデーションアクション、新規保存アクションそれぞれを変更する
register.phpprotected function validator(array $data) { return Validator::make($data, [ 'name' => ['required', 'string', 'max:255'], // 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'age' => ['required', 'integer', 'max:255'], 'tel' => ['required', 'numeric', 'digits_between:10,11'], 'address' => ['required', 'string', 'max:255'], 'password' => ['required', 'string', 'min:8', 'confirmed'], ]); } protected function create(array $data) { return User::create([ 'name' => $data['name'], // 'email' => $data['email'], 'age' => $data['age'], 'tel' => $data['tel'], 'address' => $data['address'], 'password' => Hash::make($data['password']), ]); }register.blade.phpの変更
メールアドレスの項目を削除し、name項目をコピーして、
name
と記載している箇所をすべて、適当なカラム名にする。
例として、age項目のみ下に記載している。register.blade.php//age <div class="form-group row"> <label for="age" class="col-md-4 col-form-label text-md-right">{{ __('Age') }}</label> <div class="col-md-6"> <input id="age" type="number" min="1" class="form-control @error('age') is-invalid @enderror" name="age" value="{{ old('age') }}" required autocomplete="age" autofocus> @error('age') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div>以上の編集で新規登録のカスタマイズはできているはず。
ログイン機能のカスタマイズ
bladeファイルの編集
emailの項目を参考に、emailと記載の箇所を全て、
tel
に変更している。login.blade.php//tel <div class="form-group row"> <label for="tel" class="col-md-4 col-form-label text-md-right">{{ __('Tel') }}</label> <div class="col-md-6"> <input id="tel" type="text" class="form-control @error('tel') is-invalid @enderror" name="tel" value="{{ old('tel') }}" required autocomplete="tel" autofocus> @error('tel') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div>コントローラの編集
login.blade.phpを開いてみると、ログイン処理のアクション見当たらない。ログイン処理自体は、そのファイル内に記載している、
use AuthenticatesUsers;
の中身で実行されている。だから、そのクラスの中身を変更する必要がある。
以下のアクションを以下のように変更する。
一つ目がログインの際のバリデーション設定で、二つ目はよくわからないが、
emailを対象にしているよっていうことを宣言しているだけだと思う。だからここをtelに変更して、
telがログイン対象だよってことを変更した。AuthenticatesUsers.phpprotected function validateLogin(Request $request) { $request->validate([ $this->username() => 'required', 'password' => 'required|string', ]); } public function username() { return 'tel'; }以上の編集でログインカスタムができていると思う。
- 投稿日:2021-03-04T09:49:58+09:00
Laravel クエリビルダで実行しているSQLを取得する
# 目的
- Laravelのクエリビルダで実行しているSQLを取得する方法をまとめる
環境
- ハードウェア環境
項目 情報 OS macOS Catalina(10.15.5) ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports) プロセッサ 2 GHz クアッドコアIntel Core i5 メモリ 32 GB 3733 MHz LPDDR4 グラフィックス Intel Iris Plus Graphics 1536 MB
- ソフトウェア環境
項目 情報 備考 PHP バージョン 7.4.8 Homebrewを用いてこちらの方法で導入→Mac HomebrewでPHPをインストールする Laravel バージョン 6.X commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする 情報
- 筆者はMacのローカルにて本記事に記載されている内容を実装し確認する。
方法
下記のようなクエリビルダの記載が任意のメソッド内にあったとする。
$saveInfo = $this->content; $saveInfo['user_id'] = $saveData['userId']; $saveInfo['content'] = $saveData['content']; $saveInfo->save();下記のようにすることで変数
$sql
に実行直前で実行されたSQL文が格納される。$saveInfo = $this->content; $saveInfo['user_id'] = $saveData['userId']; $saveInfo['content'] = $saveData['content']; $saveInfo->save(); $sql = $saveInfo->toSql();下記のように記載することでSQL文をログに出力する事ができる。
use Illuminate\Support\Facades\Log; $saveInfo = $this->content; $saveInfo['user_id'] = $saveData['userId']; $saveInfo['content'] = $saveData['content']; $saveInfo->save(); $sql = $saveInfo->toSql(); Log::info($sql);実際のメソッド内部に処理を記載したものを下記に記載する。筆者はRepositoryにDBアクセス処理を記載している。
アプリ名ディレクトリ/Repositories/ContntRepository.php<?php namespace App\Repositories; use App\Repositories\ContentRepositoryInterface; use App\Models\Content; use Illuminate\Support\Facades\Log; class ContentRepository implements ContentRepositoryInterface { /** * @var Content */ private $content; public function __construct(Content $content) { $this->content = $content; } /** * 全ての投稿内容を取得する * * @return Content|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null */ public function getContentAll() { return $this->content->all(); } public function saveContent($saveData) { $saveInfo = $this->content; $saveInfo['user_id'] = $saveData['userId']; $saveInfo['content'] = $saveData['content']; $saveInfo->save(); $sql = $saveInfo->toSql(); Log::info($sql); return true; } }
- 投稿日:2021-03-04T08:16:32+09:00
docker-composeでcomposer require した時にFatal error: Allowed memory size of 1610612736 bytes exhausted ....が出た対処法
解決策
php -d memory_limit=-1 /usr/bin/composer require ****/****
- 投稿日:2021-03-04T00:15:24+09:00
【Laravel】FromRequestのバリデーションルールを共通化する + おまけ
・ヮ・)あ、おはようございまーす
みなさんは複数のリクエストで同じパラメータをバリデーションするときにどうしていますか?
私はコピペで書いていたのですが、ルール変更したいときに修正コスト高くなったり
同じパラメータなのに違うルールが書いてあったり…そんなことがあったので Laravelのバリデーションルールを共通化するやつ を作りました
メモとして置いておきますインストール方法
というにはおこがましいですが、ファイル1つだけなんで ValidationRules.php を app 直下にコピーしてださい
※違うディレクトリに置く場合は、名前空間をよしなに書き換えてください
使い方
ルールを定義する
RULES
にバリデーションルールを定義しますprotected const RULES = [ 'posts' => [ 'title' => ['string', 'between:10,255'], 'body' => ['string'], 'publish_at' => ['date'], ], ];
- ネストすることもできます
- ルールは配列記法、
|
区切りの文字列記法に対応していますルールを取得する
FormRequest などバリデーションルールを取得したいところで
getRules()
を呼び出しますpublic function rules(\App\ValidationRules $rules) { return $rules->getRules([ 'posts.title' => ['required'], 'posts.body', 'posts.publish_at', ]); }
- ネストされたルールは
.
(ドット)記法で書いてください- 追加したいルールがあれば、連想配列にして渡してあげてください
- 定義していないルールを取得しようとすると
OutOfRangeException
がスローされます上の例では以下のルールが返ります
[ 'title' => ['required', 'string', 'between:10,255'], 'body' => ['string'], 'publish_at' => ['date'], ]パラメーター名に別名をつける
users.name
とpets.name
のルールを取得しようとすると、後に指定したpets.name
のルールのみname
のキーで返ってきますこのような状況を避けるには第2引数で別名を指定します
$keyAliases = [ 'users.name' => 'user_name', 'pets.name' => 'pet_name', ]; app(\App\ValidationRules::class)->getRules( [ 'users.name', 'pets.name', ], $keyAliases );また、常に別名を付けたいパラメータ名がある場合は
ValidationRules::KEY_ALIASES
に定義してください
getRules()
の第2引数
KEY_ALIASES
定数
ドット区切りの最後の文字列(デフォルト)
の優先順位でパラメータ名が採用されます※パラメータ名を変えた場合は、
required_if
のような他のパラメータを参照するルールに気をつけてください
※また、attributes
にも気をつけてくださいカスタマイズする
ルールを定義する場所を変更する
ValidationRules::RULES
じゃない場所にしたい場合はall()
を書き換えるか、ValidationRules
を継承してオーバーライドしてください指定したルールが見つからなかった場合の挙動を変えたい
デフォルトでは定義していないルールを取得しようとすると
OutOfRangeException
がスローされます定義していないルールを取得しようとしたとき、空配列を返したい場合は
notFound()
を書き換えるか、ValidationRules
を継承してオーバーライドしてくださいおまけ
FormRequestにゲッターメソッドを作る
コントローラーで Requestの
input
all
query
get
__get
only
except
使うのって怖くないですか?
FormRequestでバリデーションされているから安心?
FormRequest側でバリデーションルール変更したとき影響の調査面倒じゃないでしょうか?public function getEmail(): string { return $this->input('email'); }こんなメソッドをバリデーションを実施しているFormRequestに書いてあげて
ついでにユニットテストも書いてあげれば
コントローラーで安心してリクエストパラメーターを受け取れますねまた、バリューオブジェクトに変換してあげることもできるのでDDDニキもにっこりですね
FormRequestでゲッターを書いてよりよいLaravelライフを!