- 投稿日: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-01T22:44:31+09:00
三角形 is-a 多角形なんて嘘じゃん!―継承を利用する前にこの問題に正解してください【php】【オブジェクト指向】
0. 初めに
本記事の結論は
各フィールドに対するgetter, setter, changerのオーバライドの有無は統一する必要がある
この一言に尽きます。
本記事は、僕がつい最近まで勘違いしていたことをまとめた記事です。
オブジェクト指向で「継承」の機能を利用する場合の細かいルールについてです。
(親クラスのメンバと子クラスのメンバは別物であるとか、子クラスでオーバライドしなかったメンバがアクセスするフィールドとか)
その細かいルールを知らないとどんな問題が起きるか、また、どのようなルールになっているのかをこの記事ではまとめます。1. 問題
第5章※2のコードは、子クラス(三角形クラス)でsetter, getter, フィールドを変化させるメソッド(changer)の3種類をオーバライドしたりしなかったりを試し、三角形インスタンスのgetterがどのような値を返却するか実験するものです。実験結果を予測してみてください。
予測したら、実際にコードを実行して確認してみてください。
全問正解だった場合、本記事を読む必要はありません。
問題は全部で16問あります。2. 継承の間違った利用法
まず、次のようなphpプログラムを書いてみましょう。
<?php class 多角形 { //頂点の座標の配列 private $座標配列 = [[]]; function set座標配列(array $座標配列):void{$this->座標配列 = $座標配列;} function get座標配列():array{return $this->座標配列;} function move(int $dx, int $dy):void { $x = 0; $y = 1; $座標配列 = []; foreach($this->座標配列 as $座標) array_push($座標配列, [$座標[$x]+$dx, $座標[$y]+$dy]); $this->座標配列 = $座標配列; //nunulk様からご指摘をいただいたので修正 } } class 三角形 extends 多角形 { //オーバライド function set座標配列(array $座標配列):void { /* parent::set座標配列($座標配列); /*/ $this->座標配列 = $座標配列; //*/ if(count($座標配列)!=3) throw new Exception("頂点の個数が多すぎます"); } function get重心() { $x = 0; $y = 1; return [array_sum(array_column($this->座標配列, $x))/3.0, array_sum(array_column($this->座標配列, $y))/3.0]; } } $三角形 = new 三角形(); $三角形->set座標配列 ([ [ 1,0], [0,1.7320508], [-1,0] ]); $三角形->move(1,0); print_r($三角形->get座標配列()); print_r($三角形->get重心());このプログラムでは、まず親クラスである多角形クラスが
$座標配列フィールド、およびそのgetterとsetterを定義しています。
また、moveメソッドでは$座標配列を書き換え、平行移動を実現する機能を持っています。以下、フィールドを書き換えるメソッドをchangerと呼ぶことにします。
また子クラスである三角形クラスでは、座標配列のgetterやchangerはオーバライドせず、親(多角形)クラスのものをそのまま用いています。
そして、三角形クラスのインスタンスを作り、座標を(1,0), (0, 1.7320508), (-1, 0)にセットして、x方向に1、y方向に0だけ平行移動して、その結果を表示させます。うまくいったなら、
print_r($三角形->get座標配列())によって[[2,0], [1,1.7320508], [0,0]]が、
print_r $三角形->get重心()によって[0, 0.5773502]が表示されるはずですね。しかし、実行してみると、
print_r($三角形->get座標配列())は[[1,0]]を、
print_r $三角形->get重心()は[0, 0.57735026666667]を返却します。
get重心はともかく、get座標配列がおかしいですね、、(具体的には※1-1(第5章)のように表示されます。)
次に
set座標配列を次のように切り替えます。function set座標配列(array $座標配列):void { //* parent::set座標配列($座標配列); /*/ $this->座標配列 = $座標配列; //*/ if(count($座標配列)!=3) throw new Exception("頂点の個数が多すぎます"); }すると今度は、※1-2のように表示されます。
get座標配列は想定通りになったのですが、今度はget重心がダメになってしまいましたね。3. 継承とプロパティ(フィールド)の関係
第2章の間違いはなぜ発生したのでしょう。これを確認するために、実験をしてみます。
まず※2のようなコードを用意します。
冗長ですが、要はプロパティに対して子クラス(三角形クラス)でsetterやgetter、changerをオーバーライドした場合やしなかった場合、またオーバライドするとしても
parent::を用いた場合とそうでない場合、インスタンスのgetterは何を表示するのかを比較してみようというものです。※2を実行すると、次のように表示されます。
まず、setter/getterについて(オーバライドの有無) 0:三角形クラスのフィールドです ↑setterをオーバライドした場合 getterをオーバライドした場合 1: ↑setterをオーバライドしなかった場合 getterをオーバライドした場合 2:多角形クラスのフィールドです ↑setterをオーバライドした場合 getterをオーバライドしなかった場合 3:三角形クラスのフィールドです ↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合 setter/getterについて(オーバライド時に「parent::」を使うか否か) 8:三角形クラスのフィールドです ↑setterを「parent::」を使わずオーバライドした場合 getterを「parent::」を使わずオーバライドした場合 9: ↑setterを「parent::」を使ってオーバライドした場合 getterを「parent::」を使わずオーバライドした場合 10:多角形クラスのフィールドです ↑setterを「parent::」を使わずオーバライドした場合 getterを「parent::」を使ってオーバライドした場合 11:三角形クラスのフィールドです ↑setterを「parent::」を使ってオーバライドした場合 getterを「parent::」を使ってオーバライドした場合 次にchangerについて。 0:三角形クラスのチェンジャが書き換えました ↑setterをオーバライドした場合 getterをオーバライドした場合changerをオーバライドした場合 1:三角形クラスのチェンジャが書き換えました ↑setterをオーバライドしなかった場合 getterをオーバライドした場合changerをオーバライドした場合 2:多角形クラスのフィールドです ↑setterをオーバライドした場合 getterをオーバライドしなかった場合changerをオーバライドした場合 3:三角形クラスのフィールドです ↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合changerをオーバライドした場合 4:三角形クラスのフィールドです ↑setterをオーバライドした場合 getterをオーバライドした場合changerをオーバライドしなかった場合 5: ↑setterをオーバライドしなかった場合 getterをオーバライドした場合changerをオーバライドしなかった場合 6:多角形クラスのチェンジャが書き換えました ↑setterをオーバライドした場合 getterをオーバライドしなかった場合changerをオーバライドしなかった場合 7:多角形クラスのチェンジャが書き換えました ↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合changerをオーバライドしなかった場合3-1. setter/getterについて(オーバライドの有無)
0:三角形クラスのフィールドです ↑setterをオーバライドした場合 getterをオーバライドした場合 1: ↑setterをオーバライドしなかった場合 getterをオーバライドした場合 2:多角形クラスのフィールドです ↑setterをオーバライドした場合 getterをオーバライドしなかった場合 3:三角形クラスのフィールドです ↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合0より、setter、getter共にオーバライドした場合、getterではsetterでsetした値を正しく取得できることがわかりました。
また3より、setter、getter共にオーバライドしない場合も同様に正しく値を取得できるとわかります。
問題なのはsetterまたはgetterのいずれか片方のみをオーバライドした場合です。
1より、setterのみをオーバライドした場合、なぜでしょうか。何も表示しません。(nullが返却されています)
また2より、setterのみをオーバライドした場合、親クラスである多角形クラスのフィールドを表示してしまうことがわかります。これらのことをまとめると、
- setterのみ、またはgetterのみをオーバライドすると、setterで設定した値をgetterで取得できない。
- setterとgetterを両方オーバライドする、または両方オーバライドしないと、setterで設定した値をgetterで取得できる
となります。この事実から、次のような仮説が立てられます。
- 子クラスのメンバと親クラスのメンバは別物である
- 子クラスでオーバライドしなかったメンバは子クラスに自動的にコピーされるわけではない。子クラスにはメンバは存在しないままである。
このように考えると、setterやgetterのオーバライドの有無によって、取得できるフィールドの内容が異なるという事実を説明することができるようになります。
例えば0なら、三角形クラスはsetter、getter共にオーバライドしているので、setterやgetterは三角形クラスのメソッドです。したがって三角形クラスのフィールドにアクセスします。
また3なら、三角形クラスはsetterもgetterもオーバライドしていないので、setterやgetterは三角形の親である多角形クラスのメソッドです。したがって多角形クラスのフィールドにアクセスします。
0も3も、setterがアクセスしたフィールドにgetterがアクセスするから、setterの設定した値をgetterで取得できるというわけです。
1は、getterのみをオーバライドしているため、setterは多角形クラスのもので、getterは三角形クラスのものです。したがってsetterは多角形クラスのフィールドに値を書き込み、getterは三角形クラスのフィールド(存在しませんが)の値を読みます。したがって、setterの定義した値をgetterは取得できないわけです。そして、存在しない変数が返す値nullが取得されます。
2は、setterのみをオーバライドしているため、setterは三角形クラスのもので、getterは多角形クラスのものです。したがってsetterは三角形クラスのフィールド(存在しませんが)に値を書き込もうとし、getterは多角形クラスのフィールドの値を読みます。
3-2. setter/getterについて(オーバライド時に「parent::」を使うか否か)
0:三角形クラスのフィールドです ↑setterをオーバライドした場合 getterをオーバライドした場合 1: ↑setterをオーバライドしなかった場合 getterをオーバライドした場合 2:多角形クラスのフィールドです ↑setterをオーバライドした場合 getterをオーバライドしなかった場合 3:三角形クラスのフィールドです ↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合8:三角形クラスのフィールドです ↑setterを「parent::」を使わずオーバライドした場合 getterを「parent::」を使わずオーバライドした場合 9: ↑setterを「parent::」を使ってオーバライドした場合 getterを「parent::」を使わずオーバライドした場合 10:多角形クラスのフィールドです ↑setterを「parent::」を使わずオーバライドした場合 getterを「parent::」を使ってオーバライドした場合 11:三角形クラスのフィールドです ↑setterを「parent::」を使ってオーバライドした場合 getterを「parent::」を使ってオーバライドした場合0~3と8~11を比較しましょう。
結果が同じ順番で対応しています。
このことから、「parent::」を使ってオーバライドしても、(何か別の処理を追加していない限り、)オーバライドしていないのと同じことになってしまう
ということがわかります。言い換えれば、
「parent::」は親クラスのメソッドのコピーではなく、親クラスのメソッドそのものである
というわけです。
3-3. changerについて
0:三角形クラスのチェンジャが書き換えました ↑setterをオーバライドした場合 getterをオーバライドした場合changerをオーバライドした場合 1:三角形クラスのチェンジャが書き換えました ↑setterをオーバライドしなかった場合 getterをオーバライドした場合changerをオーバライドした場合 2:多角形クラスのフィールドです ↑setterをオーバライドした場合 getterをオーバライドしなかった場合changerをオーバライドした場合 3:三角形クラスのフィールドです ↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合changerをオーバライドした場合 4:三角形クラスのフィールドです ↑setterをオーバライドした場合 getterをオーバライドした場合changerをオーバライドしなかった場合 5: ↑setterをオーバライドしなかった場合 getterをオーバライドした場合changerをオーバライドしなかった場合 6:多角形クラスのチェンジャが書き換えました ↑setterをオーバライドした場合 getterをオーバライドしなかった場合changerをオーバライドしなかった場合 7:多角形クラスのチェンジャが書き換えました ↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合changerをオーバライドしなかった場合3-3節では、changerをオーバライドした場合とそうでない場合を比較します。
3-1節で立てた仮説の通りに説明できます。0~1番はgetterとchangerをオーバライドしているため、changerを呼んでからgetterを呼ぶと、三角形クラスのchangerが設定した値をgetterはそのまま取得します。
2~3番は、changerをオーバライドしていますがgetterをオーバライドしていません。
したがって、三角形クラスのchangerが呼ばれ、三角形クラスのフィールドが変更されるのですが、getterは多角形クラスのフィールドを読みます。そのため、changerの結果を取得することができず、3-1節の2~3番と同様の結果になります。4~5番は、getterをオーバライドしていますが、changerをオーバライドしていません。
したがって、多角形クラスのchangerが呼ばれ、多角形クラスのフィールドが変更されますが、getterは三角形クラスのフィールドを読むため、changerの結果を取得することができません。したがって、3-1節の0~1番と同様の結果になるわけです。6~7番は、changerとgetterのどちらもオーバライドしていません。
したがって、多角形クラスのchangerが呼ばれ、getterも多角形クラスのフィールドを読みます。そのため、
「多角形クラスのチェンジャが書き換えました」と表示されるわけです。4. 結論―「三角形 is-a 多角形」には無理がある
- 子クラスのメンバと親クラスのメンバは別物である
- 子クラスでオーバライドしなかったメンバは子クラスに自動的にコピーされるわけではない。子クラスにはメンバは存在しないままである。
- 「parent::」は親クラスのメソッドのコピーではなく、親クラスのメソッドそのものである
以上のことより、「子クラスのインスタンスなのに親クラスのフィールドが関係してきて、思わぬバグを生む」ことを防ぐためには、子クラスにて次のような工夫をする必要があるといえます。
- 各フィールドに対するgetterやsetterは、両方オーバライドするか、あるいは両方ともオーバライドしないようにする。
- 親クラスがchangerを持つ場合で、そのchangerをオーバライドしない場合、getterやsetterもオーバライドしない
- 親クラスがchangerを持つ場合で、そのchangerをオーバライドする場合、getterやsetterもオーバライドする。
さらに短くまとめれば、
- 各フィールドに対するgetter, setter, changerのオーバライドの有無は統一する必要がある
ということです。
そのため「座標配列」というフィールドだけを持ち、これを書き換えるchangerメソッドが無数にあるような多角形クラスを考える場合、その子クラスが一つでもchangerメソッドをオーバライドするのなら、他のchanger、getter、setterもすべてオーバライドしなくてはならないのです。
これではOAOO原則や差分プログラミングどころではありません。「継承した」というのは名ばかりで、実際には同じコードを子クラスの数だけ何回も書かなければならず、継承のメリットを全く活かすことができません。
5. 付録
※1-1Array ( [0] => Array ( [0] => 1 [1] => 0 ) ) Array ( [0] => 0 [1] => 0.57735026666667 )※1-2Array ( [0] => Array ( [0] => 2 [1] => 0 ) [1] => Array ( [0] => 1 [1] => 1.7320508 ) [2] => Array ( [0] => 0 [1] => 0 ) ) Warning: array_column() expects parameter 1 to be array, null given in C:\fakepass\test.php on line 33 Array ( [0] => 0 [1] => 0 )※2<?php class 多角形 { private $field0 = "多角形クラスのフィールドです", $field1 = "多角形クラスのフィールドです", $field2 = "多角形クラスのフィールドです", $field3 = "多角形クラスのフィールドです", $field4 = "多角形クラスのフィールドです", $field5 = "多角形クラスのフィールドです", $field6 = "多角形クラスのフィールドです", $field7 = "多角形クラスのフィールドです", $field8 = "多角形クラスのフィールドです", $field9 = "多角形クラスのフィールドです", $field10 = "多角形クラスのフィールドです", $field11 = "多角形クラスのフィールドです"; function set0(string $value){$this->field0 = $value;} function set1(string $value){$this->field1 = $value;} function set2(string $value){$this->field2 = $value;} function set3(string $value){$this->field3 = $value;} function set4(string $value){$this->field4 = $value;} function set5(string $value){$this->field5 = $value;} function set6(string $value){$this->field6 = $value;} function set7(string $value){$this->field7 = $value;} function set8(string $value){$this->field8 = $value;} function set9(string $value){$this->field9 = $value;} function set10(string $value){$this->field10 = $value;} function set11(string $value){$this->field11 = $value;} function get0(){return $this->field0;} function get1(){return $this->field1;} function get2(){return $this->field2;} function get3(){return $this->field3;} function get4(){return $this->field4;} function get5(){return $this->field5;} function get6(){return $this->field6;} function get7(){return $this->field7;} function get8(){return $this->field8;} function get9(){return $this->field9;} function get10(){return $this->field10;} function get11(){return $this->field11;} function changer0(){$this->field0 = "多角形クラスのチェンジャが書き換えました";} function changer1(){$this->field1 = "多角形クラスのチェンジャが書き換えました";} function changer2(){$this->field2 = "多角形クラスのチェンジャが書き換えました";} function changer3(){$this->field3 = "多角形クラスのチェンジャが書き換えました";} function changer4(){$this->field4 = "多角形クラスのチェンジャが書き換えました";} function changer5(){$this->field5 = "多角形クラスのチェンジャが書き換えました";} function changer6(){$this->field6 = "多角形クラスのチェンジャが書き換えました";} function changer7(){$this->field7 = "多角形クラスのチェンジャが書き換えました";} } /** * フィールドには0~15の番号が振られている。 * 0~7番については、 * 0→ changer〇, getter〇, setter〇 * 1→ changer〇, getter〇, setter× * 2→ changer〇, getter×, setter〇 * 3→ changer〇, getter×, setter× * のように、メソッドをオーバライド(「parent::」を使わない)したり(〇)しなかったり(×)を変化させている。 * 8~11番についてはメソッドをオーバライドするが、 * オーバライドで「parent::」を使わなかったり(〇)、使ったり(×)を変化させている。 */ class 三角形 extends 多角形 { function set0(string $value){$this->field0 = $value;} function set4(string $value){$this->field4 = $value;} function get0(){return $this->field0;} function get4(){return $this->field4;} function changer0(){$this->field0 = "三角形クラスのチェンジャが書き換えました";} //function changer4(){$this->field0 = "三角形クラスのチェンジャが書き換えました";} //function set1(string $value){$this->field1 = $value;} //function set5(string $value){$this->field5 = $value;} function get1(){return $this->field1;} function get5(){return $this->field5;} function changer1(){$this->field1 = "三角形クラスのチェンジャが書き換えました";} //function changer5(){$this->field0 = "三角形クラスのチェンジャが書き換えました";} function set2(string $value){$this->field2 = $value;} function set6(string $value){$this->field6 = $value;} //function get2(){return $this->field2;} //function get6(){return $this->field6;} function changer2(){$this->field2 = "三角形クラスのチェンジャが書き換えました";} //function changer6(){$this->field0 = "三角形クラスのチェンジャが書き換えました";} //function set3(string $value){$this->field3 = $value;} //function set7(string $value){$this->field7 = $value;} //function get3(){return $this->field3;} //function get7(){return $this->field7;} function changer3(){$this->field0 = "三角形クラスのチェンジャが書き換えました";} //function changer7(){$this->field0 = "三角形クラスのチェンジャが書き換えました";} function set8(string $value){$this->field8 = $value;} function get8(){return $this->field8;} function set9(string $value){parent::set9($value);} function get9(){return $this->field9;} function set10(string $value){$this->field10 = $value;} function get10(){return parent::get10();} function set11(string $value){parent::set11($value);} function get11(){return parent::get11();} } $三角形 = new 三角形(); $三角形->set0("三角形クラスのフィールドです");$三角形->set1("三角形クラスのフィールドです");$三角形->set2("三角形クラスのフィールドです");$三角形->set3("三角形クラスのフィールドです");$三角形->set4("三角形クラスのフィールドです");$三角形->set5("三角形クラスのフィールドです");$三角形->set6("三角形クラスのフィールドです");$三角形->set7("三角形クラスのフィールドです");$三角形->set8("三角形クラスのフィールドです");$三角形->set9("三角形クラスのフィールドです");$三角形->set10("三角形クラスのフィールドです");$三角形->set11("三角形クラスのフィールドです"); print "まず、setter/getterについて(オーバライドの有無)\n"; for($i = 0; $i <= 3; $i++) { $is_setter_overrided = !($i%2); $is_getter_overrided = !(floor($i/2)%2); print "\n\n$i:"; eval("print \$三角形->get$i().\"\n↑\";"); if($is_setter_overrided) print "setterをオーバライドした場合 "; else print "setterをオーバライドしなかった場合 "; if($is_getter_overrided) print "getterをオーバライドした場合"; else print "getterをオーバライドしなかった場合"; } print "\n\n\n\nsetter/getterについて(オーバライド時に「parent::」を使うか否か)\n\n"; for($i = 8; $i <= 11; $i++) { $is_setter_overrided = !($i%2); $is_getter_overrided = !(floor($i/2)%2); print "\n\n$i:"; eval("print \$三角形->get$i().\"\n↑\";"); if($is_setter_overrided) print "setterを「parent::」を使わずオーバライドした場合 "; else print "setterを「parent::」を使ってオーバライドした場合 "; if($is_getter_overrided) print "getterを「parent::」を使わずオーバライドした場合"; else print "getterを「parent::」を使ってオーバライドした場合"; } print "\n\n\n\n次にchangerについて。\n"; $三角形->changer0(); $三角形->changer1(); $三角形->changer2(); $三角形->changer3(); $三角形->changer4(); $三角形->changer5(); $三角形->changer6(); $三角形->changer7(); for($i = 0; $i <= 7; $i++) { $is_setter_overrided = !($i%2); $is_getter_overrided = !(floor($i/2)%2); $is_changer_overrided = !(floor($i/4)%2); print "\n\n$i:"; eval("print \$三角形->get$i().\"\n↑\";"); if($is_setter_overrided) print "setterをオーバライドした場合 "; else print "setterをオーバライドしなかった場合 "; if($is_getter_overrided) print "getterをオーバライドした場合"; else print "getterをオーバライドしなかった場合"; if($is_changer_overrided) print "changerをオーバライドした場合"; else print "changerをオーバライドしなかった場合"; }
- 投稿日: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:55:46+09:00
[Ubuntu]PocketMine-MPのサーバーを立てる
PocketMine-MPをUbuntuで立てます
初記事がこんなのでいいのかな。ハハッサーバー環境
・ConoHa VPS
・OS Ubuntu 18.04
・メモリ 512MB
・ストレージ SSD 20GB
・CPU 1coreポート開放
今回は例として19132ポート。
コマンドで打つべきなのは#とか$が先頭についてる奴だけです
つまり下でRules updatedって書いてあるのは打たなくていいんですわよ$ ufw allow 19132/udp Rules updatedユーザー作成
今回は例として
pmmpという名前のユーザーを作ります$ adduser pmmp Adding user `pmmp' ... Adding new group `pmmp' (1004) ... Adding new user `pmmp' (1000) with group `pmmp' ... Creating home directory `/home/pmmp' ... Copying files from `/etc/skel' ... Enter new UNIX password:こうなったら新ユーザーのパスワード打ってください
打っても何も表示されませんよ。勘違いしないでくださいねRetype new UNIX password:もう一度同じパスワードを打ってください
passwd: password updated successfully Changing the user information for pmmp Enter the new value, or press ENTER for the default Full Name []: Room Number []: Work Phone []: Home Phone []: Other []:
Full Name []:からOther []:まではそのままEnterで大丈夫ですIs the information correct? [Y/n]と聞かれたら
Yと入力。Sudo
Sudoグループに入れときます。意味わからなかったらMr.Googleに
$ gpasswd -a pmmp sudo Adding user pmmp to group sudoPMMPインストール
まずはPMMP用のディレクトリ(フォルダー)の
pmmpを作ります$ su - pmmp $ mkdir pmmp $ cd pmmpインストール
インストールします
$ wget -q -O - https://get.pmmp.io | bash -s - [*] Retrieving latest build data for channel "alpha" [*] Found PocketMine-MP 3.9.3 (build 1774) for Minecraft: PE v1.12.0 (PHP 7.3) [*] This alpha build was released on Tue Jul 30 01:29:17 JST 2019 [*] Installing/updating PocketMine-MP on directory ./ [1/3] Cleaning... [2/3] Downloading PocketMine-MP phar... done! [3/3] Obtaining PHP: detecting if build is available... Linux PHP build available... downloading 7.3 ... updating php.ini... checking... done [*] Everything done! Run ./start.sh to start PocketMine-MPはい。インストール成功です
セットアップしていきます$ ./start.shこのコマンドでPMMPを起動します
[*] PocketMine-MP set-up wizard [*] Please select a language ??????? => ara Английски => bul ?e?tina => ces 中文 (?体) => chs Deutsch => deu Ελληνικ? => ell English => eng Eesti => est Suomi => fin Francais => fra Gaeilge => gle ????? => heb Engleski => hrv Magyar => hun Bahasa Indonesia => ind Italiano => ita 日本語 => jpn ??? => kor Latvie?u => lav Malti => mlt Bahasa Melayu => msa Nederlands => nld Norsk => nor Polski => pol Portugues => por Pycc?ий => rus Espanol => spa Svenska => swe Tagalog => tgl ?????????? => tha tlhIngan => tlh Turkce => tur Укра?нська => ukr Ti?ng Vi?t => vie 中文(繁體) => zho [?] Language (eng): jpn [*] 日本語が正しく選択されました。 [*] PocketMine-MP にようこそ! サーバーのセットアップを開始する前に、ライセンスに同意する必要があります。 PocketMine-MP は LGPL ライセンスのもとで許諾されており、 このフォルダーで LICENSE ファイルを読むことができます。 This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. [?] ライセンスに同意しますか? (y/N): y [?] セットアップウィザードをスキップしますか? (y/N): N [*] サーバーのセットアップを開始します。 [*] デフォルトを変更しない場合は、Enter キーを押してください。 [*] それらの設定は後で server.properties ファイルで編集できます。 [?] サーバーの名前を付けてください (PocketMine-MP Server): PMMP Server [*] これが初めてのサーバーの場合は、デフォルトのポート値を変更しないでください。 [?] サーバーポート (19132): 19132 [*] クリエイティブモード(1)またはサバイバルモード(0)のいずれかを選択してください [?] デフォルトのゲームモード (0): 0 [?] 最大オンラインプレイヤー数 (20): 20 [*] スポーン保護を使用すると、OP を除いたユーザーがスポーン地域でブロックを設置/破壊することを防ぐことができます [?] スポーン保護を有効にしますか? (Y/n): Y [*] OP はサーバーのプレイヤー管理者です。OP は通常のプレイヤーよりも多くのコマンドを実行できます [?] OP プレイヤー名(例:あなたのゲームの名前): pixelcraft810 [*] ホワイトリストを使用すると、許可したプレイヤーのみ参加するようにすることができます。 [?] ホワイトリストを有効にしますか? (y/N): N [!] Query はサーバーとログインしているプレイヤーの情報を取得するために他のツールが使用するプロトコルです。 [!] それを無効にした場合、サーバーの一覧を使用できなくなる可能性があります。 [?] Query を無効にしますか? (y/N): N [*] RCON はパスワードを使用してサーバーコンソールにリモート接続するためのプロト コルです。 [?] RCON を有効にしますか? (y/N): N [*] 外部 IP と内部 IP を取得しています [!] あなたの外部 IP は XXX.XX.XX.XXX です。あなたの内部 IP XXX.XX.XX.XXX にポー トフォワードが必要な可能性があります [!] 必ず確認してください、フォワードが必要な場合、スキップすると、外部からプレイヤーが参加できなくなります。[Enter キーを押してください] [*] セットアップは正しく終了しました [*] 新機能、ミニゲームを追加するか、高級機能でサーバーを保護するには、プラグインリポジトリを確認してみてください [*] PocketMine-MP を開始します。利用可能なコマンドの一覧を /help で見てください 。 [14:36:58] [Server thread/INFO]: Loading pocketmine.yml... [14:36:58] [Server thread/INFO]: Loading server properties... [14:36:58] [Server thread/INFO]: 日本語(jpn)を基本言語として選択しました [14:36:58] [Server thread/INFO]: Minecraft: Bedrock Edition サーバーバージョン v1.12.0 を開始しています [14:36:58] [Server thread/NOTICE]: オンラインモードが有効になっています。サーバ ーは、プレイヤーが Xbox Live に認証されていることを確認します。 [14:36:58] [Server thread/NOTICE]: 認証を無効にするには、server.properties で「xbox-auth」を「false」に設定してください。 [14:36:58] [Server thread/INFO]: サーバーを 0.0.0.0:19132 で開始しています [14:36:58] [Server thread/INFO]: このサーバーは PocketMine-MP バージョン 3.9.3 を実行しています [14:36:58] [Server thread/INFO]: PocketMine-MP は LGPL ライセンスのもとで配布さ れています [14:36:59] [Server thread/INFO]: Loading resource packs... [14:36:59] [Server thread/NOTICE]: ワールド「world」が見つかりませんでした [14:36:59] [Server thread/INFO]: ワールド「world」を準備しています [14:36:59] [Server thread/NOTICE]: ワールド「world」のスポーン地形がバックグラウンドで生成されています [14:36:59] [Server thread/INFO]: GS4 ステータスリスナーを開始しています [14:36:59] [Server thread/INFO]: Query ポートを 19132 に設定しています [14:36:59] [Server thread/INFO]: Query は 0.0.0.0:19132 で動作しています [14:36:59] [Server thread/INFO]: デフォルトのゲームタイプ:サバイバルモード [14:36:59] [Server thread/INFO]: 完了(0.316 秒)!ヘルプについては「help」また は「?」を入力してください※設定適当だお
Screen を使う
Screenを使うことでPMMPをバックグラウンドで常時起動させておくことができます
インストール↓$ sudo apt-get install screenセッション
// セッションを開始する $ screen -S pmmp $ ./start.sh // セッションに接続する $ screen -r pmmp // セッションから離脱する Ctrl + A + D キーを同時押し終わり
グダグダ+適当でごめんなさい
間違ってるとこだらけだと思うんでなんかあったら教えてください
- 投稿日: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-01T17:31:51+09:00
製薬企業研究者がPHPの基本的な文法についてざっくりまとめてみた
はじめに
ここでは、PHPの基本的な文法をざっくりとまとめてみます。
基本的な記述方法
PHPは
.phpという拡張子のファイルに記述します。
HTMLのソースコードの中で<?phpから始まる部分がPHPのコード部分になります。
PHPでは文末にセミコロン;が必要になります。
コメントを書くときは、//を使います。
また、複数行に渡るコメントは/* */を使って書くことができます。
オブジェクトの値を出力するときは、echoを使います。変数とデータ型
変数は、
$変数名 = 値;として宣言します。variable.php<?php $str = '文字列'; // 文字列 echo $str; $num = 123; // 数値 echo $num; $isBoolean = true; // 真偽値 echo $isBoolean; $arr = array('Python', 'R', 'JavaScript', 'PHP'); // 配列 echo $arr; $dict = array( 'Python' => 'AI', 'R' => 'statistics', 'JavaScript' => 'web', 'PHP' => 'WordPress' ); // 連想配列 echo $dict; ?>制御文
条件分岐
if文
if文は、
if (条件) {処理}と記述します。
さらに場合分けするときは、elseやelse ifを使います。if.php<?php num = 3; if ($num == 1) { echo '1'; } else if ($num == 2) { echo '2'; } else if ($num == 3) { echo '3'; } else { echo '1でも2でも3でもない'; } ?>switch文
if文で場合分けが多くなる場合は、switch文を使うと簡単に書けます。
switch文は、switch (変数) {case 値: 処理; ・・・}と記述します。switch.php<?php $num = 3; switch ($num) { case 1: echo '1'; break; case 2: echo '2'; break; case 3: echo '3'; break; default: echo '1でも2でも3でもない'; break; } ?>なお、
breakを書かないと、条件に合った場合でも後続のcaseの処理が走ってしまうので注意が必要です。反復処理
for文
一定回数だけ繰り返し処理を実行する(繰り返し回数があらかじめ分かっている)ときは、for文を用います。
for文は、for ($変数名 = 初期値; $変数 <= 最大値; $変数の値の更新)と記述します。for.php<?php for ($i = 1; $i <= 5; $i++) { echo $i; } ?>ここで、
$i++となっているのは、$i = $i + 1すなわち反復処理を繰り返すごとに変数$iを1ずつ大きくしていくことを示しています。これは、$i += 1とも書けますが、変数の値を1ずつ更新していく場合は、この書き方はあまりせず、$i++または$i--と書きます。変数の更新幅が1以外の場合は、$i += 2のように書きます。また、
foreach ($配列 as $変数名) {処理}とすると、配列の要素を一つずつ取り出すことができます。foreach.php<?php $arr = array('Python', 'R', 'JavaScript', 'PHP'); foreach ($arr as $lang) { echo $lang; } ?>while文
ある条件を満たす間だけ処理を繰り返す場合は、while文を用います。
while文は、while (条件) {処理}と記述します。while.php<?php $i = 1; while ($i < 5) { echo $i; $i++; // 変数の値の更新 } ?>関数
関数は、
function 関数名(引数){処理}とすることで作成できます。function.php<?php function copyNumber($num) { return $num; } echo copyNumber(123); ?>クラス
クラスは、
class クラス名 {プロパティ、メソッドなどの内容}とすることで作成できます。
newを使うことで、新しいインスタンスを生成できます。class.php<?php class Human { public $name; public $age; public function introduceMyself() { return 'I am ' + $this->name + ' and ' + $this->age + ' years old. '; } } $yukiya = new Human(); $yukiya->name = 'Yukiya'; $yukiya->age = 31; echo $yukiya->name; echo $yukiya->age; echo $yukiya->introduceMyself(); ?>まとめ
ここでは、PHPの基本的な文法について解説しました。
ウェブページのソースコードを見るときに役立ちます。
- 投稿日: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-01T11:26:57+09:00
jupyter-php環境を構築してみた
はじめに
Virtualboxで稼働中のCentOS8に、jupyter-PHP環境を構築してみたので、その備忘録として残しておきます。
※本環境では、jupyter-PHPで使用するPHPは、brewでインストールした最新バージョン「7.4」を使用するものとする。前提条件
・OS環境は、Virtualboxで稼働中のCentOS8.1です。以下、OS詳細情報となります。
# cat /etc/centos-release CentOS Linux release 8.1.1911 (Core)・Virtualboxのネットワーク設定は、「NAT」「ホストオンリーアダプター」で構成。
・OSの初期設定が完了していること。
・jupyter環境が構築済みであること。
※私の環境ではAnacondaの中に含まれている、jupyterを使用。詳細は、これを参照してください。・Linuxbrewの環境が構築済みであること。
Linuxbrewの環境構築は、これを参照してください。手順概要
手順は、以下の順におこなっていきます。
(1)PHP環境を構築
(2)php-zmqをインストール
(3)jupyter-phpをインストール
(4)jupyterにjupyter-phpを認識させる&動作確認(1)PHP環境を構築
▼brewコマンドが設定済みの一般ユーザに切り替える
# su - qiita▼brewコマンドのアップグレード
$ brew upgrade▼brewのバージョン確認
$ brew -v ↓以下、出力結果 Homebrew 2.2.6 Homebrew/linuxbrew-core (git revision f9ad8b; last commit 2020-02-28)▼php、zmq、composerをインストール
$ brew install php zmq composer▼phpとcomposerのバージョン確認
$ php -v && composer -V ↓以下、出力結果 PHP 7.4.3 (cli) (built: Feb 29 2020 13:14:11) ( NTS ) Copyright (c) The PHP Group Zend Engine v3.4.0, Copyright (c) Zend Technologies with Zend OPcache v7.4.3, Copyright (c), by Zend Technologies Composer version 1.9.3 2020-02-04 12:58:49(2)php-zmqをインストール
▼作業ディレクトリ「/var/tmp/php」を作成&移動
$ mkdir /var/tmp/php && cd /var/tmp/php && pwd▼php-zmqをダウンロード
$ git clone http://github.com/mkoppanen/php-zmq.git▼php-zmqに移動
$ cd php-zmq && pwd▼PHPに拡張モジュールを追加(configureの作成)
$ phpizephpizeとは
拡張モジュールをビルドする低レベルなビルドツール。
autoconfやautomake m4等のビルドツールが別途必要になる。
これを使用することにより、PHPをソースから再コンパイルすることなく拡張モジュールをビルドすることができる。
参考URL:https://www.weblio.jp/content/phpize▼スクリプトファイルを実行
$ ./configure▼コンパイルを実行
$ make▼コンパイルされたphp-zmqをインストール
$ make install ↓以下が出力がされればOK Installing shared extensions: /home/linuxbrew/.linuxbrew/Cellar/php/7.4.3/pecl/20190902/※「configure」, 「make」, 「make install」については、わかりやすくまとめてくださっている記事があるので、詳しくはこれを参照してください。
▼php.iniにzmqを定義
/home/linuxbrew/.linuxbrew/etc/php/7.4/php.iniextension=zmq.so▼phpが「php-zmq」を認識できているかを確認
$ php -i | grep zmq ↓以下、出力結果 zmq libzmq version => 4.3.2 PWD => /var/tmp/php/php-zmq $_SERVER['PWD'] => /var/tmp/php/php-zmq※上記出力のように「zmq」「libzmq version」が出力されていればOK
※出力されない場合は、phpがzmqを認識していないので次項の「jupyter-php」のインストールに失敗する。(3)jupyter-phpをインストール
▼「/var/tmp/php」に移動
$ cd /var/tmp/php && pwd▼「jupyter-php-installer.phar」をダウンロードする
$ wget https://litipk.github.io/Jupyter-PHP-Installer/dist/jupyter-php-installer.phar※jupyter-phpは、ここからダウンロードする。
https://litipk.github.io/Jupyter-PHP-Installer/▼jupyter-phpをインストール
$ php jupyter-php-installer.phar install -vvv※「-vvv」をつけることで、エラー情報などを詳細に表示してくれる。
※「The Jupyter-PHP kernel has been successfully installed.」が出力されればインストール完了(4)jupyterにjupyter-phpを認識させる&動作確認
▼rootユーザに切り替える
# exit▼jupyterが認識しているカーネルの一覧を出力する
# jupyter kernelspec list ↓以下、出力結果 Available kernels: python3 /root/.pyenv/versions/anaconda3-5.3.1/share/jupyter/kernels/python3※私の環境では、「jupyter-php」をインストールしただけでは、jupyter側で認識はされなかった。
▼先ほどインストールしたjupyter-phpを検索する
# find / -name jupyter-php | grep kernels ↓以下が、私の環境のjupyter-phpがあるディレクトリ階層 /home/qiita/.local/share/jupyter/kernels/jupyter-php※トップディレクトリを指定した検索は、ディレクトリ構造が複雑な場合、サーバに負荷がかかるので自己責任で!!
▼jupyter-phpのディレクトリ階層を確認
# ls -la /home/qiita/.local/share/jupyter/kernels/jupyter-php/ 合計 4 drwxr-xr-x 2 qiita qiita 25 2月 29 13:58 . drwxr-xr-x 3 qiita qiita 25 2月 29 13:58 .. -rw-rw-r-- 1 qiita qiita 165 2月 29 13:58 kernel.json※「jupyter」で、認識するために必要な「kernel.json」ファイルがあればOK
※次の手順で、jupyterが各カーネルを認識しているディレクトリ階層にファイルをセットする。(シンボリックリンクで)▼「jupyter」に「jupyter-php」を認識させるためにシンボリックリンクをはる
# ln -s /home/qiita/.local/share/jupyter/kernels/jupyter-php /root/.pyenv/versions/anaconda3-5.3.1/share/jupyter/kernels/▼シンボリックリンクの実行後の確認
# ls -la /root/.pyenv/versions/anaconda3-5.3.1/share/jupyter/kernels/ ↓以下、出力結果 合計 0 drwxr-xr-x 3 root root 40 2月 29 14:12 . drwxr-xr-x 5 root root 52 2月 23 11:13 .. lrwxrwxrwx 1 root root 52 2月 29 14:12 jupyter-php -> /home/qiita/.local/share/jupyter/kernels/jupyter-php drwxr-xr-x 2 root root 69 2月 23 11:13 python3▼jupyterが認識しているカーネルの一覧を出力する
# jupyter kernelspec list ↓以下、出力結果 Available kernels: jupyter-php /root/.pyenv/versions/anaconda3-5.3.1/share/jupyter/kernels/jupyter-php python3 /root/.pyenv/versions/anaconda3-5.3.1/share/jupyter/kernels/python3※「jupyter」に「jupyter-php」を認識させられればOK
▼jupyter起動後、ブラウザで表示される名称を変更
/home/qiita/.local/share/jupyter/kernels/jupyter-php/kernel.json"display_name":"PHP" ↓ "display_name":"PHP74"※ブラウザで表示される名称がPHPだけでは味気ないので、バージョン情報付きに修正。
▼jupyter notebookを起動
# jupyter notebook --allow-root▼jupyter labを起動
# jupyter lab --allow-root最後に
jupyterを起動後、PHPが認識されていれば完了です。
jupyterでは、PHP以外にもほかの言語も追加できるみたいなので興味のある方はやってみてください。
終わりです( *´艸`)参考URL一覧
・OSX El CapitanにJupyterLabをインストールする
・configure, make, make install とは何か
・Weblio 辞書 > コンピュータ > PHP用語 > phpizeの意味・解説
・PHP Warning: PHP Startup: Unable to load dynamic library '/usr/lib/php5/20131226-zts/zmq.so' - /usr/lib/php5/20131226-zts/zmq.so #147
・PHPでメッセージ送受信(zeromq)
- 投稿日:2020-03-01T04:20:25+09:00
PHPでGoogle Calendar API を使って空き時間を抽出してみた
PHPでGoogleカレンダーから空き時間を抽出するプログラムを書いてみた
背景
筆者は現在就職活動中なのですが、たびたびLINEやメールで「直近で空いている日程をいくつか教えてください」と言われることがよくあります。
元々自分がスマホで字打つのがあんま好きじゃないのもあって、そのたびに毎回Googleカレンダー見て、空いてる日程をスマホで打ってまたGoogleカレンダー見て...というのが面倒だったのでGoogleカレンダーから空き時間を抽出するプログラムを作ろうと考えた次第です。概要
GoogleカレンダーAPIのFreebusyをPHPで叩いて空き時間を抽出します
本稿では、翌日から2週間以内の10:00~20:00の間で空いている時間の取得を目標とします。
現状、開始時刻と終了時刻を入力した予定のみ対応しており、「全日」の予定には対応していないため、今後対応予定です。前提
- 0,1のビット演算が分かる
- PHPをググりながらなら書ける
- GoogleカレンダーAPIをPHPで利用するための準備が済んでおり、予定の追加や取得の基本が分かる
なお、GoogleカレンダーのAPIを利用するための準備や基本操作に関しては、以下のサイトが非常に参考になります。
Google Calendar API と PHP で 予定の取得と追加をしてみるよ(準備編) | 東京のWeb制作会社LIG
Google Calendar API と PHP で 予定の取得と追加をしてみるよ(PHP編) | 東京のWeb制作会社LIG空き時間抽出とは
さて、ここで私がゴールとしているのは、例えば以下のようなスケジュールから、空き時間を抽出する作業をプログラムにやってもらうことです。
人間が目で見ればすぐに空き時間が分かるのですが、素人プログラマな私には、プログラムだとどうやって空き時間抽出するんじゃ...という感じでした。
そこで以下の記事を参考にさせて頂きました。(なおGoogleカレンダーAPIでPHPを用いて空き時間を抽出するプログラムについての記事は私が調べた限りありませんでした)
複数の予定から空き時間を算出するプログラム(bit演算利用) | Blow Up By Black Swan
上記より、「スケジュールをセグメントに分け、0or1のビットでFree(空いている)かBusy(予定がある)を表現すればよい」ということが分かりました。つまり、上記のスケジュールであれば、以下のように表現できる、ということです。
この場合、30分ごとのセグメントに分けています。これなら0or1でプログラムに予定のあるorなしを判断させることができますね、考えた方は天才だと思います。Freebusyとは
参考
Google Calendar API Freebusy - Administrator
Freebusy: query | Calendar API | Google Developers一言で言えば、その日の予定がある(Busyな)時刻を抽出してくれるAPIです。
PHPでの使い方としては以下のようになります。なお、一部不要な要素もあるため加工しています。(例では翌日〜2週間以内のものを取得しています。)freebusy.php$tomorrow = date('Y-m-d', strtotime('+1 day')); $afterTwoWeeks = date('Y-m-d', strtotime('+15 day')); $freebusyReq = new Google_Service_Calendar_FreeBusyRequest(); $freebusyReq->setTimeMin(date('c', $tomorrow)); $freebusyReq->setTimeMax(date('c', $afterTwoWeeks)); $freebusyReq->setTimeZone('Asia/Tokyo'); $freebusyReqItem = new Google_Service_Calendar_FreeBusyRequestItem(); $freebusyReqItem->setId(カレンダーID); $freebusyReq->setItems(array($freebusyReqItem)); $freebusyResObj = $service->freebusy->query($freebusyReq); $freebusy = []; foreach ($freebusyResObj['calendars'][$calendarId]['busy'] as $item) : // オブジェクト→配列に変換して代入 $freebusy[] = (array) $item; endforeach; $busyTimes = []; foreach ($freebusy as $array) { $busyTimes[] = ['start' => $array['start'], 'end' => $array['end']]; }$busyTimesの中身Array ( [0] => Array ( [start] => 2020-03-02T15:00:00+09:00 [end] => 2020-03-02T16:00:00+09:00 ) [1] => Array ( [start] => 2020-03-03T10:00:00+09:00 [end] => 2020-03-03T18:00:00+09:00 ) [2] => Array ( [start] => 2020-03-04T10:00:00+09:00 [end] => 2020-03-04T18:00:00+09:00 ) 以下同様・・・ )上記のように、Busy時刻の開始をstart、終了をendとして取得してくれます。
これを利用して空き時間抽出をやっていきます、、、Freebusyデータ→ビット列への変換
以下にFreebusyデータをビット列に変換するための関数群を記載します。
functions.php// スケジュールをビット列に変換する関数 function changeScheduleToBit($busyTimes) { $scheduleBitData = []; foreach ($busyTimes as $busyTime) { // changeEventToBitは日付とビット列を返す関数 list($date, $bitData) = changeEventToBit($busyTime); if (!array_key_exists($date, $scheduleBitData)) { $scheduleBitData[$date] = $bitData; } else { // 同じ日付のビット列は論理和を求めて再代入する $scheduleBitData[$date] = makeLogicalSum($scheduleBitData[$date], $bitData); } } return $scheduleBitData; } // イベントをビット列に変換する関数 function changeEventToBit($busyTime) { // イベントの開始時刻、終了時刻、日付をセット $eventStart = strtotime($busyTime['start']); $eventEnd = strtotime($busyTime['end']); $date = substr($busyTime['start'], 0, 10); // 1日の開始時刻と終了時刻をセット $dayStart = strtotime($date . 'T10:00:00+09:00'); $dayEnd = strtotime($date . 'T20:00:00+09:00'); // セグメント数(30分ごとに分割)をセット $segmentCount = ($dayEnd - $dayStart) / 60 / 60 * 2; // 30分ずつのセグメントを走査していくポインタをセット $dayPointer = $dayStart; $bitData = ""; // ビット列生成 for ($i = 0; $i < $segmentCount; $i++) { if ($dayPointer > $dayEnd) { break; } // FreeOrBusyをポインタにより判断 if ($eventStart <= $dayPointer && $dayPointer < $eventEnd) { $bitData .= "1"; } else { $bitData .= "0"; } // ポインタを1800秒(30分)ずらす $dayPointer = $dayPointer + 1800; } return [$date, $bitData]; } // 2進数ビット列の論理和を求める関数 function makeLogicalSum($bits1, $bits2) { $logicalSum = ''; for ($i = 0; $i < strlen($bits1); $i++) { $bit1 = substr($bits1, $i, 1); $bit2 = substr($bits2, $i, 1); if ($bit1 === "0" and $bit2 === "0") { $logicalSum .= "0"; } else { $logicalSum .= "1"; } } return $logicalSum; }changeScheduleToBit()の中でchangeEventToBit()によりビット列のスケジュールを生成し、日付の同じデータに関しては、その論理和(OR)をmakeLogicalSum()で求めることで1日にまとめています。
結果として、例えば以下のようなデータを取得できます。$scheduleBitDataArray ( [2020-03-02] => 00000000001100000000 [2020-03-03] => 11111111111111110000 [2020-03-04] => 11111111111111110000 [2020-03-05] => 11000000000110000000 [2020-03-06] => 00000000001100000000 [2020-03-10] => 00000001000000000000 )ビット列→時刻への変換+予定の前後1時間をbusyに
さて、後はビット列を時刻に戻せばいいだけかと思いますが、現実問題、人に空いているスケジュールを聞かれた際は、既に入っている予定の前後1時間ほどに余裕をもたせたスケジュールを伝えるかと思います。移動時間等もありますからね。
ということで、以下がビット列を時刻へと変換する関数と、予定の前後1時間をbusyに変換する関数です。functions.php// 各日付のスケジュールビットデータを時刻へと変換する関数(空き時間の取得) function getVacantTime($scheduleBitData) { $vacantTimes = []; foreach($scheduleBitData as $date => $bitData) { $dayStart = strtotime($date . 'T10:00:00+09:00'); $dayEnd = strtotime($date . 'T20:00:00+09:00'); $dayPointer = $dayStart; $vacantTimeStart = null; $vacantTimeEnd = null; $segmentCount = ($dayEnd - $dayStart) / 60 / 60 * 2; // 「1」の前後1時間をNG(busy)時間とする $bitData = changeOneHourBeforeAndAfterBusy($bitData, $segmentCount); // セグメントごとに0,1を判断 for ($i = 0; $i < $segmentCount; $i++) { // 0かつ開始時間なし if ($bitData[$i] === '0' && $vacantTimeStart === null) { $vacantTimeStart = $dayPointer; $dayPointer = $dayPointer + 1800; // 0かつ開始時間ありかつポインタが19:30に達していない } else if ($bitData[$i] === '0' && $vacantTimeStart !== null && $dayPointer < $dayEnd - 1800) { $dayPointer = $dayPointer + 1800; // 1かつ開始時間あり } else if ($bitData[$i] === '1' && $vacantTimeStart !== null) { $vacantTimeEnd = $dayPointer; // 日付oo/ooをキーとして配列に空き時間を格納 $vacantTimes[str_replace('-', '/', substr($date, 5, 5))][] = date('H:i', $vacantTimeStart) . '〜' . date('H:i', $vacantTimeEnd); $vacantTimeStart = null; $vacantTimeEnd = null; $dayPointer = $dayPointer + 1800; // 1かつ開始時間null } else if ($bitData[$i] === '1' && $vacantTimeStart === null) { $dayPointer = $dayPointer + 1800; // 0かつdayポインタが19:30で、開始時間あり } else if ($bitData[$i] === '0' && $dayPointer === $dayEnd - 1800 && $vacantTimeStart !== null) { $vacantTimeEnd = $dayPointer; // 日付oo/ooをキーとして配列に空き時間を格納 $vacantTimes[str_replace('-', '/', substr($date, 5, 5))][] = date('H:i', $vacantTimeStart) . '〜' . date('H:i', $dayEnd); } } } return $vacantTimes; } // 1の前後1時間をbusy(1)に変換する関数 function changeOneHourBeforeAndAfterBusy($bitData, $segmentCount) { for ($i = 0; $i < $segmentCount; $i++) { if ($bitData[$i] == '0') { // 0の1つor2つ隣が1の時刻はbusy(x)とする if( isset($bitData[$i - 1]) && $bitData[$i - 1] == '1' || isset($bitData[$i - 2]) && $bitData[$i - 2] == '1' || isset($bitData[$i + 1]) && $bitData[$i + 1] == '1' || isset($bitData[$i + 2]) && $bitData[$i + 2] == '1' ) { // 一時的にxに変更 // ここで1に変換すると上記の条件がほぼ当てはまってしまうため $bitData[$i] = 'x'; } } } // xを1に置換 for ($i = 0; $i < $segmentCount; $i++) { if ($bitData[$i] == 'x') { $bitData[$i] = '1'; } } return $bitData; }$vacantTimesArray ( [03/02] => Array ( [0] => 10:00〜14:00 [1] => 17:00〜20:00 ) [03/03] => Array ( [0] => 19:00〜20:00 ) [03/04] => Array ( [0] => 19:00〜20:00 ) [03/05] => Array ( [0] => 12:00〜14:30 [1] => 17:30〜20:00 ) [03/06] => Array ( [0] => 10:00〜14:00 [1] => 17:00〜20:00 ) [03/10] => Array ( [0] => 10:00〜12:30 [1] => 15:00〜20:00 ) )getVacantTime()では、ビット列の0or1を判断することで、0の時刻を空き時間として取得しています。changeOneHourBeforeAndAfterBusy()(ネーミングセンス...)では、0の前後1時間に予定がある、つまり0の1つor2つ隣が1である場合に、一時的に'x'という文字列に変換し、最終的に'x'→1に変換しています。
このあたりのアルゴリズムはまだ改良の余地があるかと思います...精進します。仕上げ
さて、後は予定のない日の補完をすることで、空きスケジュールの完成です。
// 予定のない日の日付、時刻を格納 for ($i = 1; $i <= 14; $i++) { $key = date('m/d', strtotime('+' . $i . ' day')); if (!array_key_exists($key, $vacantTimes)) { $vacantTimes[$key][] = '10:00〜20:00'; } } // キーを日付順にソート ksort($vacantTimes); // 空き時間を出力 $schedule .= "直近2週間以内の空きスケジュールは...<br>"; foreach ($vacantTimes as $date => $vacantTimeArray) { $schedule .= $date . ' '; if (count($vacantTimeArray) == 1) { $schedule .= $vacantTimeArray[0] . "<br>"; } else { foreach ($vacantTimeArray as $index => $elem) { if (count($vacantTimeArray) == $index + 1) { $schedule .= $elem . "<br>"; } else { $schedule .= $elem . ","; } } } } $schedule .= "やで!";なお、筆者はスマホで利用したかったので、LINEBotにメッセージを送ったら直近2週間以内の空きスケジュールを取得できるようにしてみました。
これでもういちいち予定を何度も見返して空きスケジュールを入力する必要がなくなったかと思います。(もちろんこの通りに絶対空いてるとも限りませんが...)まとめ
- PHPでGoogle Calendar APIを使って空き時間の取得を行いました。
- 現状、「全日」の予定がある場合に対応できていないので、今後対応したいと思います。
- 冗長な部分も多々あるかと思うので、そちらも改善していきたく思います。
- 投稿日:2020-03-01T00:27:30+09:00
PHP: ダブル・シングルクオートの違い
$test = "test";
シングルクオートは文字として認識してくれる
var_dump('シングル: $test');ダブルクオートは変数として認識してくれる
var_dump("ダブル: $test");JSの場合(変数として認識してくれる)
let test = "test"
console.log("ダブル:" + test);











