20191130のPHPに関する記事は11件です。

【Laravel】画面で指定したソート条件をページング先でも反映させる

画面上でボタンなどでソート条件を指定→ページング先でも反映させるための一連の手順です。

やりたいこと

image.png

環境

  • PHP:バージョン7.3.7
  • Laravel:バージョン5.8
  • OS:Windows10

手順

1. ページング機能の作成

まずはページング機能を作成します。
ControllerとViewを編集するだけなので、わりと簡単。

(1)Controllerの編集

全件を取得し、1ページ当たり5データ表示させる場合は以下の通り。
pagenate()のカッコ内の数字を任意の数字に変えましょう。

TestController.php
    public 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.php
    public 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() }}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

おれがコンピュータに筆算を教えてやる - 「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 の壁を破って計算できたのはうれしいです.
同じ考え方で他の四則演算もできるのではないかと思っています. (時間があったら実装してみたいです.)

調子乗ってすみませんでした

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP <条件分岐でのincludeとURLからのタイトル取得>

はじめに

最近はPHPについて学習しています。
ここ一ヶ月の勉強で学んだことを使用しながら、簡単なプログラムを作ってみました。
セキュリティ関係はほとんど考慮していません。ある程度のセキュリティについてはこちらの記事で説明しています。

作成した機能の説明

初期画面は以下の通り。
GETを使用したページ遷移を行いたい。また入力したURLからそのサイトの情報を一部取得したい(ここではtitleタグ取得する)。
スクリーンショット 2019-11-30 20.45.07.png
TopPage、MyPage、送信を押した場合の画面は以下の通り。
URLから取得したタイトルとそのURLをリンクとして表示する。またTopPageを押した場合はQiitaのトップページを、MyPageを押した場合は自分のMyPageを取得している。
スクリーンショット 2019-11-30 20.45.30.png

コード

先に上のプログラムのコードを載せておきます。

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公式サイト

PHPで指定したURLのタイトルタグの中身を取得する方法

基本的な正規表現の使い方

マコトのおもちゃ箱 ~ぼへぼへ自営業者の技術メモ~

PHPのincludeでGETのパラメーター(引数)の渡し方

ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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を作成して編集
関数の中身は、こちらの記事を参考にさせていただきました。

[Laravel] ログの扱い方 [5.8]

helper.php
use 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-30 19.29.19.png
きちんと保存されていますね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.25

Dockerfileのコマンドが実行されました。
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

その他のコマンドはコマンドライン・リファレンスで確認できます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelの認証機能ui(外部パッケージライブラリ)導入

ターミナルでインストール

composerを使ってuiライブラリをインストールする

$ composer require laravel/ui

Package manifest generated successfully. と表示されたらOK

artisanコマンドでvueを追加

$ php artisan ui vue --auth

スタイルシートが壊れた場合は、npmから不足するパッケージをインストールすることで修復する

$ npm install
$ npm run dev
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Twitterでのエゴサーチを便利にしたかった

TwitterREST APIを用いた検索ツールです
https://shioharu.minibird.jp/twitter/
https://github.com/shioharugit/twitter

エゴサーチ

みなさんはエゴサーチしますか?
自分は音楽作ったときとかめっちゃします。
image01.jpg

問題点

ところが作品を作るたび、検索したい項目が増えるので大変です。
エゴサーチが気軽にできるようなツールがあればなーと思い、
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検索対応
  • 検索ワード -(ハイフン)検索ワードの結果から非表示にしたいワード (いわゆるマイナス検索)に対応

image02.jpg

結果

  • 通常だと検索範囲が過去1週間しか指定できない仕様の様子 ※参考記事にて補足
  • 思いのほか検索精度が低い(?)
  • マイナス検索+複数ワード検索に対応するため、
    複数回APIにリクエストするという無理やりな実装になってしまった

総評

  • TwitterREST APIを用いれば自分のエゴサが快適化すると思ったがそうでもなかった
  • しかしながら検索範囲や検索の指定の仕方を熟考して実装すれば、
    会社や企業アカウントの評判を適切に拾うなど業務にも利用できるのではないか

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS CognitoでFacebookログインを実装してみた

こんにちは、キッド✈️と申します。
東南アジア発のスタートアップスタジオ、GAOGAOのサーバーサイドエンジニアです。

弊社は、株式会社Ancar様のサービス「Ancar」の開発をサポートしており、僕も現在関わらせていただいています。

Ancarでは、既存の認証機能をCognitoへ移行することを目指しており、そこで得た知見が、少しばかりではありますが、あるので備忘録として残すことにしました。少しでも皆さんのお役に立てたら嬉しく思います。

目次

  • Cognito移行の背景
  • AWS CognitoでFacebookログインを実装する
  • 同じemailのユーザーが二人出来てしまう問題を解決する
  • まとめ

Cognito移行の背景

Ancarは、中古車の個人間売買サービスです。中古車売買において“安心安全な個人間売買”の実現を目指し、日々サービス開発を行っております。

スクリーンショット 2019-11-26 20.00.09.png

また9月に中古車のアグリゲーションサービス「Ancar Search」を、10月にAncarへの出品車両を対象にしたカメラアプリをリリースしました。

今後も新たなサービスを続々とリリースすることを予定しており、そこで課題にあがったのが認証機能でした。

展開するサービスが増えた時に、サービスごとに別々のユーザーを作るのではなく、複数サービス間でも1つのユーザーで認証を行えるのが理想だろうという考えになったのです。

それを実現するためには、1つ、基盤となるユーザー認証基盤を据える必要があり、そのような背景からAWS Cognitoへの移行を決めました。

今回の記事では、Cognito移行の中でも苦戦したCognito Facebookログインへの移行について書きました。ちなみに記事中にある記述例は、言語はPHPで、フレームワークはSymfonyで書いています。

AWS CognitoでFacebookログインを実装する

事前準備

この記事は、あくまでCognitoでのFacebookログインについて書きたいと思います。ですので、上記の事前準備については、他のドキュメントを参考にしていただきたいです。

Cognito Facebookログインフロー

Cognito Facebookログインのフローをざっくりとまとめると、このようになります。

  1. 認可エンドポイントにリクエストし、Cognito経由でFacebookログインを行う
  2. トークンエンドポイントにリクエストし、ユーザープールからアクセストークンを取得する
  3. 取得したアクセストークンを用いて、userInfoエンドポイントにリクエストし、Cognito Facebookログインを行ったユーザーの情報を、ユーザープールから取得する
  4. ユーザープールから取得したユーザーの情報を自前DBと照合し、該当するユーザーの認証をアプリケーション側で通す

では、次から実際に上記のフローをどのように実現しているのかを見ていきたいと思います。

(1)Cognito Facebookログインを行うURLを生成する

こちらがCognito Facebookログインの流れになります。

scenario-authentication-social.png
出典: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ログイン画面が表示されます。

スクリーンショット 2019-11-29 20.04.03.png

この画面でログインを許可すると、さっそくCognitoのユーザープールにユーザーが作成されました。
cognitofb.png

これでCognitoへのFacebookログイン自体はできるようになりました。

Cognitoにログインしたユーザーを、アプリケーション側でも認証を通すためにも、CognitoでFacebookログインしたユーザーが、自前DBのどのユーザーなのかを判別できなければいけません。

そのために、CognitoでFacebookログインしたユーザーの情報をアプリケーション側へ引っ張ってきたいと思います。

先ほど、認可エンドポイントに対して、パラメータ response_type=code を付与して、リクエストしました。そのレスポンスとして、URLに以下のような形式で、 code が返ってきます。

https://YOUR_APP/REDIRECT_URI?code=AUTHORIZATION_CODE&state=STATE

response_typeにcodeを指定すると、認証コードを提供します。このコードは「トークンエンドポイント」を使用してアクセストークンと交換できます。

参考:認可エンドポイント | AWSドキュメント

(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ユーザープールのユーザーのアクセストークンを取得できました。

では、ついにこのアクセストークンを用いて、ユーザープールの情報を引っ張っていきます。

参考:トークンエンドポイント | AWSドキュメント

(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ユーザーがユーザープールに二人できてしまったことです。

cognitoemail,png.png

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に関する質問も、僕にお答えできる範囲でありましたら、お力になれればと思っております。そちらもコメント欄でお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 ... done

nginxの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の環境を構築していきます。(後日更新します。。。)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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-30 9.50.41.png
※Laravelマニュアルより引用

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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が提供しているクラウドコンピューティングサービスである。

https://cloud.google.com/?hl=ja

GCPの始め方はこちらを参考に

https://cloud.google.com/gcp/getting-started/?hl=ja

Webサーバを立てる

  • GCPプロジェクトの作成
  • 好きなOSのVMを立てる(筆者は、Ubuntu 18.04 LTS)

※無償枠で作成する場合はこちらを参考に作成してください。

https://cloud.google.com/free/docs/gcp-free-tier?hl=ja

  • 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> exit

https://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。上記コードは温度データのみ格納用。

http://zetcode.com/databases/mysqltutorial/storageengines/

プログラムの作成

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()

センサーデータをうまく利活用し、クラウドとの連携をしていきたい…

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む