- 投稿日:2019-12-03T23:40:41+09:00
Laravelマイグレーション
マイグレーションでできること
テーブル定義を管理する仕組みのこと。
マイグレーションファイルを作成し、それを実行することでファイル内で定義した内容をもとにデータベーステーブルを作成する。参照記事
https://www.hypertextcandy.com/how-laravel-migration-worksマイグレーションファイルの作成
php artisan make:migration create_{テーブル名}_table
database/migration
ディレクトリに年_月_日_時間_create_{テーブル名}_table.php
というファイルが作成される。
up
とdown
という2つのメソッドを持つクラスが存在する。
up
メソッドの中にどういうテーブルにするかの詳細を書いていく。
*カラム名・型など公式ページ参照
https://readouble.com/laravel/5.5/ja/migrations.html
down
メソッドはロールバック(マイグレーションを元に戻す機能)の時に実行される。マイグレーション実行
php artisan migrate実行することによってデータベースに3つのテーブルが作成される。
①作成したファイルのテーブル
→ファイル内で定義したテーブルが作成される
②migrationsテーブル
→マイグレーションを実行する度に追加されたファイル名が保存されるテーブル
③password_resetテーブルマイグレーションの変更
php artisan make:migration {マイグレーションファイル名} --table={テーブル名}
{マイグレーションファイル名}
の部分はそのままクラス名になるためわかりやすい名前にする
*カラム追加の場合
→add_cloumn_name_users_table現在あるマイグレーションファイルを編集するのではなく新たに変更を加える内容を記載したファイルを作成し実行する。
例えばカラム名を変更する際はrenameColumn
メソッドを使用する。
おそらくそのまま使用しても使えないため変更追加用パッケージをインストールする必要がある。composer require doctrine/dbalこれをインストールすればカラム名を変更することが可能となる。
ロールバック
php artisan migrate:rollback
down
メソッドを実行する
データベースのmigrations
テーブルのバッチ単位で処理される。
ロールバックを行うことによってバージョン管理のようにマイグレーション内容を元に戻すことができる。
- 投稿日:2019-12-03T23:07:40+09:00
【Laravel】基本的なルーティングのまとめ
はじめに
現在Laravelについて学習しています。
その学習の履歴として今回はルーティングについてまとめました。
ざっくりとまとめているので、大枠を掴むためや復習のために見ていただけると幸いです。ルーティングについて
今回はHTTPメソッドgetについてまとめます。
全部で9種類です。
ルーティングは下記の形式です。Route::get('URL', '処理');処理がクロージャとアクションの場合に分けて説明します
クロージャとはfunction(引数)
のことで無名関数という意味です。
アクションとはMVCのコントローラに含まれるものです。
クロージャの場合は6種類で、アクションの場合は3種類です。クロージャーの場合
処理がクロージャの場合について解説します
1. URLからテンプレートをレンダリング
URLからテンプレートにレンダリングする場合について解説します。
routes/web.phpRoute::get('/', function () { return view('welcome'); });
/
にアクセスすると
view関数によりresources/views/welcome.blade.php
をレンダリングする2. URLからHTMLを直書き
レンダリングするだけでなくHTMLのソースコードを直書きすることもできます。
routes/web.phpRoute::get('hello', function () { return '<html><body><h1>Hello</h1></body></html>'; });
/hello
にアクセスすると
<html><body><h1>Hello</h1></body></html>
を表示します3. 返すHTMLを変数として扱う(ヒアドキュメント)
上記の直書きの場合、HTMLが長くなるとソースが見にくくなります。
変数に代入して、見やすくしましょうroutes/web.php$html = <<<EOF <html><body><h1>Hello</h1></body></html> EOF; Route::get(‘hello‘, function () use ($html){ return $html; });
/hello
にアクセスすると、
$html
を表示します。ヒアドキュメント<<<EOF ~~ EOF;
~~
の部分を変数に代入できるようにするために、EOFで挟みます。4. URLにパラメータを挿入する
URLにルートパラメータを記載する方法を説明します。
routes/web.phpRoute::get('hello/{msg}', function ($msg) { $html = <<<EOF <html> <body> <h1>Hello</h1> <p>{$msg}</p> </body> </html> EOF; return $html; });ルートパラメータに
$msg
を指定します。5. ルートパラメータを2種類にする場合
ルートパラメータを2種類にする場合について説明します。
routes/web.phpRoute::get('hello/{msg}/{pass}', function ($msg,$pass) { });URLの部分にパラメータを記述し、処理の部分に引数にルートパラメータを設定しましょう。
6. パラメタの入力を任意にする場合
上記の記述方法だとルートパラメータが必須とになります。
そのため、任意のパラメータにする方法を説明します。routes/web.phpRoute::get('hello/{msg?}', function ($msg='no_message') { });注意する部分は二つです。
- URL部分のルートパラメータに?をつける。
- 処理の引数の部分に初期値を設定する。コントローラの場合
まず、コントローラを作成
ターミナルphp artisan make::controller HelloController次に、コントローラにアクションを追加する
app/Http/Controllers/HelloController.phppublic function index(){ Return <<<EOF <html> <body> <h1>Hello</h1> if (count($msg)>0){ <p>{$msg}</p> } </body> </html> EOF; }1. ルーティングでアクションを指定する
処理をアクションに指定しましょう
routes/web.phpRoute::get(‘hello’, ‘HelloController@index’);
/hello
にアクセスするとHelloController
のindex
アクションに飛びます2. ルートパラメータの利用
ルートパラメータを設定します。
設定のやり方は上で説明したものと同じです。
入力を任意にするために?
をつけています。routes/web.phpRoute::get(‘hello/{msg?}’, ‘HelloController@index’);app/Http/Controllers/HelloController.phppublic function index($id= ‘noname’, $pass=‘unknown’){}3. シングルアクションコントローラ
シングルアクションコントローラ(1つのアクションしかないコントローラ)の場合について説明します。
routes/web.phpRoute::get{'hello', 'HelloController'}上記の場合はアクション名の指定はいりません。
app/Http/Controllers/HelloController.phppublic function __invoke() {}コントローラが少しだけ特殊です。
__invoke()
が必要です。以上で基本的なルーティングの説明は終わります。
疑問、気になるところがございましたら、質問、コメントよろしくお願いします!!!
- 投稿日:2019-12-03T19:16:28+09:00
なぜテストでWithoutMiddlewareするとRoute Model Bindingが無効化されるのか確認してみた
はじめに
Laravel Advent Calendar 2019 - Qiita の 3日目 の記事です。
今回は、Route Model Bindingしているコントローラに関連したテストが落ちる現象で小一時間ハマったのでそのことを記事にしてみました。
Route Model Bindingとは
突然ですが、getパラメータでユーザーからIDを受けとった後、該当するユーザーのEmailを返す例だとこんな感じになるかと思います。
// Route Model Bindingなしバージョン Route::get('api/users/{id}', function (int $id) { $user = Entity\User::find($id); return $user->email; });Route Model Binding使って書くとこんな感じにかけます。
// Route Model Bindingありバージョン Route::get('api/users/{user}', function (Entity\User $user) { return $user->email; });コントローラーの中で該当するユーザーのモデルを取得する処理を書かなくていいのは便利ですよね! :)
公式の説明はこちらになります。
https://laravel.com/docs/6.x/routing#route-model-binding解決方法
テストコードの中で
use WithoutMiddleware
が記述されているとMiddlewareが全て無効化されるのですが、そのせいでRoute Model Bindingの機能も無効化されているようでした。下記のように無効化するMiddlewareを絞れば解決しました。$this->withoutMiddleware([VerifyCsrfToken::class, YourAwesomeMiddleware::class]);こちらの解決方法は以前にも @gomaaa さんがQiitaにまとめてくださっているので詳しくはそちらをご覧ください。
Route Model Bindingしているルートに対してテストするときのコツ
せっかくなのでちょっとだけ深堀りしてみた
どのmiddlewareがRoute Model Bindingの役割を担っているのか気になったので調べてみました。
Laravelのバージョンは
v6.5.1
です。1.
/app/Http/Kernel.php
を確認したところ$routeMiddleware
の配列内に下記の記述を発見'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,こちらはLaravel5.3で追加されたRoute Model Bindingの機能を担うMiddlewareとのこと。こちらのリンクを参考にさせていただきました。[Laravel]ミドルウェアを整理してLaravelを軽くする - Qiita
/** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { $this->router->substituteBindings($route = $request->route()); $this->router->substituteImplicitBindings($route); return $next($request); }どうも
substituteImplicitBindings()
メソッドが怪しい...2.コードを追っていくと
Illuminate\Routing\Router.php
にsubstituteImplicitBindings
の実際のコードを発見/** * Substitute the implicit Eloquent model bindings for the route. * * @param \Illuminate\Routing\Route $route * @return void * * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ public function substituteImplicitBindings($route) { ImplicitRouteBinding::resolveForRoute($this->container, $route); }
resolveForRoute()
の中身/** * Resolve the implicit route bindings for the given route. * * @param \Illuminate\Container\Container $container * @param \Illuminate\Routing\Route $route * @return void * * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ public static function resolveForRoute($container, $route) { $parameters = $route->parameters(); foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) { if (! $parameterName = static::getParameterName($parameter->name, $parameters)) { continue;Container.php } $parameterValue = $parameters[$parameterName]; if ($parameterValue instanceof UrlRoutable) { continue; } $instance = $container->make($parameter->getClass()->name); if (! $model = $instance->resolveRouteBinding($parameterValue)) { throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]); } $route->setParameter($parameterName, $model); } }
$instance = $container->make($parameter->getClass()->name);
でLaravelのDIの仕組みを使ってEloquentのモデルを取得した後、
Illuminate/Database/Eloquent/Model.php
のresolveRouteBinding()
を実行して実際のデータを取得していたのです。/** * Retrieve the model for a bound value. * * @param mixed $value * @return \Illuminate\Database\Eloquent\Model|null */ public function resolveRouteBinding($value) { return $this->where($this->getRouteKeyName(), $value)->first(); }まとめと反省
- テストでMiddlewareの処理を無効にしたいときは無効にしたいMiddlewareだけを無効にするのが良さそう
- 助けてもらいながらコードを読んだので今度は一人で読めるようになる
おわりに
最後まで読んでくださりありがとうございました!
- 投稿日:2019-12-03T18:19:47+09:00
PHPUnitで特定のクラス群だけのテストカバレッジを算出する
この記事はうるる Advent Calendar 2019 3日目の記事です。
はじめに
今回はPHPUnitにおけるカバレッジを、特定のクラスにだけ絞って算出する方法を紹介いたします。
テストカバレッジは、一般的に高ければいいというものでもなければ、低くてもいいというものではありません。
そのサービスにおいて最適な品質保証として、どの程度テストを網羅していればいいかどうかは、サービスにより異なります。もちろんカバレッジを100%にしてあるに越したことはないですが
開発コストなどを考えると、どうでもいい機能はテストを書きたくないし、その代わりに重要な機能に対してはちゃんとテストコードを書きたい
なんてこともあると思います。この重要な機能に対してのみのカバレッジを計測したいという思いから
特定のクラス群のカバレッジを計測しましたので、その紹介をしたいと思います。システム構成
今回使用するアプリケーションは
Laravel5.5を使用しています。
PHPは7.0、PHPUnitは6.5.14です。通常のCoverage出力
PHPUnitによる一般的なカバレッジ出力は以下です。
$phpunit --coverage-html "./coverage" tests/この結果から
./coverage
が生成され、./coverage/index.html
を開くとカバレッジが表示されます。
(これは適当に拾って来たサンプルです)カバレッジレポートの出力
今度はカバレッジレポートも出力してみます。
$phpunit --coverage-html "./coverage" --coverage-clover "./clover.xml" tests/こうすることで、
clover.xml
が生成されます。
今回はCircleCI上で実行していたので、パスにcircleci
と書いてありますがあまり気にしないでください。こちらのカバレッジレポートには、どのクラスがどれくらいの大きさで、どれくらいカバレッジしているかという数値が細かく入っています。
そこから一部抜粋し、計算してみます。
<file name="/home/circleci/app/app/Http/Controllers/Admin/UserController.php"> <class name="App\Http\Controllers\Admin\UserController" namespace="App\Http\Controllers\Admin"> <metrics complexity="7" methods="5" coveredmethods="4" conditionals="0" coveredconditionals="0" statements="25" coveredstatements="22" elements="30" coveredelements="26"/> </class> <line num="18" type="method" name="__construct" visibility="public" complexity="1" crap="1" count="1"/> <line num="20" type="stmt" count="1"/> ...略 <line num="63" type="stmt" count="0"/> <metrics loc="65" ncloc="62" classes="1" methods="5" coveredmethods="4" conditionals="0" coveredconditionals="0" statements="27" coveredstatements="23" elements="32" coveredelements="27"/> </file>↓
file > metrics
のパラメーターを参照
metrics count covered coverage methods 5 4 4/5 = 80% statements(lines) 27 23 23/27 = 85.16% つまり、この
UserController
のカバレッジはlineベースで85%ほどあるというわけです。最初にHTMLで出力したものは、このカバレッジレポート値をいい感じにUIに変換したようなイメージで考えると良いでしょう。
特定クラス群のCoverage計算
さてここからが本題です。
PHPUnitでカバレッジを出力すると、必ず全体のLine数やメソッド数をベースにして計算してしまうため
本来押さえておくべきクラスやメソッドのみのカバレッジ(割合)を算出することができません。そのため、phpを用いて上記のカバレッジレポート(clover.xml)から、本当に欲しい部分のみのカバレッジを算出できる機能を実装します。
クラスの指定
phpに定数配列で直接指定してしまいます。
ここでは、clover.xmlに出力されるnamespace
をそのまま指定してあげます。
(指定したクラスのみを取り出して計算するため、検索トリガーにnamespaceを使います)const TARGET_CLASSES = [ 'App\Http\Controllers\UserController', 'App\Http\Controllers\PurcahseController', 'App\Http\Controllers\LoginController', ];xmlの読み込み
$xml = simplexml_load_file('/clover.xml'); // 適切なパスを指定集計
$coverages = []; $totalCoverage = [ 'title' => '【Total】', 'methods' => 0, 'coveredmethods' => 0, 'statements' => 0, 'coveredstatements' => 0 ]; foreach ($xml->project->package as $package) { foreach ($package->file as $file) { $class = (string)$file->class['name']; if (in_array($class, self::TARGET_CLASSES)) { // 設定したクラスに対して、カバレッジを取得する // クラスごとにメソッド数ライン数を配列に入れ込んでいく $coverages[$class] = [ 'title' => '【' . $class . '】', 'methods' => (integer)$file->metrics['methods'], 'coveredmethods' => (integer)$file->metrics['coveredmethods'], 'statements' => (integer)$file->metrics['statements'], 'coveredstatements' => (integer)$file->metrics['coveredstatements'] ]; // 各クラスのカバレッジ計測 $coverages[$class]['MethodCoverage'] = round(($coverages[$class]['coveredmethods']/$coverages[$class]['methods'])*100, 2); $coverages[$class]['StatementsCoverage'] = round(($coverages[$class]['coveredstatements']/$coverages[$class]['statements'])*100, 2); // 全体のカバレッジ集計 $totalCoverage['methods'] += $coverages[$class]['methods']; $totalCoverage['coveredmethods'] += $coverages[$class]['coveredmethods']; $totalCoverage['statements'] += $coverages[$class]['statements']; $totalCoverage['coveredstatements'] += $coverages[$class]['coveredstatements']; } } } // 各クラスの集計が終わったところでトータルのカバレッジを計算 $totalCoverage['MethodCoverage'] = round(($totalCoverage['coveredmethods']/$totalCoverage['methods'])*100, 2); $totalCoverage['StatementsCoverage'] = round(($totalCoverage['coveredstatements']/$totalCoverage['statements'])*100, 2);var_dump($totalCoverage); array(7) { 'title' => string(11) "【Total】" 'methods' => int(95) 'coveredmethods' => int(91) 'statements' => int(1190) 'coveredstatements' => int(1173) 'MethodCoverage' => double(95.79) 'StatementsCoverage' => double(98.57) }これで、本当に計測したいクラスだけを集めたカバレッジを計測することができました。
おまけ1:CircleCIからカバレッジレポートを取得
私が担当しているサービスでは、前提のテストカバレッジを毎晩夜中にCiecleCIで計測しています。
全体のカバレッジは、CircleCIで出力したhtmlを開くことで参照ができますが
上記のロジックを用いて、特定のクラス群のみのカバレッジを計測する方法をご紹介します。方法はいたってシンプルで、最初に読み込むxmlの参照先を変えるだけです。
$url = 'https://circleci.com/api/v1.1/project/github/:username/:project/latest/artifacts'; $token = $this->circleCiToken; $branch = 'develop'; $filter = 'completed'; $artifacts = $this->execCurlCommand($url . '?' . http_build_query(['circle-token' => $token, 'branch' => $branch, 'filter' => $filter])); $clover = Arr::first($artifacts, function ($file) { return $file['path'] === 'build/logs/clover.xml'; }); $xml = simplexml_load_file($clover['url'].'?circle-token='.$this->circleCiToken);こちらはCircleCIのAPIを参考に実装しています。
https://circleci.com/docs/api/#artifacts-of-the-latest-build
:vcs-type
= github
:username/
= CircleCIのユーザーネーム
:project
= repogitory名
を入れます。APITokenは、ドキュメントを参考に生成します。
https://circleci.com/docs/api/#add-an-api-token
artifacts-of-the-latest-build
を実行するとArtifactの中身が全て返って来ます。var_dump($artifacts); array(902) { [0] => array(4) { 'path' => string(17) "phpunit/junit.xml" 'pretty_path' => string(17) "phpunit/junit.xml" 'node_index' => int(0) 'url' => string(65) "https://1111-11111111-gh.circle-artifacts.com/0/phpunit/junit.xml" } [1] => array(4) { 'path' => string(24) "schemaspy/anomalies.html" 'pretty_path' => string(24) "schemaspy/anomalies.html" 'node_index' => int(0) 'url' => string(72) "https://1111-11111111-gh.circle-artifacts.com/0/schemaspy/anomalies.html" } ...略 array(4) { 'path' => string(21) "build/logs/clover.xml" 'pretty_path' => string(21) "build/logs/clover.xml" 'node_index' => int(0) 'url' => string(69) "https://1111-11111111-gh.circle-artifacts.com/0/build/logs/clover.xml" } 以下省略ここから、
clover.xml
へアクセスするためのurlを取得することで、xmlを取得することが可能です。おまけ2:Slackへカバレッジを通知
最後に計測したカバレッジをSlackへ通知します。
今回はLaravelを使ってい実装しているため、Slack通知自体はLaravelの基本機能を使用します。
https://readouble.com/laravel/5.5/ja/notifications.html実際に通知を送るロジックは以下です。
// 各クラスのカバレッジを1つずつSlackへ通知する collect($coverages)->each(function ($coverage) { $slackMessage = (new SlackMessage) ->from($this->name) ->to($this->channel); $this->setAtachmentType($slackMessage, $coverage['MethodCoverage']); $slackMessage->attachment(function ($attachment) { $attachment ->fallback($this->content['title']) ->title($this->content['title']) ->fields($this->content['message']); }); });ここでのポイントは、
setAtachmentType
メソッドで、slackで表示されるメッセージに色をつけている部分です。private function setAtachmentType(&$slack, $coverageRate) { switch (true) { case ($coverageRate >= 80): return self::SUCCESS; case ($coverageRate >= 50): return self::WARNING; default: return self::ERROR; } switch ($this->type) { case self::ERROR: $slack->error(); break; case self::WARNING: $slack->warning(); break; default: $slack->success(); break; } }カバレッジの数値を判定し、メッセージへ色をつけています。
0~49%なら赤
50~79%ならオレンジ
80~100%なら緑
がつくようにしています。
全部緑ならテンションが上がりますし、気づかぬうちにオレンジや赤になった時でも視覚的に気付きやすくしています。※色分けをわかりやすくするために、数値は変えています。
まとめ
以上でCircleCIからカバレッジを読み取り、特定のクラス群のカバレッジを計算しSlackへ通知する一連の流れを説明しました。
実際のロジックは部分的に切り取って紹介していますので、実際に実装する際にはアプリケーションロジックに合わせて適宜修正してください。さらに通知の自動化をさせるために、定期的にバッチなどでこの機能を実行させれば
勝手に計算してSlackで通知してくれるところまでできそうです。また、カバレッジの推移も取りたい場合はどこかのDBなどに突っ込んだり
それが面倒であればGASを用いてスプレッドシートに突っ込んでもいいかもしれませんね。ひとまずこれで重要な機能のカバレッジの見える化ができました!
あとがき
Advent Calendar 3日目でした。
明日4日目は Yuuki Noda さんによる記事を乞うご期待!
https://adventar.org/calendars/4548参考
https://blog.leko.jp/post/how-to-parse-of-coverage-report-with-phpunit/
- 投稿日:2019-12-03T17:38:58+09:00
Laravelで弐寺のクリアランプマネージャーをつくる
Laravel #2 Advent Calendar 20195日目のの記事です。
概要・動機
beatmania IIDXというゲームが好きです(唐突)
既存のクリアランプマネージャもあるにはあるが、機能が多くて僕には使いこなせません。
(低難度とか普段やらんしフォルダ分けもいらん)身の丈にあったツールを使いたいので、クリアランプマネージャー兼地力推定サービスを自分用に
車輪の再開発します。前提として、自分はMVCが何ぞやということはゆるふわ理解している程度で、フルスタックのwebフレームワークは初心者です。
クリアできそうな楽曲も教えてくれます!!(目玉機能)
github
https://github.com/dhaiibfiukkiu/estimator
MVCそれぞれの概要
モデル
正直ココが全てを決める気がする。
アプリの動作としては、
1.indexページに全楽曲の表とクリアした難易度のラジオボタン,submitボタン
2.ログイン後、submitでそのuserとクリアした楽曲を紐付けたclearsデータを挿入。
3.clearsデータに基づいてラジオボタンをchecked。また、地力の推定も行う。といったことを想定しているので、userテーブル、musicsテーブル、clearsテーブルがいるかなと思った。
本来userとmusicsは多対多の関係であるので、clearsがうまく中間テーブルとして作用してくれる。
つまり、clearsの複合主キーがu_idとm_idであるから、新たに主キーidを作る必要は無いと感じた。musicsテーブルのeからfcまではそれぞれのクリア難易度(地力値)です。
値はこちらから
パクっ拝借してきました。単純にページをコピペすると、それぞれの楽曲は改行(\n)で、それぞれの要素はタブ文字(\t)で区切られていたので、こちらをlaravelのシーディングにうまく合う形に加工します。
musicTableSeeder.php<?php use Illuminate\Database\Seeder; use App\Music; class musicTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $param=[ 'name' => '#MAGiCVLGiRL_TRVP_B3VTZ', 'e' => '-2.914433', 'n' => '-2.038145', 'h' => '-1.965019', 'exh' => '3.882106', 'fc' => '8.346122', ]; $musics = new Music; $musics->fill($param)->save(); //(以下略)目標はこの形です。そのために以下のPythonスクリプトを書きました。
makelist.pyinput='''(ここに楽曲のリスト) ''' input=input.split('\n') for line in input: tmp=line.split('\t') if tmp[1]=='Infinity': continue name=tmp[0].replace("'",r"\'") e=tmp[1] n=tmp[2] h=tmp[3] exh=tmp[4] fc=tmp[5] print(''' $param=[ 'name' => '{}', 'e' => '{}', 'n' => '{}', 'h' => '{}', 'exh' => '{}', 'fc' => '{}', ]; $musics = new Music; $musics->fill($param)->save(); '''.format(name,e,n,h,exh,fc))あとはこのスクリプトの出力をmusicのseederスクリプトにぶち込んで
php artisan db:seed
してやるだけです。(勿論テーブルのマイグレーションをした後にです)あと、appディレクトリ以下のMusic,Clearモデルには以下のように記述しておきます。
Music.php<?php namespace App; use Illuminate\Database\Eloquent\Model; class Music extends Model { protected $table = 'musics'; protected $guarded = [ 'm_id', 'name', 'e', 'n', 'h', 'exh', 'fc' ]; }Clear.php<?php namespace App; use Illuminate\Database\Eloquent\Model; class Clear extends Model { protected $table = 'clears'; protected $primaryKey = 'id'; //仕方ないのでidをサロゲート(代理)キーに protected function rules(){ return [ 'id' => 'integer', 'm_id' => 'integer', 'u_id' => 'integer', 'info' => 'integer' ]; } protected $fillable=[ 'id', 'm_id', 'u_id', 'info' ]; }ここに各要素のバリデーションのルールやらレコードの更新設定とかをするわけですね。
トラブル
Clear.phpの中に
//仕方ないのでidをサロゲートキーに
というコメントがあります。
実は、はじめにClearモデルを作成した際はidカラムは存在せず、m_idとu_idの複合主キーだけでした。しかし、いざレコードをsaveメソッドで保存しようとすると、エラー。
どうやら複合主キーを使うとsaveメソッドが使えなくなるらしい。なんやそれ。
まあ、メソッドをオーバーライドすればできないこともないらしい。
メンドクセー参考
https://qiita.com/wrbss/items/7245103a5fef88cbdde9
https://github.com/laravel/framework/issues/5517仕方がないので代理キーとしてidを追加しました。
ビュー
laravelでの開発と銘打っているし見た目にはそこまで拘りません。
resources/viewsフォルダにlayoutsフォルダ、estimatorフォルダを作成し、
layoutsにはおおもとのレイアウト、estimatorではそれを継承した具体的なビューを管理します。/layouts/estimator.blade.php<html> <head> <title>@yield('title')</title> <style> body {font-size:16pt; color:#708090; margin: 5px;} h1.title{ position: relative; padding: 0.2em 0.5em; background: -webkit-linear-gradient(to right, rgb(255, 124, 111), #ffc994); background: linear-gradient(to right, rgb(255, 124, 111), #ffc994); color: white; font-weight: lighter; box-shadow: 0 0 4px rgba(0, 0, 0, 0.56); } #logout{font-size:20pt; text-align:right; color:#f6f6f6; margin:-20px 0px -30px 0px; letter-spacing:-4pt;} ul{font-size:12pt;} td{font-size:16pt;} hr{margin:25px 100px; border-top: 1px dashed #ddd;} .menutitle{font-size:14pt; font-weight:bold; margin: 0px;} .content{margin:10px;} .footer{text-align:right; font-size:10pt; margin:10px; border-bottom:solid 1px #ccc; color:#ccc;} .blink { animation: blinkAnimeA 0.1s infinite alternate; } @keyframes blinkAnimeA{ 0% { background: #4dffff } 95% { background: #ffff1a } 100% { background: #ffff1a } } .highlighted{ color: #0000ff; text-decoration: underline; } .blinkchr { animation: blinkAnimeB 0.6s infinite alternate; } @keyframes blinkAnimeB{ 0% { color: #ff0000 } 97% { color: rgba(255, 255, 255, 0.99) } 100% { color: rgba(255, 255, 255, 0.99) } } .fixed_btn { font-size: 1.5em; width: 30%; height: 10%; position: fixed; bottom: 10px; right: 10px; padding: 6px 40px; display: inline-block; padding: 0.5em 1em; text-decoration: none; background: #668ad8;/*ボタン色*/ color: #FFF; border-bottom: solid 4px #627295; border-radius: 3px; } .fixed_btn:active{ /*ボタンを押したとき*/ -webkit-transform: translateY(4px); transform: translateY(4px);/*下に動く*/ border-bottom: none;/*線を消す*/ } </style> </head> <body> <h1 class='title'>@yield('title')</h1> <h2>@yield('loginfo')</h2> <hr size="1"> <div class="content"> @yield('content') </div> <div class="footer"> @yield('footer') </div> </body> </html>ここにはめ込むビューが以下
estimator/index.blade.php@extends('layouts.estimator') @section('title','地力Estimator') @section('loginfo') @php//@parent @endphp @if(Auth::check()) <p>DJ NAME:{{$user->name}}</p> <p id='logout'><a href='/logout'>ログアウト</a></p> @else <p>ログインしていません(<a href='/login'>ログイン</a>|<a href='/register'>登録</a>)</p> @endif @endsection @section('content') <form action='/' method='post'> <center> <button type='submit' class='fixed_btn'>record</button> <!--推定値--> <h1 class='suiteichi'> @if($jiriki==-100&&!Auth::check()) 推定値を取得するためにはログインしてください @elseif($jiriki==-100&&Auth::check()) 推定値を取得するためには、チェックボックスにチェックを入れたあとに、右下のボタンを押下してください @else 推定地力:{{$jiriki}} @endif </h1> <table border = '2'> <tr><th>曲名</th><th>NO PLAY</th><th>EASY</th><th>NORMAL</th><th>HARD</th><th>EX-HARD</th><th>FULLCOMBO</th></tr> {{csrf_field()}} <!--<input type='submit' value='Estimate'>--> @foreach($musics as $item) @if(!isset($clears)) @php $check=-1 @endphp @else @php $check=$clears->where('m_id',$item->m_id)->first()->info @endphp @endif <tr> <!--<td>{{$item->name}}</td>--> <td @if($check==1) bgcolor='#98fb98' @elseif($check==2) bgcolor='#87cefa' @elseif($check==3) bgcolor='#ff6347' @elseif($check==4) bgcolor='#ffff00' @elseif($check==5) class='blink' @endif >{{$item->name}}</td> <td bgcolor='#a9a9a9'> <label><input type="radio" name="{{$item->m_id}}" value=0 checked >NULL</input></label> </td> <td bgcolor='#98fb98'> <label @if($item->e <= $jiriki) class=highlighted @endif ><input type="radio" name="{{$item->m_id}}" value=1 @if($check==1) checked @endif >{{round($item->e,7)}}</input></label> </td> <td bgcolor='#87cefa'> <label @if($item->n <= $jiriki) class=highlighted @endif ><input type="radio" name="{{$item->m_id}}" value=2 @if($check==2) checked @endif >{{round($item->n,7)}}</input></label> </td></label> <td bgcolor='#ff6347'> <label @if($item->h <= $jiriki) class=highlighted @endif ><input type="radio" name="{{$item->m_id}}" value=3 @if($check==3) checked @endif >{{round($item->h,7)}}</input></label> </td> <td bgcolor='#ffff00'> <label @if($item->exh <= $jiriki) class=highlighted @endif ><input type="radio" name="{{$item->m_id}}" value=4 @if($check==4) checked @endif >{{round($item->exh,7)}}</input></label> </td> <td class='blink'> <label @if($item->fc <= $jiriki) class=highlighted @endif ><input type="radio" name="{{$item->m_id}}" value=5 @if($check==5) checked @endif >{{round($item->fc,7)}}</input></label> </td> </tr> @endforeach </table> </center> </form> @endsection @section('footer') copyright 2019 Okada Hibiki @endsection今回は、主要なページが一つだけだったこともあり、ロジックをビュー側に少し任せすぎてしまったかなというのが反省点。
コントローラ
estimatorController.php<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Music; use App\Clear; use Illuminate\Support\Facades\Auth; class estimatorController extends Controller { public function index(Request $request){ $user = Auth::user(); $musics = Music::all(); if(Auth::check()){ $clears = Clear::where('u_id',$user->id)->get();///getをかかないとダメ!!! if(Clear::where('m_id',1)->where('u_id',$user->id)->count()==0){ foreach($musics as $music){ $clear = new Clear; $clear->m_id = $music->m_id; $clear->u_id = $user->id; $clear->info = 0; $clear->save(); } } } else{ $clears=null; } if($request->method()=='POST'){ if(Auth::check()){ foreach($musics as $music){ $clear = Clear::where('m_id',$music->m_id)->where('u_id',$user->id)->first(); $m_id = $music->m_id; $clear->info = (int)$request->$m_id; //$clear->timestamps = false; $clear->save(); } } else{ // } } if(Auth::check()){ $clears = Clear::where('u_id',$user->id)->get(); $jiriki = array(); foreach($clears as $clear){ switch($clear->info){ case 0: break; case 1: array_push($jiriki,$musics->where('m_id',$clear->m_id)->first()->e); break; case 2: array_push($jiriki,$musics->where('m_id',$clear->m_id)->first()->n); break; case 3: array_push($jiriki,$musics->where('m_id',$clear->m_id)->first()->h); break; case 4: array_push($jiriki,$musics->where('m_id',$clear->m_id)->first()->exh); break; case 5: array_push($jiriki,$musics->where('m_id',$clear->m_id)->first()->fc); break; } } } //ここに推定値の処理 if(!isset($jiriki)){$jiriki=-100;} elseif(count($jiriki)==0){$jiriki=-100;} else{ rsort($jiriki); $num=ceil(count($jiriki)*0.3); $sum=0; for($i=0;$i<$num;$i++){ $sum+=$jiriki[$i]; } $sum=$sum/$num; $jiriki=$sum; } $param = ['musics' => $musics,'user' => $user,'clears' => $clears,'jiriki' => $jiriki]; return view('estimator.index',$param); } public function logout(){ Auth::logout(); return redirect()->action('estimatorController@index'); } }注意点としては、モデルの利用の際にuseしなきゃいけないことくらいか。
処理の概要としては、ログインがあるかをチェックして、しているのであればデータベースをチェック。そのユーザのクリア情報があれば取得し、なければNO PLAYとしたレコードをclearsテーブルに挿入。
POSTメソッドでのアクセスであれば、それを元にクリア情報を更新。
さらに、最新のクリア情報から自力を計算します。
この地力の計算が困ったもので、それらしい確率分布を適用しても、地力低めなEASYクリアとかが増えると地力が下がる、という現象がどうしても起こってしまう。
ので、クリアした楽曲の地力上位3割の平均値を推定地力としています。こっちのほうがシンプルでいいし、さっきみたいな不具合も幾らか少なくなるので良い。
Auth
Laravelには標準で認証機能があります。
これがものすごく便利で、php artisan ui vue --auth
するだけでログイン機能が実装できます。
Laravelのバージョン5ではphp artisan make:auth
で(読んだ本はこっちだった)
6系ではそれが使えない点に注意してください。今回のアプリケーションでは、トップページの閲覧にログインは必須ではないが、データの保存や推定値の表示にはログインが必須であるような実装にしました。
そして、標準のAuthを利用すると、/register,/login,/homeといったpathが用意されます。
初期状態だと、/registerで登録、/loginでログイン後、/homeにリダイレクトされる仕様だったのですが、これを/にリダイレクトするよう変更します。
この処理はAuth内のコントローラに記述されています。
/app/Http/Controllers/Auth/以下のRegisterController.php,LoginController.php内の
protected $redirectTo = '/home';
という記述を
protected $redirectTo = '/';
に変更すればOK。ルーティング
routes/web.php<?php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', 'estimatorController@index'); Route::post('/','estimatorController@index'); Route::get('/logout','estimatorController@logout'); Auth::routes(); Route::get('/home', 'HomeController@index')->name('home');ルーティングとはいえどメインは/で動作するestimatorControllerだけなのでシンプルですね。
おわりに
フルスタックのフレームワークを使ったのは初めてですが、簡単に自分にとって実用的なアプリが作れました。
正直今回のアプリならSPAと相性が良さそうなので、気が向いたらそれもつくってみようかな(たぶんやらん)
参考文献
David Skler(2017)初めてのPHP
掌田津耶乃(2017)PHPフレームワーク Laravel入門
- 投稿日:2019-12-03T14:51:11+09:00
nginxとlaravelでlog rotateをしてみる
ログローテートで対象のlogファイルをrenameする
logファイルをrotateしたかったか 理由↓↓
サイトのフォーム等で離脱があったり(アナリティクス)とかをlogに書き込み、そのログを1日1回解析するシステムにログファイルを読ませる必要があったため
その際に解析するシステム側には同じlogファイル名を参照するようにしたかったから!!!!!
では コード例から
フレームワークはlaravel、ミドルはnginxを使用してます。
# 対象のログファイル /var/www/laravel/shared/storage/logs/analyze.log { su nginx nginx # 毎日ローテートする daily # 30世代分古いログを残す rotate 30 # ログファイルがなくてもエラーを出さない missingok # ログファイルが空ならローテーションしない notifempty # ログファイルをコピーし、内容を削除 copytruncate # dateフォーマットを任意のものに変更する dateformat %Y-%m-%d # ログローテーション後に実行 postrotate /bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true endscript }これをプロジェクトソース内でテキストファイルとして持っておけばサーバーに配置された後に
cp analyze.txt /etc/logrotate.d/analyze
でlog rotateに設定完了。
テストしたいときは
logrotate analyze
なり
logrotate -f analyzede
実行してください。
これで毎日、
analyze.log2019-09-04 analyze.log2019-09-05 analyze.log2019-09-06 analyze.logといった具合にファイルが作らて、
analyze.logが本日付のファイルで次の日になったら
analyze.log2019-09-07が作成されます。
以上
- 投稿日:2019-12-03T14:36:59+09:00
推測せずに計測してLaravelパフォーマンスチューニング
はじめに
Laravelで開発した検索APIを、blackfireというプロファイラーを用いてパフォーマンスチューニングをしたので、その知見を共有します。
blackfireには有料プランが存在しますが、本記事で扱う内容は全て無料プランの範囲内です。
この記事で扱うこと
- 僕のプロジェクトを高速化した方法
この記事で扱わないこと
- blackfireのインストール方法
- blackfireでの計測方法
- あなたのプロジェクトを高速化する方法
→ 自力で見つけて下さい。本記事はそのヒントとなることを目指しています。
前提条件
- PHP: 7.3
- Apache: 2.4
- MySQL: 5.7
- Laravel: 5.6.1
- memcached (本記事内で導入)
- 検索API
- ページングなし
- 検索結果は最大で1,000件程度
- 検索結果1行ごとに、Laravelのrouteヘルパー関数で生成した、それ自身のURLを含む
1,000件検索
画面左上の表示から、1.4sかかっていることが分かります。
画面右のツリーを見て処理が重そうな箇所を探しましょう。route()
改善前
まず、
MasterDatumViewModel::document()
という関数が気になりました。これは検索結果のViewModelクラスで、検索結果1行ごとのURLを返すメソッドです。ソースコードを下記します。public function document(): string { return route('document', ['masterDatumId' => $this->masterDatum->id()->toInt()]); }この関数は検索結果の数だけ呼び出されます。1,000件検索では1,000回呼び出され、トータルで542msかかっています。これは全体の実行時間の40%弱を占め、そのほとんどが
route()
呼び出しによるものです。1行ごとのURLが変わることは滅多にないので、検索結果のIDをキーに含めてキャッシュしてしまいましょう。
改善後
public function document(): string { $key = $this->documentCacheKey(); $cache = cache()->store(); return $cache->get($key, function () use ($key, $cache) { $route = route('document', ['masterDatumId' => $this->masterDatum->id()->toInt()]); $cache->put($key, $route); return $route; }); } public function documentCacheKey(): string { $id = $this->masterDatum->id()->toInt(); return "master_datum:document:route:{$id}"; }改善後のソースコードです。キャッシュドライバはmemcachedを使用しました。
1.57sと、却って遅くなってしまいました。画面右のツリーを見ると、キャッシュリポジトリを取得する
cache()
とCacheManager::store()
の呼び出しがかなりの割合を占めます。ViewModelクラスですし、インスタンス毎に異なるキャッシュリポジトリを使うことも考えにくいので、ここは思い切ってstatic変数にしてしまいましょう。
再改善後
public function document(): string { $key = $this->documentCacheKey(); // 呼び出し回数が多いため初回呼び出し時にstaticとして保持 static $cache; $cache = $cache ?? cache()->store(); return $cache->get($key, function () use ($key, $cache) { $route = route('document', ['masterDatumId' => $this->masterDatum->id()->toInt()]); $cache->put($key, $route); return $route; }); } public function documentCacheKey(): string { $id = $this->masterDatum->id()->toInt(); return "master_datum:document:route:{$id}"; }キャッシュリポジトリをstatic変数とし、クラス内で初回のみ取得するよう変更しました。
結果は1.1sとなり、0.3sの高速化ができました。
余談
デプロイ時にはキャッシュを忘れずに削除しましょう。
\Illuminate\Database\Eloquent\Builder\get()
改善前
次に気になったのは、
SearchMasterDataAdapter::findMasterData()
という関数です。これは、入力された検索条件をクエリビルダーに適用し、検索結果をドメインモデルに変換して返すメソッドです。ソースコードを下記します。
public function findMasterData(SearchQuery $query): iterable { $builder = $this->eloquentMasterData->newQuery(); $keyword = $query->keyword(); $sort = $query->sort(); if ($keyword !== null) { $builder->ofKeyword($keyword); } foreach ($query->businessCategories() as $businessCategory) { $builder->ofBusinessCategory($businessCategory); } if ($sort !== null) { $builder->sort($sort); } return $builder->get()->map(static function (MasterData $masterDatum) { return $masterDatum->toDomainModel(); }); }この中で大きな比率を占めるのが
$builder->get()
です。ツリーを見ると、Eloquentが検索結果をMasterDataモデルにhydrateする処理に時間がかかっているようです。
適切にデータがセットされたMasterDataモデルのtoDomainModel()
さえ呼べれば用は足りるので、PDOで取得した検索結果をMasterDataモデルにセットしてみましょう。改善後
public function findMasterData(SearchQuery $query): iterable { // 省略 $pdo = $builder->getConnection()->getPdo(); $bindings = $builder->getBindings(); $stmt = $pdo->prepare($builder->toSql()); $results = $stmt->fetchAll(\PDO::FETCH_ASSOC); return \array_map(static function (array $row) { static $masterData; $masterData = $masterData ?? new MasterData(); return $masterData->setRawAttributes($row)->toDomainModel(); }, $stmt->fetchAll(\PDO::FETCH_ASSOC)); }まずはクエリビルダーをSQLとパラメータに変換して、PDOで取得します。それを
MasterData::setRawAttributes()
に渡すことでMasterDataモデルに値をセットし、ドメインモデルに変換します。先程の1.1sから、911msまで高速化できました。
1,000件検索のプログラム改善はここまでとします。
gzip
改善前
次はブラウザでAPI呼び出しを見てみましょう。
1,000件ものデータとなるとその容量も小さくなく、433kbとなっています。その転送にはおよそ456msかかっています。
これをWebサーバーで圧縮して、転送速度を改善しましょう。改善後
.htaccess<IfModule mod_deflate.c> <IfModule mod_filter.c> FilterDeclare COMPRESS FilterProvider COMPRESS DEFLATE "%{Content_Type} =~ m#^application/json#" FilterChain COMPRESS FilterProtocol COMPRESS DEFLATE change=yes;byteranges=no </IfModule> </IfModule>コンテンツの圧縮の方法はここでは扱いませんが、
Content-Type
がapplication/json
で始まるレスポンスを圧縮するよう設定しました。
(ちなみにm#^application/json
の後に$を付けて完全一致にすると、Content-Type: application/json; charset=utf-8
のようなケースで圧縮されません。)データ量が73.5kbまで低減しました。転送時間は大差ありませんが、これは開発環境がローカルであることが原因です。
本番環境では転送時間が大きく改善していることを確認できました。1件検索
次に1件検索時のプロファイルをします。1,000件検索時とは違って、もっとアプリケーションのベース部分の問題が判るはずです。
サードパーティ製ライブラリ
改善前
1件検索時の結果は478msとなりました。
この内、およそ50%程度をサードパーティ製ライブラリの処理が占めていました。ソースコードを読むと改善の余地が十分にありそうですが、vendor
以下のコードを変更する訳にはいきません。ライブラリの修正を待つのも一つの手ですが、今回は時間的猶予がありませんでした。そのため、自分で改善したクラスをサービスコンテナに登録する形で対応しました。
改善後
./app/Providers/AspectServiceProvider.phpuse App\Aop\AspectManager; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; use Ytake\LaravelAspect\AnnotationConfiguration; final class AspectServiceProvider extends ServiceProvider { public function register() { $this->app->singleton('aspect.manager', static function (Application $app) { /** @var AnnotationConfiguration $annotationConfiguration */ $annotationConfiguration = $app->make(AnnotationConfiguration::class); $annotationConfiguration->ignoredAnnotations(); // ↓自分で作成したクラス return new AspectManager($app); }); } public function boot() { } }詳細は省きます。重要なのは、サービスコンテナを利用することで既存の処理も置換可能ということです。
Laravelドキュメントのサービスコンテナのページを読んで下さい。
https://readouble.com/laravel/6.x/ja/container.html284msとなり、およそ200msもの改善ができました。
まとめ
- 1,000件検索では1.4s→911ms。約35%の改善。
- 1件検索では478ms→284ms。約40%の改善。
本記事で行なった施策が、そのままあなたのプロジェクトに適用できることは稀だと思います。
あなたのプロジェクトに最適な施策を見つけて下さい。推測するな、計測せよ。
よきパフォーマンスチューニングを。
- 投稿日:2019-12-03T14:11:26+09:00
GitHub Actions で Laravel + Vue 環境を自動デプロイ
GitHub Actions が正式リリースされましたね。
これで外部サービスを使用しなくても GitHub 単体で CI/CD などのワークフローを自動化できるようになりました。
https://github.com/features/actions弊社は社内の GitLab でソースコード管理+ CI/CD していますが、ちょうど担当していた案件の納品先が GitHub ということもあり、Laravel + Vue アプリケーションをビルドし本番サーバにデプロイするところまでを実装してみました。
ゴール
特定のブランチに push すると自動で本番環境にデプロイする
流れ
ワークフローとして実行するタスクはこんな感じです。
- 特定のブランチ(master)へのプッシュ(or プルリク) をトリガーにワークフローを起動
- GitHub 上で docker コンテナを起動
- 以下コンテナ上で
- git clone (ソースコード取得)
- composer install (PHP環境構築)
- npm install (node環境構築)
- npm run prod (フロントエンドビルド)
- rsync でコンテナ上で構築したファイルを本番サーバへ転送
秘匿情報をいかにセキュアに管理するか
今回の案件ではフロントエンド(Vue.js)=>バックエンド(Laravel)間の API 通信の認証に Laravel Passport を使用していました。Laravel Passport では認証用のトークンをフロントエンドのコードに含めてビルドする必要があるため、トークンをコンテナ内に持ち込む必要がありますが、他にも DB のパスワードや、AWS のトークンなど、またビルドした成果物を rsync + ssh で本番サーバに同期する際の秘密鍵、などなど、知られてはいけない情報が多々あり、当然これら セキュアな情報を記載したコードをリポジトリにコミット出来ない問題 に直面します。
秘匿情報は Secrets に保存
これを解決するために、GitHub のリポジトリ設定画面には Secrets というメニューが追加されており、セキュアな情報に名前を付けて保存できます。これは GitHub Actions だけに使用する目的で追加された機能で、
Secrets are environment variables that are encrypted and only exposed to selected actions. Secrets are not passed to workflows that are triggered by a pull request from a fork of this repository.
とある通り、情報は暗号化され、選択されたアクションにのみ使用されれます。リポジトリのフォークで継承されることもなく、また、他のコントリビューターからも閲覧されることはありません。(そもそも登録した本人でさえ一度登録した内容は閲覧できません)
Secrets は、GitHub のリポジトリメニューの「Settings」からサイドメニューの「Secrets」より遷移し、「Add a new secret」をクリックして、Name と Value を登録します。
今回はここに、Laravel Passport の認証用トークン
OAUTH_CLIENT_SECRET
と rsync で使用する SSH の秘密鍵SSH_PRIVATE_KEY
を登録しました。これらの Name はワークフローの定義時に変数として使用することができるので後述の定義ファイル自体はセキュアな記述となります。MIX_OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_CLIENT_SECRET }}ワークフローの作成
それではワークフローを作っていきましょう。リポジトリのタブメニューにある Actions をクリックすると予め用意されたワークフローが表示されます。これから作成するワークフローに近いものがあればそれをテンプレートとして使用しても良いですが、今回は使用しません。実は画面から作成しなくても、定義ファイルを直接リポジトリに追加しプッシュすればワークフローとして認識します。
定義ファイルは下記のパスになります。
.github/workflows/main.yml
拡張子からも分かる通り、GitHub Actions ではワークフローを YAML 形式で定義します。
name: 本番サーバへのデプロイ on: push: branches: - master jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: バックエンドの準備 run: composer install - name: フロントエンドのビルド run: npm install && npm run prod env: MIX_OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_CLIENT_SECRET }} APP_ENV: "production" APP_DEBUG: "false" APP_URL: "https://xxxxxx.test" - name: 秘密鍵のコピー run: echo "$SSH_PRIVATE_KEY" > id_rsa && chmod 600 id_rsa env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - name: ファイルをサーバに同期 run: rsync -rlOtcv --delete --exclude-from=.rsyncignore -e "ssh -i id_rsa -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" ./ deploy@xxxxxxx.test:/var/www/vhost/xxxxxxxx/このファイルをpushするだけで指定したブランチに更新があった際にワークフローが開始されます。
ファイルの解説
簡単に定義ファイルの中を見ていきましょう。
name: (ワークフローの名称)
name: 本番サーバへのデプロイこのワークフローの名前を定義します。GitHub のサイト上から見えるためわかりやすいものにしましょう。日本語も可能です。
on: (アクション起動条件)
on: push: branches: - masterトリガーとなる条件を記述します。ここでは master ブランチに push があった場合の条件を指定しています。
runs-on: (コンテナの指定)
runs-on: ubuntu-latest使用するコンテナを指定します。windows や macos も指定できるのでネイティブアプリのビルドなども可能。
steps: (アクションの定義)
steps:ここからワークフローをステップごとに定義します。
uses: (既存のアクションの使用)
- uses: actions/checkout@v1GitHub Actions は既に用意されたアクションや、GitHub Marketplace に公開されている自作アクションを部分的に使用することができます。ここでは actions/checkout と言うアクションを使用し、リポジトリから最新のソースを clone しています。
バックエンドの準備
- name: バックエンドの準備 run: composer installこのステップでは
run
アクションでcomposer install
コマンドを実行しています。フロントエンドのビルド
- name: フロントエンドのビルド run: npm install && npm run prod env: MIX_OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_CLIENT_SECRET }} APP_ENV: "production" APP_DEBUG: "false"ここではフロントエンドのビルドを実行しています。
run
アクションでnpm install
とnpm run prod
を実行し、Vue.js のソースをトランスパイルしています。
また、前述のトークンや環境ごとの依存情報などは、本来 .env に記述しますが、リポジトリには追加したくないため環境変数経由で Secrets の値を渡しています。秘密鍵のコピー
Secrets に保存した SSH プライベートキーを環境変数経由でファイルに書き出しています。パーミッションも忘れずに。
- name: 秘密鍵のコピー run: echo "$SSH_PRIVATE_KEY" > id_rsa && chmod 600 id_rsa env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}ファイルをサーバに同期
あとは成果物をサーバに rsync+ssh でコピーするだけです。転送量を節約するため .rsyncignore に不要なファイルを定義し、いくつかのフォルダを除外しています。
- name: ファイルをサーバに同期 run: rsync -rlOtcv --delete --exclude-from=.rsyncignore -e "ssh -i id_rsa -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" ./ deploy@xxxxxxx.test:/var/www/vhost/xxxxxxxx/.rsyncignore
.env .git .github node_modules/ storage/いざ実行
ワークフローの実行は
on
で指定したタイミングでの自動起動なので、master ブランチに git push するだけです。
GitHub の Actions タブをクリックすると実行中のタスクをリアルタイムにチェックできます。
まとめ
社内の GitLab では、CI/CD をフル活用しておりユニットテストやLintチェックなどを常に自動化していますが、自社サーバに構築しているので環境を自前で構築する必要があったりスペックに悩まされてきましたが、GitHub Actions ではいとも簡単にできてしまいました。Marketplace での Action も充実してくると思うので今後さらに期待ができますね。非常に楽しみです。
- 投稿日:2019-12-03T10:33:58+09:00
OneSignalで特定のユーザーにWEBプッシュ通知を送る方法(PHP編)
OneSignalは無料で使いはじめることができるプッシュ通知配信サービスです。(WEBプッシュは3万ユーザーまで。モバイルは無制限で)
こちらを使うと驚くほどかんたんにプッシュ通知を送れました。今回はLaravelにてOneSignalを使ってWEBブラウザ向け(Chrome、Firefox)にプッシュ通知を配信する方法をまとめます。OneSignalでアプリを作成
Website Push を選択。
SITE NAMEとSITE URLを入力。
ローカルでテストする場合はSITE URLは「https://localhost 」など適宜変更してつくってください。
DEFAULT ICON URLはプッシュ通知の許可ダイアログででてくるアイコンです。
Permission Prompt Setupは、最初にサイトを訪れた時にプッシュ通知の許可確認をとる方法を選べます。
Welcome Notificationでは許可された場合に送るプッシュ通知を設定。お礼のメッセージなどを。
ほかにもいろいろ設定はありますが、基本的にはこれだけで「SAVE」でOK。
次に、「DOWNLOAD ONESIGNAL SDK FILES」を押してファイルをダウンロードして、下記の2つのファイルをサイト直下にアップしてください。
最後に、JavaScriptのコードを埋め込めば完了です。OneSignalでプッシュ通知を送る
これでプッシュ通知を送れるようになりました。OneSignalの管理画面からMessagesを開いてNEW PUSHをクリック。
Send to Subscribed Usersで、購読している人に送れます。
個別にプッシュ通知を送りたい
全体に送るものだけじゃなく、特定のユーザーにだけ通知を送りたいケースはよくあります。
アプリ側から下記のようにUserIdをセットすることができます。(下記の例はLaravelのbladeに書いています)
<script> var OneSignal = window.OneSignal || []; OneSignal.push(function () { OneSignal.init({ appId: "ONESIGNALのAPP_ID", }); @if(isset($loginUser)) //onesignalにuser_idをセット OneSignal.on('subscriptionChange', function (isSubscribed) { if (isSubscribed == true) { OneSignal.setExternalUserId('{{ $loginUser->id }}'); OneSignal.getExternalUserId().then(function (id) { }); } else if (isSubscribed == false) { OneSignal.removeExternalUserId(); } }); @endif }); </script>これでOneSignalのユーザーとアプリ側のユーザーを一致させます。
次にプッシュ通知を送りたいところで下記のようにして個別に送ることができます。headingsはタイトル、contentsは内容をいれます。
SendPush.php$fields = array( 'app_id' => ONESIGNALのAPP_ID, 'include_external_user_ids' => [$user_id], 'url' => アプリのURL, 'headings' => array('en' => $title), 'contents' => array('en' => $body) ); $fields = json_encode($fields); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "https://onesignal.com/api/v1/notifications"); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json; charset=utf-8', 'Authorization: Basic '.ONESIGNALのAPP_ID)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $fields); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $response = curl_exec($ch); curl_close($ch);いかがだったでしょうか。OneSignalがあればこれだけかんたんにプッシュ通知を送ることができるようになります。ブラウザだけでなく、スマホアプリにも対応していますのでWEBもアプリもプッシュ通知はOneSignalさえあれば捗ります。