- 投稿日:2020-08-02T23:30:52+09:00
LaravelでWebSocket
目次
Laravelの記事一覧は下記
PHPフレームワークLaravelの使い方Laravelバージョン
動作確認はLaravel Framework 7.19.1で行っています
前提条件
eclipseでLaravel開発環境を構築する。デバッグでブレークポイントをつけて止める。(WindowsもVagrantもdockerも)
本記事は上記が完了している前提で書かれています
プロジェクトの作成もapacheの設定も上記で行っていますLaravelでDIを使う
本記事は上記で作成したフォルダとファイルを使用しますLaravelでRedisを操作する
本記事は上記で作成したフォルダとファイルを使用します。LaravelでRedisを使う設定は完了済みで、sessionもRedisに格納されている前提で書かれていますRatchetのインストール
今回はRatchetを使っていきます
Ratchet (http://socketo.me/)
コマンドラインで
cd sample
composer require cboden/ratchet
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します
eclipseのプロジェクトを右クリック→リフレッシュサービスクラス修正
LaravelでRedisを操作するで作成したサービスクラスを修正します
(1) /sample/app/Services/Interfaces/RedisService.php修正
RedisService.php<?php namespace App\Services\Interfaces; interface RedisService { public function __construct(); public function setKey1($value); public function getKey1(); public function getSession($key); }getSessionメソッドを追加しました
(2) /sample/tests/Services/Impl/RedisServiceImpl.php修正
Tests\Services\Impl\RedisServiceImpl.php‥‥ public function getSession($key){ } ‥‥getSessionメソッドを追加しました
(3) /sample/app/Services/Impl/RedisServiceImpl.php修正
App\Services\Impl\RedisServiceImpl.php‥‥ private $sessionConn = null; public function __construct() { $this->redis = Redis::connection('db2'); $this->sessionConn = Redis::connection(config('session.connection')); } ‥‥ public function getSession($key) { $key = config('cache.prefix') . ':' . $key; $value = $this->sessionConn->get($key); return $value; } ‥‥
$sessionConn
プロパティを追加しました
コンストラクタを修正しました
getSessionメソッドを追加しましたWebSocket処理作成
(1) /sample/app/Http/Controllers/ChatController.php作成
ChatController.php<?php namespace App\Http\Controllers; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; class ChatController implements MessageComponentInterface { protected $userList; public function __construct() { $this->userList = []; } public function addUser($resourceId, $user) { $this->userList[$resourceId] = ["user" => $user]; } public function onOpen(ConnectionInterface $from) { if (!array_key_exists($from->resourceId, $this->userList)) { $this->userList[$from->resourceId] = ["user" => null]; } $this->userList[$from->resourceId]["conn"] = $from; $msg = 'ゲストさんが入室しました'; if (!is_null($this->userList[$from->resourceId]["user"])) { $msg = $this->userList[$from->resourceId]["user"]->name . 'さんが入室しました'; } foreach ($this->userList as $userInfo) { $data = ['msg' => $msg, 'position' => 'center']; $userInfo["conn"]->send(json_encode($data)); } } public function onMessage(ConnectionInterface $from, $msg) { foreach ($this->userList as $userInfo) { $data = ['msg' => $msg]; if (!is_null($this->userList[$from->resourceId]["user"])) { $data['name'] = $this->userList[$from->resourceId]["user"]->name; } else { $data['name'] = 'ゲスト'; } if ($from === $userInfo["conn"]) { $data['position'] = 'right'; } else { $data['position'] = 'left'; } $userInfo["conn"]->send(json_encode($data)); } } public function onClose(ConnectionInterface $conn) { unset($this->userList[$conn->resourceId]); } public function onError(ConnectionInterface $conn, \Exception $e) { $conn->close(); } }onOpenはクライアントが接続してきたときに自動的に実行されます
onMessageはクライアントがsendしてきたときに自動的に実行されます
onCloseはクライアントが切断したときに自動的に実行されます
$this->userList
にEloquentのユーザーモデルとWebSocketのコネクションが格納されます
$this->userList
をforeachで回してWebSocket接続している全ユーザーにJSONをsendしています(2) /sample/app/Serversフォルダ作成
/sample/app/Servers/WsServer.php作成WsServer.php<?php namespace App\Servers; use Ratchet\ComponentInterface; use Ratchet\ConnectionInterface; use Psr\Http\Message\RequestInterface; use App\Services\Interfaces\RedisService; use App\Models\User; class WsServer extends \Ratchet\WebSocket\WsServer { private $component; public function __construct(ComponentInterface $component) { parent::__construct($component); $this->component = $component; } public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { $sessionName = config('session.cookie'); $sessKey = null; $sessValue = null; $headerCookie = $request->getHeader('Cookie'); if (isset($headerCookie[0])) { $cookieList = explode(';', $headerCookie[0]); foreach ($cookieList as $cookie) { $cookie = trim($cookie); $cookieKeyValue = explode('=', $cookie, 2); if ($sessionName === $cookieKeyValue[0]) { $sessKey = decrypt(urldecode($cookieKeyValue[1]), false); break; } } } $redisService = app()->makeWith(RedisService::class); if (!is_null($sessKey)) { $sessValue = $redisService->getSession($sessKey); $sessValue = unserialize(unserialize($sessValue)); foreach ($sessValue as $k => $v) { if (strpos($k, 'login_web_') === 0) { $user = User::find($v); $this->component->addUser($conn->resourceId, $user); } } } return parent::onOpen($conn, $request); } }コンストラクタに渡されてくる
$component
は先ほど作成したChatControllerです
onOpenはクライアントが接続してきたときに自動的に実行されます
Cookieを取得し、その中からsessionに使っているCookie値を取りだします(if ($sessionName === $cookieKeyValue[0])
)。
取り出したCookie値を使いRedisからsessionを取得します($sessValue = $redisService->getSession($sessKey);
)。
LaravelでRedisを操作するでsessionはRedisに格納されるようにしました。
そこからユーザーIDをとります。sessionの中のlogin_web_から始まるキーにユーザーIDがvalueとして格納されています(if (strpos($k, 'login_web_') === 0)
)。(3) /sample直下にwsServer.php作成
wsServer.php<?php use Ratchet\Server\IoServer; use Ratchet\Http\HttpServer; use Illuminate\Contracts\Console\Kernel; use App\Http\Controllers\ChatController; use App\Servers\WsServer; require dirname(__FILE__) . '\vendor\autoload.php'; $app = require __DIR__.'/bootstrap/app.php'; $app->make(Kernel::class)->bootstrap(); $server = IoServer::factory( new HttpServer( new WsServer( new ChatController() ) ), 8282 ); $server->run();webSocketのエントリポイントになります
8282は今回WebSocketで使うポートです。firewallで許可しているポートにしてくださいControllerにメソッド追加
(1) /sample/app/Http/Controllers/SampleController.phpにwebSocketメソッドを追記
SampleController.phppublic function webSocket(Request $request) { return view('sample.webSocket'); }(2) /sample/routes/web.phpに下記を追記
Route::get('sample/web-socket', 'SampleController@webSocket');
viewの作成
(1) /sample/resources/views/sample/webSocket.blade.phpファイル作成
webSocket.blade.php<html> <head> <title>sample</title> <style type="text/css"> .container { width: 100%; height: 100%; box-sizing: border-box; } .msg-log { width: 100%; height: 92%; vertical-align:top; box-sizing: border-box; padding: 0px; border: black solid 1px; overflow-y: scroll; } .input-area { width: 100%; box-sizing: border-box; } .msg { width: 90%; height: 8%; vertical-align:top; box-sizing: border-box; padding: 0px; float: left; } .btn { width: 10%; height: 8%; vertical-align:top; box-sizing: border-box; padding: 0px; } .receive-msg-left { border-radius: 10px; border: black solid 1px; padding: 10px; margin: 0px 10px 10px 10px; display: inline-block; float: left; background-color: #FFFFFF; } .receive-msg-right { border-radius: 10px; border: black solid 1px; padding: 10px; margin: 0px 10px 10px 10px; display: inline-block; float: right; background-color: #00FF00; } .receive-msg-center { border: none; padding: 0px; margin: 10px; display: block; text-align: center; background-color: transparent; } .name-left { border: none; padding: 0px; margin: 0px 0px 0px 10px; display: inline-block; float: left; background-color: transparent; } .name-right { border: none; padding: 0px; margin: 0px 10px 0px 0px; display: inline-block; float: right; background-color: transparent; } .br { line-height: 0px; clear: both; } </style> <script type="text/javascript"> var conn = ""; function open(){ conn = new WebSocket('ws://localhost:8282'); conn.onopen = function(e) { }; conn.onerror = function(e) { alert("エラーが発生しました"); }; conn.onmessage = function(e) { var data = JSON.parse(e.data); var msgLog = document.getElementById("msg_log"); var divObj = document.createElement("DIV"); var msg = null; var msgSplit = null; var nameObj = null; var rowObj = null; var br = null; if (data["name"]) { msg = document.createTextNode(data["name"]); nameObj = document.createElement("DIV"); if (data["position"] == "left") { nameObj.className = 'name-left'; } else { nameObj.className = 'name-right'; } nameObj.appendChild(msg); msgLog.appendChild(nameObj); br = document.createElement("BR"); br.className = 'br'; msgLog.appendChild(br); } if (data["position"] == "left") { divObj.className = 'receive-msg-left'; } else if (data["position"] == "center") { divObj.className = 'receive-msg-center'; } else { divObj.className = 'receive-msg-right'; } msgSplit = data["msg"].split('\n'); for (var i in msgSplit) { msg = document.createTextNode(msgSplit[i]); rowObj = document.createElement("DIV"); rowObj.appendChild(msg); divObj.appendChild(rowObj); } msgLog.appendChild(divObj); br = document.createElement("BR"); br.className = 'br'; msgLog.appendChild(br); msgLog.scrollTop = msgLog.scrollHeight; }; conn.onclose = function() { alert("切断しました"); setTimeout(open, 5000); }; } function send(){ conn.send(document.getElementById("msg").value); } function close(){ conn.close(); } open(); </script> </head> <body> <div class="container"> <div id="msg_log" class="msg-log"></div> <div class="input-area"> <textarea id="msg" class="msg"></textarea> <button class="btn" onclick="send();" >送信</button> </div> </div> </body> </html>
conn = new WebSocket('ws://localhost:8282');
で接続してます。8282は先ほどwsServer.phpに書いたポートです
conn.send(document.getElementById("msg").value);
でWebSocketサーバーにデータ送信しています
conn.onmessage
はwebSocketサーバーからsendがあった場合に自動的に実行されます動作確認
コマンドラインで
cd sample
php wsServer.php
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します
これでWebSocketサーバーが起動しますChromeから
http://localhost/laravelSample/
左上のLOGINリンクからログインする
Firefoxから
http://localhost/laravelSample/
左上のLOGINリンクからログインする
Chromeから
http://localhost/laravelSample/sample/web-socket
にアクセスする
Firefoxから
http://localhost/laravelSample/sample/web-socket
にアクセスするChromeで
あいうえお
かきくけこ
と入力し、送信ボタンをクリックしますFirefoxで
さしすせそ
と入力し、送信ボタンをクリックします動きました
- 投稿日:2020-08-02T22:37:16+09:00
ニートがPHPで求人サイト(Webアプリ)を作ってみた
技術記事ではなく、「作ったものを共有する」ライトな記事です。
経緯
小学5年生でゲームにハマる
↓
ニートになって暇人
↓
アプリ作ってみよう、みたいな公開する理由
頑張って勉強されている方に少しでも参考になればいいなと思ったからです。
また、リリース後はしっかり運用・保守していく予定なので、エンジニアさんからフィードバックをもらえるかも?という下心もあります作ったもの
地元「釧路」限定の求人サイトです。
※釧路:北海道の避暑地であり、魚が美味しい海沿いの町■サイトURL
https://job-cinema.com
■github
https://github.com/haruyaono/jobcinema-prodただ、サイトURLがリンク切れしていたらごめんなさい。
ニートは節約せずに生き残れないので、AWSのリソースを止めています。代わりにドキュメントにまとめております。暇な方はどうぞ。
https://docs.google.com/document/d/1VUamIAlg_RNReFJJk1KSRj7qHN_bp-UEiwZcwO_1Lqk/edit?usp=sharing使用技術
- PHP(Laravel)
- JavaScript(Vue.js)
- AWS(ECS)
※githubに詳細を記してあります
気をつけたこと
コーディング中は、「いきなり難しいことはしない」を意識していました。
概念やロジックの理解が中途半端なまま進めた結果、基本的なことで詰まった経験が1万回ほどあったので、例えば
①まずは比較的簡単な書き方で実装
②1が出来たら、次は可読性が上がる書き方(少し難しい)に置き換えてみるという感じです。
コードについて
3つポイントがあると思っています。
①非同期処理
メール送信は全て非同期で処理しているので、ユーザーはメール送信処理の完了を待つ必要がありません。
ユーザーに会員登録完了画面を表示させて、裏ではメール送信処理をジョブとしてキューへ投入し、ジョブを実行させていく流れです。箱に入ったボールを取り出すイメージ。
本番では、プロセス管理ツール「supervisor」を使用して、Amazon ElasticCache(Redis)にメール送信のキューを保存するプロセスを管理しています。
プロセス管理ツールを使うことで、キューを保存するためのコマンドを自動で実行してくれるので、わざわざサーバーに入って手動でコマンド打つ必要がなく超絶便利でした!
■参考にさせていただいた記事
https://reffect.co.jp/laravel/laravel-ubuntu-supervisor
https://docs.docker.jp/engine/admin/using_supervisord.html②リポジトリパターンを採用
同じようなデータ操作のロジックを一箇所にまとめるために採用しました。
リポジトリパターンを用いることで、ビジネスロジックからデータ操作に関するロジックを切り離して、(データ操作を)抽象化したレイヤに任せることで拡張性・保守性の向上に努めました。
初見は概念が複雑すぎてぶっちゃけ理解に苦しみました。
しかし、ドキュメントや記事(Qiita、その他ブログ)を参考にしながら、手を動かして実装を続けていくと、少しずつ点と点が繋がっていきました■参考にさせていただいた記事
https://www.ritolab.com/entry/165
https://qiita.com/bmf_san/items/c8d7b38b5f1f5747c2fd③テスト駆動開発(TDD)
※あくまでリポジトリのみです
コントローラーからリポジトリを使用する時に「エラーが出る→修正→エラーが出る→修正」の繰り返しにうんざりしたため、リポジトリに記述したデータ操作ロジックが正しく動作するかどうかをテストしながら開発していきました。
しかし、TDDの知見はかなり浅く、本格的に導入するには勉強不足というのが本音。
体系的に網羅されている情報を吸収して実践していく必要があると感じています
■参考にさせていただいた記事
https://www.ritolab.com/entry/168
https://qiita.com/bmf_san/items/c8d7b38b5f1f5747c2fd課題
以下、3つ書きました。
正直、たくさん課題が湧き出てくるので挙げたらキリがないです。■機能が物足りない
・検索項目の数が少ない
・レコメンド機能が微妙すぎる
・求職者と採用担当者間でチャットを導入したいという欲求不満をどうにかしたいです。
また、他サービスを参考にしつつ、どうやればオリジナリティやユーザビリティが向上するのか?も分析していきたい。
■結合テストが書かれていない
実際にアプリを動かしてテスト・デバッグしてますが、執筆時点で結合テストが全く書かれていない状況です。バグ温床の回避やテストへの理解を深めるためにも、早急に取り掛かるべき問題と思っています。また、今後は「機能追加とテストは必ずセット」というルールで、開発に取り込んでいきます。..あたりまえ?
■インフラをコードで管理したい
ニートでお金がないので、開発中はAWS環境を常に維持できません。
使い終わったリソースは削除→変更を確認するためにリソース作成→削除→...AWSコンソールでテンプレの如く毎回ポチポチするのは億劫なので、巷で噂のterraformを検討しています。
まとめ
筆者は、Webアプリ実務経験なしのニート、つまり社会のゴ*であり、
・エンジニアを目指して勉強されている方
・すでにエンジニアとして就労されている方にとって有益かは分かりません。
ただ、少しでも参考になれば嬉しいです。間違っていることをご指摘いただけたり、フィードバック・アドバイスをいただけますと幸いです
- 投稿日:2020-08-02T20:53:19+09:00
LINE Messaging APIを使って、ポケモンの弱点を教えてくれるBotを作る【Laravel・初心者向け】
1.はじめに
先日、久しぶりにポケモンの対人対戦をやっていたときに、
「相手(ポケモン)の弱点何???」
といった疑念を抱き、対戦時間と戦略を見失いモチベーションが下がってしまいました。
見た目だけだとタイプが分からないポケモンもいるし、タイプが2つあるポケモンもいるとなると覚えられない、、と悩んでました。
これって対戦初心者にありがちでは・・・
と思ったのがきっかけで、これを解決したいと思い
簡単・気軽・素早く弱点を知れるもの作ってみよう!!
と考えて実際に作ってみました。
本記事は、以下のようなコンセプトの記事になります。
・製作の経緯と意図
・簡単な製作の手順とコードのポイントの説明
・初心者向け(今後外部APIを使用したいと考えている方)
といった記事になります。
注意
実際には、とくせいや環境変化によってバトル戦略は変化すると思うので、タイプ相性だけではガチ対戦者さんにとっては不向きかと思います。あくまで、LINE botを作ってみよう!という趣旨です。また、実際のゲームに基づいていますので、掲載において不適切な表現等が判明した場合は、真摯に受け止め対処致します。
2.作ったもの
LINEで、相手ポケモンの名前を入力して送信する
これだけ!!!
自動で弱点を返答して教えてくれます。
またおまけ程度ですが、ゲーム画面上では2倍弱点と4倍弱点の違いがないので
(どちらも【効果ばつぐん】の表記で2倍なのか4倍なのかは分からない。)4倍弱点のタイプがあるときはメッセージで分かるようにしました。
シンプルですが、自分も含めた対戦初心者にとってはちょっとは使えると思えるものになりました。
言語については、php(Laravel)を使用しています。
3.なぜ作ったのか
シンプルな答えが欲しいと思ったから。
ポケモン対戦初心者勢の私がまず知りたいと思ったのはタイプ相性でした。
攻略サイトにはタイプ相性が記載されているのですが、とくせい、性格などタイプ相性以外など様々な情報が載っています。
初心者にとって対戦中には情報が多すぎて迷ってしまい判断を鈍らせるので、シンプルに弱点だけ教えてくれるものもあっていいかと思って製作しようと思いました。
LINE Messaging APIを使おうと思った理由
・ポケモンは数が多いので、チェックボックス等の選択肢から選ぶ方式だと時間がかかりすぎる(入力した方が早い。)
・対話式なので答えを知るにはうってつけである。
・単純にLINE Messaging APIを使ってみたかった。Laravelで製作した理由
今回はDBを用意していないし、単純に入力された値を元に返すだけなのでわざわざLaravelに書く必要はなかったかもしれない。
理由としては、
・DB持った時に対応できそう(ポケモンの追加など)
・UIの管理画面上で色々できそう。(将来的に)
・アクセストークンなど退避して書く事に慣れている。API通信が安全に行える。
・配列を多く扱うと思ったので、使い慣れているPHPを使おうと思った。
・デバッグがしやすい。辺りが理由でした。
LINE Messaging APIで他の方が作られているのを見ると、JSで書いている方が多い印象を受けましたが、今回はPHPで書くことにしました。
4.製作手順
4-1. LINE Developersに登録する。
まず、LINE Developersに登録します。
LINEに既に使っている場合は、登録した際のアカウントでDevelopersにも登録できます。
特に理由がなければ、使っているアカウントでそのまま登録していいかと思います。
LINEのBot開発 超入門(前編) ゼロから応答ができるまで
https://qiita.com/nkjm/items/38808bbc97d6927837cdこちらの記事を参考しました。
頻繁にUIが変わっていますが、必要な情報は
・チャネルシークレット
・チャネルアクセストークン(長期)
の二つです。後でWebhookの利用を設定するのでログインしたままにしときます。
4-2. Laravel プロジェクトの立ち上げと初期設定
command$laravel new laravel-project名前は自由で。
.envファイルの設定
envは隠しファイルなので普通は見れないです。
macの場合はcommand + shift + . を押すと表示されます。
envファイルを開いて、4-1で記載されたLINEの情報を記述します。
LINE_CHANNEL_SECRET = "チャネルシークレット" LINE_ACCESS_TOKEN = "チャネルアクセストークン"config.servicesに登録
envファイルを直接参照してもいいのですが、本番用と環境用でenvに書いてある設定(DB接続情報など)が違う場合は不便なので、config.servicesからenvの内容を参照する様にします。
config.servicesreturn [ //省略 'line' => [ 'access_token' => env('LINE_ACCESS_TOKEN'), 'channel_secret' => env('LINE_CHANNEL_SECRET'), ], ]後でコントローラから上記を参照する様にします。
4-3.LINE Messaging API SDK for PHP をインストール
LINE公式から開発のためのSDK(Software Development Kit)が用意されているのでコンポーザを使ってインストールします。
commandcomposer require linecorp/line-bot-sdkインストールすることで、LINEが用意してくれたクラス・メソッドなどを使う事ができる様になります。
4-4.ルーティングを設定する
LINEからAPIで通信を受けたときに、どのコントローラに処理をお願いするかを記述します。
今回はブラウザを介さずにコントローラを呼び出すのでroutes/api.phpに記述します。
routes/api.phpRoute::post('/pokename', 'LineBotPokenameController@input_pokemon');LINEからapi/pokenameに通信を受けたときに、LineBotPokenameControllerクラスのinput_pokemonメソッドを実行しなさい。という意味になります。
4-5で実際にコントローラを作ってきます。
4-5.コントローラを作成する
commandphp artisan make:controller LineBotPokenameControllerLarabelプロジェクトのルートフォルダで上記artsianコマンドを打ちます。
ここでは、4-4で設定したルートで同じコントローラ名(LineBotPokenameController)で作成します。
開きます。
/app/Http/Controllers/LineBotPokenameController<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class LineBotPokenameController extends Controller { public function input_pokemon(Request $request) { //ここに書いていく } }こんな感じになっているかと思います。
続けて、input_pokemonメソッドに愚直に処理を書いてきます。
4-6.クラスを利用してAPIを受け取るための設定をする
LINE Messaging API SDK for PHPをインストールしたことで使えるクラスを追加します。
/app/Http/Controllers/LineBotPokenameController<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use LINE\LINEBot; //追加 use LINE\LINEBot\HTTPClient\CurlHTTPClient; //追加 use LINE\LINEBot\Event\MessageEvent\TextMessage; //追加 class LineBotPokenameController extends Controller //省略上記ではuseを使い、コントローラを呼び出します。
次にinput_pokemonメソッド内で、インスタンス化します。(下記)
その他にも、LINEから情報を受け取った際に、署名の検証などの情報も一緒に記載します。
/app/Http/Controllers/LineBotPokenameController//省略 public function input_pokemon(Request $request) { //認証を行う $lineAccessToken = config('services.line.access_token'); $lineChannelSecret = config('services.line.channel_secret'); $httpClient = new CurlHTTPClient($lineAccessToken); $lineBot = new LINEBot($httpClient, ['channelSecret' => $lineChannelSecret]); $signature = $request->header('x-line-signature'); if (!$lineBot->validateSignature($request->getContent(), $signature)) { //送信元に400エラーを伝える abort(400, 'Invalid signature'); } //LINEで入力されたメッセージ情報を受け取る $events = $lineBot->parseEventRequest($request->getContent(), $signature); } //省略最後にある$eventsの所で、LINEからの送られてきたテキスト情報を受け取って入れ込んでます。
が、今の段階では通信がうまくいきません。
LINE DevelopersのWebhookの部分にURLを記載していないからです。
Webhookの設定をしないといけないのですが、こちらはローカルPC内では作動してくれないため、インターネット上でのURLを記載しないといけません。
やり方としては、herokuなどホスティングサービスを利用し、インターネット上でのURL(https://)を取得した後に、LINE DevelopersのWebhook URLの欄にURLを記載します。
(私は、ローカル上でのURLを一時的にインターネットでのURLに変換してくれるngrokを利用しました。)
記載するURLは
https://herokuなどのURL/api/pokename
となります。うまくいくと、LINEからメッセージを送ったときに$eventsに入力したテキストの情報が入ります。
デバッグで変数$eventsを見てみます。
command[2020-08-02 20:47:55] local.DEBUG: array ( 0 => LINE\LINEBot\Event\MessageEvent\TextMessage::__set_state(array( 'emojis' => NULL, 'message' => array ( 'type' => 'text', 'id' => '12428220966824', 'text' => 'ピカチュウ', ), 'event' => array ( 'type' => 'message', 'replyToken' => 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'source' => array ( 'userId' => 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'type' => 'user', ), 'timestamp' => 1596368874886, 'mode' => 'active', 'message' => array ( 'type' => 'text', 'id' => '12428220966824', 'text' => 'ピカチュウ', ), ), )), )こんな感じで情報を受け取ってくれます。
ここまでで、設定完了です!!
後は、返信をするための処理を書いていきます。
4-7.返信するための処理をコントローラーに記述する
今回作成したものは、この様に書きました。
作りたいアプリによって自由に書いてみるといいかもです。
/app/Http/Controllers/LineBotPokenameControllerpublic function input_pokemon(Request $request) { //省略 foreach ($events as $event) { if (!($event instanceof TextMessage)) { continue; } $replyToken = $event->getReplyToken(); $replyText = $event->getText(); //エンコードを行う $replyText = mb_convert_encoding($replyText,'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN'); //Jsonを取得する $url = public_path() . '/data/double_weakness.json'; $json = file_get_contents($url); $json = mb_convert_encoding($json, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN'); $type_array = json_decode($json, true); //ポケモン情報を取得 $url = 'https://raw.githubusercontent.com/kotofurumiya/pokemon_data/master/data/pokemon_data.json'; $json = file_get_contents($url); $json = mb_convert_encoding($json, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN'); $pokemon_array = json_decode($json, true); //入力チェック if(in_array($replyText, array_column($pokemon_array, 'name'), true)) { //該当するポケモンがいる場合 $pokemon_data = $pokemon_array[array_search($replyText, array_column($pokemon_array, 'name'))]; //タイプを入れる $type1 = $pokemon_data['types'][0]; //単タイプかどうか if(isset($pokemon_data['types'][1])){ $type2 = $pokemon_data['types'][1]; }else{ $type2 = 'mono_type'; $sp_weak = ""; } //タイプ判定 foreach($type_array as $key1 => $value1){ $array_list = $value1; //タイプ1 $type_match1 = array_filter($array_list, function($element) use($type1) { //必ずある前提なので返すだけ return $element['type'] == $type1; }); //該当するものを取得 $type_a = array_column($type_match1, 'double_weakness_type'); //タイプ2 if ($type2 === 'mono_type'){ $type_match = $type_a[0]; }else{ $type_match2 = array_filter($array_list, function($element) use($type2) { return $element['type'] == $type2; }); //該当するものを取得 $type_b = array_column($type_match2, 'double_weakness_type'); //複合弱点をマージ $type_match = array_merge($type_a[0],$type_b[0]); //4倍弱点 $sp_weak = array_intersect($type_a[0],$type_b[0]); //2倍弱点 $type_match = array_diff($type_match,$sp_weak); } //重複削除 $type_match = array_unique($type_match); $type_match = array_values($type_match); }; }else{ //ポケモン以外の入力の場合 $type_match = array("no_much"); } //変数宣言 $weak_text = ""; $spweak_text = ""; //タイプ以外の入力がされてきた場合 if(in_array("no_much", $type_match)){ $weak_text = "ポケモンのなまえを入力してね"; }else{ for($i = 0; $i < count($type_match); $i++) { $weak_text = $type_match[$i].",".$weak_text; } if($sp_weak){ $weak_text = $weak_text. "\n \n4倍弱点だよ \n".$sp_weak[0]; } } //LINEへ送信する $lineBot->replyText($replyToken, $weak_text); } }いくつかポイント
・ポケモンのデータはgithub上でJSONデータを配布されている方から拝借しました。
https://github.com/kotofurumiya/pokemon_data・通常の2倍弱点を一覧にしたJSONデータをLaravelのパブリックに置いて情報を取得しています。基本的には入力されたポケモンの弱点と、JSONで取得したデータを比較してるだけです。
・配列を多用しているので、加工したり制御する関数を多く使っています。
(もっといい書き方があると思う。)・ポケモン以外の名前が入力されて来た時、タイプが一つだけのポケモン、複合タイプのポケモンの3パターンがあると想定して書いています。
5.実際に使ってみて
使いやすかった!! LINEめっちゃ便利!!
画面に相手の名前が書いてあるので入力に迷うことはないし、限られた時間の中で素早く弱点が分かるのは結構便利だった。
まあ、勝てるかどうかは別として。
・・・対戦初心者には十分活用できるかなと思いました!!!
6.反省点
・例外処理(仮にJSONを取得できなかったときに処理が止まる)を直さないといけない。
・一応自分としてはやりたい事ができたが、書き方の部分では甘い部分があるはずなので満足しない。
・これだけだったらあまりLaravelで書く意味がないので、DBを持ったり一覧表示や修正をUI上で行える様にすれば多少は意味が出てくるかもしれないです。
7.最後に
Line Messaging APIはリファレンスも丁寧な日本語で分かりやすくて、始める壁も低く感じました。
あとは、自分で使って便利なだと思って制作できたので楽しく作れました。
Qiitaとかも結構参考になる記事が多くて助かったので、外部APIを使ったアプリ作成のとっかかりとしてもLINEはいいと思います!!おすすめ!!
アイデア次第で他にも結構やれることもありそうなので、思いついたらまた何か作ってみようと思います!
- 投稿日:2020-08-02T20:53:19+09:00
LINE Messaging APIを使って、ポケモンの弱点を教えてくれるBotを作る【初心者向け】
1.はじめに
先日、久しぶりにポケモンの対人対戦をやっていたときに、
「相手(ポケモン)の弱点何???」
といった疑念を抱き、対戦時間と戦略を見失いモチベーションが下がってしまいました。
見た目だけだとタイプが分からないポケモンもいるし、タイプが2つあるポケモンもいるとなると覚えられない、、と悩んでました。
これって対戦初心者にありがちでは・・・
と思ったのがきっかけで、これを解決したいと思い
簡単・気軽・素早く弱点を知れるもの作ってみよう!!
と考えて実際に作ってみました。
本記事は、以下のようなコンセプトの記事になります。
・製作の経緯と意図
・簡単な製作の手順とコードのポイントの説明
・初心者向け(今後外部APIを使用したいと考えている方)
といった記事になります。
注意
実際には、とくせいや環境変化によってバトル戦略は変化すると思うので、タイプ相性だけではガチ対戦者さんにとっては不向きかと思います。あくまで、LINE botを作ってみよう!という趣旨です。また、実際のゲームに基づいていますので、掲載において不適切な表現等が判明した場合は、真摯に受け止め対処致します。
2.作ったもの
LINEで、相手ポケモンの名前を入力して送信する
これだけ!!!
自動で弱点を返答して教えてくれます。
またおまけ程度ですが、ゲーム画面上では2倍弱点と4倍弱点の違いがないので
(どちらも【効果ばつぐん】の表記で2倍なのか4倍なのかは分からない。)4倍弱点のタイプがあるときはメッセージで分かるようにしました。
シンプルですが、自分も含めた対戦初心者にとってはちょっとは使えると思えるものになりました。
言語については、php(Laravel)を使用しています。
3.なぜ作ったのか
シンプルな答えが欲しいと思ったから。
ポケモン対戦初心者勢の私がまず知りたいと思ったのはタイプ相性でした。
攻略サイトにはタイプ相性が記載されているのですが、とくせい、性格などタイプ相性以外など様々な情報が載っています。
初心者にとって対戦中には情報が多すぎて迷ってしまい判断を鈍らせるので、シンプルに弱点だけ教えてくれるものもあっていいかと思って製作しようと思いました。
LINE Messaging APIを使おうと思った理由
・ポケモンは数が多いので、チェックボックス等の選択肢から選ぶ方式だと時間がかかりすぎる(入力した方が早い。)
・対話式なので答えを知るにはうってつけである。
・単純にLINE Messaging APIを使ってみたかった。Laravelで製作した理由
今回はDBを用意していないし、単純に入力された値を元に返すだけなのでわざわざLaravelに書く必要はなかったかもしれない。
理由としては、
・DB持った時に対応できそう(ポケモンの追加など)
・UIの管理画面上で色々できそう。(将来的に)
・アクセストークンなど退避して書く事に慣れている。API通信が安全に行える。
・配列を多く扱うと思ったので、使い慣れているPHPを使おうと思った。
・デバッグがしやすい。辺りが理由でした。
LINE Messaging APIで他の方が作られているのを見ると、JSで書いている方が多い印象を受けましたが、今回はPHPで書くことにしました。
4.製作手順
4-1. LINE Developersに登録する。
まず、LINE Developersに登録します。
LINEに既に使っている場合は、登録した際のアカウントでDevelopersにも登録できます。
特に理由がなければ、使っているアカウントでそのまま登録していいかと思います。
LINEのBot開発 超入門(前編) ゼロから応答ができるまで
https://qiita.com/nkjm/items/38808bbc97d6927837cdこちらの記事を参考しました。
頻繁にUIが変わっていますが、必要な情報は
・チャネルシークレット
・チャネルアクセストークン(長期)
の二つです。後でWebhookの利用を設定するのでログインしたままにしときます。
4-2. Laravel プロジェクトの立ち上げと初期設定
command$laravel new laravel-project名前は自由で。
.envファイルの設定
envは隠しファイルなので普通は見れないです。
macの場合はcommand + shift + . を押すと表示されます。
envファイルを開いて、4-1で記載されたLINEの情報を記述します。
LINE_CHANNEL_SECRET = "チャネルシークレット" LINE_ACCESS_TOKEN = "チャネルアクセストークン"config.servicesに登録
envファイルを直接参照してもいいのですが、本番用と環境用でenvに書いてある設定(DB接続情報など)が違う場合は不便なので、config.servicesからenvの内容を参照する様にします。
config.servicesreturn [ //省略 'line' => [ 'access_token' => env('LINE_ACCESS_TOKEN'), 'channel_secret' => env('LINE_CHANNEL_SECRET'), ], ]後でコントローラから上記を参照する様にします。
4-3.LINE Messaging API SDK for PHP をインストール
LINE公式から開発のためのSDK(Software Development Kit)が用意されているのでコンポーザを使ってインストールします。
commandcomposer require linecorp/line-bot-sdkインストールすることで、LINEが用意してくれたクラス・メソッドなどを使う事ができる様になります。
4-4.ルーティングを設定する
LINEからAPIで通信を受けたときに、どのコントローラに処理をお願いするかを記述します。
今回はブラウザを介さずにコントローラを呼び出すのでroutes/api.phpに記述します。
routes/api.phpRoute::post('/pokename', 'LineBotPokenameController@input_pokemon');LINEからapi/pokenameに通信を受けたときに、LineBotPokenameControllerクラスのinput_pokemonメソッドを実行しなさい。という意味になります。
4-5で実際にコントローラを作ってきます。
4-5.コントローラを作成する
commandphp artisan make:controller LineBotPokenameControllerLarabelプロジェクトのルートフォルダで上記artsianコマンドを打ちます。
ここでは、4-4で設定したルートで同じコントローラ名(LineBotPokenameController)で作成します。
開きます。
/app/Http/Controllers/LineBotPokenameController<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class LineBotPokenameController extends Controller { public function input_pokemon(Request $request) { //ここに書いていく } }こんな感じになっているかと思います。
続けて、input_pokemonメソッドに愚直に処理を書いてきます。
4-6.クラスを利用してAPIを受け取るための設定をする
LINE Messaging API SDK for PHPをインストールしたことで使えるクラスを追加します。
/app/Http/Controllers/LineBotPokenameController<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use LINE\LINEBot; //追加 use LINE\LINEBot\HTTPClient\CurlHTTPClient; //追加 use LINE\LINEBot\Event\MessageEvent\TextMessage; //追加 class LineBotPokenameController extends Controller //省略上記ではuseを使い、コントローラを呼び出します。
次にinput_pokemonメソッド内で、インスタンス化します。(下記)
その他にも、LINEから情報を受け取った際に、署名の検証などの情報も一緒に記載します。
/app/Http/Controllers/LineBotPokenameController//省略 public function input_pokemon(Request $request) { //認証を行う $lineAccessToken = config('services.line.access_token'); $lineChannelSecret = config('services.line.channel_secret'); $httpClient = new CurlHTTPClient($lineAccessToken); $lineBot = new LINEBot($httpClient, ['channelSecret' => $lineChannelSecret]); $signature = $request->header('x-line-signature'); if (!$lineBot->validateSignature($request->getContent(), $signature)) { //送信元に400エラーを伝える abort(400, 'Invalid signature'); } //LINEで入力されたメッセージ情報を受け取る $events = $lineBot->parseEventRequest($request->getContent(), $signature); } //省略最後にある$eventsの所で、LINEからの送られてきたテキスト情報を受け取って入れ込んでます。
が、今の段階では通信がうまくいきません。
LINE DevelopersのWebhookの部分にURLを記載していないからです。
Webhookの設定をしないといけないのですが、こちらはローカルPC内では作動してくれないため、インターネット上でのURLを記載しないといけません。
やり方としては、herokuなどホスティングサービスを利用し、インターネット上でのURL(https://)を取得した後に、LINE DevelopersのWebhook URLの欄にURLを記載します。
(私は、ローカル上でのURLを一時的にインターネットでのURLに変換してくれるngrokを利用しました。)
記載するURLは
https://herokuなどのURL/api/pokename
となります。うまくいくと、LINEからメッセージを送ったときに$eventsに入力したテキストの情報が入ります。
デバッグで変数$eventsを見てみます。
command[2020-08-02 20:47:55] local.DEBUG: array ( 0 => LINE\LINEBot\Event\MessageEvent\TextMessage::__set_state(array( 'emojis' => NULL, 'message' => array ( 'type' => 'text', 'id' => '12428220966824', 'text' => 'ピカチュウ', ), 'event' => array ( 'type' => 'message', 'replyToken' => 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'source' => array ( 'userId' => 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'type' => 'user', ), 'timestamp' => 1596368874886, 'mode' => 'active', 'message' => array ( 'type' => 'text', 'id' => '12428220966824', 'text' => 'ピカチュウ', ), ), )), )こんな感じで情報を受け取ってくれます。
ここまでで、設定完了です!!
後は、返信をするための処理を書いていきます。
4-7.返信するための処理をコントローラーに記述する
今回作成したものは、この様に書きました。
作りたいアプリによって自由に書いてみるといいかもです。
/app/Http/Controllers/LineBotPokenameControllerpublic function input_pokemon(Request $request) { //省略 foreach ($events as $event) { if (!($event instanceof TextMessage)) { continue; } $replyToken = $event->getReplyToken(); $replyText = $event->getText(); //エンコードを行う $replyText = mb_convert_encoding($replyText,'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN'); //Jsonを取得する $url = public_path() . '/data/double_weakness.json'; $json = file_get_contents($url); $json = mb_convert_encoding($json, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN'); $type_array = json_decode($json, true); //ポケモン情報を取得 $url = 'https://raw.githubusercontent.com/kotofurumiya/pokemon_data/master/data/pokemon_data.json'; $json = file_get_contents($url); $json = mb_convert_encoding($json, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN'); $pokemon_array = json_decode($json, true); //入力チェック if(in_array($replyText, array_column($pokemon_array, 'name'), true)) { //該当するポケモンがいる場合 $pokemon_data = $pokemon_array[array_search($replyText, array_column($pokemon_array, 'name'))]; //タイプを入れる $type1 = $pokemon_data['types'][0]; //単タイプかどうか if(isset($pokemon_data['types'][1])){ $type2 = $pokemon_data['types'][1]; }else{ $type2 = 'mono_type'; $sp_weak = ""; } //タイプ判定 foreach($type_array as $key1 => $value1){ $array_list = $value1; //タイプ1 $type_match1 = array_filter($array_list, function($element) use($type1) { //必ずある前提なので返すだけ return $element['type'] == $type1; }); //該当するものを取得 $type_a = array_column($type_match1, 'double_weakness_type'); //タイプ2 if ($type2 === 'mono_type'){ $type_match = $type_a[0]; }else{ $type_match2 = array_filter($array_list, function($element) use($type2) { return $element['type'] == $type2; }); //該当するものを取得 $type_b = array_column($type_match2, 'double_weakness_type'); //複合弱点をマージ $type_match = array_merge($type_a[0],$type_b[0]); //4倍弱点 $sp_weak = array_intersect($type_a[0],$type_b[0]); //2倍弱点 $type_match = array_diff($type_match,$sp_weak); } //重複削除 $type_match = array_unique($type_match); $type_match = array_values($type_match); }; }else{ //ポケモン以外の入力の場合 $type_match = array("no_much"); } //変数宣言 $weak_text = ""; $spweak_text = ""; //タイプ以外の入力がされてきた場合 if(in_array("no_much", $type_match)){ $weak_text = "ポケモンのなまえを入力してね"; }else{ for($i = 0; $i < count($type_match); $i++) { $weak_text = $type_match[$i].",".$weak_text; } if($sp_weak){ $weak_text = $weak_text. "\n \n4倍弱点だよ \n".$sp_weak[0]; } } //LINEへ送信する $lineBot->replyText($replyToken, $weak_text); } }いくつかポイント
・ポケモンのデータはgithub上でJSONデータを配布されている方から拝借しました。
https://github.com/kotofurumiya/pokemon_data・通常の2倍弱点を一覧にしたJSONデータをLaravelのパブリックに置いて情報を取得しています。基本的には入力されたポケモンの弱点と、JSONで取得したデータを比較してるだけです。
・配列を多用しているので、加工したり制御する関数を多く使っています。
(もっといい書き方があると思う。)・ポケモン以外の名前が入力されて来た時、タイプが一つだけのポケモン、複合タイプのポケモンの3パターンがあると想定して書いています。
5.実際に使ってみて
使いやすかった!! LINEめっちゃ便利!!
画面に相手の名前が書いてあるので入力に迷うことはないし、限られた時間の中で素早く弱点が分かるのは結構便利だった。
まあ、勝てるかどうかは別として。
・・・対戦初心者には十分活用できるかなと思いました!!!
6.反省点
・例外処理(仮にJSONを取得できなかったときに処理が止まる)を直さないといけない。
・一応自分としてはやりたい事ができたが、書き方の部分では甘い部分があるはずなので満足しない。
・これだけだったらあまりLaravelで書く意味がないので、DBを持ったり一覧表示や修正をUI上で行える様にすれば多少は意味が出てくるかもしれないです。
7.最後に
Line Messaging APIはリファレンスも丁寧な日本語で分かりやすくて、始める壁も低く感じました。
あとは、自分で使って便利なだと思って制作できたので楽しく作れました。
Qiitaとかも結構参考になる記事が多くて助かったので、外部APIを使ったアプリ作成のとっかかりとしてもLINEはいいと思います!!おすすめ!!
アイデア次第で他にも結構やれることもありそうなので、思いついたらまた何か作ってみようと思います!
- 投稿日:2020-08-02T17:16:01+09:00
LaravelにSassを入れて好きなCSSフレームワークを使う
準備
- 導入したいCSSフレームワークのSassのソースフォルダをダウンロードしておきます。
- Laravel標準のBootstrapではなく、どうしてもMaterializeを使ってみたかったのでこれを導入します。
環境
- Laravel Framework 7.22.4
- Materialize 1.0.0
Sassを使えるようにする
Sassの導入は他の方が解説されていたので、こちらを参考にしました。
LaravelにSCSSの導入方法フレームワークを導入する
npmをインストールしたら、プロジェクトフォルダ直下に作られるwebpack.mix.jsを開きます。
webpack.mix.jsmix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css');この.sass~の部分でwebpackが
resources/sass/app.scss
ファイルをpublic/css/app.css
にコンパイルしてくれるということなのでコンパイル元の
resources/sass/app.scss
を見てみます。resources/sass/app.scss// Fonts @import url('https://fonts.googleapis.com/css?family=Nunito'); // Variables @import 'variables'; // Bootstrap @import '~bootstrap/scss/bootstrap';一番下でBootstrapを読み込んでいるようです。
インポート元のパスはプロジェクトフォルダの
node_modules/bootstrap/scss/bootstrap
となっていました。同じようにして
node_modules
フォルダにダウンロードしたフォルダ(今回はmaterialize-src)を入れます。そしてパスを変更します。
esources/sass/app.scss// Materialize @import '~materialize-src/sass/materialize.scss';あとは
npm run dev
でコンパイルしてあげれば変更が適用されます。念の為、コンパイル先を確認します。
public/css/app.css@import url(https://fonts.googleapis.com/css?family=Nunito);@charset "UTF-8"; .materialize-red { background-color: #e51c23 !important; } .materialize-red-text { color: #e51c23 !important; } .materialize-red.lighten-5 { background-color: #fdeaeb !important; ~省略~コンパイルされていました。
最後に、CSSを適用させたいビューテンプレートの中に
<link href="{{ asset('/css/app.css') }}" rel="stylesheet">
を書いてあげれば完成です。
- 投稿日:2020-08-02T17:16:01+09:00
Laravelで好きなCSSフレームワークを使う
準備
導入したいCSSフレームワークのSassのソースフォルダをダウンロードしておきます。- ソースフォルダのダウンロードではなく、npmなどのパッケージマネージャでのインストールが間違いが起きないのでおすすめです。
- Laravel標準のBootstrapではなく、どうしてもMaterializeを使ってみたかったのでこれを導入します。
環境
- Laravel Framework 7.22.4
- Materialize 1.0.0
フレームワークを導入する
npmをインストールしたら、プロジェクトフォルダ直下に作られるwebpack.mix.jsを開きます。
webpack.mix.jsmix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css');この.sass~の部分でwebpackが
resources/sass/app.scss
ファイルをpublic/css/app.css
にコンパイルしてくれるということなのでコンパイル元の
resources/sass/app.scss
を見てみます。resources/sass/app.scss// Fonts @import url('https://fonts.googleapis.com/css?family=Nunito'); // Variables @import 'variables'; // Bootstrap @import '~bootstrap/scss/bootstrap';一番下でBootstrapを読み込んでいるようです。
インポート元のパスはプロジェクトフォルダの
node_modules/bootstrap/scss/bootstrap
となっていました。
同じようにしてnode_modules
フォルダにダウンロードしたフォルダ(今回はmaterialize-src)を入れます。
【訂正】
パッケージマネージャでのインストールをおすすめします。
materializeの場合は以下です。npm install materialize-css
そしてパスを変更します。
esources/sass/app.scss// Materialize @import '~materialize-src/sass/materialize.scss';あとは
npm run dev
でコンパイルしてあげれば変更が適用されます。念の為、コンパイル先を確認します。
public/css/app.css@import url(https://fonts.googleapis.com/css?family=Nunito);@charset "UTF-8"; .materialize-red { background-color: #e51c23 !important; } .materialize-red-text { color: #e51c23 !important; } .materialize-red.lighten-5 { background-color: #fdeaeb !important; ~省略~コンパイルされていました。
最後に、CSSを適用させたいビューテンプレートの中に
<link href="{{ asset('/css/app.css') }}" rel="stylesheet">
を書いてあげれば完成です。番外編:Sassを使う
導入時に一緒にSassも入れたのでわかりやすかったページのリンクを貼っておきます。
LaravelにSCSSの導入方法
- 投稿日:2020-08-02T16:33:15+09:00
【PHP/Laravel】マイグレート時に発生したエラー解決
はじめに
Laravelでアプリ開発をしております。
環境構築が完了し、いざマイグレートをする段階で今回のエラーが発生しました。
備忘のために記録しております。
同じような方の助けになれば幸いです。開発環境
・MacOS:10.14.6 (Mojave)
・PHP7.3
・Laravel6発生したエラー
$ php artisan migrate ************************************** * Application In Production! * ************************************** Do you really wish to run this command? (yes/no) [no]: > yes Illuminate\Database\QueryException : SQLSTATE[HY000] [1044] Access denied for user ''@'localhost' to database 'forge' (SQL: select * from information_schema.tables where table_schema = forge and table_name = migrations and table_type = 'BASE TABLE') at /Users/ユーザーネーム/projects/stg/8001-laravel/phpsample/vendor/laravel/framework/src/Illuminate/Database/Connection.php:669 665| // If an exception occurs when attempting to run a query, we'll format the error 666| // message to include the bindings with SQL, which will make this exception a 667| // lot more helpful to the developer instead of just the database's errors. 668| catch (Exception $e) { > 669| throw new QueryException( 670| $query, $this->prepareBindings($bindings), $e 671| ); 672| } 673| Exception trace: 1 PDOException::("SQLSTATE[HY000] [1044] Access denied for user ''@'localhost' to database 'forge'") /Users/ユーザーネーム/projects/stg/8001-laravel/phpsample/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70 2 PDO::__construct("mysql:host=127.0.0.1;port=3306;dbname=forge", "forge", "", []) /Users/ユーザーネーム/projects/stg/8001-laravel/phpsample/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70 Please use the argument -v to see more details.解決方法
発生したエラーより、指定してるデータベースにアクセスができないとのこと。
これは「/config/database.php」の修正してやることで、指定できます。
私の場合は以下の「修正」あたりをMAMPの設定に合わせました。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', 'php_sample_db'), /*修正*/ 'username' => env('DB_USERNAME', 'root'), /*修正*/ 'password' => env('DB_PASSWORD', 'root'), /*修正*/ 'unix_socket' => env('DB_SOCKET', '/Applications/MAMP/tmp/mysql/mysql.sock'), /*修正*/ 'charset' => 'utf8', /*修正*/ 'collation' => 'utf8_general_ci', /*修正*/ 'prefix' => '', 'prefix_indexes' => true, 'strict' => false, /*修正*/ 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], ],
これで再度マイグレートを実行すると、無事に通りました。$ php artisan migrate ************************************** * Application In Production! * ************************************** Do you really wish to run this command? (yes/no) [no]: > yes Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (0.04 seconds) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (0.02 seconds) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (0.01 seconds) Migrating: 2020_08_02_063050_create_categories_table Migrated: 2020_08_02_063050_create_categories_table (0.01 seconds) Migrating: 2020_08_02_063128_create_shops_table Migrated: 2020_08_02_063128_create_shops_table (0.01 seconds)参考文献
以下の記事を参考にさせていただき、解決できました。
ありがとうございました。本件に限らず、見やすいですし理解もしやすいのでオススメです。
- 投稿日:2020-08-02T13:22:10+09:00
Laravel PassportのOAuth2認証をやる
Laravel Passportを使ったPKCEを使った認可コード引き換えをするOAuth2認証をやってみよう!
ノリで先行してやってしまって、あとから備忘録的に書いているので間違っている箇所があるかもしれません。使ったもの
Laravel 7.22.2
Docker Desktop for Windows (on WSL2)またフロントエンドにはNuxtをつかいました。
ユースケースとしてはLaravelをWeb APIサーバに、NuxtをWebクライアントに、という形ですが応用は効くかと思います。Laravelの準備
インストールや環境構築は飛ばします。
認証機能でLaravel Mixを使うのでnpmコマンドを使えるように仕込む必要があります。認証機能を準備する
認証 7.x Laravelに従ってコマンドをポチポチしたりします。
ルート定義
composer require laravel\ui php artisan ui vue --auth2行目のコマンドをタイプすると「
npm install
とnpm run dev
よろしく~」みたいなメッセージがでるのでやっておきましょう。npm install npm run dev
Passportの準備
Laravel PassportはLaravel公式パッケージのひとつでOAuth2.0認証をLaravelで手早く行えるようになるものです。
インストール
composer require laravel/passportインストールするとマイグレーションされるテーブルが増えるので
php artisan migrate
します。
その後、認証に使われたりする暗号キーをつくるためにphp artisan passport:install
をします。php artisan migrate php artisan passport:install
プロダクション環境にデプロイする時には...
開発環境でPassportを使いつつ開発をして、本番環境でデプロイした際、暗号キーの生成だけ必要なことがあると思います。暗号キーは
.gitignore
でバージョニングされないよう指定されているし、外部へ公開されるべきでなく、プロダクション環境ではプロダクション環境用の暗号キーが必要になるでしょう。その場合は以下のコマンドをタイプすることで暗号キーの生成のみを行うことができます。
php artisan passport:keys
User
、AuthServiceProvider
、config/auth.php
の書き換えUserモデルで
HasApiTokens
トレイトを使うように追記してやります。<?php namespace App; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Passport\HasApiTokens; class User extends Authenticatable { use HasApiTokens, Notifiable; }そして
AuthServiceProvider
のboot
メソッドにPassportの機能がルーティングされるように処理を追加します。<?php namespace App\Providers; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; use Laravel\Passport\Passport; class AuthServiceProvider extends ServiceProvider { /** * アプリケーションのポリシーのマップ * * @var array */ protected $policies = [ 'App\Model' => 'App\Policies\ModelPolicy', ]; /** * 全認証/認可サービスの登録 * * @return void */ public function boot() { $this->registerPolicies(); Passport::routes(); }最後に
config/auth.php
を書き換えて、API認証にPassportが使われるように仕向けます。'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'passport', // <- これ 'provider' => 'users', ], ],Passportクライアントをつくる
artisan
でつくります。
この時--public
オプションを設定します。
Laravelはそもそも普通のWebアプリケーションを構築するフレームワークであるため、Passport自体もWebアプリケーション間で認証をし合うように設計されているものです。認証のやりとりに
client_secret
を利用することになるのですが、Web APIを利用するクライアントにclient_secret
を配信してしまってはマズいため、--public
オプションを付け、クライアント-サーバ間でclient_secret
を送受信することなくトークンを受け取れるようにします。(PKCEを使った認可コードグラントというらしい)php artisan passport:client --public
このコマンドを実行すると対話形式でセットアップがすすみます。
どのユーザに関連づけるか、クライアントの名前は何か、認証後のリダイレクト先はどこかと聞かれるので、適切な値を入れましょう。
認証後のリダイレクト先は後程作ります。クライアントの然るべきページのURIを設定してください。クライアントの名前は後で晒されるので、ストレスが溜まっていたとしてもほどほどの名前をつけてあげましょうね?
クライアントが作成されると
Client ID
とClient Secret
が出ますが、Publicクライアントの場合Client Secret
が空欄になっています。
Client ID
は後々使うので控えておきましょう!
忘れちゃった場合はもう一度Passportクライアントを作り直すか、データベースに格納されている値を見ちゃいましょう。CORSの設定
Laravelサーバとクライアント開発機内だったとしても、ポートが異なる場合はオリジンをまたぐことになるのでCORSを設定する必要があります。
LaravelのCORSについてはあまり深く理解できていないので、説明はこのリンク先にお任せします...
CORS を許可する - LarapetLaravelの
config/cors.php
のpaths
の値を変えます。
デフォルトではapi/*
のみが設定されており、APIルート全体でオリジンをまたぐ通信を許すようになっています。
これにoauth/token
を追加してください。クライアントでは認可サーバでのユーザ認証後、クライアントにリダイレクトされ、トークンとの引き換え用コードが渡されます。
この引き換えコードとトークンを引き換えるときにオリジンをまたぐリクエストを送信することになるので、このレスポンスを読むためにこの設定が必要です。
ここまで来たらLaravelの設定は終わりです。
クライアントの準備
Nuxtを使ったのでNuxtで説明していきます。
パスワード・メールアドレスの入力フォームはLaravel側のものを使います。
入力フォームのあるページへクライアントからリダイレクト、Laravelで認証後にリダイレクトでクライエントへ返される、という流れです。ページをつくる
認証をするのに最低限2つのページが必要です。
1つ目はページを認可サーバのフォームへ置き換えるもの、2つ目は認証後に認可コードとトークンの引き換えを行うものです。
例として前者をlogin.vue
、後者をauthenticated.vue
としてpages/
ディレクトリ配下に追加します。
login.vue
<template> </template> <script lang="ts"> import {Component, Vue} from "nuxt-property-decorator"; @Component export default class login extends Vue { async mounted() { const state = createRandomString(40); const verifier = createRandomString(128); const challenge = await AuthCode.makeCodeChallenge(verifier); sessionStorage.setItem('state', state); sessionStorage.setItem('verifier', verifier); location.replace(`http://localhost:8080/oauth/authorize?${this.transformQueryString({ 'client_id': '2', 'redirect_uri': 'http://localhost:3000/authenticated', 'response_type': 'code', 'scope': '', 'state': state, 'code_challenge': challenge, 'code_challenge_method': 'S256' })}`); } transformQueryString(obj: any) { let str = ''; for (const it in obj) { str += `${encodeURIComponent(it)}=${encodeURIComponent(obj[it])}&`; } return str.slice(0, -1); } hash(message: string){ const encoder = new TextEncoder(); const data = encoder.encode(message); return crypto.subtle.digest('SHA-256', data); } async makeCodeChallenge(verifier: string) { const encoded = btoa(String.fromCharCode(...new Uint8Array(await this.hash(verifier)))); return encoded .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, '') } createRandomString(num: number){ return [...Array(num)].map(() => Math.random().toString(36)[2]).join(''); } } </script> <style scoped> </style>
state
とcode_verifier
を作り、後程ページ内で読める場所に保管します。
これを使う機会は認可後のリダイレクトを受けた時だけなのでsessionStorageが最適かと思います。
state
はランダムな文字列を指定します。Passportの例が40文字だったのでそれに倣って40文字の適当な文字列を入れました。
code_verifier
はRFC 7636仕様で決められているのだそう。43文字から128文字の文字、数字、それと4種の記号を含んだランダムな文字列である必要があるそうです。
が、Passportの例ではStr::random(128)
を使っていたので、少々いい加減ですがjsで似たような感じにしました。
code_challenge
はcode_verifier
のハッシュ値を作り、これをBase64エンコードしたものになります。Passportの例はこんな感じです。
$encoded = base64_encode(hash('sha256', $code_verifier, true)); $codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');認可後のコード引き換え時に
code_verifier
をサーバ側でこの処理をしたものとクライアントが生成したcode_challenge
が比較されるのでこれと同じ動きをするものをクライアント側で書かなくてはなりません。jsだとこんな感じです。
SHA256ハッシュ値生成を文字列にせずArrayBuffer
を保つのがポイントです。// ハッシュ値をつくる const encoder = new TextEncoder(); const data = encoder.encode(message); const hashed = await crypto.subtle.digest('SHA-256', data); // <- DOMが持つ関数ですが、Promiseが返ります // base64エンコード const encoded = btoa(String.fromCharCode(...new Uint8Array(await this.hash(verifier)))); const codeChallenge = encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');これで下ごしらえは完了です。
location.replace()
でページを置き換えます。
location.replace()
を使うことで置き換え後のページはブラウザの履歴に残らず、このページで生成するいろいろな値が用意されない状態で認可サーバへ飛んでしまうことを防ぎます。クエリパラメータとして以下のものが必要です。
{ 'client_id': '1', // <- Passportクライアントを作ったときに表示されたもの 'redirect_uri': 'http://localhost:3000/authenticated', // <- Passportクライアントを作ったときに指定したもの 'response_type': 'code', 'scope': '', // <- 空だよ Laravelの設定で認証ユーザの権限が表現できるみたい 'state': '...', // <- 先ほど作ったランダムな文字列 'code_challenge': '...', // <- 先ほど作ったハッシュ化とかエンコードとかしたやつ 'code_challenge_method': 'S256' }
client_id
はPassportクライアント作成後に表示された数字を指定してあげます。
redirect_uri
はPassportクライアントをコマンドで作ったときに入力したURIを入力します。
これが一致しないと認可されません。リダイレクトして処理を続けることで認証に必要だけど漏れてしまったらマズい情報を漏らさずに認証ができる、というわけです。
authenticated.vue
<template> <div> 引き換えコードをもらった <p v-if="!!token"> {{ token }} </p> </div> </template> <script lang="ts"> import {Component, Vue} from "nuxt-property-decorator"; import axios from 'axios'; @Component export default class authenticated extends Vue { token: string | null = null; mounted() { const state = sessionStorage.getItem('state'); const verifier = sessionStorage.getItem('verifier'); const code = this.$route.query.code; if (state != this.$route.query.state) { console.error('ステートが違う'); return; } axios.post('http://localhost:8080/oauth/token', { grant_type: 'authorization_code', client_id: 2, redirect_uri: 'http://localhost:3000/authenticated', code_verifier: verifier, code: code }).then(({data}) => console.log(data)) } } </script> <style scoped> </style>
login.vue
でsessionStorageへ保存しておいたstate
とcode_verifier
を取り出します。
そして、リダイレクトURIのクエリパラメータに認可コードが含まれてますのでcode
パラメータを参照し、取り出します。次に、認可コード引き換えの規約としてクライアント側で
state
を検査する必要があるとのことなので、クエリパラメータのstate
とsessionStorageから取り出したstate
が一致するか検証します。そして、認証処理の大詰め、認可コードとトークンの引き換えを行います。
Laravelサーバのoauth/token
へPOSTリクエストを送信します。必要なパラメータは以下の通り。
{ grant_type: 'authorization_code, client_id: 1, // <- Passportクライアントを作ったときに表示されたもの redirect_uri: 'http://localhost:3000/authenticated', // <- Passportクライアントを作ったときに指定したもの code_verifier: verifier, // <- sessionStorageから取り出したverifier code: code // <- クエリパラメータから取り出したcode }リクエストをサーバに送信するとCORSになるので、まずプリフライトがされ、航路が安全か確認されます。
そのあと、トークンのリクエスト本体が飛んで、トークンがレスポンスされます。ここでCORSに引っかかったり、
code_verifier
がなんのといわれることがありますが、エラーレスポンスがしっかりしていて、原因の断定がしやすいかと思うので、頑張って解決しましょう!うごかす
クライアントの
login.vue
がマッピングされてるところにアクセスしてみます。
そしたらLaravelのログインページへリダイレクトされたような感じになって、ログインするとOAuthらしさのある「アクセスしちゃう?する?」みたいな画面になって、「Authorize」をクリックするとクライアントに戻ってきて、トークンが獲得できると思います。
以上Laravel Passportを使ったPKCEを使った認可コード引き換えをするOAuth2認証でした!!
Web APIの認証をひとつステップアップさせましょう!見たもの
CORS を許可する - Larapet
PHP: hash - Manual
SubtleCrypto.digest() - Web API | MDN
JavaScriptでバイナリを扱いたい - Qiita
sessionStorageをつかってみる - Qiita
- 投稿日:2020-08-02T02:27:25+09:00
バリデーションの検証ツールについて簡単にまとめてみた!(テキストのみ)
accept
- 主にチェックボックスに使う。
- true、on、yes、1の値かどうか判断できる。
- チェックされているかどうかの処理を実行できる。
active_url、url
active_urlはアドレスで指定されたドメインが実際に有効かどうかをチェックできる。(dns_get_record関数でDNS情報を取得し、有効なIPアドレスかどうかをチェック)
- https://www.php.net/manual/ja/function.dns-get-record.php (dns_get_record関数については公式リファレンス参照)
urlはurlの形式で書かれているかどうかをチェック。
after: 日付、 after_or_equal: 日付
- afterは指定した日付よりも後かどうかをチェックする。
- after_or_equalは指定した日付と同じかそれよりもあとであるかチェックできる。
(これらは「:」の後に日付を表す文字列をつけて利用します。あるいは、日付を入力する他のフィールド名を指定することもできる。)
before: 日付、 before_or_equal: 日付
- beforeは指定の日付より前かどうかをチェックできる。
- before_or_equalは指定の日付と同じかそれより前であるかをチェックできる。
これらはafterの反対です。
日付についてはafterと同じです。alpha、alpha-dash、alpha-num
- alphaは入力したテキストが全て「A~Z(a~z)」であるかチェックできる。
- alpha-dashは「A~Z(a~z)」と「-」と「_」であるかチェックできる。
- alpha-numは「A~Z(a~z)」と数字であるかをチェックできる。
array
- フィールドが配列となっているかどうかをチェックできる。
between: 最小値, 最大値
- 数値のフィールドで用いる。
- 値が指定の範囲内かをチェックできる。
- betweenの後には、最小値と最大値を「,」で区切って記述しておく。
boolean
- 値が真偽値かチェックできる。
- true、false、0、1といった値であれば判別できる。(それ以外は不可)
date、date_format: フォーマット
- dateは入力されたテキストが日時の値として扱えるものかをチェックできる。これはstrtotime関数でタイムスタンプに変換できればOKです。(https://www.php.net/manual/ja/function.strtotime.php strtotime関数については公式リファレンスを参照)
- date_formatは、入力された値が指定フォーマットの定義に一致しているかどうかをチェックします。フォーマットに沿った形式ならばOKです。
different: フィールド、 same: フィールド
- 指定されたフィールドと同じ値かどうかをチェックできる。
- differentは異なる値であればOKです。
- sameは反対に同じ値ならばOKです。
digits: 桁数、 digits_between: 最小桁数, 最大桁数
- 数値で使う。
- 入力された値が指定された桁数かチェックできる。
- digitsは指定の桁数ならばOKです。
- digits_betweenは最小桁数〜最大桁数の範囲内であればOKです。
distinct
- 配列として用意されている項目で使う。
- 配列内に同じ値がないかチェックできる。(同じ値が複数あった場合不可)
- 電子メールアドレスの形式かチェックできる。
- これは形式をチェックするだけで実際にそのアドレスが使えるかはチェックしない
exists: テーブル, カラム
- データベースを利用する場合に使われる。
- 入力された値が指定のデータベースの指定のカラムにあるかどうかをチェックできる。(あればOK、なければ不可)
filled、 required
- filledはその項目が空でないかチェックできる。(入力されていれば不可)
- requiredはそれが必須項目であることを示す。(入力されていればOKです)
image
- ファイルのフィールドで指定します。
- 指定されたファイルがイメージファイルかどうかをチェックできる。
in: 値1, 値2, ・・・・・、not_in: 値1, 値2, ・・・・・
- inは入力された値がin: ・・・・・に用意した値に含まれているかチェックできる。
- not_inは逆です。
integer、 numeric
- integerは値が整数であることをチェックできる。
- numericは値が数値であることをチェックできる。
ip、 ipv4、 ipv6
- 値がIPアドレスかどうかチェックできる。
- ipv4はIPアドレスの中でipv4かどうかチェックできる。
- ipv6はIPアドレスの中でipv6かどうかチェックできる。
json
- 値がjson形式の文字列かどうかをチェックできる。
min: 値、 max: 値
- 値が指定よりも大きいか小さいかをチェックできる。
- minは指定の値よりも小さい、maxは大きいかをチェックできる。
- 最大か最小かを調べる場合は前述したbetweenを使う。
regix: パターン
- 指定した正規表現パターンにマッチするかチェックできる。
size: 値
- 値の大きさをチェックできる。
- 文字列ならば文字数、数値の場合は整数値、配列の場合は要素の数をチェックできる。
string
- 文字列の値かどうかをチェックできる。
unique: テーブル, カラム
- データベース利用の際に用います。
- 指定のテーブルの指定のカラムに同じ値が存在しないかチェックできる。
学習のまとめとして書いてみました。
もし間違いや記述が古いなど指摘をいただけますと幸いです。
- 投稿日:2020-08-02T01:13:27+09:00
【Laravel】定義した「名前付きルート」からのURL生成方法
名前付きルートへのURLを生成するには
ルート定義ファイル
routes/web.php
に名前付きルートの設定をしていれば、その名前をグローバルなroute
関数を使用することで、URLを生成したり、設定した名前のルートへリダイレクトできたりします。例// URLの生成 $url = route('profile'); // リダイレクトの生成 return redirect()->route('profile');上記のように、コントローラ内とかで利用可能です。
ルート定義で名前付きルートをするだけで、リダイレクトの文が、とても見やすくなります。ぜひ活用してみましょう。パラメータも付け加えて、URLを生成することもできる
ちなみに、名前付きルートを設定したルートで、なおかつ、パラメータも定義してある場合は、
route
関数の第2引数にパラメータを渡すことができます。routes/web.phpRoute::get('user/{id}/profile', function ($id) { // })->name('profile');上記のように、ルート定義で
profile
という名前を指定のルートにつけてHogeController.php$url = route('profile', ['id' => 1]); // $urlには「 /user/1/profile 」コントローラでパラメータを渡してあげて、URLを生成することができます。