- 投稿日:2020-03-01T23:20:18+09:00
laravelの学習のために音楽レビューを投稿できるサイトを作成した
Web製作会社でマークアップエンジニアとして1年業務を行いました。
その中で、今後はバックエンドに携わっていきたいと思いPHPの勉強を始め、laravelでウェブサービスを作ってみました。
まだまだ、理解していないところがあるので引き続き勉強します。トップページ
トップページはメインビジュアルとサービスの紹介です。
下の方にある「投稿一覧ページへ」のボタンから投稿一覧へリンクします。投稿一覧ページ
検索フォームにテキストを入れると該当のテキストが入力されている投稿だけ表示します。
「投稿一覧詳細ページへ」のボタンから各投稿の詳細ページに飛ぶことが出来ます。投稿詳細ページ
各カテゴリの詳細を表示します。
投稿の「編集」と「削除」については、投稿したアカウントのみ可能な仕様です。投稿編集ページ
現在の投稿内容がテキストボックスに表示され、投稿を修正後に「作成する」を押すと確定します。投稿について
ログインした状態のみ、ウインドウの右下に「レビューする」が表示され、投稿ページに移動できます。投稿ページ
各カテゴリの内容を入力して「作成する」を押すと投稿が作成できます。今後の修正点
・投稿一覧ページでのページネーションの実装。
・カテゴリ、投稿者名での検索をできるようにする。
- 投稿日:2020-03-01T22:51:53+09:00
LaravelでISO-2022-JPのメールを送りたい
Laravel でメールを送信すると、デフォルトでは文字コードが UTF-8 になります。
そこで、ISO-2022-JP で送信する方法を考えてみました。前提
- Laravel 6.12
やり方1
まずは、
Mailableクラスで頑張るやり方です。コンストラクタの中に処理を追加します。また、
withSwiftMessage()の中でもsetCharset()とsetEncoder()をコールしています。app/Mail/CustomerRegistered.phpnamespace App\Mail; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; use Swift_DependencyContainer; use Swift_Mime_ContentEncoder_PlainContentEncoder; use Swift_Preferences; class CustomerRegistered extends Mailable { use Queueable, SerializesModels; /** * Create a new message instance. * * @return void */ public function __construct() { /* 処理追加 ここから */ Swift_DependencyContainer::getInstance() ->register('mime.qpheaderencoder') ->asAliasOf('mime.base64headerencoder'); Swift_Preferences::getInstance()->setCharset('iso-2022-jp'); /* 処理追加 ここまで */ } /** * Build the message. * * @return $this */ public function build() { return $this->from('admin@example.com') ->subject('件名テスト') ->text('mails.customer_registered') ->withSwiftMessage(function ($message) { /* 処理追加 ここから */ $message->setCharset('iso-2022-jp'); $message->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder('7bit')); /* 処理追加 ここまで */ }); }このやり方の良いところは、メール(
Mailableクラス)ごとに文字コードを設定することが明確になることだと思います。1
Swift_Preferences::getInstance()->setCharset('iso-2022-jp')をコールすると、Laravel が送信する全ての文字コードが ISO-2022-JP に設定されます。2そのため、UTF-8 で送信したい場合は、以下のように
setCharset()にutf-8を指定する必要があります。app/Mail/CustomerChanged.phpnamespace App\Mail; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; use Swift_Preferences; class CustomerChanged extends Mailable { use Queueable, SerializesModels; /** * Create a new message instance. * * @return void */ public function __construct() { /* 処理追加 ここから */ Swift_Preferences::getInstance()->setCharset('utf-8'); /* 処理追加 ここまで */ } /** * Build the message. * * @return $this */ public function build() { return $this->from('admin@example.com') ->subject('件名テスト') ->text('mails.customer_changed'); } }やり方2
続いて、サービスプロバイダでがんばるやり方。
Laravel 5.4でiso-2022-jpエンコードのメールを送信する に記載されているものを、ほぼ引用させていただきました。
app/Providers/AppServiceProvider.phpnamespace App\Providers; use Illuminate\Support\ServiceProvider; use Swift; use Swift_DependencyContainer; use Swift_Preferences; class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { /* 処理追加 ここから */ Swift::init(function () { Swift_DependencyContainer::getInstance() ->register('mime.qpheaderencoder') ->asAliasOf('mime.base64headerencoder'); Swift_Preferences::getInstance()->setCharset('iso-2022-jp'); }); /* 処理追加 ここまで */ } /** * Bootstrap any application services. * * @return void */ public function boot() { // } }app/Mail/CustomerRegistered.phpnamespace App\Mail; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; use Swift_Mime_ContentEncoder_PlainContentEncoder; class CustomerRegistered extends Mailable { use Queueable, SerializesModels; /** * Create a new message instance. * * @return void */ public function __construct() { } /** * Build the message. * * @return $this */ public function build() { return $this->from('admin@example.com') ->subject('件名テスト') ->text('mails.customer_registered') ->withSwiftMessage(function ($message) { /* 処理追加 ここから */ $message->setCharset('iso-2022-jp'); $message->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder('7bit')); /* 処理追加 ここまで */ }); }システム全体でメールを送るときは必ず ISO-2022-JP にするということであれば、このやり方が一番良いように思います。
あえて難点を挙げると、サービスプロバイダで設定するだけでなく、
Mailableクラスでも相変わらず設定しなければならないということでしょうか。たかが 2 行と思ったけど、setEncoder()とかコールし忘れそうで...やり方3
最後は、SwiftMailer のプラグインでがんばるやり方です。
「もう Laravel じゃないじゃん!」というツッコミもあるかと思いますが、無視します(笑)
まず、以下のようなプラグインを作ります。
beforeSendPerformed()はメール送信前に自動的にコールされるようです。app/Common/JapaneseMailPlugin.phpnamespace App\Common; use Swift_Events_SendListener; use Swift_Events_SendEvent; use Swift_Mime_ContentEncoder_PlainContentEncoder; use Swift_DependencyContainer; use Swift_Preferences; class JapaneseMailPlugin implements Swift_Events_SendListener { public function __construct() { Swift_DependencyContainer::getInstance() ->register('mime.qpheaderencoder') ->asAliasOf('mime.base64headerencoder'); Swift_Preferences::getInstance()->setCharset('iso-2022-jp'); } /** * Invoked immediately before the Message is sent. * * @param Swift_Events_SendEvent $evt */ public function beforeSendPerformed(Swift_Events_SendEvent $evt) { $message = $evt->getMessage(); $message->setCharset('iso-2022-jp'); $message->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder('7bit')); } /** * Invoked immediately after the Message is sent. * * @param Swift_Events_SendEvent $evt */ public function sendPerformed(Swift_Events_SendEvent $evt) { // 特に何もしない } }一方、
Mailableクラスでは、コンストラクタで、プラグインの登録のみを行います。namespace App\Mail; use App\Common\JapaneseMailPlugin; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; class CustomerRegistered extends Mailable { use Queueable, SerializesModels; /** * Create a new message instance. * * @return void */ public function __construct() { /* 処理追加 ここから */ app()->make('swift.mailer')->registerPlugin(new JapaneseMailPlugin()); /* 処理追加 ここまで */ } /** * Build the message. * * @return $this */ public function build() { return $this->from('admin@example.com') ->subject('件名テスト') ->text('mails.customer_registered'); } }このやり方の良いところは、
Mailableクラスで1行だけ書けば良いということです。3 また、上記の例ではwithSwiftMessage()を消してしまいましたが、文字コード以外の設定を行うためにwithSwiftMessage()を記載しても問題ありません。難点は、Laravel で処理が完結せず、SwiftMailer まで理解する必要があるということでしょうか。保守性を考えると、あまり良くないかもしれません。
まとめ的な感想
キレイさでいえば やり方3 でしょうけど、ちょっとトリッキーかもしれません。
そう考えると、一番理解してもらえそうなのは、 やり方2 でしょうか。でも一番は UTF-8 にすること だと思いますけど(笑)
参考
- Laravel 公式ドキュメント
- SwiftMailer 公式ドキュメント
- Laravelで文字コードISO-2022-JPのメールを送る
- [PHP]Swift Mailerで日本語(ISO-2022-JP)のメールを送信
そもそも、システム内で、送信するメールごとに文字コードを分けるようなことが現実にあるのか?とは思いますが... ↩
一方、
withSwiftMessage()の中でコールしているsetCharset()は、メール本文に文字コードを設定するためのものです。なぜ、こちらのsetCharset()は各メールごとに設定する前提なのに、Swift_Preferences::setCharset()は SwiftMailer 全体に設定する前提なのか。一貫性がなくて気持ち悪い。 ↩逆に言えば「1行はまだ書かないといけない」ということでもあります。 SwiftMailer はシングルトン結合されているので、
registerPlugin()は1回だけ行えば十分だとは思いますが、思いの外、良い方法が思いつかなかったので、一旦、そのままにしておきます。 ↩
- 投稿日:2020-03-01T19:58:37+09:00
【Laravel】フォームリクエストでバリデーションをしよう
入力フォームやクエリパラメータをフォームリクエストでバリデーションする方法について解説します。
フォームリクエスト作成
まず、フォームリクエストを作成します
ターミナルから下記のコマンドで作成しましょう
下記のコマンドでapp/Http/Requests/ディレクトリにSampleRequest.phpという名前のRequestクラスを作成できますターミナルphp artisan make:request SampleRequestSampleRequestクラスについて
作成されたSampleRequestクラスで確認しましょう
SampleRequestクラス内にauthorizeメソッドとrulesメソッドができていると思いますrulesメソッドはバリデーションの規則を定義する
下記のように
rulesメソッドを定義すると、title、bodyそれぞれに対して下記のような条件を設定しています。
titleカラムには必須、postテーブル内での一意性、最大文字数を255に設定しています。
bodyカラムには必須条件を設定しています
他の条件については公式ページのバリデーションのページを参考に設定してみてくださいapp/Http/Requests/SampleRequest.phppublic function rules() { return [ 'title' => 'required|unique:posts|max:255', 'body' => 'required', ]; }authorizeメソッドはフォームリクエストの使用できる条件を定義
ビューから送信されるURLの設定
保存処理を行うアクションへのURLを下記のように設定している場合を考えています
Route::post('comment/{comment}');
authorizeメソッドの説明
authorizeメソッドはユーザーが、指定されたデータを更新する権限を持っているのかを確認します。
下記のコードでは、ユーザーが更新しようとしているブログコメントを所有しているかを確認します。app/Http/Requests/SampleRequest.phppublic function authorize() { $comment = Comment::find($this->route('comment')); return $comment && $this->user()->can('update', $comment); }常に判定に成功する場合
もし、権限判定を他の場所でしている、または権限判定が不要な場合は、
下記のように返り値をtrueにして常に判定が通るようにしてください。app/Http/Requests/SampleRequest.phppublic function authorize() { return true; }上記の設定でリクエストでバリデーションを使用することができます!!
フォームリクエストの使い方
フォームから送信された値、クエリパラメータを使用する時はコントローラーアクション引数にRequestクラスのインスタンスを設定すると思います。
そこのクラス名を今回独自で設定したSampleRequestを指定してください。
そうするとインスタンス$requestを作成する時に自動でバリデーションをかけてくれます。コントローラー.phppublic function store(SampleRequest $request) { // 送信されたリクエストが正しい場合実行される }バリデーションに失敗すると、前のアドレスにユーザーを戻します。
その時に、エラーメッセージも表示できるように、フラッシュデーターとしてセッションに保存されます。エラーメッセージのカスタマイズ
上記の設定のままではエラーメッセージはデフォルトのままです。
英語で表示され、ユーザーに対して優しい設定とは言えません。
より具体的にエラー内容を設定しましょう
messagesメソッドでメッセージの配列を設定しましょう。app/Http/Requests/SampleRequest.phppublic function messages() { return [ 'title.required' => 'タイトルは必須項目です', 'body.required' => '内容は必須項目です', ]; }以上です!!!
ここまで読んでいただきありがとうございました!!
疑問、気になるところがございましたら、質問、コメントよろしくお願いします!!!
- 投稿日:2020-03-01T19:54:18+09:00
【Laravel】ルーティングを甘くみてたら痛い目にあった話
web.php のルーティングは甘くみちゃいけない話
練習用で画像投稿機能つきの簡単なウェブアプリを制作しています。
PHP/Laravel学習歴3ヶ月ほどのプログラミング初学者の僕が個人的な備忘録として記述しました。とりあえず書いておけば良い!と思っていた自分
web.phpにはとりあず必要なものを記述しておけば良いと思っていた。
しかし、エラーが表示されたり、表示されなかったとしても、思い通りに関数が機能してくれないなど問題ばかり発生して、結果的に解決するのに数日を要した。初めに記述したルーティング(間違い)
---(略)--- Route::group(['middleweare' => 'auth'], function () { Route::get('/', 'StoriesController@index'); Route::post('/stories/create', 'StoriesController@store'); Route::post('/stories/create', 'StoriesController@upload'); Route::get('/stories/create', 'StoriesController@upload'); Route::get('/stories/create', 'StoriesController@add'); Route::post('/stories/create', 'StoriesController@add'); }); ---(略)---
StoriesControllerのuploadに写真をアップロードするアクションにした。
しかしうまく機能してくれない。
ただ写真を表示したいページにリダイレクトするだけで写真は投稿できていない。修正後のルーティング(正しい)
---(略)--- Route::group(['middleweare' => 'auth'], function () { Route::get('/', 'StoriesController@index'); Route::post('/', 'StoriesController@store'); Route::get('/stories/create', 'StoriesController@add'); Route::post('/stories/create', 'StoriesController@uplaod'); }); ---(略)---これで、写真を投稿、保存、の二つの機能を実装することができた。
何がしたかったのか
ユーザーからのアクセスを
StoriesControllerのuploadアクションに渡して写真を投稿できるようにしたい。どんなエラーが起こっていたのか
The POST method is not supported for this route. Supported methods:GET,HEAD.
というエラー文が表示されるか、何も変化せず指定したページにリダイレクトする。何が問題だったのか
POSTメソッドと/stories/createの組み合わせが複数存在する
最初の間違えている方のコードの3,4,7行目を実際見てみると、
~::post('/stories/create',~の部分が共通していて、同じ組み合わせが存在していた。
コードは上から書いた順に実行される(当たり前)
間違いの方のコードの最下行のコードをみてみると、最下行のコードが、
Route::post('/stories/create', 'StoriesController@add');と記述されている。
今回実行したかったStoriesControllerのuploadアクションを、このaddアクションが上書きしていた。
それはuploadアクションとaddアクションの~::post('/stories/create',~)が共通していたからである。
コードは上から順に実行される(あたりまえ)。補足
$php artisan route:list
これを実行することによってどのルーティングが読み込まれているのか確認することができる。
- 投稿日:2020-03-01T16:55:56+09:00
スパイを利用してテストコードを書いてみた
概要
背景
テストコードのデザインパターンとしてArrange-Act-Assertパターンというものがあります。
一般的にはこのAAAパターンに従う事で、わかりやすいテストコードが書けますが、実はこの方法で書けない場合があり、ずっと気になっていました。結論
メソッドの差し替えや返り値の検証を行う必要がなく、メソッドが想定通りに呼び出されたかを検証したい場合は、「スパイ」を使う事で、よりわかりやすく書きなおせる事がわかりました。
詳細
実行環境
- PHP 7.2
- Laravel 6.0
スパイとは?
メソッドの呼び出し情報(コール回数など)を記録し、テスト対象の実行後にアサートできるようにするものです。
スパイに関するより詳しい内容は、以下の記事をご参照ください。
- Mockery1.0:テストダブル作成
- Mockery1.0:スパイ
- Wikipedia:テストダブル
- Qiita:Sinon.JS でテストダブルを理解するテスト対象
処理の合間に、フィードバックとしてログを出力するメソッドを対象とします。このメソッドを実行した時に、想定通りにログが出力されているかを確認するテストコードを書いていきたいと思います。
use Illuminate\Support\Facades\Log; class LogClass { public function exec(): void { Log::info('処理開始'); // 実際はログの出力の合間に何かしらの処理が行われる想定 Log::info('処理序盤'); Log::info('処理中盤'); Log::info('処理終盤'); Log::info('処理終了'); } }スパイを使わないテストコード
Laravelにはファサードを最初からモックできるヘルパが準備されているため、以下のように書く事で、想定通りにログが出力されているかを確認する事ができます。
use Tests\TestCase; use Illuminate\Support\Facades\Log; class LogTest extends TestCase { public function testスパイを使わない場合() { // 準備:なし // 期待兼アサート Log::shouldReceive('info')->with('処理開始'); Log::shouldReceive('info')->with('処理序盤'); Log::shouldReceive('info')->with('処理中盤'); Log::shouldReceive('info')->with('処理終盤'); Log::shouldReceive('info')->with('処理終了'); // 実行 (new LogClass)->exec(); }しかし、この書き方だと、以下のような点がテストをわかりにくくしていると思います。
わかりにくい点
- 実行後にアサートを書いていないので、どこで結果の検証を行っているのか伝わりにくい
shouldReceiveという期待用のメソッドがアサートも兼ねているので、ログの呼び出しをモックしているように見えるスパイを使った書き方
前述したコードをスパイを使って書き直してみます。
public function testスパイを使う場合() { // 準備 $spy = Log::spy(); // 実行 (new LogClass)->exec(); // アサート $spy->shouldHaveReceived('info')->with('処理開始'); $spy->shouldHaveReceived('info')->with('処理序盤'); $spy->shouldHaveReceived('info')->with('処理中盤'); $spy->shouldHaveReceived('info')->with('処理終盤'); $spy->shouldHaveReceived('info')->with('処理終了'); } }わかりやすくなった点
- AAAパターンに従った書き方をしているので、検証を行っている箇所がわかりやすい
- 期待用のメソッドではなく、アサート用のメソッドで検証しているので、振る舞いはモックせず元のメソッドを呼び出している事がわかりやすい
まとめ
メソッドの差し替えや返り値の検証を行う必要がなく、メソッドの呼び出しを検証したい場合は、是非「スパイ」を使ってみてください!
- 投稿日:2020-03-01T00:34:45+09:00
Mac laravel環境構築時にエラーが出た話
目的
- 環境構築のlaravelインストール時にでたとあるエラーの解決方法をまとめる
エラー概要
- 下記の手順を参考に環境構築を進めていた。
laravelインストール時に下記のエラーが発生した。
$ composer global require laravel/installer Changed current directory to /Users/shun/.composer Using version ^3.0 for laravel/installer ./composer.json has been created Loading composer repositories with package information Updating dependencies (including require-dev) Your requirements could not be resolved to an installable set of packages. Problem 1 - laravel/installer v3.0.1 requires ext-zip * -> the requested PHP extension zip is missing from your system. - laravel/installer v3.0.0 requires ext-zip * -> the requested PHP extension zip is missing from your system. - Installation request for laravel/installer ^3.0 -> satisfiable by laravel/installer[v3.0.0, v3.0.1]. Installation failed, deleting ./composer.json.エラー内容の和訳を下記に記載する。
現在のディレクトリを/Users/shun/.composerに変更しました laravel / installerにバージョン^ 3.0を使用する ./composer.jsonが作成されました コンポーザリポジトリにパッケージ情報をロードする 依存関係の更新(require-devを含む) 要件をインストール可能なパッケージのセットに解決できませんでした。 問題1 -laravel / installer v3.0.1にはext-zipが必要です*->要求されたPHP拡張zipがシステムにありません。 -laravel / installer v3.0.0にはext-zipが必要です*->要求されたPHP拡張zipがシステムにありません。 -laravel / installer ^ 3.0のインストール要求-> laravel / installer [v3.0.0、v3.0.1]で満足できる。 インストールが失敗し、。/ composer.jsonが削除されました。原因
- HomebrewでPHPがインストールできていないことがわかった。
- エラー分から「ext-zip」と言うものが足りないことがわかった。
解決方法
HomebrewでのPHPのインストール
下記コマンドを実行してPHP7.4をインストールする。
- 下記コマンドでエラーが出た方はこちら
$ brew install php@7.4再度問題のコマンドを実行する。
$ composer global require laravel/installer問題なく実行できた。下記に参考のために
$ composer global require laravel/installer実行時の出力を記載する。Changed current directory to /Users/shun/.composer Using version ^3.0 for laravel/installer ./composer.json has been created Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 14 installs, 0 updates, 0 removals - Installing symfony/process (v5.0.4): Downloading (100%) - Installing symfony/polyfill-ctype (v1.14.0): Downloading (100%) - Installing symfony/filesystem (v5.0.4): Downloading (100%) - Installing psr/container (1.0.0): Downloading (100%) - Installing symfony/service-contracts (v2.0.1): Downloading (100%) - Installing symfony/polyfill-php73 (v1.14.0): Downloading (100%) - Installing symfony/polyfill-mbstring (v1.14.0): Downloading (100%) - Installing symfony/console (v5.0.4): Downloading (100%) - Installing ralouphie/getallheaders (3.0.3): Downloading (100%) - Installing psr/http-message (1.0.1): Downloading (100%) - Installing guzzlehttp/psr7 (1.6.1): Downloading (100%) - Installing guzzlehttp/promises (v1.3.1): Downloading (100%) - Installing guzzlehttp/guzzle (6.5.2): Downloading (100%) - Installing laravel/installer (v3.0.1): Downloading (100%) symfony/service-contracts suggests installing symfony/service-implementation symfony/console suggests installing symfony/event-dispatcher symfony/console suggests installing symfony/lock symfony/console suggests installing psr/log (For using the console logger) guzzlehttp/psr7 suggests installing zendframework/zend-httphandlerrunner (Emit PSR-7 responses) guzzlehttp/guzzle suggests installing psr/log (Required for using the Log middleware) Writing lock file Generating autoload files





