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

web上にある表からSQL文を自動生成する

webサイトなどにある表から一行ごとにINSERT文を自動で生成するプログラムをphpで作成してみました。

実行環境

Windows10
PHP 7.1.33

データのコピー

まず、INSERT文にするためのデータをコピーし、適当なファイルに貼り付けます。
例えば、以下のような表がweb上にあるとします。

ID 名前 価格
001 りんご 100円
002 ぶどう 200円
003 いちご 300円
004 バナナ 100円
005 ナシ 250円
006 スイカ 3000円
007 メロン 4000円
008 さくらんぼ 350円
009 モモ 600円
010 レモン 300円

この表のIDと名前だけでSQL文を作成したい場合は、Chromeの拡張機能にある「Copytables」を使うことで、以下の画像のように特定の列や行のみを選択することが可能となります。
image.png

コピーができたら、適当な名前でファイルを作成し、貼り付けます。(今回はdata.txt)
image.png

SQL文を生成するプログラム

次にPHPファイル(ここではinsert.php)を作成し、下のコードを記述して実行します。(解説は後ほど)

insert.php
<?php
header("Content-type: text/html; charset=utf-8");
$filename = "data.txt";
$file = file($filename);
$fp = fopen("insert_sql.txt","w");
for($i=0; $i<count($file); $i++){
  $data =  preg_split("/\s/", $file[$i], -1, PREG_SPLIT_NO_EMPTY);
  $data_values = "'".$data[0]."'";
  if(count($data) >= 2){
    for($j=1; $j<count($data); $j++){
      $data_values .= ", '".$data[$j]."'";
    }
  }
  $contents = "INSERT INTO test VALUES(".$data_values.");";
  fwrite($fp,$contents."\r\n");
}
fclose($fp);
?>

実行すると以下のように「insert_sql.txt」というファイルが作成され、そのなかにSQL文が記述されています。
image.png

ソースコードの解説

PHPファイルに記述したコードの内容について詳しく解説します。

まず、3~5行目ではデータを記述した「data.txt」というファイルをfile()関数を使って 、1行ずつ配列に格納します。その後、fopen()関数で書き込み可能なファイルを新規で作成し、開いています。

insert.php
$filename = "data.txt";
$file = file($filename);
$fp = fopen("insert_sql.txt","w");

次にfor文でデータの行数分だけSQL文を生成する準備をします。
count()関数で配列変数$fileにある要素の数を数えることで、その分だけ処理を繰り返すことができます。

insert.php
for($i=0; $i<count($file); $i++){

}

7行目ではpreg_split()関数を使って文字列をある文字列で区切って配列に格納します。
もし、データの区切りがカンマだった場合は第一引数を “/,/” にし、半角スペースだった場合は “/\s/” にしたりします。

insert.php
$data =  preg_split("/\s/", $file[$i], -1, PREG_SPLIT_NO_EMPTY);

8~11行目でinsert文の VALUES の中身を作成します。データの1行の中身を文字列で区切って配列に格納した配列変数$dataの要素分だけカンマで繋げていきます。

insert.php
$data_value = "'".$data[0]."'";
  if(count($data) >= 2){
    for($j=1; $j<count($data); $j++){
      $data_value .= ", '".$data[$j]."'";

その後、SQL文を作成し、変数に格納します。

insert.php
$contents = "INSERT INTO test VALUES(".$data_values.");";

fwite()関数で最初に作成して開いたファイルにSQL文を記述します。

insert.php
fwrite($fp,$contents."\r\n");

最後にfor文の外で開いていたファイルを閉じ、完成です。

insert.php
fclose($fp);

ソースコードを少し変えることで、いろんなパターンのSQL文に対応できると思います。

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

web上にある表やExcelからSQL文を自動生成する

webサイトなどにある表やExcelなどから一行ごとにINSERT文を自動で生成するプログラムをphpで作成してみました。

実行環境

Windows10
PHP 7.1.33

データのコピー

まず、INSERT文にするためのデータをコピーし、適当なファイルに貼り付けます。
例えば、以下のような表がweb上にあるとします。

ID 名前 価格
001 りんご 100円
002 ぶどう 200円
003 いちご 300円
004 バナナ 100円
005 ナシ 250円
006 スイカ 3000円
007 メロン 4000円
008 さくらんぼ 350円
009 モモ 600円
010 レモン 300円

この表のIDと名前だけでSQL文を作成したい場合は、Chromeの拡張機能にある「Copytables」を使うことで、以下の画像のように特定の列や行のみを選択することが可能となります。(Excelの場合はctrlキーなどを使ってうまくコピーして下さい)
image.png

コピーができたら、適当な名前でファイルを作成し、貼り付けます。(今回はdata.txt)
image.png

SQL文を生成するプログラム

次にPHPファイル(ここではinsert.php)を作成し、下のコードを記述して実行します。(解説は後ほど)

insert.php
<?php
header("Content-type: text/html; charset=utf-8");
$filename = "data.txt";
$file = file($filename);
$fp = fopen("insert_sql.txt","w");
for($i=0; $i<count($file); $i++){
  $data =  preg_split("/\s/", $file[$i], -1, PREG_SPLIT_NO_EMPTY);
  $data_values = "'".$data[0]."'";
  if(count($data) >= 2){
    for($j=1; $j<count($data); $j++){
      $data_values .= ", '".$data[$j]."'";
    }
  }
  $contents = "INSERT INTO test VALUES(".$data_values.");";
  fwrite($fp,$contents."\r\n");
}
fclose($fp);
?>

実行すると以下のように「insert_sql.txt」というファイルが作成され、そのなかにSQL文が記述されています。
image.png

ソースコードの解説

PHPファイルに記述したコードの内容について詳しく解説します。

まず、3~5行目ではデータを記述した「data.txt」というファイルをfile()関数を使って 、1行ずつ配列に格納します。その後、fopen()関数で書き込み可能なファイルを新規で作成し、開いています。

insert.php
$filename = "data.txt";
$file = file($filename);
$fp = fopen("insert_sql.txt","w");

次にfor文でデータの行数分だけSQL文を生成する準備をします。
count()関数で配列変数$fileにある要素の数を数えることで、その分だけ処理を繰り返すことができます。

insert.php
for($i=0; $i<count($file); $i++){

}

7行目ではpreg_split()関数を使って文字列をある文字列で区切って配列に格納します。
もし、データの区切りがカンマだった場合は第一引数を “/,/” にし、半角スペースだった場合は “/\s/” にしたりします。

insert.php
$data =  preg_split("/\s/", $file[$i], -1, PREG_SPLIT_NO_EMPTY);

8~11行目でinsert文の VALUES の中身を作成します。データの1行の中身を文字列で区切って配列に格納した配列変数$dataの要素分だけカンマで繋げていきます。

insert.php
$data_value = "'".$data[0]."'";
  if(count($data) >= 2){
    for($j=1; $j<count($data); $j++){
      $data_value .= ", '".$data[$j]."'";

その後、SQL文を作成し、変数に格納します。

insert.php
$contents = "INSERT INTO test VALUES(".$data_values.");";

fwite()関数で最初に作成して開いたファイルにSQL文を記述します。

insert.php
fwrite($fp,$contents."\r\n");

最後にfor文の外で開いていたファイルを閉じ、完成です。

insert.php
fclose($fp);

ソースコードを少し変えることで、いろんなパターンのSQL文に対応できると思います。

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

Laravel、プロジェクトを作成して最初にやること

今回は私がプロジェクト作成して最初に行うことをメモ程度にまとめました。
かなり説明は省いています。すいません:sweat_smile:

開発環境

・Mac
・php 7.4.6
・Laravel 7.15.0
・MySQL

プロジェクトの作成と.envファイルの編集

1. laravel new プロジェクト名
2. config/app.phpのtimezonelocaleの変更

config/app.php
'timezone' => 'Asia/Tokyo',
'locale' => 'ja',

データベースに関する設定

(1) .envファイルのデータベースの設定

.env
DB_CONNECTION=mysql
DB_HOST=
DB_DATABASE=データベース名
DB_USERNAME=
DB_PASSWORD=
DB_SOCKET=

(2) マイグレーションファイルの設定

まず、マイグレーションを行う意味を知りましょう!
マイグレーションは、環境が変わった時などにデータベースを一から作るのは大変だからチャチャっと作って!という時の為にあります。
ローカル環境から本番環境に移行することを考えましょう。マイグレーションを使えば、本番環境でローカル環境と同じデータベースをコマンド一発で作成してくれます。まずはマイグレーションファイルを作っていきましょう!

以下のコマンドでファイルが作成されます。
$php artisan make:migration create_posts_table --create=posts

作成日時_create_posts_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

このファイルの中にpostsテーブルの記述をしていきましょう。マイグレーションファイルが書き終わったら、以下のコマンドでmigrateしましょう。

$php artisan migrate

※ テーブルを加えたり、データベースに変更を加えた場合は以下のコマンドで更新出来ます。

$php artisan migrate:refresh

このコマンドによってデータベースにテーブルが作成されます。

(3)モデルの設定

モデルを作成する意味は、モデルクラスを使うことでデータベースにデータを挿入したり、削除することができるようにする為です。
以下のコマンドでファイルを作成出来ます。

モデルの名前は、テーブルの名前がpostsだったらPost.phpになる。どういうことかというと、テーブルの名前の単数形の名前をモデルにつけると、自動的にそのモデルはそのテーブルと紐づけられる。

以下のコマンドでモデルのファイルを作成しましょう!

$php artisan make:model Post

ファイルに記述する内容は、今回は割愛します。

(4)シーディング

シーディングは、サンプルデータをコマンド一発で作成するためのものです。
以下のコマンドで、ファイルを作成しましょう。

$php artisan make:seeder PostsTableSeeder

ファイルの中身を記入したら、DatabaseSeeder.phpにPostsTableSeeder.phpを登録します。

DatabaseSeeder.php
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $this->call(PostsTableSeeder::class);
    }
}

これで、一通り最初にやることは完了です!

seederの書き方については以下の記事にまとめているので参照してください。

Laravel、シーディングの設定方法(基礎)

まとめ

今回は、私がプロジェクトを作成してから最初に行うことをまとめて見ました。
間違いありましたら、指摘をお願いします!

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

位置情報を送ると近くの餃子か焼肉のお店を教えてくれるLINE BOTを作った

経緯

なんか面白い物が作りたいと思い、位置情報を送ると近くの餃子屋さんと焼肉屋さんを教えてくれるLINE BOTを作りました。
なぜ餃子と焼肉かと言うと、私が好きだからです。

参考

【公式リファレンス】
https://developers.line.biz/ja/docs/messaging-api

【LINE SDK for PHP】
https://github.com/line/line-bot-sdk-php

【[LINE Bot] 位置情報から食べログ3.5以上の優良店を検索するbot作った】
(これをお手本につくりました。)
https://qiita.com/NARI_Creator/items/f29112e6f604c86b3c0d

【ぐるなびAPIの取得方法 そのまま使えるソースコード付き】
https://enjoy-surfing.com/gurunavi/

【LineBotを作るときの雛形 for Laravel】
https://qiita.com/sh-ogawa/items/2238e579d7ee538025a0

【LaravelでLINEにチャットボットをつくる(QRコード作成)】
https://blog.capilano-fw.com/?p=4285#i-4

大変参考になりました?ありがとうございます。

仕様

1.餃子が食べたいか焼肉が食べたいか入力する
2.位置情報を送信する
3.近くのお店を教えてくれる

簡単な仕様はこんな感じです。?

作成前にやっておくこと

herokuのアプリケーションを作る

ここら辺みておけばできるんじゃないかなーと思います。

Heroku アカウント登録してデプロイするまでの簡単な使い方
Laravelをherokuにデプロイする(データベースはMySQL)

herokuはクレカを登録すると無料で1ヶ月あたり1000時間使えます。
無料プランだと30分ごとにスリープしてしまうのでスリープ後の初アクセスはレスポンスが遅いですが、バッチ処理をしておけば問題ないみたいです。

LINE Message APIの登録

LINE BOTのアカウントを作ります。

公式ドキュメント
公式が割と丁寧に説明してくれてるので、見ながらやったらできると思います。

実装

LINE Messaging API SDKをインストールする

$ composer require linecorp/line-bot-sdk

環境変数を追加する

#line
LINE_SECRET_TOKEN = 
LINE_ACCESS_TOKEN =

トークンはアカウントのBasic Settingから参照できます。

ルートの作成

ユーザーがLINE BOTにメッセージを送ると、LINEプラットフォームから指定されたアプリケーションのURLにHTTPリクエストを送るので、そのURLを決めます。

web.php
Route::post('line','LineController@post')->name('line.post');

ちなみにリクエストはPOSTで送られます?
あとついでにControllerも作っちゃいます。

クライアントを作成する

今回はサービスクラスにまとめました。

LineService.php
<?php

namespace App\Services;

use LINE\LINEBot;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;

class LineService
{
    /**
     * linesdkの使用開始
     *
     * @return LINEBot
     */
    public static function lineSdk()
    {
        $token = config('services.line.token');
        $secret = config('services.line.secret');

        $httpClient = new CurlHTTPClient($token);
        $bot = new LINEBot($httpClient,['channelSecret' => $secret]);

        return $bot;
    }
}

$token$serviceは.envに登録したLINE_SECRET_TOKENLINE_ACCESS_TOKENですが、configのserviceに登録してからもってきてます。

基本公式のREADMEに書いてあるまんまですね?

署名を検証する

ラインプラットフォーム以外のわるものから危険なリクエストが送られてくる可能性があるので、検証する必要があります。
ラインのプラットフォームからリクエストが送られてきた時は、リクエストヘッダーにX-Line-Signatureというものが含まれます。これを検証するのですが、

チャネルシークレットを秘密鍵として、HMAC-SHA256アルゴリズムを使用してリクエストボディのダイジェスト値を取得します。
ダイジェスト値をBase64エンコードした値と、リクエストヘッダーのX-Line-Signatureに含まれる署名が一致することを確認します。

と丁寧に書かれているのでその通りに検証します。

LineController.php
<?php

namespace App\Http\Controllers;

use App\Services\LineService;
use LINE\LINEBot\SignatureValidator;

class LineController extends Controller
{
    public function post()
    {

        $signarure = request()->header('X-Line-Signature');
        $validateSignature = SignatureValidator::validateSignature($httpRequestBody, $channelSecret, $signarure);
        if ($validateSignature) {
            return response()->json(200);
        } else {
            abort(400);
        }
    }
}

とりあえずsignatureの検証ができれば200、できなければ400を返すようにします。

このSignatureValidator::validateSignatureが何をしているのかと言うと、
リクエストボディのダイジェスト値をbase64にエンコードして、リクエストヘッダーのX-Line-Signatureと比較しています。

SignatureValidator.php
public static function validateSignature($body, $channelSecret, $signature)
{
    if (!isset($signature)) {
        throw new InvalidSignatureException('Signature must not be empty');
    }

    return hash_equals(base64_encode(hash_hmac('sha256', $body, $channelSecret, true)), $signature);
}

こういうことです?

$channelSecret = config('services.line.secret');
$httpRequestBody = request()->getContent();
$hash = hash_hmac('sha256', $httpRequestBody, $channelSecret, true);
$signature = base64_encode($hash);

Webhook URLを登録する

次LINEのコントロールパネルからWebhook URLを登録します。
herokuのurl(×××××.herokuapp.com/line)登録して、verifyボタンを押します。
successというメッセージがでてきたらOKです?
コントローラで200を返さないようにしないとエラーになります。

テキストメッセージが送られてきたら返事を返してみる

とりあえず、テキストメッセージが送られてきたらこんにちは!と返してみます。

LineController.php
        $bot = LineService::lineSdk();

        try{
            $events = $bot->parseEventRequest($httpRequestBody, $signature);
            foreach ($events as $event) {
                if ($event instanceof LINE\LINEBot\Event\MessageEvent\TextMessage) {
                    $bot->replyText($event->getReplyToken(), 'こんにちは!');
                }
            }
        }catch(\Exception $e){
            Log::debug($e);
        }

parseEventRequestで署名が正当であるか検証して、正当であればリクエストをパースします。

webhookで送られてくるイベントはいろいろ種類がある↓?
Webhookイベントオブジェクト
SDK

送られてきたイベントがテキストメッセージであるかは
$event instanceof LINE\LINEBot\Event\MessageEvent\TextMessageのインスタンスであるかで確認します。

応答できるリクエストには応答トークンというものが発行されるので、$event->getReplyToken()で取得して、応答メッセージと一緒に$bot->replyTextで返しています?

イベントの種類でレスポンスを変えてみる

とりあえず、以下のイベントに対するレスポンスを実装します。
・フォローイベント
・フォロー解除イベント
・スタンプメッセージイベント
・テキストメッセージイベント
・位置情報メッセージイベント

LineController.php
        try{
            $events = $bot->parseEventRequest($httpRequestBody, $signature);
            foreach ($events as $event) {
                //フォローイベント
                if($event instanceof FollowEvent){
                    $followMessage = '友達登録ありがとう!何が食べたいか入力してね。近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)';
                    $bot->replyText($event->getReplyToken(), $followMessage);
                    continue;
                }
                //フォロー解除イベント
                else if ($event instanceof UnfollowEvent) {
                    continue;
                }
                //スタンプメッセージイベント
                else if($event instanceof StickerMessage){
                    $stampMessage = '可愛いスタンプだね〜!何が食べたいか入力してくれたら近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)'
                    $bot->replyText($event->getReplyToken(), $stampMessage);
                    continue;
                }
                //テキストメッセージイベント
                else if($event instanceof TextMessage){
                    //TODO テキストメッセージ実装
                }
                //位置情報メッセージイベント
                else if($event instanceof LocationMessage){
                    //TODO 位置情報実装
                }
            }
        }catch(\Exception $e){
            Log::debug($e);
        }

エイリアスはこちらを参考に??‍♀️

テキストメッセージに対するリスポンス

このBOTでは最初に何を食べたいか入力してもらってから位置情報を要求します?
餃子か焼肉かを判定したいのですが、
餃子、ぎょうざ、ぎょーざ、ギョーザなどいろいろなパターンの餃子と焼肉に対応したいと思います。
単純にif文に全部ぶち込みます。多分よくない?

LineService.php
    /**
     * 入力された文字が餃子か判定
     *
     * @param $text
     * @return bool
     */
    public function isGyoza($text)
    {
        return ($text === '餃子' || $text === 'ぎょうざ' || $text === 'ぎょーざ' || $text === 'ギョーザ' || $text === 'ギョーザ');
    }

    /**
     * 入力された文字が餃子か判定
     *
     * @param $text
     * @return bool
     */
    public function isYakiniku($text)
    {
        return ($text === '焼肉' || $text === '焼き肉' || $text === 'やきにく' || $text === 'ヤキニク' || $text === 'ヤキニク');
    }

焼肉か餃子かの結果はここで受け取ります。↓
焼肉か餃子が入力された場合は後述するrequireLocationで位置情報を求めます?
また、putCategoryでDBにuser_idcategoryを保存するようにしました。
categoryは餃子であれば1、焼肉は2が保存されます。
もし他の単語が入力された場合は、テキストメッセージを返します?

LineService.php
    /**
     * 送られてきたメッセージに対するレスポンス
     *
     * @param $bot
     * @param $event
     * @return void
     */
    public function getMessage($bot, $event)
    {
        try{
            $text = $event->getText();
            if ($this->isGyoza($text)) {
                //DBに保存
                $user_id = $event->getUserId();
                $category = Line::GYOZA;
                $this->putCategory($user_id,$category);
                //位置情報を求める
                $word = '餃子';
                $this->requireLocation($bot, $event, $word);
            } else if ($this->isYakiniku($text)) {
                //DBに保存
                $user_id = $event->getUserId();
                $category = Line::YAKINIKU;
                $this->putCategory($user_id,$category);
                //位置情報を求める
                $word = '焼肉';
                $this->requireLocation($bot, $event, $word);
            }else{
                $bot->replyText($event->getReplyToken(), '焼肉か餃子しか調べられないよ、、ごめんね。');
            }
        }catch(\Exception $e){
            Log::debug($e);
        }
    }

    /**
     * カテゴリーとユーザーIDをDBに保存
     *
     * @param $user_id
     * @param $category
     * @return void
     */
    public function putCategory($user_id, $category)
    {
        Line::create([
            'user_id' => $user_id,
            'category' => $category
        ]);
    }

    /**
     * 位置情報を求めるメッセージを送る
     *
     * @param $bot
     * @param $event
     * @param $word  //餃子か焼肉か
     * @return void
     */
    public function requireLocation($bot, $event, $word)
    {
        $uri = new UriTemplateActionBuilder('現在地を送る!', 'line://nv/location');
        $message = new ButtonTemplateBuilder(null, $word.'が食べたいんだね!今どこにいるか教えてほしいな!', null, [$uri]);

        $bot->replyMessage($event->getReplyToken(), new TemplateMessageBuilder('位置情報を送ってね', $message));

    }

UriTemplateActionBuilder
このアクションが関連づけられたコントロールがタップされると、第二引数に登録されているURLが開きます。
line://nv/locationは、lineで位置情報が開かれます。

ButtonTemplateBuilder
ボタンテンプレートが作られます。引数は、タイトル、本文、画像URL、アクションの順番です。
アクションは配列にしなきゃダメなので注意!

TemplateMessageBuilder
テンプレートメッセージを作ります。引数は代替テキスト、ButtonTemplateBuilderです。

ここまで作れば、こんな感じになるかな、と思います。?
Screenshot_20200701-181017.png

送られた住所から経度と緯度を取得する

getLatitude()getLongitude()という関数で緯度経度が取れるので、これをつかいます?‍♀️
https://line.github.io/line-bot-sdk-php/source-class-LINE.LINEBot.Event.MessageEvent.LocationMessage.html#65-68

LineController.php
 try{
            $events = $bot->parseEventRequest($httpRequestBody, $signature);
            foreach ($events as $event) {
                //フォローイベント
                if($event instanceof FollowEvent){
                    $followMessage = '友達登録ありがとう!何が食べたいか入力してね。近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)';
                    $bot->replyText($event->getReplyToken(), $followMessage);
                    continue;
                }
                //フォロー解除イベント
                else if ($event instanceof UnfollowEvent) {
                    continue;
                }
                //スタンプメッセージイベント
                else if($event instanceof StickerMessage){
                    $stampMessage = '可愛いスタンプだね〜!何が食べたいか入力してくれたら近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)';
                    $bot->replyText($event->getReplyToken(), $stampMessage);
                    continue;
                }
                //テキストメッセージイベント
                else if($event instanceof TextMessage){
                    (new LineService())->getMessage($bot,$event);
                    continue;
                }
                //位置情報メッセージイベント
                else if($event instanceof LocationMessage){
                    (new GurunaviService())->returnGurunaviList($bot, $event,$event->getLatitude(), $event->getLongitude()); //これを追加
                }
                else{
                    break;
                }
            }

ぐるなびから情報を取得する

ではreturnGurunaviListを実装します。
レストラン検索APIを使用します。

必要なパラメータはこのくらい??↓

・keyid(ぐるなびから与えられるアクセスキー)

・category_s(カテゴリーコード)
https://api.gnavi.co.jp/api/manual/categorysmaster/
このAPIを叩けばカテゴリーコードの一覧が取得できます。

・latitude(緯度)

・longitude(経度)

・range(緯度/経度からの検索範囲(半径))
1:300m、2:500m、3:1000m、4:2000m、5:3000mって感じです。最大3000m?

GurunabiService.php
     /**
     *
     * カテゴリーが存在すればぐるなびで検索をかける
     *
     * @param $bot
     * @param $event
     * @param $lat
     * @param $lng
     */
    public function returnGurunaviList($bot, $event, $lat, $lng)
    {
        //DBにカテゴリーがあるか検証
        $category = Line::where(['user_id' => $event->getUserId()])->latest()->first();
        if ($category) {
            $getGurunaviResult = $this->getGurunavi($lat, $lng, $category);
            if (property_exists($getGurunaviResult, 'error')) {
                $bot->replyText($event->getReplyToken(), 'お店が見つからなかったよ〜、ごめんね。');
            } else {
                $category->delete();
                (new FlexMessage())->returnFlexMessage($event, $getGurunaviResult);
            }
        } else {
            $bot->replyText($event->getReplyToken(), '先に何が食べたいか教えてね!');
        }
    }

   /**
     *
     * ぐるなびで検索
     *
     * @param $lat
     * @param $lng
     * @param $category
     * @return mixed
     */
    public static function getGurunavi($lat, $lng, $category)
    {
        $endpoint = 'https://api.gnavi.co.jp/RestSearchAPI/v3/';
        //自分のアクセスキー
        $keyId = config('services.gurunavi.key');
        //検索範囲
        $range = 5;
        //カテゴリーコードを取得


        if ($category->category === 1) {
            $categoryCode = 'RSFST14008';
        } else {
            if ($category->category === 2) {
                $categoryCode = 'RSFST05001';
            } else {
                $categoryCode = 'RSFST05003';
            }
        }

        $url = $endpoint.'?keyid='.$keyId.'&category_s='.$categoryCode.'&latitude='.$lat.'&longitude='.$lng.'&range='.$range;
        $json = file_get_contents($url);

        return json_decode($json);
    }

returnGurunaviListでカテゴリーがあることを検証してから、getGurunaviでぐるなび検索をかけています。DBに保存してあったカテゴリーをぐるなびのカテゴリーコードと照らし合わせています。(最初からぐるなびのカテゴリーコードを格納したほうがいいですね?)
ちなみにここまできてカテゴリーが餃子と焼肉以外だったらたこ焼きのカテゴリーコードで検索がかかるようにしました?
そして検索結果はjsonで返ってくるので、デコードして返します。

検索結果はreturnFlexMessageで色々といじって返します?‍♀️

食べログの検索結果をFlex Messageに変換して送る

Flex Messageはjsonで書くっぽいのですが、絶対できないと思ったので、
LINE Bot Designerを入れました。

LINE Bot DesignerでFlex Messageのテンプレートデザインを確認しながら作成し、jsonを配列に変換します。

先ほどのデコードした検索結果をreturnFlexMessageで受け取って、generateFlexTemplateContentでいじくりまわして最終的にcurlでPOSTします。
ここにヘッダーとかボディとかかいてあります?

flexMessage.php
    /**
     * ぐるなびの情報を送信
     *
     * @param $event
     * @param $gurunavi
     * @return void
     */
    public function returnFlexMessage($event,$gurunavi)
    {
        $token = config('services.line.token');
        $postJson = $this->generateFlexTemplateContent($gurunavi);

        $result = json_encode(['replyToken' => $event->getReplyToken(), 'messages' => [$postJson]]);
        $curl = curl_init();
        //curl_exec() の返り値を文字列で返す
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        //POSTリクエスト
        curl_setopt($curl, CURLOPT_POST, true);
        //ヘッダを指定
        curl_setopt($curl, CURLOPT_HTTPHEADER, array('Authorization: Bearer '.$token, 'Content-type: application/json'));
        //リクエストURL
        curl_setopt($curl, CURLOPT_URL, 'https://api.line.me/v2/bot/message/reply');
        //送信するデータ
        curl_setopt($curl, CURLOPT_POSTFIELDS, $result);

        $curlResult = curl_exec($curl);

        curl_close($curl);

        return $curlResult;
    }

generateFlexTemplateContentでは取得したぐるなびの情報をeachでまわして、Flex Messageのテンプレートに当てはめています。
テンプレートはjsonで書くのですが、LINE Bot Designerを使うと、デザインを確認しながら簡単にjsonを作れます。

スクリーンショット 2020-07-07 17.41.53.png

あとはここを参考にjsonの形を整えます。?‍♀️

flexMessage.php
    /**
     * ぐるなびの情報をflexMessageテンプレートにあてめる
     *
     * @param $gurunavis
     * @return array
     */
    private function generateFlexTemplateContent($gurunavis)
    {
        $lists = [];
        foreach ($gurunavis->rest as $rest) {
            $lists[] = $this->getFlexTemplate($rest);
        }

        $contents = ["type" => "carousel", "contents" => $lists];
        return ['type' => 'flex', 'altText' => 'searchResult', 'contents' => $contents];
    }

    /**
     * flexMessageテンプレート
     *
     * @param $gurunavi
     * @return array
     */
    private function getFlexTemplate($gurunavi)
    {
        return [
            "type" => "bubble",
            "hero" => [
                "type" => "image",
                "url" => $gurunavi->image_url->shop_image1 ? $gurunavi->image_url->shop_image1: "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_1_cafe.png",
                "size" => "full",
                "aspectRatio" => "20:13",
                "aspectMode" => "cover",
                "action" => [
                    "type" => "uri",
                    "label" => "Line",
                    "uri" => $gurunavi->url_mobile ? $gurunavi->url_mobile : '不明',
                ]
            ],
            "body" => [
                "type" => "box",
                "layout" => "vertical",
                "contents" => [
                    [
                        "type" => "text",
                        "text" => $gurunavi->name ? $gurunavi->name : '不明',
                        "size" => "xl",
                        "weight" => "bold",
                        "wrap" => true
                    ],
                    [
                        "type" => "box",
                        "layout" => "baseline",
                        "margin" => "md",
                        "contents" => [
                            [
                                "type" => "text",
                                "text" => $gurunavi->opentime ? $gurunavi->opentime : '不明',
                                "flex" => 0,
                                "margin" => "md",
                                "size" => "sm",
                                "color" => "#999999",
                                "wrap" => true
                            ]
                        ]
                    ],
                    [
                        "type" => "box",
                        "layout" => "vertical",
                        "spacing" => "sm",
                        "margin" => "lg",
                        "contents" => [
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "spacing" => "sm",
                                "contents" => [
                                    [
                                        "type" => "text",
                                        "text" => "種類",
                                        "flex" => 1,
                                        "size" => "sm",
                                        "color" => "#AAAAAA"
                                    ],
                                    [
                                        "type" => "text",
                                        "text" => $gurunavi->code->category_name_l[0] ? $gurunavi->code->category_name_l[0] : '不明',
                                        "flex" => 5,
                                        "size" => "sm",
                                        "color" => "#666666",
                                        "wrap" => true
                                    ]
                                ]
                            ],
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "spacing" => "sm",
                                "contents" => [
                                    [
                                        "type" => "text",
                                        "text" => "場所",
                                        "flex" => 1,
                                        "size" => "sm",
                                        "color" => "#AAAAAA"
                                    ],
                                    [
                                        "type" => "text",
                                        "text" => $gurunavi->access->station.'徒歩'.$gurunavi->access->walk.'分' ? $gurunavi->access->station.'徒歩'.$gurunavi->access->walk.'分' : '不明',
                                        "flex" => 5,
                                        "size" => "sm",
                                        "color" => "#666666",
                                        "wrap" => true
                                    ]
                                ]
                            ],
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "spacing" => "sm",
                                "contents" => [
                                    [
                                        "type" => "text",
                                        "text" => "駐車場",
                                        "flex" => 2,
                                        "size" => "sm",
                                        "color" => "#AAAAAA"
                                    ],
                                    [
                                        "type" => "text",
                                        "text" => $gurunavi->parking_lots ? $gurunavi->parking_lots : '不明',
                                        "flex" => 5,
                                        "size" => "sm",
                                        "color" => "#666666",
                                        "wrap" => true
                                    ]
                                ]
                            ]
                        ]
                    ]
                ]
            ],
            "footer" => [
                "type" => "box",
                "layout" => "vertical",
                "flex" => 0,
                "spacing" => "sm",
                "contents" => [
                    [
                        "type" => "button",
                        "action" => [
                            "type" => "uri",
                            "label" => "ぐるなびで見る",
                            "uri" => $gurunavi->url_mobile ? $gurunavi->url_mobile : '不明'
                        ],
                        "height" => "sm",
                        "style" => "link"
                    ],
                    [
                        "type" => "spacer",
                        "size" => "sm"
                    ]
                ]
            ]
        ];
    }
}

完成?

エンジニアになって1ヶ月目に作ろうとして挫折して、4ヶ月経ったのでもう一回やってみたら割とできてびっくりしました。
まだまだきれいなソースコード書けないし、サービスクラスの使い分けやメソッドの分け方もきっとぐちゃぐちゃなのでこれからも精進します?

LINEのリファレンスすごくわかりやすかった!
何か違うよ!ってところがあれば教えてください?

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

[Laravel]位置情報を送ると近くの餃子か焼肉のお店を教えてくれるLINE BOTを作った

経緯

なんか面白い物が作りたいと思い、位置情報を送ると近くの餃子屋さんと焼肉屋さんを教えてくれるLINE BOTを作りました。
なぜ餃子と焼肉かと言うと、私が好きだからです。

参考

【公式リファレンス】
https://developers.line.biz/ja/docs/messaging-api

【LINE SDK for PHP】
https://github.com/line/line-bot-sdk-php

【[LINE Bot] 位置情報から食べログ3.5以上の優良店を検索するbot作った】
(これをお手本につくりました。)
https://qiita.com/NARI_Creator/items/f29112e6f604c86b3c0d

【ぐるなびAPIの取得方法 そのまま使えるソースコード付き】
https://enjoy-surfing.com/gurunavi/

【LineBotを作るときの雛形 for Laravel】
https://qiita.com/sh-ogawa/items/2238e579d7ee538025a0

【LaravelでLINEにチャットボットをつくる(QRコード作成)】
https://blog.capilano-fw.com/?p=4285#i-4

大変参考になりました?ありがとうございます。

仕様

1.餃子が食べたいか焼肉が食べたいか入力する
2.位置情報を送信する
3.近くのお店を教えてくれる

簡単な仕様はこんな感じです。?

作成前にやっておくこと

herokuのアプリケーションを作る

ここら辺みておけばできるんじゃないかなーと思います。

Heroku アカウント登録してデプロイするまでの簡単な使い方
Laravelをherokuにデプロイする(データベースはMySQL)

herokuはクレカを登録すると無料で1ヶ月あたり1000時間使えます。
無料プランだと30分ごとにスリープしてしまうのでスリープ後の初アクセスはレスポンスが遅いですが、バッチ処理をしておけば問題ないみたいです。

LINE Message APIの登録

LINE BOTのアカウントを作ります。

公式ドキュメント
公式が割と丁寧に説明してくれてるので、見ながらやったらできると思います。

実装

LINE Messaging API SDKをインストールする

$ composer require linecorp/line-bot-sdk

環境変数を追加する

#line
LINE_SECRET_TOKEN = 
LINE_ACCESS_TOKEN =

トークンはアカウントのBasic Settingから参照できます。

ルートの作成

ユーザーがLINE BOTにメッセージを送ると、LINEプラットフォームから指定されたアプリケーションのURLにHTTPリクエストを送るので、そのURLを決めます。

web.php
Route::post('line','LineController@post')->name('line.post');

ちなみにリクエストはPOSTで送られます?
あとついでにControllerも作っちゃいます。

クライアントを作成する

今回はサービスクラスにまとめました。

LineService.php
<?php

namespace App\Services;

use LINE\LINEBot;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;

class LineService
{
    /**
     * linesdkの使用開始
     *
     * @return LINEBot
     */
    public static function lineSdk()
    {
        $token = config('services.line.token');
        $secret = config('services.line.secret');

        $httpClient = new CurlHTTPClient($token);
        $bot = new LINEBot($httpClient,['channelSecret' => $secret]);

        return $bot;
    }
}

$token$serviceは.envに登録したLINE_SECRET_TOKENLINE_ACCESS_TOKENですが、configのserviceに登録してからもってきてます。

基本公式のREADMEに書いてあるまんまですね?

署名を検証する

ラインプラットフォーム以外のわるものから危険なリクエストが送られてくる可能性があるので、検証する必要があります。
ラインのプラットフォームからリクエストが送られてきた時は、リクエストヘッダーにX-Line-Signatureというものが含まれます。これを検証するのですが、

チャネルシークレットを秘密鍵として、HMAC-SHA256アルゴリズムを使用してリクエストボディのダイジェスト値を取得します。
ダイジェスト値をBase64エンコードした値と、リクエストヘッダーのX-Line-Signatureに含まれる署名が一致することを確認します。

と丁寧に書かれているのでその通りに検証します。

LineController.php
<?php

namespace App\Http\Controllers;

use App\Services\LineService;
use LINE\LINEBot\SignatureValidator;

class LineController extends Controller
{
    public function post()
    {

        $signarure = request()->header('X-Line-Signature');
        $validateSignature = SignatureValidator::validateSignature($httpRequestBody, $channelSecret, $signarure);
        if ($validateSignature) {
            return response()->json(200);
        } else {
            abort(400);
        }
    }
}

とりあえずsignatureの検証ができれば200、できなければ400を返すようにします。

このSignatureValidator::validateSignatureが何をしているのかと言うと、
リクエストボディのダイジェスト値をbase64にエンコードして、リクエストヘッダーのX-Line-Signatureと比較しています。

SignatureValidator.php
public static function validateSignature($body, $channelSecret, $signature)
{
    if (!isset($signature)) {
        throw new InvalidSignatureException('Signature must not be empty');
    }

    return hash_equals(base64_encode(hash_hmac('sha256', $body, $channelSecret, true)), $signature);
}

こういうことです?

$channelSecret = config('services.line.secret');
$httpRequestBody = request()->getContent();
$hash = hash_hmac('sha256', $httpRequestBody, $channelSecret, true);
$signature = base64_encode($hash);

Webhook URLを登録する

次LINEのコントロールパネルからWebhook URLを登録します。
herokuのurl(×××××.herokuapp.com/line)登録して、verifyボタンを押します。
successというメッセージがでてきたらOKです?
コントローラで200を返さないようにしないとエラーになります。

テキストメッセージが送られてきたら返事を返してみる

とりあえず、テキストメッセージが送られてきたらこんにちは!と返してみます。

LineController.php
        $bot = LineService::lineSdk();

        try{
            $events = $bot->parseEventRequest($httpRequestBody, $signature);
            foreach ($events as $event) {
                if ($event instanceof LINE\LINEBot\Event\MessageEvent\TextMessage) {
                    $bot->replyText($event->getReplyToken(), 'こんにちは!');
                }
            }
        }catch(\Exception $e){
            Log::debug($e);
        }

parseEventRequestで署名が正当であるか検証して、正当であればリクエストをパースします。

webhookで送られてくるイベントはいろいろ種類がある↓?
Webhookイベントオブジェクト
SDK

送られてきたイベントがテキストメッセージであるかは
$event instanceof LINE\LINEBot\Event\MessageEvent\TextMessageのインスタンスであるかで確認します。

応答できるリクエストには応答トークンというものが発行されるので、$event->getReplyToken()で取得して、応答メッセージと一緒に$bot->replyTextで返しています?

イベントの種類でレスポンスを変えてみる

とりあえず、以下のイベントに対するレスポンスを実装します。
・フォローイベント
・フォロー解除イベント
・スタンプメッセージイベント
・テキストメッセージイベント
・位置情報メッセージイベント

LineController.php
        try{
            $events = $bot->parseEventRequest($httpRequestBody, $signature);
            foreach ($events as $event) {
                //フォローイベント
                if($event instanceof FollowEvent){
                    $followMessage = '友達登録ありがとう!何が食べたいか入力してね。近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)';
                    $bot->replyText($event->getReplyToken(), $followMessage);
                    continue;
                }
                //フォロー解除イベント
                else if ($event instanceof UnfollowEvent) {
                    continue;
                }
                //スタンプメッセージイベント
                else if($event instanceof StickerMessage){
                    $stampMessage = '可愛いスタンプだね〜!何が食べたいか入力してくれたら近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)'
                    $bot->replyText($event->getReplyToken(), $stampMessage);
                    continue;
                }
                //テキストメッセージイベント
                else if($event instanceof TextMessage){
                    //TODO テキストメッセージ実装
                }
                //位置情報メッセージイベント
                else if($event instanceof LocationMessage){
                    //TODO 位置情報実装
                }
            }
        }catch(\Exception $e){
            Log::debug($e);
        }

エイリアスはこちらを参考に??‍♀️

テキストメッセージに対するリスポンス

このBOTでは最初に何を食べたいか入力してもらってから位置情報を要求します?
餃子か焼肉かを判定したいのですが、
餃子、ぎょうざ、ぎょーざ、ギョーザなどいろいろなパターンの餃子と焼肉に対応したいと思います。
単純にif文に全部ぶち込みます。多分よくない?

LineService.php
    /**
     * 入力された文字が餃子か判定
     *
     * @param $text
     * @return bool
     */
    public function isGyoza($text)
    {
        return ($text === '餃子' || $text === 'ぎょうざ' || $text === 'ぎょーざ' || $text === 'ギョーザ' || $text === 'ギョーザ');
    }

    /**
     * 入力された文字が餃子か判定
     *
     * @param $text
     * @return bool
     */
    public function isYakiniku($text)
    {
        return ($text === '焼肉' || $text === '焼き肉' || $text === 'やきにく' || $text === 'ヤキニク' || $text === 'ヤキニク');
    }

焼肉か餃子かの結果はここで受け取ります。↓
焼肉か餃子が入力された場合は後述するrequireLocationで位置情報を求めます?
また、putCategoryでDBにuser_idcategoryを保存するようにしました。
categoryは餃子であれば1、焼肉は2が保存されます。
もし他の単語が入力された場合は、テキストメッセージを返します?

LineService.php
    /**
     * 送られてきたメッセージに対するレスポンス
     *
     * @param $bot
     * @param $event
     * @return void
     */
    public function getMessage($bot, $event)
    {
        try{
            $text = $event->getText();
            if ($this->isGyoza($text)) {
                //DBに保存
                $user_id = $event->getUserId();
                $category = Line::GYOZA;
                $this->putCategory($user_id,$category);
                //位置情報を求める
                $word = '餃子';
                $this->requireLocation($bot, $event, $word);
            } else if ($this->isYakiniku($text)) {
                //DBに保存
                $user_id = $event->getUserId();
                $category = Line::YAKINIKU;
                $this->putCategory($user_id,$category);
                //位置情報を求める
                $word = '焼肉';
                $this->requireLocation($bot, $event, $word);
            }else{
                $bot->replyText($event->getReplyToken(), '焼肉か餃子しか調べられないよ、、ごめんね。');
            }
        }catch(\Exception $e){
            Log::debug($e);
        }
    }

    /**
     * カテゴリーとユーザーIDをDBに保存
     *
     * @param $user_id
     * @param $category
     * @return void
     */
    public function putCategory($user_id, $category)
    {
        Line::create([
            'user_id' => $user_id,
            'category' => $category
        ]);
    }

    /**
     * 位置情報を求めるメッセージを送る
     *
     * @param $bot
     * @param $event
     * @param $word  //餃子か焼肉か
     * @return void
     */
    public function requireLocation($bot, $event, $word)
    {
        $uri = new UriTemplateActionBuilder('現在地を送る!', 'line://nv/location');
        $message = new ButtonTemplateBuilder(null, $word.'が食べたいんだね!今どこにいるか教えてほしいな!', null, [$uri]);

        $bot->replyMessage($event->getReplyToken(), new TemplateMessageBuilder('位置情報を送ってね', $message));

    }

UriTemplateActionBuilder
このアクションが関連づけられたコントロールがタップされると、第二引数に登録されているURLが開きます。
line://nv/locationは、lineで位置情報が開かれます。

ButtonTemplateBuilder
ボタンテンプレートが作られます。引数は、タイトル、本文、画像URL、アクションの順番です。
アクションは配列にしなきゃダメなので注意!

TemplateMessageBuilder
テンプレートメッセージを作ります。引数は代替テキスト、ButtonTemplateBuilderです。

ここまで作れば、こんな感じになるかな、と思います。?
Screenshot_20200701-181017.png

送られた住所から経度と緯度を取得する

getLatitude()getLongitude()という関数で緯度経度が取れるので、これをつかいます?‍♀️
https://line.github.io/line-bot-sdk-php/source-class-LINE.LINEBot.Event.MessageEvent.LocationMessage.html#65-68

LineController.php
 try{
            $events = $bot->parseEventRequest($httpRequestBody, $signature);
            foreach ($events as $event) {
                //フォローイベント
                if($event instanceof FollowEvent){
                    $followMessage = '友達登録ありがとう!何が食べたいか入力してね。近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)';
                    $bot->replyText($event->getReplyToken(), $followMessage);
                    continue;
                }
                //フォロー解除イベント
                else if ($event instanceof UnfollowEvent) {
                    continue;
                }
                //スタンプメッセージイベント
                else if($event instanceof StickerMessage){
                    $stampMessage = '可愛いスタンプだね〜!何が食べたいか入力してくれたら近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)';
                    $bot->replyText($event->getReplyToken(), $stampMessage);
                    continue;
                }
                //テキストメッセージイベント
                else if($event instanceof TextMessage){
                    (new LineService())->getMessage($bot,$event);
                    continue;
                }
                //位置情報メッセージイベント
                else if($event instanceof LocationMessage){
                    (new GurunaviService())->returnGurunaviList($bot, $event,$event->getLatitude(), $event->getLongitude()); //これを追加
                }
                else{
                    break;
                }
            }

ぐるなびから情報を取得する

ではreturnGurunaviListを実装します。
レストラン検索APIを使用します。

必要なパラメータはこのくらい??↓

・keyid(ぐるなびから与えられるアクセスキー)

・category_s(カテゴリーコード)
https://api.gnavi.co.jp/api/manual/categorysmaster/
このAPIを叩けばカテゴリーコードの一覧が取得できます。

・latitude(緯度)

・longitude(経度)

・range(緯度/経度からの検索範囲(半径))
1:300m、2:500m、3:1000m、4:2000m、5:3000mって感じです。最大3000m?

GurunabiService.php
     /**
     *
     * カテゴリーが存在すればぐるなびで検索をかける
     *
     * @param $bot
     * @param $event
     * @param $lat
     * @param $lng
     */
    public function returnGurunaviList($bot, $event, $lat, $lng)
    {
        //DBにカテゴリーがあるか検証
        $category = Line::where(['user_id' => $event->getUserId()])->latest()->first();
        if ($category) {
            $getGurunaviResult = $this->getGurunavi($lat, $lng, $category);
            if (property_exists($getGurunaviResult, 'error')) {
                $bot->replyText($event->getReplyToken(), 'お店が見つからなかったよ〜、ごめんね。');
            } else {
                $category->delete();
                (new FlexMessage())->returnFlexMessage($event, $getGurunaviResult);
            }
        } else {
            $bot->replyText($event->getReplyToken(), '先に何が食べたいか教えてね!');
        }
    }

   /**
     *
     * ぐるなびで検索
     *
     * @param $lat
     * @param $lng
     * @param $category
     * @return mixed
     */
    public static function getGurunavi($lat, $lng, $category)
    {
        $endpoint = 'https://api.gnavi.co.jp/RestSearchAPI/v3/';
        //自分のアクセスキー
        $keyId = config('services.gurunavi.key');
        //検索範囲
        $range = 5;
        //カテゴリーコードを取得


        if ($category->category === 1) {
            $categoryCode = 'RSFST14008';
        } else {
            if ($category->category === 2) {
                $categoryCode = 'RSFST05001';
            } else {
                $categoryCode = 'RSFST05003';
            }
        }

        $url = $endpoint.'?keyid='.$keyId.'&category_s='.$categoryCode.'&latitude='.$lat.'&longitude='.$lng.'&range='.$range;
        $json = file_get_contents($url);

        return json_decode($json);
    }

returnGurunaviListでカテゴリーがあることを検証してから、getGurunaviでぐるなび検索をかけています。DBに保存してあったカテゴリーをぐるなびのカテゴリーコードと照らし合わせています。(最初からぐるなびのカテゴリーコードを格納したほうがいいですね?)
ちなみにここまできてカテゴリーが餃子と焼肉以外だったらたこ焼きのカテゴリーコードで検索がかかるようにしました?
そして検索結果はjsonで返ってくるので、デコードして返します。

検索結果はreturnFlexMessageで色々といじって返します?‍♀️

食べログの検索結果をFlex Messageに変換して送る

Flex Messageはjsonで書くっぽいのですが、絶対できないと思ったので、
LINE Bot Designerを入れました。

LINE Bot DesignerでFlex Messageのテンプレートデザインを確認しながら作成し、jsonを配列に変換します。

先ほどのデコードした検索結果をreturnFlexMessageで受け取って、generateFlexTemplateContentでいじくりまわして最終的にcurlでPOSTします。
ここにヘッダーとかボディとかかいてあります?

flexMessage.php
    /**
     * ぐるなびの情報を送信
     *
     * @param $event
     * @param $gurunavi
     * @return void
     */
    public function returnFlexMessage($event,$gurunavi)
    {
        $token = config('services.line.token');
        $postJson = $this->generateFlexTemplateContent($gurunavi);

        $result = json_encode(['replyToken' => $event->getReplyToken(), 'messages' => [$postJson]]);
        $curl = curl_init();
        //curl_exec() の返り値を文字列で返す
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        //POSTリクエスト
        curl_setopt($curl, CURLOPT_POST, true);
        //ヘッダを指定
        curl_setopt($curl, CURLOPT_HTTPHEADER, array('Authorization: Bearer '.$token, 'Content-type: application/json'));
        //リクエストURL
        curl_setopt($curl, CURLOPT_URL, 'https://api.line.me/v2/bot/message/reply');
        //送信するデータ
        curl_setopt($curl, CURLOPT_POSTFIELDS, $result);

        $curlResult = curl_exec($curl);

        curl_close($curl);

        return $curlResult;
    }

generateFlexTemplateContentでは取得したぐるなびの情報をeachでまわして、Flex Messageのテンプレートに当てはめています。
テンプレートはjsonで書くのですが、LINE Bot Designerを使うと、デザインを確認しながら簡単にjsonを作れます。

スクリーンショット 2020-07-07 17.41.53.png

あとはここを参考にjsonの形を整えます。?‍♀️

flexMessage.php
    /**
     * ぐるなびの情報をflexMessageテンプレートにあてめる
     *
     * @param $gurunavis
     * @return array
     */
    private function generateFlexTemplateContent($gurunavis)
    {
        $lists = [];
        foreach ($gurunavis->rest as $rest) {
            $lists[] = $this->getFlexTemplate($rest);
        }

        $contents = ["type" => "carousel", "contents" => $lists];
        return ['type' => 'flex', 'altText' => 'searchResult', 'contents' => $contents];
    }

    /**
     * flexMessageテンプレート
     *
     * @param $gurunavi
     * @return array
     */
    private function getFlexTemplate($gurunavi)
    {
        return [
            "type" => "bubble",
            "hero" => [
                "type" => "image",
                "url" => $gurunavi->image_url->shop_image1 ? $gurunavi->image_url->shop_image1: "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_1_cafe.png",
                "size" => "full",
                "aspectRatio" => "20:13",
                "aspectMode" => "cover",
                "action" => [
                    "type" => "uri",
                    "label" => "Line",
                    "uri" => $gurunavi->url_mobile ? $gurunavi->url_mobile : '不明',
                ]
            ],
            "body" => [
                "type" => "box",
                "layout" => "vertical",
                "contents" => [
                    [
                        "type" => "text",
                        "text" => $gurunavi->name ? $gurunavi->name : '不明',
                        "size" => "xl",
                        "weight" => "bold",
                        "wrap" => true
                    ],
                    [
                        "type" => "box",
                        "layout" => "baseline",
                        "margin" => "md",
                        "contents" => [
                            [
                                "type" => "text",
                                "text" => $gurunavi->opentime ? $gurunavi->opentime : '不明',
                                "flex" => 0,
                                "margin" => "md",
                                "size" => "sm",
                                "color" => "#999999",
                                "wrap" => true
                            ]
                        ]
                    ],
                    [
                        "type" => "box",
                        "layout" => "vertical",
                        "spacing" => "sm",
                        "margin" => "lg",
                        "contents" => [
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "spacing" => "sm",
                                "contents" => [
                                    [
                                        "type" => "text",
                                        "text" => "種類",
                                        "flex" => 1,
                                        "size" => "sm",
                                        "color" => "#AAAAAA"
                                    ],
                                    [
                                        "type" => "text",
                                        "text" => $gurunavi->code->category_name_l[0] ? $gurunavi->code->category_name_l[0] : '不明',
                                        "flex" => 5,
                                        "size" => "sm",
                                        "color" => "#666666",
                                        "wrap" => true
                                    ]
                                ]
                            ],
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "spacing" => "sm",
                                "contents" => [
                                    [
                                        "type" => "text",
                                        "text" => "場所",
                                        "flex" => 1,
                                        "size" => "sm",
                                        "color" => "#AAAAAA"
                                    ],
                                    [
                                        "type" => "text",
                                        "text" => $gurunavi->access->station.'徒歩'.$gurunavi->access->walk.'分' ? $gurunavi->access->station.'徒歩'.$gurunavi->access->walk.'分' : '不明',
                                        "flex" => 5,
                                        "size" => "sm",
                                        "color" => "#666666",
                                        "wrap" => true
                                    ]
                                ]
                            ],
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "spacing" => "sm",
                                "contents" => [
                                    [
                                        "type" => "text",
                                        "text" => "駐車場",
                                        "flex" => 2,
                                        "size" => "sm",
                                        "color" => "#AAAAAA"
                                    ],
                                    [
                                        "type" => "text",
                                        "text" => $gurunavi->parking_lots ? $gurunavi->parking_lots : '不明',
                                        "flex" => 5,
                                        "size" => "sm",
                                        "color" => "#666666",
                                        "wrap" => true
                                    ]
                                ]
                            ]
                        ]
                    ]
                ]
            ],
            "footer" => [
                "type" => "box",
                "layout" => "vertical",
                "flex" => 0,
                "spacing" => "sm",
                "contents" => [
                    [
                        "type" => "button",
                        "action" => [
                            "type" => "uri",
                            "label" => "ぐるなびで見る",
                            "uri" => $gurunavi->url_mobile ? $gurunavi->url_mobile : '不明'
                        ],
                        "height" => "sm",
                        "style" => "link"
                    ],
                    [
                        "type" => "spacer",
                        "size" => "sm"
                    ]
                ]
            ]
        ];
    }
}

完成?

エンジニアになって1ヶ月目に作ろうとして挫折して、4ヶ月経ったのでもう一回やってみたら割とできてびっくりしました。
まだまだきれいなソースコード書けないし、サービスクラスの使い分けやメソッドの分け方もきっとぐちゃぐちゃなのでこれからも精進します?

LINEのリファレンスすごくわかりやすかった!
何か違うよ!ってところがあれば教えてください?

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

【PHP】 Composer コマンドで tty を Docker につなげて開発したい(TTY as Interactive Shell/対話式シェルで tty)

composerscripts 設定で、composer dev と打ったら PHP の Docker コンテナを起動して触りたい。つまりコンテナにソースコードをマウントしてコンテナ内で開発・デバッグできる状態。

しかし、Interactive shell と表示され終了してしまう。Docker に限らず、他の対話式のシェル・スクリプトでも同じ。

「composer ユーザーコマンド 対話式 実行 tty interactive shell」でググってもドンピシャの Qiita 記事が出てこなかったので、自分のググラビリティとして。

TL; DR

[コマンド] < /dev/ttytty をコマンドとつなげる。

その際、Composer\\Config::disableProcessTimeout も設定しないと、composer制限で 300 秒で接続が切れてしまう

TS; DR

以下は3つのユーザー・コマンドを composer に設定している例です。

composer.json
{
    ...
    "scripts" :{
        "test": [
            "./vendor/bin/phpunit --configuration=./config/phpunit.xml"
        ],
        "compile": [
            "./vendor/bin/box compile --config=./config/box.json"
        ],
        "dev": [
            "Composer\\Config::disableProcessTimeout",
            "docker run --rm --entrypoint='/bin/bash' --workdir='/app' -v $(pwd):/app php:7-cli-alpine < /dev/tty"
        ]
    }
}

composer test コマンドで PHPUnit を使ってユニット・テストをローカルで実行。composer compile コマンドで Box3 を使って ./bin ディレクトリ設置用の Phar アーカイブの作成。そして composer dev コマンドで Docker の PHP7 コンテナを起動して対話式(bash シェル)で触れるようにしている例です。

この時、composer dev のメリットは、任意の PHP バージョンで Docker 上で実行できるので、Travis CI で回す前の簡易テストに便利です。

composer のユーザー・コマンド経由でターミナルの tty(入出力)を Docker に繋げるのがうまく行かなかったのですが、< /dev/tty によりターミナルの tty がコンテナに繋がるようになり、SSH した時のような操作感があるので便利です。

?   注意点
ターミナルの種類(macOS のターミナルなど)によってコンテナから exit すると tty 接続が壊れてターミナルを立ち上げ直さないといけない時があります。これは composer が使っている依存パッケージの symfonyconsole の仕様です。しかし、VSCode のターミナルからだと exit した以降でもローカルに戻ってきます(VSCode を立ち上げ直さなくても大丈夫です)。これは VSCode 側が、戻ってきたらターミナルを初期化をするからです。

参考文献

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

【PHP】 Composer コマンドで tty を Docker につなげて開発したい(Scripts, TTY and Interactive Shell)

composerscripts 設定で、composer dev と打ったら PHP の Docker コンテナを起動して触りたい。つまりコンテナにソースコードをマウントしてコンテナ内で開発・デバッグできる状態。

しかし、Interactive shell と表示され終了してしまう。Docker に限らず、他の対話式のシェル・スクリプトでも同じ。

「composer ユーザーコマンド 対話式 実行 tty interactive shell」でググってもドンピシャの Qiita 記事が出てこなかったので、自分のググラビリティとして。

TL; DR

[コマンド] < /dev/ttytty をコマンドとつなげる。

その際、Composer\\Config::disableProcessTimeout も設定しないと、composer制限で 300 秒で接続が切れてしまう

TS; DR

以下は3つのユーザー・コマンドを composer に設定している例です。

composer.json
{
    ...
    "scripts" :{
        "test": [
            "./vendor/bin/phpunit --configuration=./config/phpunit.xml"
        ],
        "compile": [
            "./vendor/bin/box compile --config=./config/box.json"
        ],
        "dev": [
            "Composer\\Config::disableProcessTimeout",
            "docker run --rm --entrypoint='/bin/bash' --workdir='/app' -v $(pwd):/app php:7-cli-alpine < /dev/tty"
        ]
    }
}

composer test コマンドで PHPUnit を使ってユニット・テストをローカルで実行。composer compile コマンドで Box3 を使って ./bin ディレクトリ設置用の Phar アーカイブの作成。そして composer dev コマンドで Docker の PHP7 コンテナを起動して対話式(bash シェル)で触れるようにしている例です。

この時、composer dev のメリットは、任意の PHP バージョンで Docker 上で実行できるので、Travis CI で回す前の簡易テストに便利です。

composer のユーザー・コマンド経由でターミナルの tty(入出力)を Docker に繋げるのがうまく行かなかったのですが、< /dev/tty によりターミナルの tty がコンテナに繋がるようになり、SSH した時のような操作感があるので便利です。

?   注意点
ターミナルの種類(macOS のターミナルなど)によってコンテナから exit すると tty 接続が壊れてターミナルを立ち上げ直さないといけない時があります。これは composer が使っている依存パッケージの symfonyconsole の仕様です。しかし、VSCode のターミナルからだと exit した以降でもローカルに戻ってきます(VSCode を立ち上げ直さなくても大丈夫です)。これは VSCode 側が、戻ってきたらターミナルを初期化をするからです。

参考文献

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

PHPのWhile文にif文を書く!

PHPの学習中のアウトプットとして書きます!

例えば変数$xを定義し、変数$xを用いて2〜100までの偶数をechoする場合!

index.php
 $i = 2;              //変数定義
 while ($i <= 100){    //$iが100になるまでループ
   if ($i % 2 == 0){    //if分で$iを2で割った時余り0になる数字をechoする
    echo $i;
    echo '<br>';
    }
   $i++;               //$iが100になるまで1を足す
 }

結果

2
4
6
8
10
12
14
16
18
20
22
24
26
28
30
32
34
36
38
40
42
44
46
48
50
52
54
56
58
60
62
64
66
68
70
72
74
76
78
80
82
84
86
88
90
92
94
96
98
100

このようになります!!

以上です!!!

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

PHPで配列を比較して差分を抽出するときに使えるメソッド

はじめに

PHPで配列の要素の比較をしたいと思い、調べていたら、「array_diff」というメソッドがあり、使ってみたら便利だったので、備忘録を残します。

使い方(例)

hoge.php
<?PHP
$array1 = ['a', 'b', 'c'];
$array2 = ['a', 'b', 'c'];
$array3 = ['b', 'c', 'a'];
$array4 = ['a', 'b'];
$array5 = ['a', 'b', 'c', 'd'];

// 順序、値が一致する場合
$diff1and2 = array_diff($array1, $array2);
print_r($diff1and2);
// Array()

// 順序が不一致、値が一致する場合
$diff1and3 = array_diff($array1, $array3);
print_r($diff1and3);
// Array()

// $array1と$array4の値が不一致($array1に「c」が存在)
$diff1and4 = array_diff($array1, $array4);
print_r($diff1and4);
// Array([2] => c)

// $array1と$array5の値が不一致($array5に「d」が存在)
$diff1and5 = array_diff($array1, $array5);
print_r($diff1and5);
// Array()

参考

array_diff

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

Laravel データベース接続の設定

Laravelで作成したデータベースに接続するための設定を解説します。

database.php

config/database.phpにデータベースの接続設定を記述します。

初期設定はmysqlになっています。
ここを切り替えることで、connectionsキー内にあるその他のデータベースも使用することができます。

'default' => env('DB_CONNECTION', 'mysql'),

使用するデータベースの内容を確認します。

config/database.php
'mysql' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', ''),
            'username' => env('DB_USERNAME', ''),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

env()は、Laravelのヘルパ関数です。
デフォルトでは、.envに記載した値をdatabase.phpが読み込むようになっています。

config/database.php
 'database' => env('DB_DATABASE', 'quick_laravel'),
 'username' => env('DB_USERNAME', 'quickusr'),
 'password' => env('DB_PASSWORD', 'qucikpass'),

第二引数に値を直接記述することも可能です。

.env

通常は、.envにDB環境設定を記述します。

.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=quick_laravel
DB_USERNAME=quickusr
DB_PASSWORD=quickpass

.envはGitリポジトリのソースコントール外にあるので、環境設定が公開されることはありません。

SQLSTATE[HY000] [2002] Connection refused 等のエラーが表示される場合、上記の設定のどこかが間違っていると考えられます。

.envを変更しても接続できない場合は、下記の記事を参照してください。
Laravelで.envファイルが反映されない時

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

PHPサーバーで、悪質脆弱性スキャナ(Vulnerability Scanner)や悪いボット(Bad Bots)を制裁します。

悪質脆弱性スキャナ(Vulnerability Scanner)や悪いボット(Bad Bots)とは

悪質脆弱性スキャナ(Vulnerability Scanner)や悪いボット(Bad Bots)とは、許可なくサーバーを強制的にスキャンするプログラム。通常、このようなスキャナは、大規模かつ規則的な規模でインターネット全体をスキャンします。

Snipaste_2020-07-06_23-59-07.png

確かに、安全なサーバー中ではありますが、危害はありません。 しかし、このアクセスログを見るたびに、うっとうしいと思う。

制裁準備

今回は、このような悪質な脆弱性スキャナーと戦うために、非常に古いテクニックを使ってみました。

それは、「GZIP爆弾」です。

この方法は、少なくとも20年以上前からありましたが、今もなお良い解決策を持っていません。

でも、その原因で、このような悪質脆弱性スキャナを制裁するために、この方法を使うのもいいかもしれない。

そのため、私は具体的に「この方法を使う自分のサーバーを守るが犯罪行為ではないか」と弁護士に尋ねました。

(関連記事: 《みんなで逮捕されようプロジェクト》 《消せない画面…不正URL貼り付けた疑いで中1女子ら家宅捜索》

それに対する弁護士の答えは、「この場合、このウェブアドレスへのアクセスの人はすでに不正アクセスで、つまりこのアクセスの人は自分が本当の犯罪者です。」というものでした。

法律面の準備が整いましたので、次は技術的な準備に取り掛かりましょう。

設定開始

まず、このトラップをデフォルトのホストの中で設定することだけをお勧めします。

何故なら、一般できに、デフォルトのホストの中は何もありません。

そして、普通の悪質脆弱性スキャナはただのデフォルトのホストの中をスキャナする。

もし、本番環境の設定と私の説明するの環境が違ってあれば、ぜひとも諦めていただきたいと思います。そして、サーバー環境のセキュリティ設定が正しいかどうかを確認してください。

そして、もしあなたのサーバーが無制限の転送データ通信量を持っていない場合は、私もそれをお勧めしません。

以下のすべてが Linux オペレーティングシステム上で動作します。

1. GZIP爆弾ファイルを生成する

dd if=/dev/zero bs=1M count=10240 | gzip -9 > /root/bomb.gzip

上記のコマンドを使用して、Linuxで10GBサイズのGZIPファイルを生成します。この処理には多少の待ち時間が必要です。

もちろん、私が書いたシェルスクリプトを使って生成することもできます。

ファストモード:

wget https://raw.githubusercontent.com/DeepSkyFire/BadBotsTraps/master/src/Generate.sh && chmod +x Generate.sh && bash Generate.sh -s 10 -m fast

ノーマルモード:

wget https://raw.githubusercontent.com/DeepSkyFire/BadBotsTraps/master/src/Generate.sh && chmod +x Generate.sh && bash Generate.sh -s 10 -m normal

そして、生成されたbomb.gzipファイルの名前を変更します。

2. トラップ充填

先に生成したのGZIP爆弾ファイルの名前は「1ba286020e414afb.gzip」に変更しました。

続きから、PHPファイルを作成してみましょう。

PHPコード:

<?php

$GzipFile = '1ba286020e414afb.gzip'; //先に生成したのGZIP爆弾ファイルの名前。

header("Content-Encoding: gzip"); 
header("Content-Type: text/html; charset=utf-8");
header("Cache-control: no-store"); //CDN環境にキャッシュしないのため、この設定が必要だ。
header("Content-Length: ".filesize($GzipFile));
Header("HTTP/1.1 200 OK"); //'HTTP/1.1 404 Not Found'もできる。好きなコードに設定しよう。

if (ob_get_level()){
    ob_end_clean(); //自分のサーバーを傷つけないように。
}

readfile($GzipFile);
exit;
?>

以上、PHPのコントロールファイルが完成しました。名前は「1ba286020e414afb.php」にしました。

3. トラップをインストール

1ba286020e414afb.php」と「1ba286020e414afb.gzip」この2つのファイルをデフォルトのホストのフォルダに置きます。

そして、nginxの設定ファイル「nginx.conf」を設定する:

server
     {
        listen 80 default_server reuseport;
        ......
        error_page   404 403   /1ba286020e414afb.php; //このような設定する
        ......
     }

もちろん、Apacheでも設定可能です。デフォルトのホストのフォルダに「.htaccess」ファイルを設定する:

......
errorDocument 404 /1ba286020e414afb.php
errorDocument 403 /1ba286020e414afb.php
......

以上、すべての設定全部完成しました。

効果は?

今までのテストによると、70%の悪質脆弱性スキャナを1回の訪問したで訪問を停止しています。

しかし、残りの30%はどうでしょうか? それは後だ。

今日は初めてqiitaに日本語で投稿しました。 何か間違いがあったら教えてください、よろしくお願いします。

上記のコードはすべてGithubにあります:《Github DeepSkyFire/BadBotsTraps》

以上です。

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