- 投稿日:2019-11-30T22:48:11+09:00
【Laravel】画面で指定したソート条件をページング先でも反映させる
画面上でボタンなどでソート条件を指定→ページング先でも反映させるための一連の手順です。
やりたいこと
環境
- PHP:バージョン7.3.7
- Laravel:バージョン5.8
- OS:Windows10
手順
1. ページング機能の作成
まずはページング機能を作成します。
ControllerとViewを編集するだけなので、わりと簡単。(1)Controllerの編集
全件を取得し、1ページ当たり5データ表示させる場合は以下の通り。
pagenate()のカッコ内の数字を任意の数字に変えましょう。TestController.phppublic function initialize(){ $test_forms = TestForm::all(); // テーブル:test_formsから全件を取得 $test_forms = TestForm::paginate(5); // 1ページに表示されるデータを5件に設定 return view('test_list', ['test_forms' => $test_forms]); }(2)Viewの編集
Viewにページングを効かせる記述を追加します。
アロー演算子(->)の左辺にはControllerで渡した変数を記述。test_list.blade.php{{ $test_forms->links() }}2. ソート機能の追加
続いてソート機能。
専用のプロジェクトを追加するので、ページングよりは少し手間がかかります。(1)ソート用パッケージのインストール
ソート用パッケージ、kyslik/column-sortableをインストールします。
コマンド実行場所は作成したLaravelプロジェクト直下です。> composer require kyslik/column-sortable(2)ソート用パッケージの設定ファイルの作成
続いて設定ファイルの作成。
こちらもコマンド実効場所はプロジェクト直下。> php artisan vendor:publish --provider="Kyslik\ColumnSortable\ColumnSortableServiceProvider" --tag="config"(3)config/app.phpへの追記
config/app.phpのproviders配列に以下の通り追記します。
これで、今回インストールしたサービスプロバイダクラスが読み込まれるようになります。config/app.php'providers' => [ Kyslik\ColumnSortable\ColumnSortableServiceProvider::class, //追加 ],(4)モデルの編集
モデルクラスにソート対象を記述します。
「public $sortable =」に、ソート対象を配列形式で。
※要素は後の手順で出てくるViewの@sortablelink()の第1引数に一致させます。TestForm.php<?php namespace App; use Illuminate\Database\Eloquent\Model; use Kyslik\ColumnSortable\Sortable; // 追加 class TestForm2 extends Model { use Sortable; // 追加 public $sortable = ['id', 'name', 'created_at', 'updated_at']; // 追加 }(5)Controllerの編集
Controllerの「paginate()」の前に「sortable()->」を追記します。
TestController.phppublic function initialize(){ $test_forms = TestForm::all(); $test_forms = TestForm::sortable()->paginate(5); // sortable()->を追記 return view('test_list', ['test_forms' => $test_forms]); }(6)Viewの編集
①ソート対象を@sortablelink()で記述
- 第1引数:ソート対象のカラム名
- 第2引数:画面に表示する文字列(省略するとカラム名が表示される)
を入力します。必要に応じて、
- 第3引数:デフォルトのクエリストリング(省略可)
- 第4引数:追加のアンカータグの属性(省略可)
も入力しましょう。
test_list.blade.php@section('content') <table class = "table"> <thead> <tr> <td>@sortablelink('id', 'ID') <td>@sortablelink('name', '名前') <td>@sortablelink('created_at', '作成日') <td>@sortablelink('updated_at', '更新日') </thead> @foreach($test_form2s as $test_form2) <tbody> <tr> <td>{{ $test_form2->id }} <td>{{ $test_form2->name }} <td>{{ $test_form2->created_at }} <td>{{ $test_form2->updated_at }} </tbody> @endforeach </table>②ページングを効かせる記述を変更 →【ココが重要!】
ページング機能作成時にViewに記述した「 {{ $test_forms->links() }}」を以下のように修正します。
「->appends(request()->query())->」を追加してあげることで、ページングで次のページの内容を読み込んでも画面でしていたソート条件が反映されます。
※appendsのsを忘れないようにしましょう。test_list.blade.php{{ $test_forms->appends(request()->query())->links() }}参考
- 投稿日:2019-11-30T22:31:36+09:00
おれがコンピュータに筆算を教えてやる - 「100 桁の足し算」編
環境
$ php -v PHP 7.3.11 (cli) (built: Oct 22 2019 11:20:10) ( NTS MSVC15 (Visual C++ 2017) x64 ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.3.11, Copyright (c) 1998-2018 Zend Technologiesまずは普通に計算させる
試しに 2 つの整数を足す関数を作って 100 桁の足し算を実行してみます.きっとコンピュータだからできて当たり前でしょう.
function add($a, $b) { return $a + $b; } var_dump(add( 2345760293460295683704558703456102394871348756109347501936410934756102913874187344541930193470934875, 7947618927365019364019257691823461928734634570193248703473987562093471093576293478628949398873429857 ));結果
float(1.0293379220825E+100)あれ,正確に出てこないうえに float って...
果たしてコンピュータには 100 桁の足し算はできないのでしょうか?
でも,人間は小学校のときに習った筆算を使えば,どんなに大きい桁の足し算も計算できたはずですよね?
どうやって計算していたかというと, 1 桁ずつ計算していたはずです.
それを関数の形で実装してみたいと思います.
簡単のため,自然数での実装とします.コンピュータに筆算させる
完成品がこちらになります.
function addBigInt(string $a, string $b): string { // 自然数を受け取る $reversedDigit1 = array_reverse(str_split($a)); $reversedDigit2 = array_reverse(str_split($b)); $reversedSumDigit = array_fill( 0, /* 足す数のうち,大きい方の桁数 */ max(count($reversedDigit1), count($reversedDigit2)), /* 0 で埋めた配列 */ 0 ); // 各位の足し算をする $carryOver = 0; // 繰り上がりの数字 (2 つの数の足し算なら必ず 0 or 1) foreach ($reversedSumDigit as $power => $digit) { // 直接要素をいじりたいので, $reversedSumDigit[$power] を変数化しない $reversedSumDigit[$power] += $carryOver; $carryOver = 0; $reversedSumDigit[$power] += $reversedDigit1[$power] + $reversedDigit2[$power]; if ($reversedSumDigit[$power] >= 10) { $carryOver += floor($reversedSumDigit[$power] / 10); $reversedSumDigit[$power] %= 10; } } // 最後の桁の繰り上がりを考慮 if ($carryOver > 0) { $reversedSumDigit[] = $carryOver; } // 再び逆順にすれば答えになる return implode(array_reverse($reversedSumDigit)); }100 桁の整数を受け取る
今回の環境で int 型の最大の数は次の通りです.
var_dump(PHP_INT_MAX); // int(9223372036854775807)19 桁.
100 桁には到底及ばないので,整数を文字列で受け取り,文字列で返すことにします.function addBigInt(string $a, string $b): string { return ''; }1 桁ずつ配列に収めて各位で足し算をする
筆算は一般的に 1 の位から順に行うので,受け取った文字列を逆順で配列に収めます.
// 自然数を受け取る $reversedDigit1 = array_reverse(str_split($a)); $reversedDigit2 = array_reverse(str_split($b));逆順に収めておくと,配列のインデックス番号が 10 の累乗部分そのものになるので計算しやすくなります.
// 各位の足し算をする $carryOver = 0; // 繰り上がりの数字 (2 つの数の足し算なら必ず 0 or 1) foreach ($reversedSumDigit as $power => $digit) { // 直接要素をいじりたいので, $reversedSumDigit[$power] を変数化しない $reversedSumDigit[$power] += $carryOver; $carryOver = 0; $reversedSumDigit[$power] += $reversedDigit1[$power] + $reversedDigit2[$power]; if ($reversedSumDigit[$power] >= 10) { $carryOver += floor($reversedSumDigit[$power] / 10); $reversedSumDigit[$power] %= 10; } } // 最後の桁の繰り上がりを考慮 if ($carryOver > 0) { $reversedSumDigit[] = $carryOver; }数字が逆順になっていたので,元に戻して完成
文字列で返します.
// 再び逆順にすれば答えになる return implode(array_reverse($reversedSumDigit));再計算!
var_dump(addBigInt( '2345760293460295683704558703456102394871348756109347501936410934756102913874187344541930193470934875', '7947618927365019364019257691823461928734634570193248703473987562093471093576293478628949398873429857' ));結果
string(101) "10293379220825315047723816395279564323605983326302596205410398496849574007450480823170879592344364732"うおおおおおおおおおおお
ちゃんと計算できてますね.もっと増やしてもちゃんと計算してくれます.
var_dump(addBigInt( '912737487163405918273410730417265019827364107277350123412012745625927341972568902713780850717457812370418984756601723460197236495188772346318765091765127363109837412873561009237462421287346041972650192837461873568364192837464193865103948571238741602957163184971620398471348160239471694857629384761528375165299384710397610347102938471057109471929387912347103945761348571039847102934633329457693487120398855703947198434618237466193744642173654165247634516234278458645056978560980690867798069684974562938740234875601982374918723659517364716523498172625398176230471645981732948710234971602359175234567324', '123652387065670597706023401234982497590123407123680183455013284812330953490713456773049987353980572364096780450287409287645097665450287342987439276093478610932476093745620985710934476103498761923874629478612384762839439878563545862399958304984756983409233938793038473949458162862368345760806798080800609819236732348345628345222735234725325734821978378294398249756924877266895898237946170284762875887039846752934762330953872469857872394368754654245343375176247702983567623478610943865759675412563561984357209568793645982345727483865817236460394867249128621423873458374458623485886442938467634715623412' ));もはや意味わからないけどw
string(601) "1036389874229076515979434131652247517417487514401030306867026030438258295463282359486830838071438384734515765206889132747842334160639059689306204367858605974042313506619181994948396897390844803896524822316074258331203632716027739727503906876223498586366397123764658872420806323101840040618436182842328984984536117058743238692325673705782435206751366290641502195518273448306743001172579499742456363007438702456881960765572109936051617036542408819492977891410526161628624602039591634733557745097538124923097444444395628357264451143383181952983893039874526797654345104356191572196121414540826809950190736"感想
int の壁を破って計算できたのはうれしいです.
同じ考え方で他の四則演算もできるのではないかと思っています. (時間があったら実装してみたいです.)調子乗ってすみませんでした
- 投稿日:2019-11-30T21:40:50+09:00
PHP <条件分岐でのincludeとURLからのタイトル取得>
はじめに
最近はPHPについて学習しています。
ここ一ヶ月の勉強で学んだことを使用しながら、簡単なプログラムを作ってみました。
セキュリティ関係はほとんど考慮していません。ある程度のセキュリティについてはこちらの記事で説明しています。作成した機能の説明
初期画面は以下の通り。
GETを使用したページ遷移を行いたい。また入力したURLからそのサイトの情報を一部取得したい(ここではtitleタグ取得する)。
TopPage、MyPage、送信を押した場合の画面は以下の通り。
URLから取得したタイトルとそのURLをリンクとして表示する。またTopPageを押した場合はQiitaのトップページを、MyPageを押した場合は自分のMyPageを取得している。
コード
先に上のプログラムのコードを載せておきます。
main.php<html> <head> <title>PHP Test</title> </head> <body> <h1>Qiitaのページ検索</h1> <p>↓にincludeしたい</p> <?php if(!isset($_GET['title']) and !isset($_GET['pageid'])){ include 'search.php'; } else { include 'result.php'; } ?> <p>↑にincludeしたい</p> </body> </html>search.php<form name = "form1" action = "main.php" method = "GET"> <!-- TopPage --> <button type="submit" name="pageid" value=2>TopPage</button> <br/> <!-- MyPage --> <button type="submit" name="pageid" value=1>MyPage</button> <br/> <!-- 記事検索(入力チェックあり) --> <input type = "text" name ="title" pattern="^https://qiita.com{1}[a-zA-Z0-9!-/:-@¥[-`{-~]*$"> <br/> <input id="send" value="送信" type="submit"> </form>result.php<?php // エラー確認用 ini_set('display_errors', "On"); ini_set('error_reporting', E_ALL); //タイトルを取得したいURL $url = ''; // 表示するテキスト $text = ''; $urlText = ''; if(isset($_GET['title'])){ //タイトルを取得したいURL $url = $_GET["title"]; } if(isset($_GET['pageid'])){ $pageid = $_GET['pageid']; // urlを設定 switch ($pageid) { case 1: $url = 'https://qiita.com/vber_program'; break; case 2: $url = 'https://qiita.com/'; break; } } if($url==''){ echo "入力が空です<br>"; return; } if(!preg_match("#^https://qiita.com+[a-zA-Z0-9!-/:-@¥[-`{-~]*$#",$url)){ //urlがQiita以外のものは不正なページとして扱う echo "不正なページです。<br>"; return; } //ソースの取得 $source = @file_get_contents($url); //取得できたかどうかを判定 if($source){ //取得できたのでページは存在する //文字コードをUTF-8に変換し、正規表現でタイトルを抽出 if (preg_match('/<title>(.*?)<\/title>/i', mb_convert_encoding($source, 'UTF-8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS'), $result)) { $urlText = $result[1]; $urlText = str_replace("- Qiita", "", $urlText); //URL置換 $pattern = '(https?://[-_.!~*\'()a-zA-Z0-9;/?:@&=+$,%#]+)'; $replacement = '<a href="\1" target="_blank">\1</a>'; $text = mb_ereg_replace($pattern, $replacement, htmlspecialchars($url)); } }else{ //取得できなかったのでページは存在しない $text = "ページは存在しません"; } echo $urlText."<br>"; echo $text."<br>"; ?> <a href="main.php" >前のページに戻る</a>注意した点と簡単な解説
条件分岐によるinclude
[main.php]
純粋にIF文を使うだけでした。
GETで値を取得していなければ「検索画面のsearch.php」を表示し、GETに値が入っていれば「結果表示画面のresult.php」を表示させています。正規表現チェック
[search.php , result.php]
入力画面とPHPの処理の両方に入れています。今回はQiitaのURL以外は弾くようにしています。
またtitleタグ内を取得する際にも使用しています。file_get_contents
[result.php]
入力したURLのソースを取得しています。失敗した場合にはFalseを返すのでIF文で判断します。preg_match
[result.php]
正規表現を使用して「file_get_contents」で取得したソースの中からtitleタグ内の情報を検索しています。mb_ereg_replace
[result.php]
正規表現を使用した文字置換。入力した値をURLに変換する際に使用しました。
また「htmlspecialchars」を使用して特殊文字をHTMLエンティティに変換しています。return
[result.php]
exitを使用して処理を抜けてしまうと、それ以降の処理(今回の場合は「↑にincludeしたい」を表示する部分)が中断されてしまいます。そのためreturnを使用して処理を抜けるようにしました。おわりに
絶対に関数を使った方が
今回使用した関数でわかりにくい部分があれば、公式サイトで調べてみて下さい。参考にさせて頂いたサイト様
PHPのincludeでGETのパラメーター(引数)の渡し方
ありがとうございました!
- 投稿日:2019-11-30T19:48:00+09:00
【Laravel】自作artisanコマンド実行中のログを保存する方法
はじめに
Laravelで自作artisanコマンド実行中の様子を
storage\logs
内にログファイルとして保存したい場合の方法を書いてみます。artisanコマンドを生成
$ php artisan make:command HelloCommand
app\HelloCommand.php
を以下のように編集<?php namespace App\Console\Commands; use Illuminate\Console\Command; class HelloCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'HelloCommand'; /** * The console command description. * * @var string */ protected $description = 'Command description'; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { createHelloLog('ログが出力されました!'); } }自作関数を作成
app\helper.php
を作成して編集
関数の中身は、こちらの記事を参考にさせていただきました。helper.phpuse Monolog\Formatter\LineFormatter; use Monolog\Handler\RotatingFileHandler; use Monolog\Logger; if (!function_exists('createHelloLog')) { function createHelloLog($data) { $log_path = storage_path('logs/hello.log'); $days = 90; $log_level = 'debug'; $bubble = true; $filePermission = 0777; $handler = new RotatingFileHandler($log_path, $days, $log_level, $bubble, $filePermission); $formatter = new LineFormatter(null, null, true, true); $handler->setFormatter($formatter); $logger = new Logger('hello'); $logger->pushHandler($handler); $logger->info($data); } }エラーを出力して保存した場合はエラー用の関数を用意して、
$logger->info($data);
の部分を$logger->error($data);
とするといいですね!オートローダの設定
自作関数を読み込むためにオートローダの設定をします。
composer.json
に"files"
以下を追記composer.json"autoload": { "classmap": [ "database/seeds", "database/factories" ], "psr-4": { "App\\": "app/" }, "files": [ "app/helpers.php" ] }
composer dumpautoload
を実行$ composer dumpautoload
実行してみる
$ php artisan HelloCommand
storage\logs\hello-xxxx-xx-xx.log
が生成されているので中身を確認
きちんと保存されていますね!
- 投稿日:2019-11-30T16:27:31+09:00
【Docker】はじめてのDocker〜概要・PHPコマンドの実行まで
名前しか知らなかったDockerを使ってみました。
備忘録です。Dockerとは
コンテナ型の仮想環境を提供するツール
docker初心者の方が知っておいた方がよい基礎知識なぜ使うのか
- 従来の仮想環境(ハイパーバイザ型)と比較して軽く、リソース消費が少ないから
- 作成した環境を簡単に配布できるから
用語集
解説はDocker-docs-ja/用語集を参考にしています。
ハイパーバイザ型・コンテナ型の概要も含め、図で見た方が分かりやすいです。
Dockerのすべてが5分でわかるまとめ!(コマンド一覧付き)Dockerコンテナ
- Dockerイメージを実行するときの実体(runtime instance)
- Dockerイメージを実行(run)するとDockerコンテナが生成される
- OSレベルの仮想化を行う
Dockerイメージ
- Dockerコンテナの元のようなもの
- 環境を作るための設定がひとまとめになっている
Dockerfile
- Dockerイメージを構築するために必要な命令の一覧
- DockerfileをビルドするとDockerイメージが構築される
- テキスト形式のドキュメント
リポジトリ
- Dockerイメージの集合体
レジストリ
- リポジトリを預かるサービス(Docker Hub)
実行
ファイル作成
必要なディレクトリ・ファイルを作成します。
$ mkdir docker_test $ cd docker_test $ touch main.php $ touch Dockerfile実行ファイル
main.php<?php echo "Hello! World!\n"; echo phpversion()."\n";Dockerfile
イメージは
php:7.2-cli
を指定したので、7.2系で最新のバージョンが呼び出されます。Docker Hub - php
Dockerfile リファレンス
【入門】Dockerfileの基本的な書き方# イメージを指定 FROM php:7.2-cli # イメージ上で実行するコマンドを指定 # helloディレクトリを作成 RUN mkdir /hello # helloディレクトリにmain.phpをコピー COPY main.php /hello # 実行コマンド # php /hello/main.phpを実行 CMD [ "php", "/hello/main.php" ]ビルド
buildコマンド
でビルドし、新しいイメージを生成します。
tオプション
でリポジトリ名hello-app
と実行場所
を指定します。
タグ名
は指定していないので、デフォルトのlatestが設定されます。同名のリポジトリが複数存在する場合は、タグをつけて識別します。
イメージのタグ付け、送信、取得
リポジトリ(repository)
タグ$ docker build -t hello-app . # 省略 Successfully built b9d2c9893a00 Successfully tagged hello-app:latestビルドが成功しました。
起動
runコマンド
で新しくコンテナを起動します。$ docker run hello-app Hello! World! 7.2.25Dockerfileのコマンドが実行されました。
phpのバージョンもFROMで指定したバージョン(7.2系で最新)になっています。コンテナはイメージIDを指定して起動することもできます。
$ docker run b9d2c9893a00 Hello! World! 7.2.25
imagesコマンド
でイメージの一覧が表示されます。
ここでリポジトリやタグの確認ができます。$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE hello-app latest b9d2c9893a00 11 minutes ago 398MBその他のコマンドはコマンドライン・リファレンスで確認できます。
- 投稿日:2019-11-30T16:22:40+09:00
Laravelの認証機能ui(外部パッケージライブラリ)導入
- 投稿日:2019-11-30T16:07:38+09:00
Twitterでのエゴサーチを便利にしたかった
TwitterREST APIを用いた検索ツールです
https://shioharu.minibird.jp/twitter/
https://github.com/shioharugit/twitterエゴサーチ
みなさんはエゴサーチしますか?
自分は音楽作ったときとかめっちゃします。
問題点
ところが作品を作るたび、検索したい項目が増えるので大変です。
エゴサーチが気軽にできるようなツールがあればなーと思い、
TwitterREST APIを使用して実現しようとしました。導入方法
『Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2019年8月時点の情報』
Twitterのアカウントを取得し、こちらの内容に沿ってTwitterREST APIを使用する申請を行います。
英語での申請が必要です。
以前は公式が申請の許可を手動で行っていたようで申請後めっちゃ時間が掛かったようですが、
今は自動応答になったらしくすんなり申請が通りました。『TwistOAuth』
APIの申請が完了したら実際にAPIを使用するわけですが、コード書くのが大変なのでライブラリを使用します。
Qiitaで有名なmpywさんのTwistOAuthを使います。
使い方は簡単で、ライブラリーをダウンロード後、TwistOAuth.pharをrequireしてあげるだけでokです。
curlを使用しているライブラリなので、phpにcurlを導入するする必要ありです。
windows環境はcurl導入が面倒なので注意。実装
https://shioharu.minibird.jp/twitter/
- 検索ワードをクッキーに1週間保存するので次回からはブラウザを開いただけで検索可能
- 半角スペースでOR検索対応
- 検索ワード -(ハイフン)検索ワードの結果から非表示にしたいワード (いわゆるマイナス検索)に対応
結果
- 通常だと検索範囲が過去1週間しか指定できない仕様の様子 ※参考記事にて補足
- 思いのほか検索精度が低い(?)
- マイナス検索+複数ワード検索に対応するため、
複数回APIにリクエストするという無理やりな実装になってしまった総評
- TwitterREST APIを用いれば自分のエゴサが快適化すると思ったがそうでもなかった
- しかしながら検索範囲や検索の指定の仕方を熟考して実装すれば、
会社や企業アカウントの評判を適切に拾うなど業務にも利用できるのではないか参考記事
『Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2019年8月時点の情報』
https://qiita.com/kngsym2018/items/2524d21455aac111cdee『TwistOAuth』
https://github.com/mpyw-junks/TwistOAuth『Twitterの検索API & Twitterでの検索術』
https://gist.github.com/cucmberium/e687e88565b6a9ca7039
TwitterのAPIで使用できるパラメータがまとまっています。『Twitter公式/非公式クライアントのコンシューマキー』
https://gist.github.com/uhfx/3922268
過去1週間以上の検索を行うにはこちらのコンシューマキーの指定が必要なようです。
しかしながらこちらを指定したとしても、認証エラーになってしまい過去1週間以上の範囲を検索できませんでした…
- 投稿日:2019-11-30T14:38:50+09:00
AWS CognitoでFacebookログインを実装してみた
こんにちは、キッド✈️と申します。
東南アジア発のスタートアップスタジオ、GAOGAOのサーバーサイドエンジニアです。弊社は、株式会社Ancar様のサービス「Ancar」の開発をサポートしており、僕も現在関わらせていただいています。
Ancarでは、既存の認証機能をCognitoへ移行することを目指しており、そこで得た知見が、少しばかりではありますが、あるので備忘録として残すことにしました。少しでも皆さんのお役に立てたら嬉しく思います。
目次
- Cognito移行の背景
- AWS CognitoでFacebookログインを実装する
- 同じemailのユーザーが二人出来てしまう問題を解決する
- まとめ
Cognito移行の背景
Ancarは、中古車の個人間売買サービスです。中古車売買において“安心安全な個人間売買”の実現を目指し、日々サービス開発を行っております。
また9月に中古車のアグリゲーションサービス「Ancar Search」を、10月にAncarへの出品車両を対象にしたカメラアプリをリリースしました。
今後も新たなサービスを続々とリリースすることを予定しており、そこで課題にあがったのが認証機能でした。
展開するサービスが増えた時に、サービスごとに別々のユーザーを作るのではなく、複数サービス間でも1つのユーザーで認証を行えるのが理想だろうという考えになったのです。
それを実現するためには、1つ、基盤となるユーザー認証基盤を据える必要があり、そのような背景からAWS Cognitoへの移行を決めました。
今回の記事では、Cognito移行の中でも苦戦したCognito Facebookログインへの移行について書きました。ちなみに記事中にある記述例は、言語はPHPで、フレームワークはSymfonyで書いています。
AWS CognitoでFacebookログインを実装する
事前準備
- Cognitoユーザープールの作成 参考:Amazon Cognito ユーザープール | AWSドキュメント
- Facebookアプリの登録
- Facebookアプリをユーザープールに登録する 参考:ユーザープールへのソーシャル ID プロバイダーの追加 | AWSドキュメント
この記事は、あくまでCognitoでのFacebookログインについて書きたいと思います。ですので、上記の事前準備については、他のドキュメントを参考にしていただきたいです。
Cognito Facebookログインフロー
Cognito Facebookログインのフローをざっくりとまとめると、このようになります。
- 認可エンドポイントにリクエストし、Cognito経由でFacebookログインを行う
- トークンエンドポイントにリクエストし、ユーザープールからアクセストークンを取得する
- 取得したアクセストークンを用いて、userInfoエンドポイントにリクエストし、Cognito Facebookログインを行ったユーザーの情報を、ユーザープールから取得する
- ユーザープールから取得したユーザーの情報を自前DBと照合し、該当するユーザーの認証をアプリケーション側で通す
では、次から実際に上記のフローをどのように実現しているのかを見ていきたいと思います。
(1)Cognito Facebookログインを行うURLを生成する
こちらがCognito Facebookログインの流れになります。
出典:AWSドキュメントアプリからURLにアクセスすると、Cognitoを経由してFacebookの認証を行い、またCognitoを経由してレスポンスが返ってくるようになっています。
では、早速Cognito Facebookログインを実行するURLから見ていきたいと思います。
Cognito Facebookログインでは、 認可エンドポイント
/oauth2/authorize
にパラメータを付与してこのようにリクエストを送ります。https://COGNITO_DOMAIN.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?response_type=code&client_id=COGNITO_CLIENT_ID&redirect_uri=REDIRECT_URI&identity_provider=Facebook&state=STATE&scope=openid+profile+email各パラメータの内容は、このようになっています。
パラメータ名 内容 response_type レスポンスのタイプ client_id CognitoユーザープールのクライアントID redirect_uri 認証後にリダイレクトされるURL identity_provider 利用するプロバイダ名、ここではFacebook state 初期リクエストに追加する OPAQUE 値 scope アクセストークンがアクセスできる値を指定 パラメータを付与したこの認可エンドポイントにリクエストを送ると、おなじみのFacebookログイン画面が表示されます。
この画面でログインを許可すると、さっそくCognitoのユーザープールにユーザーが作成されました。
これでCognitoへのFacebookログイン自体はできるようになりました。
Cognitoにログインしたユーザーを、アプリケーション側でも認証を通すためにも、CognitoでFacebookログインしたユーザーが、自前DBのどのユーザーなのかを判別できなければいけません。
そのために、CognitoでFacebookログインしたユーザーの情報をアプリケーション側へ引っ張ってきたいと思います。
先ほど、認可エンドポイントに対して、パラメータ
response_type=code
を付与して、リクエストしました。そのレスポンスとして、URLに以下のような形式で、code
が返ってきます。https://YOUR_APP/REDIRECT_URI?code=AUTHORIZATION_CODE&state=STATEresponse_typeにcodeを指定すると、認証コードを提供します。このコードは「トークンエンドポイント」を使用してアクセストークンと交換できます。
(2)トークンエンドポイントにリクエストして、アクセストークンを取得する
では先ほど、認可エンドポイントからのレスポンスとして返ってきた、codeを用いてユーザーの情報を引っ張っていきたいと思います。こちらのcodeを付与して、トークンエンドポイントにリクエストすると、アクセストークンを取得することができます。
cognito.php$baseUrl = 'https://COGNITO_DOMAIN.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?'; $client = new \GuzzleHttp\Client([ 'base_uri' => $baseUrl, ]); $tokenEndpointPath = '/oauth2/token'; $code = $request->query->get('code'); if (!$code) { throw new \RuntimeException('No code'); } $tokenEndpointResponse = $client->request( 'POST', $tokenEndpointPath, [ "headers" => [ "Content-Type" => "application/x-www-form-urlencoded", ], "form_params" => [ "grant_type" => 'authorization_code', "client_id" => COGNITO_APP_CLIENT_ID, "code" => $code, "redirect_uri" => REDIRECT_URI, ], ] );これでCognito Facebookログインでログインした、Cognitoユーザープールのユーザーのアクセストークンを取得できました。
では、ついにこのアクセストークンを用いて、ユーザープールの情報を引っ張っていきます。
(3)userInfoエンドポイントにリクエストして、Cognitoにログインしたユーザーの情報を取得する
では、先ほど取得したアクセストークンを付与した、userInfoエンドポイントにリクエストしてみます。
cognito.php$userInfoEndpointPath = '/oauth2/userInfo'; $token = $tokenEndpointResponseData['access_token']; if (!$token) { throw new \RuntimeException('No access_token'); } $userInfoEndpointResponse = $client->request( 'GET', $userInfoEndpointPath, [ "headers" => [ 'Authorization' => 'Bearer ' . $token, "Content-Type" => "application/x-www-form-urlencoded", ], ] );レスポンスとしてこのような形式で返ってきます。これで無事にユーザープールから、Cognito Facebookログインしたユーザー情報を取得してこれました。
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 { "sub": "248289761001", "name": "test test", "email": "test@example.com", "username": "Facebook_000000000000", }あとは、アプリケーション側で返ってきたユーザーを自前のDBと照合して、新規登録orログインさせればOKです。
参考:USERINFO エンドポイント | AWSドキュメント
同じemailのユーザーが二人出来てしまう問題を解決する
Cognito導入予定のサービスには、Facebook認証に加え、メールアドレス/パスワードによる認証を用意しています。そのため、Cognito Facebookログインに加えて、メールアドレス/パスワードによるCognito認証フローも用意しました。
メールアドレス/パスワードによるCognito認証フローの実装を終え、Cognito Facebookログインの実装を始めたところで、想定外の問題が発生したのです。
問題となったのは、メールアドレス/パスワードでのCognito認証フローを通ったユーザーが、ログアウト後にCognito Facebookログインを行った場合、メールアドレス/パスワード認証のCognitoユーザー、Cognito FacebookログインのCognitoユーザー、というように同じemailのCognitoユーザーがユーザープールに二人できてしまったことです。
emailで絞り込むと、ユーザーが2人いる...。
さてどうしたものかと、しばらく頭を抱えていましたが、調べていく中でCognitoのAPIで
AdminLinkProviderForUser
というものがあることを知りました。このメソッドは、Cognitoのユーザープールに存在するユーザーと、それとは別のプロバイダ経由で認証したユーザーをリンクさせるものです。
cognito.php$client->adminLinkProviderForUser([ 'DestinationUser' => [ 'ProviderAttributeValue' => COGNITO_USER_NAME, 'ProviderName' => "Cognito", ], 'SourceUser' => [ 'ProviderAttributeName' => 'Cognito_Subject', 'ProviderAttributeValue' => FACEBOOK_USER_ID, 'ProviderName' => "Facebook", ], 'UserPoolId' => COGNITO_USER_POOL_ID, ]);これを実行すると、一覧では依然として、メールアドレス/パスワード認証のCognitoユーザー、Cognito FacebookログインのCognitoユーザーの2人が存在するのですが、
AdminLinkProviderForUser
実行後には詳細ページでは同じ情報が表示されるようになり、1人のユーザーとしてメールアドレス/パスワード認証もCognito Facebookログインもできるようになります。参考:AdminLinkProviderForUser | AWS Documentation
まとめ
Cognitoの実装例は、ネイティブアプリが多く、特にFacebookログインとなると日本語の記事では、公式ドキュメント以外にはほとんどないという状況で、実装方法にたどり着くまでにかなり時間がかかってしまいました。
公式ドキュメントを読んでも分からないことが多く、何度か目黒のAWS Loftにも足を運び、Ask An Expert カウンター(Loftにて、AWSのプロダクトやソリューションを熟知したエンジニアに、技術的な相談ができる場)で相談させていただいたりもしました。その節は大変お世話になりました。
今回の備忘録が、少しでも皆さんのお役に立てれば嬉しいです。
最後に
よく分からない点や間違っている点などがありましたら、コメント欄で教えていただければと思います。
またCognitoに関する質問も、僕にお答えできる範囲でありましたら、お力になれればと思っております。そちらもコメント欄でお願いいたします。
- 投稿日:2019-11-30T14:19:47+09:00
docker 19 + nginx 1.17.6 + PHP 7.4の環境構築
docker 19 + nginx 1.17.6 + PHP 7.4の環境構築
環境情報
2019/11/30 時点
- MacOS X 10.15.1(19B88)
- Docker 19.03.5
- nginx 1.17.6
事前準備
Mac用の
docker-for-mac
をインストールする必要があります。
インストールする際、Docker Hubのアカウントが必要になりますので、事前にDocker Hubへのアカウント登録を済ませておいてください。https://docs.docker.com/docker-for-mac/install/
$ docker --version Docker version 19.03.5, build 633a0ea環境構築
基本的にはdocker-composeを使って環境構築を進めていきます。
nginxの構築
以下のディレクトリ構成を前提に環境構築を進めていきます。
. ├── docker-compose.yml ├── php-docker.md └── web ├── conf │ └── default.conf └── src └── index.html
default.conf
ファイルの内容は下記の通りです。server { listen 80; server_name 127.0.0.1; #ルートディレクトリの設定 location / { root /var/www/; index index.html index.htm; } access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; }
index.html
ファイルの内容は下記の通りです。<!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>ようこそDocker</title> </head> <body> <h1>ようこそDocker!</h1> </body> </html>
docker-compose.yml
ファイルの内容は下記の通りです。version: '3' services: web: image: nginx:latest ports: - "8000:80" volumes: - ./web/conf/default.conf:/etc/nginx/conf.d/default.conf - ./web/src:/var/www/
- version
- Docker 19.03.5に対応しているdocker-composeのファイルフォーマットは
3.7
なので、yamlのversionには3
を指定する。- 対応バージョンの確認はこちらから確認してください。
- services -> web は、任意の名前をつけられます。今回はnginxをWebサーバーとして利用する環境構築となり、わかりやすい名称として
web
と定義しています。- services -> web -> image に設定している
nginx:latest
は、docker hubのnginxでどのバージョンに対応しているかなどが確認できます。
- ここではlatestとしていますが、
1.17.6
と指定しても同じ結果になります。(2019/11/30時点では。)- services -> web -> ports の
8000:80
は、ホスト(ここではMacPC)で受け付けたポート8000
へのリクエストを、Dockerコンテナの80
ポートに転送するという設定です。ポートフォワーディング
- volumes ホスト側で編集したソースやconfなどをDockerコンテナと同期するための設定。
カレントディレクトリを
docker-compose.yml
のあるディレクトリに移動して、下記コマンドを実行します。docker-compose up -d
コマンドの実行結果
$ docker-compose up -d Creating network "php-docker_default" with the default driver Pulling web (nginx:latest)... latest: Pulling from library/nginx 000eee12ec04: Pull complete eb22865337de: Pull complete bee5d581ef8b: Pull complete Digest: sha256:50cf965a6e08ec5784009d0fccb380fc479826b6e0e65684d9879170a9df8566 Status: Downloaded newer image for nginx:latest Creating php-docker_web_1 ... donenginxのDockerコンテナが正常に動作しているか確認するため以下のURLにアクセスします。
http://127.0.0.1:8000/「ようこそDocker!」というページ(index.htmlで作ったページ)が表示されればOKです。
いったん起動したnginxのコンテナを停止します。
docker-compose stop$ docker-compose stop Stopping php-docker_web_1 ... doneここまでで、dockerによるnginx環境の構築がいったん完了です。
続いて、phpの環境を構築していきます。(後日更新します。。。)
- 投稿日:2019-11-30T07:01:42+09:00
Laravel ルーティング設定
Laravelのルーティング設定
web.phpの設定
ディレクトリ内の routes → web.php
まず、useの処理を追記
<?php // useはディレクトリにショートカットを作成するという意味 use App\モデル名; use Illuminate\Http\Request; /* |-------------------------------------------------------------------------- | 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('/', function () { return view('テンプレートファイル(blade)の最初の単語名'); });デフォルトは、return view('welcome');となっているので、
テンプレートファイル(blade)の最初の単語名に書き換える。
変数の定義
1.データベースからデータを取得して値を返す処理を追記
2.Bladeテンプレートで参照する変数名を引数に設定Route::get('/', function () { $変数名 = モデル名::all(); return view('テンプレートファイル(blade)の最初の単語名', ['変数名' => $変数名 ]); });フォームから入力したデータを受け取る
Route::post('formのactionに指定したURI(パス)', function (Request $request) { $validator = Validator::make($request->all(),[ 'name' => 'required|max:255', ]); // データベースに登録する値の変数を宣言して、モデルのクラス定義からオブジェクトを作成 $変数名 = new モデル名; $変数名->title = $request->name; $変数名->save(); return redirect('/');登録したデータを削除する
implicit binding(暗黙のバインディング)
→オブジェクトのID番号を返す処理URIとidを同じ名前にすると、一致するインスタンスを返してくれる
Route::delete('URI/{id}', function(モデル名 $変数名){ $変数名->delete(); return redirect('/'); });
- 投稿日:2019-11-30T00:09:49+09:00
Raspberry PiのデータをGCPに送ってみた(無料)
Raspberry PiのデータをGCPに送ってみた(無料)
Raspberry PiのデータをMySQLに格納し、Web上で確認できる仕組みを作りたかった。
VPSにサーバーを立てて、データのやり取りをしたい人向け。※本記事は、Rasberry Pi上で気温データを取得し、送信することを目的としています。
開発環境
- Windows10 Pro
- Powershell
- Windows Subsystem for Linux(Ubuntu)
GCPについて
Google Cloud Platformの略称で、Googleが提供しているクラウドコンピューティングサービスである。
GCPの始め方はこちらを参考に
Webサーバを立てる
- GCPプロジェクトの作成
- 好きなOSのVMを立てる(筆者は、Ubuntu 18.04 LTS)
※無償枠で作成する場合はこちらを参考に作成してください。
- SSH認証を行う(Windowsからアクセスする用)
※補足:公開鍵をscpで送りたい場合
scp ~/.ssh/[公開鍵] [ユーザー名]@[転送先サーバのIP]:~/.ssh/
- apache2のインストール
sudo apt-get update sudo apt-get install apache2 -y curl http://[外部IP]※外部IPはGoogle Cloud Platform -> Computer Engine -> VMインスタンス
- ファイアーフォールの設定 (ufwのインストールと設定)
#使っているTCP/UDPポートを調べる sudo ufw status #nmapでも可能 sudo nmap -sTU localhost #ポートの解放(ルールの登録) sudo ufw allow [許可したいポート番号] #ルールの削除 sudo ufw status numbered sudo ufw delete [number]Windows Subsystem for Linux(WSL)について
- Ubuntu(バージョン明記なし)をMicrosoft Storeからインストール
- どうやら自動的に最新版になるらしい
- ホームディレクトリは
/home/[作成ユーザー名]/
になっている
- Powershellは
/mnt/c/Users/[ユーザー名]/
/mnt/c/Users/[ユーザー名]/
にcdコマンドで移動して作業開始DBの構築(MySQL)
sudo apt install mysql-server mysql-client #サービスの起動確認 sudo service mysql status #MySQLの初期設定 sudo mysql_secure_installation #コンソールからMySQLサーバへ接続 sudo mysql -u root -p
- MySQLのユーザー情報の確認等
#状態表示 mysql> status #データベース一覧表示 mysql> show database; #ユーザー一覧表示 mysql> select user, host from mysql.user; #特定ユーザの権限確認(ex) ユーザー:root, ホスト名:localhost) mysql> show grants for 'root'@'localhost'; #終了 mysql> exithttps://www.yokoweb.net/2018/05/13/ubuntu-18_04-server-mysql/
- 実際に構築をする
- 今回は、取得時間と気温データの2つのフィールドを作成する
sudo mysql #データベースの作成 mysql> create database [db_name] mysql> show databases; #データベースの移動 mysql> use [db_name] #テーブルの作成(シングルコートいらない) mysql> create table [tbl_name] ( id int(11) NOT NULL AUTO_INCREMENT, -> ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -> temp float NOT NULL, -> PRIMARY KEY(id)) ENGINE=MyISAM #確認 mysql> show tables from [db_name]; mysql> show columns from [tbl_name]; +-------+-----------+------+-----+-------------------+-----------------------------+ | Field | Type | Null | Key | Default | Extra | +-------+-----------+------+-----+-------------------+-----------------------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | ts | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | | temp | float | NO | | NULL | | +-------+-----------+------+-----+-------------------+-----------------------------+ 3 rows in set※トランザクション処理は行わないので、MyISAM。上記コードは温度データのみ格納用。
- 外部から直接MySQLにアクセスするやり方もあるらしいです
プログラムの作成
PHPの環境設定
- PHPをインストール
#最新版でなければこれでOK(最新はver7.3、aptの場合ver7.2) sudo apt install php
- ドキュメントルート(apacheがブラウザに送信するHTML)は
/var/www/html/
- パーミッションを変更
- PHPの動作確認はphpinfoで可能
<?php phpinfo(); ?>PHPからDB(MySQL)へのアクセス確認
- PHP Data Objects(PDO) ※今回はPDOを使用
- PHPからデータベースのアクセスを抽象的にやってくれる
- mysqli
- MySQL改良版拡張モジュール
<?php try{ // PDOクラスのオブジェクトを作成 $pdo = new PDO('mysql:host=localhost;dbname=[db_name];charset=utf8', '[ユーザー名]', '[パスワード]', array(PDO::ATTR_EMULATE_PREPARES => false)); }catch(PDOException $e){ exit('データベース接続失敗。'.$e->getMessage()); } // DB処理 switch ($_SERVER['REQUEST_METHOD']) { case 'GET': $st = $pdo->query("select * from [テーブル名]"); echo json_encode($st->fetchAll(PDO::FETCH_ASSOC)); break; case 'POST': $data = json_decode(file_get_contents('php://input'), true); $st = $pdo->prepare("insert into [テーブル名](ts, temp) values (:datetime,:temp)"); $st->execute($data); header('Content-Type: application/json'); echo json_encode("end"); break; } ?>Raspberry PiからPHPへのデータ送信処理(Python)
# coding: utf-8 import requests import json from datetime import datetime def main(): url = 'http://[外部IP]/[プログラム名].php' #気温取得の処理 ... temp = #値 data = {'datetime':datetime.now().strftime("%Y/%m/%d %H:%M:%S"),'temp':temp} #JSON形式にdataをエンコード print(json.dumps(data)) #JSON形式でPOSTリクエストを送信する(json形式のレスポンスが欲しいため、Content-type指定) response = requests.post(url, json.dumps(data), headers={'Content-type': 'application/json'}) #正しく返された場合にresponseに値が入る print(response.json()) pass if __name__ == '__main__': main()センサーデータをうまく利活用し、クラウドとの連携をしていきたい…