20200707のlaravelに関する記事は6件です。

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で続きを読む

laravel 多対多 タグ付け

参考
https://qiita.com/Ioan/items/bac58de02b826ae8e9e9

Profile テーブル
Tag テーブル

をそれぞれ 多対多 で結びつけたい。
profile の綴りを間違えないように気をつけて。

Profile.php
    public function tag(){
        return $this->belongsToMany('App\Tag', 'profile_tag', 'profile_id', 'tag_id');
    }


Tag.php
    public function profile(){
        return $this->belongsToMany('App\Profile', 'profile_tag', 'tag_id', 'profile_id');
    }

データ読み出し

        //タグをすべて読みだし、タグがいくつのプロフィールを持っているか取得
        $hoge = Tag::withCount('profile')
            ->orderBy('profile_count', 'desc')
            ->get();


//        タグとともにすべてのデータを取得
        $res = Profile::query()//単数形 User
            ->with('tag')
            ->get();

これでOK。
中間テーブルを複数作れば
User と Profile にTagテーブルのタグをつけれるようになる。

User.php
    public function tag(){
        return $this->belongsToMany('App\Tag', 'user_tag', 'user_id', 'tag_id');
    }


Tag.php
public function user(){
    return $this->belongsToMany('App\User', 'user_tag', 'tag_id', 'user_id');
}

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

Laravel-mixのnpm watch-pollのcpu負荷が重いのでwatch-pollの間隔を変える

windowsのdockerの環境をhyper-vからwsl2に乗り換えた時にnpm run watchが聞かなくなったので、npm run watch-pollに切り替えました。
が、npm run watch-poll をするとcpuの負荷が重すぎてファンがうるさかったので、なんとかcpu負荷を軽減できないかと対策したときのメモ。

結論

laravelのプロジェクト直下のpackage.jsonに対して

package.json
{
    "scripts": {
        "watch-poll": "npm run watch -- --watch-poll=5000",
    },
}

と時間(watch-poll=5000で5000ms=5sの意味)を指定しましょう。(参考[1])

解説

watchとwatch-pollの違い

そもそもwatchはwebpackの仕組みです。
ファイルを監視し、ファイルの変更があった時に差分だけをトランスパイル先にアップロードしてくれます。
watchのおかげで、何回も全体のトランスパイルを繰り返すことなく、最小限の時間で更新を確認することができます。

このwatchは仕組みとしては、ファイル変更のイベントを検知して、それに処理をするようです。
なので、ファイルシステムやパスの等に問題が発生して適切にファイル変更のイベントを受信できなければwatchは機能しません。[注釈1]

watchが動作しない環境のために、webpackにはpollというオプションがあります。
webpackのwatchが変更の通知を待つのに対して、pollはwebpackから更新処理を定期的に確認しにいきます。
webpackをラップしているlaravel-mixでは、pollオプション付きのwatchが--watch-pollで使用することができます。

watch-pollの処理間隔の仕様

pollingの間隔はデフォルトでは1秒になっています。
webpackにおいて、処理の間隔は変更が可能です。
Laravel-mixでも--watch-poll=(時間) とすればpollingの間隔を変更できます。

注釈

  1. 自分の場合はwsl2のdockerコンテナで稼働するlaravelを使っています。 この場合はおそらくは、vscode等によるwindows上でのファイルの変更がdockerの使っているwsl2上のlinuxに通知されなかったことが原因です(参考[2])。

参考文献

[1]https://github.com/JeffreyWay/laravel-mix/issues/1418
[2]https://github.com/microsoft/WSL/issues/4739

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む