20200802のPHPに関する記事は16件です。

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.php
    public 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で
さしすせそ
と入力し、送信ボタンをクリックします

a.png

動きました

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

ニートがPHPで求人サイト(Webアプリ)を作ってみた

技術記事ではなく、「作ったものを共有する」ライトな記事です。

経緯

小学5年生でゲームにハマる

ニートになって暇人

アプリ作ってみよう、みたいな

公開する理由

頑張って勉強されている方に少しでも参考になればいいなと思ったからです。
また、リリース後はしっかり運用・保守していく予定なので、エンジニアさんからフィードバックをもらえるかも?という下心もあります:rolling_eyes:

作ったもの

地元「釧路」限定の求人サイトです。
※釧路:北海道の避暑地であり、魚が美味しい海沿いの町

■サイト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)にメール送信のキューを保存するプロセスを管理しています。

プロセス管理ツールを使うことで、キューを保存するためのコマンドを自動で実行してくれるので、わざわざサーバーに入って手動でコマンド打つ必要がなく超絶便利でした!:sunglasses:

■参考にさせていただいた記事
https://reffect.co.jp/laravel/laravel-ubuntu-supervisor
https://docs.docker.jp/engine/admin/using_supervisord.html

②リポジトリパターンを採用

同じようなデータ操作のロジックを一箇所にまとめるために採用しました。

リポジトリパターンを用いることで、ビジネスロジックからデータ操作に関するロジックを切り離して、(データ操作を)抽象化したレイヤに任せることで拡張性・保守性の向上に努めました。

初見は概念が複雑すぎてぶっちゃけ理解に苦しみました。
しかし、ドキュメントや記事(Qiita、その他ブログ)を参考にしながら、手を動かして実装を続けていくと、少しずつ点と点が繋がっていきました:grin:

■参考にさせていただいた記事
https://www.ritolab.com/entry/165
https://qiita.com/bmf_san/items/c8d7b38b5f1f5747c2fd

③テスト駆動開発(TDD)

※あくまでリポジトリのみです

コントローラーからリポジトリを使用する時に「エラーが出る→修正→エラーが出る→修正」の繰り返しにうんざりしたため、リポジトリに記述したデータ操作ロジックが正しく動作するかどうかをテストしながら開発していきました。

しかし、TDDの知見はかなり浅く、本格的に導入するには勉強不足というのが本音。

体系的に網羅されている情報を吸収して実践していく必要があると感じています:sob:

■参考にさせていただいた記事
https://www.ritolab.com/entry/168
https://qiita.com/bmf_san/items/c8d7b38b5f1f5747c2fd

課題

以下、3つ書きました。
正直、たくさん課題が湧き出てくるので挙げたらキリがないです。

機能が物足りない
・検索項目の数が少ない
・レコメンド機能が微妙すぎる
・求職者と採用担当者間でチャットを導入したい

という欲求不満をどうにかしたいです。

また、他サービスを参考にしつつ、どうやればオリジナリティやユーザビリティが向上するのか?も分析していきたい。

結合テストが書かれていない
実際にアプリを動かしてテスト・デバッグしてますが、執筆時点で結合テストが全く書かれていない状況です。バグ温床の回避やテストへの理解を深めるためにも、早急に取り掛かるべき問題と思っています。

また、今後は「機能追加とテストは必ずセット」というルールで、開発に取り込んでいきます。..あたりまえ?

インフラをコードで管理したい

ニートでお金がないので、開発中はAWS環境を常に維持できません。
使い終わったリソースは削除→変更を確認するためにリソース作成→削除→...

AWSコンソールでテンプレの如く毎回ポチポチするのは億劫なので、巷で噂のterraformを検討しています。

まとめ

筆者は、Webアプリ実務経験なしのニート、つまり社会のゴ*であり、

・エンジニアを目指して勉強されている方
・すでにエンジニアとして就労されている方

にとって有益かは分かりません。
ただ、少しでも参考になれば嬉しいです。

間違っていることをご指摘いただけたり、フィードバック・アドバイスをいただけますと幸いです:mask:

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

LINE Messaging APIを使って、ポケモンの弱点を教えてくれるBotを作る【Laravel・初心者向け】

1.はじめに

先日、久しぶりにポケモンの対人対戦をやっていたときに、

「相手(ポケモン)の弱点何???」

といった疑念を抱き、対戦時間と戦略を見失いモチベーションが下がってしまいました。

見た目だけだとタイプが分からないポケモンもいるし、タイプが2つあるポケモンもいるとなると覚えられない、、と悩んでました。

これって対戦初心者にありがちでは・・・

と思ったのがきっかけで、これを解決したいと思い

簡単・気軽・素早く弱点を知れるもの作ってみよう!!

と考えて実際に作ってみました。

本記事は、以下のようなコンセプトの記事になります。

・製作の経緯と意図

・簡単な製作の手順とコードのポイントの説明

・初心者向け(今後外部APIを使用したいと考えている方)

といった記事になります。

注意
実際には、とくせいや環境変化によってバトル戦略は変化すると思うので、タイプ相性だけではガチ対戦者さんにとっては不向きかと思います。あくまで、LINE botを作ってみよう!という趣旨です。

また、実際のゲームに基づいていますので、掲載において不適切な表現等が判明した場合は、真摯に受け止め対処致します。

2.作ったもの

circleAnimationMuvie.gif

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.services
   return [
      //省略
      '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)が用意されているのでコンポーザを使ってインストールします。

command
composer require linecorp/line-bot-sdk

インストールすることで、LINEが用意してくれたクラス・メソッドなどを使う事ができる様になります。

4-4.ルーティングを設定する

LINEからAPIで通信を受けたときに、どのコントローラに処理をお願いするかを記述します。

今回はブラウザを介さずにコントローラを呼び出すのでroutes/api.phpに記述します。

routes/api.php
Route::post('/pokename', 'LineBotPokenameController@input_pokemon');

LINEからapi/pokenameに通信を受けたときに、LineBotPokenameControllerクラスのinput_pokemonメソッドを実行しなさい。という意味になります。

4-5で実際にコントローラを作ってきます。

4-5.コントローラを作成する

command
php artisan make:controller LineBotPokenameController

Larabelプロジェクトのルートフォルダで上記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/LineBotPokenameController
   public 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はいいと思います!!おすすめ!!

アイデア次第で他にも結構やれることもありそうなので、思いついたらまた何か作ってみようと思います!

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

LINE Messaging APIを使って、ポケモンの弱点を教えてくれるBotを作る【初心者向け】

1.はじめに

先日、久しぶりにポケモンの対人対戦をやっていたときに、

「相手(ポケモン)の弱点何???」

といった疑念を抱き、対戦時間と戦略を見失いモチベーションが下がってしまいました。

見た目だけだとタイプが分からないポケモンもいるし、タイプが2つあるポケモンもいるとなると覚えられない、、と悩んでました。

これって対戦初心者にありがちでは・・・

と思ったのがきっかけで、これを解決したいと思い

簡単・気軽・素早く弱点を知れるもの作ってみよう!!

と考えて実際に作ってみました。

本記事は、以下のようなコンセプトの記事になります。

・製作の経緯と意図

・簡単な製作の手順とコードのポイントの説明

・初心者向け(今後外部APIを使用したいと考えている方)

といった記事になります。

注意
実際には、とくせいや環境変化によってバトル戦略は変化すると思うので、タイプ相性だけではガチ対戦者さんにとっては不向きかと思います。あくまで、LINE botを作ってみよう!という趣旨です。

また、実際のゲームに基づいていますので、掲載において不適切な表現等が判明した場合は、真摯に受け止め対処致します。

2.作ったもの

circleAnimationMuvie.gif

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.services
   return [
      //省略
      '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)が用意されているのでコンポーザを使ってインストールします。

command
composer require linecorp/line-bot-sdk

インストールすることで、LINEが用意してくれたクラス・メソッドなどを使う事ができる様になります。

4-4.ルーティングを設定する

LINEからAPIで通信を受けたときに、どのコントローラに処理をお願いするかを記述します。

今回はブラウザを介さずにコントローラを呼び出すのでroutes/api.phpに記述します。

routes/api.php
Route::post('/pokename', 'LineBotPokenameController@input_pokemon');

LINEからapi/pokenameに通信を受けたときに、LineBotPokenameControllerクラスのinput_pokemonメソッドを実行しなさい。という意味になります。

4-5で実際にコントローラを作ってきます。

4-5.コントローラを作成する

command
php artisan make:controller LineBotPokenameController

Larabelプロジェクトのルートフォルダで上記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/LineBotPokenameController
   public 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はいいと思います!!おすすめ!!

アイデア次第で他にも結構やれることもありそうなので、思いついたらまた何か作ってみようと思います!

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

Laravel Excel を用いてCSV出力する手順

Laravel Excelに関しての情報があまり多くないので、備忘録としてアウトプット。
手順は、
1.パッケージのインストール
2.Excel.phpの生成
3.サービスプロバイダーの登録
4.エクスポートクラスの生成
となる。

1.パッケージのインストール

composerを用いて、LaravelExcel3.1をインストールする。
必要なバージョン、PHP拡張モジュールは以下の通り。
・PHP 7.0以上
・Laravel 5.5以上
・PhpSpreadsheet 1.6以上
・php_zip、php_xml、php_gd2、php_iconv、php_simplexml、php_xmlreader、php_zlib

ルートディレクトリで以下のコマンドを実行する。

$ composer require maatwebsite/excel

インストール完了後、composer.lockを確認する。

"name": "maatwebsite/excel",
"version": "3.1.20",

ちゃんとインストールされている。

2.Excel.phpの生成

ディレクトリ直下で以下のコマンドを実行する。

$ php artisan vendor:publish --provider="Maatwebsite\Excel\ExcelServiceProvider"

・・・
Copied File [/vendor/maatwebsite/excel/config/excel.php] To [/config/excel.php]

config直下にexcel.phpが生成される。

3.サービスプロバイダとファサードの登録

サービスプロバイダーにLaravel Excelを登録していく。
config/app.phpのprovidersに追記

'providers' => [

  /*
  * Package Service Providers...
  */
  Maatwebsite\Excel\ExcelServiceProvider::class,

 ]

続いてファサードに登録する。
config/app.phpのaliasesに追記

'aliases' => [

  'Excel' => Maatwebsite\Excel\Facades\Excel::class,

]

4.エクスポートクラスを生成

エクスポートクラスを生成する。
以下のSampleの部分には、出力したいデータを持つモデル名を適宜記述する。

$ php artisan make:export SamplesExport --model=App\\Sample

app直下にExportsディレクトリ、Exports直下にSampleExport.phpが生成される。

実際に出力してみる

コントローラーにexportメソッドを追加する。

public function export()
{
  return Excel::download(new SamplesExport, 'samples.csv');
}

ルーティングを追加する。

Route::get('export','SampleController@export');

設定したURLにアクセスすることで、出力されるはずです。

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

LaravelにSassを入れて好きなCSSフレームワークを使う

準備

  • 導入したいCSSフレームワークのSassのソースフォルダをダウンロードしておきます。
  • ソースフォルダのダウンロードではなく、npmなどのパッケージマネージャでのインストールが間違いが起きないのでおすすめです。
  • Laravel標準のBootstrapではなく、どうしてもMaterializeを使ってみたかったのでこれを導入します。

Materialize
Materialize01.png

環境

  • Laravel Framework 7.22.4
  • Materialize 1.0.0

Sassを使えるようにする

Sassの導入は他の方が解説されていたので、こちらを参考にしました。
LaravelにSCSSの導入方法

フレームワークを導入する

npmをインストールしたら、プロジェクトフォルダ直下に作られるwebpack.mix.jsを開きます。

webpack.mix.js
mix.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">
を書いてあげれば完成です。

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

Laravelで好きなCSSフレームワークを使う

準備

  • 導入したいCSSフレームワークのSassのソースフォルダをダウンロードしておきます。
  • ソースフォルダのダウンロードではなく、npmなどのパッケージマネージャでのインストールが間違いが起きないのでおすすめです。
  • Laravel標準のBootstrapではなく、どうしてもMaterializeを使ってみたかったのでこれを導入します。

Materialize
Materialize01.png

環境

  • Laravel Framework 7.22.4
  • Materialize 1.0.0

フレームワークを導入する

npmをインストールしたら、プロジェクトフォルダ直下に作られるwebpack.mix.jsを開きます。

webpack.mix.js
mix.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の導入方法

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

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

参考文献

以下の記事を参考にさせていただき、解決できました。
ありがとうございました。

本件に限らず、見やすいですし理解もしやすいのでオススメです。

PHPフレームワークのLaravelララベルをつかおう

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

PHPで世界のナベアツさんのネタを実装する

元ネタ
rubyで世界のナベアツさんのネタを実装してテストを書く - Qiita

これを書いたあとPHPでも書きたくなりやってみました。
テストは書いてません。
実行したらrubyでやったものと同じように出力できたのでよしとします。

仕様

最初に「これから面白いことつまりオモローなことをします」という
1から40まで数を数える
3がつく数字のときアホになる
3の倍数のときアホになる
5の倍数のとき犬になる(3と5の倍数なら犬になる)
最後に「オモロー」という

omoro_class.php
<?php
class Omoro
{
  const TSUKAMI = "これから面白いことつまりオモローなことをします";
  const OCHI = "オモロー";
  const TSUJO_KAO_MOJI = "( ・`ω・´)";
  const AHO_KAO_MOJI = "ʅ( ՞ਊ՞)ʃ≡";
  const DOG_KAO_MOJI = "∪・ω・∪";

  // ネタ
  public function main($max_kazu = 40)
  {
    $this->speak(self::TSUKAMI);

    for ($figure = 1; $figure <= $max_kazu; $figure++) {
      sleep(1);
      $kao_moji = $this->verification($figure);
      $serif = $this->make_serif($figure, $kao_moji);
      $this->speak($serif);
    }

    $this->speak(self::OCHI);
  }

  // セリフを作る
  public function make_serif($figure, $kao_moji)
  {
    return $figure . $kao_moji;
  }

  // 判断する
  private function verification($figure)
  {
    if ($figure % 5 == 0) {
      return self::DOG_KAO_MOJI;
    }

    if ($figure % 3 == 0 || strpos($figure, '3') !== false) {
      return self::AHO_KAO_MOJI;
    }

    return self::TSUJO_KAO_MOJI;
  }

  // 話す
  private function speak($serif)
  {
    echo $serif . "\n";
  }
}

$omoro = new Omoro();
$omoro->main(40);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPのマジックメソッドについて調べてみた件

マジックメソッドとは

PHPには様々な特殊機能をもった関数をもとから持っています。

__で始まる関数名を特殊関数と呼び、あらかじめ取りおいているようです。
なので、特殊な機能を持たない関数を__で始まる名前にしないほうがいいみたいです。

また、全てのマジックメソッドはpublicで宣言しないといけないようです。

コンストラクタ(__construct())などもマジックメソッドです。

マジックメソッドの一覧

メソッド 引数 戻り値 内容
__construct() 任意 なし インスタンス生成時
__destruct() なし なし インスタンスを破棄するとき
__call() 任意 任意 アクセス不能メソッドをオブジェクトのコンテキストで実行したとき
__callStatic() 任意 任意 アクセス不能メソッドを静的コンテキストで実行したとき
__get() プロパティ名 任意 アクセス不能(protected または private)または存在しないプロパティからデータを読み込むとき
__set() プロパティ名と値 なし アクセス不能(protected または private)または存在しないプロパティへデータを書き込むとき
__isset() プロパティ名 true/false isset() あるいは empty() をアクセス不能(protected または private)または存在しないプロパティに対して実行したとき
__unset() プロパティ名 なし unset() をアクセス不能(protected または private)または存在しないプロパティに対して実行したとき
__sleep() なし 配列 インスタンスに対してserialize()を実行したとき
__wakeup() なし なし インスタンスに対してunserialize()を実行したとき
__toString() なし 文字列 インスタンスを文字列に変換しなければならないとき
__invoke() 任意 任意 スクリプトがオブジェクトを関数としてコールしようとした
__set_state() 配列 インスタンス インスタンスに対してvar_export()を実行しようとしたとき
__clone() なし なし cloneキーワードを使ってインスタンスのクローン生成を実行したとき
__debugInfo() なし 配列 インスタンスに対してvar_dump()を実行したとき

使い方(一部)

__construct()

インスタンス生成時に実行される

class hoge {
  private $name = "";

  public function __construct(string $name) {
    $this->name = $name;
  }
}

__call()

アクセス不能メソッドをオブジェクトのコンテキストで実行したとき

class hoge {  
  public function __call($name, $argments) {
    return $name($argments); //関数$nameに引数として$argmentsを渡して実行
  }
}

参考にさせていただいた記事など

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

PHPでポートフォリオ 開発(イベント管理アプリ)

記事の概要

私が開発したWebアプリ(ポートフォリオ)の解説をします。
開発にいたる背景から、作成手順、機能、工夫したところ、課題などをまとめました。

開発したWebアプリはEventRunnerにて公開中。

以下のテストユーザーでログインできます。↓↓
アドレス :test0001@gmail.com
パスワード:test0001

開発に必要な設計書や要件定義書は以下のリンクからご覧ください。
GitHub
設計書URL

開発の背景

私は、学生の頃から兵庫県に拠点を置く、ボランティア団体に所属しています。
この団体では、2ヶ月に1回のペースで、行事(地域奉仕活動やワークショップなど)が行われます。

それらの行事への参加者を募るために、毎回SNSで情報を共有し、出欠をとり、参加者をSNSグループに招待し、運営していました。

これが、かなり手間で、特に取りまとめする人「リーダー」の仕事量がかなり多く大変でした。
(私は、この「リーダー」の経験があり、とても大変だったと記憶しております)

行事の情報収集や参加表明などができるプラットフォームがあり、
「個人で、情報収集や登録などができれば良いなぁ〜〜〜」と思い、
イベントの参加や登録ができるWebアプリを作ってみることにしました。

また、Web系開発企業のバックエンドエンジニアへの転職を考えていたので、
このアプリを転職活動のポートフォリオとすることに決めました。

目的

  • PHPを用いたWebアプリケーション開発経験を積む
  • フルスクラッチ開発によってWebアプリの基本的な構成、動作を知る
  • Webの仕組みの理解を深めて、自分の言葉でWebについて説明できるようになる
  • 企画〜開発〜運用までを経験することで、アプリケーション開発の流れを掴む
  • 成果品はポートフォリオとして、転職活動で使う
  • 開発アプリによって所属団体の行事運営の円滑化を計る

スペック (as of 2020/08/01)

言語
PHP 7.2.31

DBMS
MySQL 7.4.6

CSSフレームワーク
bootstrap4

開発環境
MacOS Catalina 10.15.6
Apache 2.4.41

バージョン管理
Git 2.27.0

本番環境
Conoha

主な機能

  • 開催イベントの登録
    開催される基本情報を登録・公開することができる(開催場所・時間・概要など)。

  • 参加表明
    ログイン済みの参加者が、「出席」ボタンを押すと、参加者として登録される。
    また、参加の取り消しをすることもできる。

  • トップページでは、本サイトの使い方を簡単に記載しておく

  • マイページで参加予定のイベント詳細情報が閲覧できる

  • 絞り込み検索ができる
    新着・定員数で絞り込み検索ができる

テーブル設計

以下の通り、テーブルを設計した。
ゼロからテーブル設計するのは初めてだったため、解説動画や解説サイトで情報収集し、行った。

会員テーブル(users)

No カラム名 カラム名(日本語) 必須 AI 主キー
1 user_id 会員ID integer Yes Yes Yes
2 name 名前 string Yes
3 email メールアドレス string Yes
4 password パスワード integer Yes
5 img 写真 string Yes
6 introduction 紹介文 text Yes
7 status 会員ステータス integer Yes
8 create_datetime 登録日時 datetime Yes Yes
9 update_datetime 更新日 datetime Yes Yes

開催場所テーブル(location)

No カラム名 カラム名(日本語) 必須 AI 主キー
1 location_id 開催箇所id integer Yes Yes Yes
2 location 開催箇所名 string Yes
3 address 住所 string Yes
4 create_datetime 登録日時 datetime Yes Yes
5 update_datetime 更新日 datetime Yes Yes

イベントテーブル(events)

No カラム名 カラム名(日本語) 必須 AI 主キー
1 event_id イベントid integer Yes Yes Yes
2 user_id 会員ID(主催者) integer Yes
3 location_id 開催箇所id integer Yes
4 event_name 題名 string Yes
5 introduction 説明文 string Yes
6 date 開催日時 date Yes
7 time 開催時間 time Yes
8 create_datetime 作成日時 datetime Yes Yes
9 update_datetime 更新日時 datetime Yes Yes

参加者テーブル(paticipants)

No カラム名 カラム名(日本語) 必須 AI 主キー
1 paticipant_id 参加ステータスid integer Yes Yes Yes
2 user_id 会員ID integer Yes
3 event_id イベントID integer Yes
4 create_datetime 作成日時 datetime Yes Yes
5 update_datetime 更新日時 datetime Yes Yes

開発手順

1. 要件定義

まずは、開発するWebアプリの全体像を把握するため、以下のドキュメントを作成。

  • アプリ概要
  • 機能一覧
  • テーブル設計書
  • スケジュール

実際に、作成したものを以下のリンクからご覧ください。

設計書URL

アプリ概要には、開発にいたる背景や目的などをまとめた。

機能一覧には、Webアプリにつける機能をまとめた。

実際に、クライアントから依頼があったという想定で、進めるためスケジュールを作成。
納期まで、「2週間で完成させてクライアントに返す」、と想定した。

2. 環境選定

学習言語がPHPだったため、PHPでの開発を決める。
また、所有するラップトップにLAMP環境を構築したので、その環境を試したかった。
データベースは、スクールでMySQLの利用経験がある、かつ、情報量が多いことを考慮し、選んだ。

(開発途中で、Conohaサーバー内にCloud9を置いたので、開発後期ではCloud9を利用して開発した)

データベース設計

必要機能から、どんな情報を保存するか、それらをどう関連付けて管理するかを考えながらデータベース設計を行った。

コーディング

  • MVCモデルで開発をする。 繰り返しのプログラムをなるべく避け、ユーザー関数を利用するなどし、第三者にも理解しやすいソースコードを心がけた。
  • まずは、フロント部分を開発し全体のイメージを掴めるように開発した。
    ある程度、全体像が掴んでから、サーバーサイドの開発をした。

  • Gitを利用し、バージョン管理
    チーム開発を想定しているので、開発内容をissueで管理した。
    ひとつの課題を終えるごとに、都度、pull request/mergeなどを利用した。

(これは、gitコマンドの練習にもなって、本当によかったと思っています)

CSSフレームワーク実装

PHPを書くことに集中するため、
CSSフレームワーク「Bootstrap4」を使用し、フロントは最小限の労力で実装した。

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

本番環境へ(Conoha)

初めてのレンタルサーバーを契約し、作ったWebアプリを公開した。

SFTPとして、FileZillaを使用した。

公開鍵を設定し、セキュリティを高めた。

バージョン管理

GitHubにソースコードをシェアした。

工夫したところや感想

開発直後は、MVCの考え方を理解できずなかなか進まなかった。。。
どうなることかと心配したが、納期に間に合うように開発することができた。

工夫したことを以下に記録する。

実際の現場に近い状況で開発
実際の開発現場に近い状況で取り組みたかったので、開発に取り掛かる前に、
スクールのメンター先生に、現場の開発の流れを事前に情報収集し実行した。

メンター先生からのアドバイスとしては、以下があった。

  • フロント画面を先に作成し、客先承認をもらう。承認をもらったフロント部分には、PHPをなるべく書かない。
  • グループ開発なら、GitHubでバージョン管理(pull request/mergeなど)する。
  • グループ内の情報共有はSlack。

Gitでバージョン管理
グループ開発を想定し、gitHubでバージョン管理した。
開発中に遭遇した課題に対して、都度issueを発行し、その課題を解決することを目指した。

積極的にGitコマンドを利用することで、苦手だった、黒い画面でのやりとりへの苦手意識が少なくなった。

サーバー契約し、実際に公開する
当初は、一般公開までするつもりはなかった。
「自分のPCの中だけで、運用できて、ポートフォリオ になればいいなぁ」
くらいに考えていた。

開発していくに連れて、作ったWebアプリに愛着が湧いてきて、
「本番環境に置いて、みんなに見てもらいたい」
と思うようにり、

結果的には、サーバーを契約していた!!!

本番環境に置いたことで、web全体の仕組みの理解を深めることにつながったように思う。

開発後期では、Conohaサーバー内にCloud9を置いた。
それにより、直接サーバーのディレクトリを編集することができたので、作業が捗った。

実際に抱えている問題を解決する
やるなら、自分の身の回りにある課題を解決できるようなシステムを作りたいと思った。

10年近く所属している、団体で感じていた悩みから、このテーマを思いついた。

まだまだ、実運転するには、問題があるので、今後も継続して小さな問題点を解消していきたい。
実際に、使えるアプリになるまで作り込んでいきたい。

その他

スクールで学んだことを盛り込んだWebアプリができた。
ものづくりの楽しさに触れられたのが、一番大きな収穫だったと思う。

実際に開発したことで、Webの仕組みについて、理解が深まった。
レスポンスやリクエスト、サーバーや、IPアドレスなど、
言葉としてしか理解できていなかったことが、実体として理解することができた。

学習してスキルを身につければ身につけるほど、より洗練されたモノが作れるようになることが、
本当に、楽しい。

一方で、自分の苦手とすることや、今後の課題が明確になった。
以下で、課題について記述している。

課題点と今後の展望

  • フィードバックもらった箇所を改修する
    所属している団体の友人に、実際に使用感をフィードバックしてもらった。
    (フィードバック内容は、要件定義書の「今後の課題」に記載。)
    継続して、開発していく。

  • ひと昔前のWebサイトのよう。。。
    Bootstrapの力を借りて、なんとなく良い感じにできているが、、、
    PHPを書くことに集中しているため、フロントがダサい。。
    JavaScriptを学び、動きのあるWebアプリに改修していきたい。

  • 本当は、もっと実装したい機能がたくさんある。例えば、登録したイベントを編集できる機能とか、会員同士の友達登録(フォロー)など、、、、
    それらを実装していく

参考文献

HTMLクイックリファレンス
サル先生のGit入門
データベース設計入門#3 テーブル設計の手順【実戦形式で解説】


最後まで、読んでいただきありがとうございます。

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

本当は怖い論理値の真実~if true or false~

はじめに

この記事はphpにおける論理値についてまとめたものです。
ノリで映画みたいなタイトルをつけてみましたが当たり前のことを当たり前にまとめただけなので、
対象読者はプログラミング初級者、又は死ぬほど暇なエンジニアの方になります。

まずはテスト

さっそくですが、みなさん下記処理の結果はわかりますか?

$function = '';

if($function):
echo 'おはよう';
endif;

if(!empty($function)):
echo 'こんにちわ';
endif;

「両方とも$functionが定義されていればifの中の処理を実行するだろう」と思った方は本記事必読です。
ちなみに答えは「何も出力されない」です。

論理値とは

まずは論理値ってなんやねんという人のために。

論理値 とは、「何かが真であるか、真でないか(偽であるか)」を表す値のことです。ブール(bool)値 とも言われます。又、論理値 のデータ型は 論理型 となります。論理型 は、ブール(bool)型、ブーリアン型(Boolean)とも言われます。

ちなみに、PHPの 論理値 は キーワードの TRUE (真)と FALSE (偽)です。キーワードとして予め TRUE と FALSE がPHPで用意されています(このようなワードを予約語と言います)。大文字小文字は区別しません。true false でも同じ意になります。

なんとなく論理値に関しては上記引用でご理解いただけたかと思いますが、ここで厄介なのが
phpにおいてTRUE(真)やFALSE(偽)と見なされる値があり、それらには決まりがあるということです。

FALSEとみなされる値

キーワードFALSEと同じ偽と見なされる値。

表示なし
キーワード FALSE
整数 0
浮動小数点 0.0
空の文字列 " "
空の文字列 ' '
文字列 '0' (文字列としての0)
要素数が 0 の配列 $ary = array();
プロパティーやメソッドを含まない空のオブジェクト
NULL値

はい、最初に提示させていただいたテスト問題。

$function = '';

if($function):
echo 'おはよう';
endif;

if(!empty($function)):
echo 'こんにちわ';
endif;

もうご理解いただけたかと思いますが、空の文字列 ' 'はphpにおいてはFALSEとして扱われるのですね。
そしてif文、empty関数は、条件式の結果が 真 であるか 偽 であるかによって処理を分岐させます。
なので分岐処理を行わず、「おはよう」「こんにちわ」が出力されなかったのです。
ややこしいですねぇ。。
ちなみに私は論理値に関して理解するまで「こんにちわ」が出力されると思っていました、お恥ずかしい。。

TRUEとみなされる値

FALSE 以外のすべて

こちらは非常にシンプルで分かりやすいですね。

if(1):
echo 'おはよう';
endif;

if('真です'):
echo 'こんちにわ';
endif;

出力される内容は「おはようこんにちわ」です。

おわりに

はい、またも30分で調べた内容を書き殴らせていただきました。
論理値は分かっているようで分かっていない人が多いと思いまとめさせていただきました。
疲れたマン。

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

PHP 数値を整数にする

目的

  • 小数点の値を整数へ変換する方法をまとめる。

  • 下記に少数の値を整数へ変換するキャストを用いた方法の例を記載する。少数が切り捨てられた整数の値が出力される。

    echo (int)小数点の数値;
    
  • 下記に少数の値を整数へ変換する関数を用いた方法の例を記載する。少数が切り捨てられた整数の値が出力される。

    echo intval(小数点の数値);
    

具体例

  • 変数$aに格納された数値を整数に変換するキャストを用いた方法を下記に記載する。下記を実行すると「2」が出力される。

    $a = 2.2;
    echo (int)$a;
    
  • 変数$aに格納された数値を整数に変換する関数を用いた方法を下記に記載する。下記を実行すると「2」が出力される。

    $a = 2.2;
    echo intval($a);
    

参考文献

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

カリー化チートシート

拙作『不動点コンビネータを用いた無名再帰関数の実行まとめ』の補足説明として書き始めたところ,カリー化関数を記述・利用するための独立したチートシートとした方が少なくとも約1名(自分自身)には役立ちそうだったので,新しく記事にした.なお,カリー化してくれる関数の定義ではないことに注意.複数言語にわたっている都合上,各言語に精通している方々のツッコミ歓迎.

記法のみの一覧は次の通り.なお,fは関数,aは引数を指す.

言語 各引数の戻り値 各引数の指定 備考
Haskell \->による無名関数 (・・・(f a) a)・・・a 自動的にカリー化
Scheme lambdaによる無名関数 (・・・((f a) a)・・・a)
Python 関数内部で定義した関数 f(a)(a)・・・(a) 無名関数も使用可
Ruby ->を用いた無名関数 f.(a).(a)・・・.(a)またはf[a][a]・・・[a] カリー化メソッドあり
JavaScript =>functionを用いた無名関数 f(a)(a)・・・(a)
Scala =>を用いた無名関数 f(a)(a)・・・(a) カリー化メソッドあり,強い型付け
Perl subを用いた無名サブルーチン f(a)->(a)・・・->(a) $f->(a)・・・の場合あり
Go言語 funcを用いた無名関数 f(a)(a)・・・(a) 強い型付け
PHP functionuseを用いた無名関数 f(a)(a)・・・(a) 7.4よりfn=>を用いた無名関数が利用可

Haskell(GHC)

Haskellでは,複数引数で定義しても自動的にカリー化される.カリー化関数の引数指定は(・・・(関数 引数) 引数)・・・引数である.

Prelude> func x y z = if x > 0 then y else z
Prelude> func (-100) 0 (-1)
-1
Prelude> ((func (-100)) 0) (-1)
-1

\および->を用いた無名関数を戻り値にして実現する方法は次の通り.ただし,この場合でも引数の複数指定が可能.

Prelude> func = \x -> \y -> \z -> if x > 0 then y else z
Prelude> ((func (-100)) 0) (-1)
-1
Prelude> func (-100) 0 (-1)
-1
Prelude> func = \x y z -> if x > 0 then y else z
Prelude> ((func (-100)) 0) (-1)
-1
Prelude> func (-100) 0 (-1)
-1

Scheme(Gauche)

lambdaを用いた無名関数を戻り値にして実現する.カリー化関数の引数指定は(・・・((関数 引数) 引数)・・・引数)である.

gosh> (define (func x y z) (if (> x 0) y z))
func
gosh> (func -100 0 -1)
-1
gosh> (define func (lambda (x) (lambda (y) (lambda (z) (if (> x 0) y z)))))
func
gosh> (((func -100) 0) -1)
-1

Python(Python3,Python2)

lambdaを用いた無名関数を用いることもできるが,無名関数を直接変数に代入するのがPEP8非推奨ということもあり,関数内部で定義した関数を戻り値にする方法が一般的である.カリー化関数の引数指定は関数(引数)(引数)・・・(引数)である.

>>> def func(x, y, z): return y if x > 0 else z
... 
>>> func(-100, 0, -1)
-1
>>> def func(x):
...     def func(y):
...         def func(z): return y if x > 0 else z
...         return func
...     return func
... 
>>> func(-100)(0)(-1)
-1
>>> func = lambda x: lambda y: lambda z: y if x > 0 else z    # PEP8非推奨
>>> func(-100)(0)(-1)
-1

Ruby(CRuby,JRuby)

Rubyでは,カリー化するメソッドcurryが用意されている.ただし,複数引数の無名関数を->によって一度定義してからcurryを適用する.カリー化関数の引数指定は関数.(引数).(引数)・・・.(引数)または関数[引数][引数]・・・[引数]である.

def func1(x,y,z) x > 0 ? y : z end
p func1(-100,0,-1)              # => -1

func2 = -> x,y,z { x > 0 ? y : z }
p func2.curry.(-100).(0).(-1)    # => -1
p func2.curry[-100][0][-1]       # => -1

->のみを用いた無名関数によって実現する方法は次の通り.

func3 = -> x { ->  y { -> z { x > 0 ? y : z } } }
p func3.(-100).(0).(-1)          # => -1
p func3[-100][0][-1]             # => -1

JavaScript(Node.js)

=>functionを用いた無名関数を戻り値にする方法で実現する.カリー化関数の引数指定は関数(引数)(引数)・・・(引数)である.

function func1(x,y,z) { return x > 0 ? y : z }
console.log(func1(-100,0,-1))      // => -1

func2 = x => y => z => x > 0 ? y : z
console.log(func2(-100)(0)(-1))    // => -1

function func3(x) {
  return function (y) {
    return function (z) {
      return x > 0 ? y : z
    }
  }
}
console.log(func3(-100)(0)(-1))    // => -1

Scala(Scala 2.11 + Java VM 12)

Scalaでは,カリー化するメソッドcurriedが用意されている.ただし,複数引数の無名関数を=>によって一度定義してからcurriedを適用する.カリー化関数の引数指定は関数(引数)(引数)・・・(引数)である.

scala> def func(x: Int, y: Int, z: Int): Int = if (x > 0) y else z
func: (x: Int, y: Int, z: Int)Int

scala> func(-100,0,-1)
res0: Int = -1

scala> val func = (x: Int, y: Int, z: Int) => if (x > 0) y else z
func: (Int, Int, Int) => Int = <function3>

scala> val func_curried = func.curried
func_curried: Int => (Int => (Int => Int)) = <function1>

scala> func_curried(-100)(0)(-1)
res1: Int = -1

=>のみを用いた無名関数によって実現する方法は次の通り.強い型付け言語であるため,関数全体の型の推移を明示する必要がある.

scala> val func: Int => (Int => (Int => Int)) = (x: Int) => (y: Int) => (z: Int) => if (x > 0) y else z
func: Int => (Int => (Int => Int)) = <function1>

scala> func(-100)(0)(-1)
res2: Int = -1

Perl(perl 5)

subを用いた無名関数(サブルーチン)を戻り値にして実現する.カリー化関数の引数指定は関数(引数)->(引数)・・・->(引数)である.なお,関数本体の名前も無名関数とした場合は$関数->(引数)->(引数)・・・->(引数)である.

sub func { my ($x,$y,$z) = @_; $x > 0 ? $y : $z; };
print func(-100,0,-1), "\n";    # => -1

sub func_curried { my $x = shift; return sub { my $y = shift; return sub { my $z = shift; return $x > 0 ? $y : $z; }; }; };
print func_curried(-100)->(0)->(-1), "\n";    # => -1

my $func_curried2 = sub { my $x = shift; return sub { my $y = shift; return sub { my $z = shift; return $x > 0 ? $y : $z; }; }; };
print $func_curried2->(-100)->(0)->(-1), "\n";    # => -1

Go言語(gc)

funcを用いた無名関数を戻り値にして実現する.カリー化関数の引数指定は関数(引数)(引数)・・・(引数)である.なお,強い型付け言語であるため,扱う引数が増えるほど,各引数の関数の戻り値に対する型付け記述が増えていく.

package main
import "fmt"

func func1 (x, y, z int) int { if x > 0 { return y } else { return z } }
func func2 (x int) func(int) func(int) int {
    return func(y int) func(int) int {
        return func(z int) int {
            if x > 0 { return y } else { return z }
        }
    }
}

func main() {
    fmt.Println(func1(-100,0,-1))      // => -1
    fmt.Println(func2(-100)(0)(-1))    // => -1
}

PHP(PHP 7.3,PHP 7.4)

PHP7.3までは,functionuseを用いた無名関数を戻り値にする方法で実現する.カリー化関数の引数指定は関数(引数)(引数)・・・(引数)である.

<?php

function func1($x,$y,$z) {
    return ($x > 0) ? $y : $z;
}
echo func1(-100,0,-1) . PHP_EOL;
// => -1

function func2($x) {
    return function($y) use ($x) {
        return function($z) use ($x,$y) {
            return ($x > 0) ? $y : $z;
        };
    };
}
echo func2(-100)(0)(-1) . PHP_EOL;
// => -1

PHP 7.4からは,fn=>を用いた無名関数が利用可能.

function func2($x) { return fn($y) => fn($z) =>  ($x > 0) ? $y : $z; }
echo func2(-100)(0)(-1) . PHP_EOL;
// => -1

備考

カリー化の概要

カリー化とは,高階関数の機能を利用して,複数の引数を指定する関数を,ひとつの引数のみを指定する関数の繰り返しに変換することである.処理記述の共有が可能な『部分適用』の手段として有名であるが,あくまで利用例のひとつである.カリー化自体の特徴は,ラムダ計算などの数学的な理論を適用しやすいことに加え,引数を再帰的に受け取る汎用的な関数を定義したり,引数ごとに値の適用を調節したり,データ構造を要素ごとに受け取ったりすることで,より簡潔で柔軟なプログラミングが可能となることである.

なお,下記のPythonの例のように,今回の記述方法を用いることで,複数引数をもつ既存の関数のカリー化も容易に行える.ただし,複数引数をもつ既存の関数をカリー化関数に変換してくれる関数やマクロは作成できない.理由は,既存関数の引数の数が不定であり,任意の数の無名関数や内部関数を(クロージャ機能を含めて)生成できないためである.RubyやScalaのカリー化メソッドは個別定義された複数引数をもつ無名関数から変換しており,Haskellは言語仕様としてカリー化関数のみを扱っている(関数定義を行った時点で,型をもつ引数ごとのカリー化関数となる).

Pythonでのカリー化関数利用例

>>> def func(y):
...     def func(z):
...         def func(w):
...             def func(x): return y if x > 0 else z if x < 0 else w
...             return func
...         return func
...     return func
... 
>>> func1 = func('positive')('negative')
>>> func2 = func1('zero')
>>> func2(1)
'positive'
>>> func2(-1)
'negative'
>>> func2(0)
'zero'
>>> func2 = func1('ゼロ')
>>> func2(0)
'ゼロ'
>>> T = (3, -2, 0, 1, -7)
>>> dict(zip(T, map(func2, T)))
{3: 'positive', -2: 'negative', 0: 'ゼロ', 1: 'positive', -7: 'negative'}
>>> def recur(f, t): return f if not t else recur(f(t[0]), t[1:])
... 
>>> dict(zip(T, map(recur(func, ('正', '負', 'ゼロ')), T)))
{3: '正', -2: '負', 0: 'ゼロ', 1: '正', -7: '負'}
>>> def is_t(t):
...     def r(v): return isinstance(v, t)
...     return r
... 
>>> T = 10, "hoge", 20.4, False, "hage"
>>> tuple(map(is_t(str), T))
(False, True, False, False, True)
>>> tuple(map(is_t(int), T))
(True, False, False, True, False)

(同様の内容をSchemeで記述したものはこちら

変更履歴

  • 2020-08-03:カリー化の概要説明部分を修正(コメントより)
  • 2020-08-03:Rubyのカリー化関数を追加(コメントより)
  • 2020-08-03:PHP 7.4を追加(コメントより)
  • 2020-08-02:PHPを追加
  • 2020-08-02:記法のみの一覧を追加
  • 2020-08-02:Go言語を追加
  • 2020-08-02:初版公開(Haskell,Scheme,Python,Ruby,JavaScript,Scala,Perl,Python利用例)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[mac]MAMPでxdebugを使いたいとき

参考記事

大変わかりやすくまとまっている記事。初心者向けに書いてあると思うので
これ読めばわかるはず。
https://qiita.com/miriwo/items/6edd6e1817def4fc11d5

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

【Qiigle】というQiitaの記事を検索するサービスを作りました

Qiigleという、Qiitaの記事を検索するサービスを作りました。

サイトURL: https://qiigle.com/
GitHub: https://github.com/nyshk97/qiigle
how_to_use_qiigle (1).gif

何ができるか(仕様)

  • Qiitaの記事を「ユーザー名」「タイトル」「本文」「タグ」で絞り込んで表示します
  • 複数入力した場合、AND条件になります
  • ユーザー名とタグは完全一致、タイトルと本文は含むです

なぜ作ったか

開発中に過去自分が書いた記事を参照することがよくあるのですが、数が多くなってきて探すのが大変になってきました。(ブックマークで管理するのツラい)

ユーザー名 + 記事の内容で簡単に記事を検索する方法がありそうでなかった(どの方法もちょっと面倒だった)ので作ってみました。

使用技術

  • ConohaWing
  • PHP
  • Tailwind CSS(CDN)
  • HTML

やったことまとめ

  • GoogleDomainsでドメインを取得
  • レンタルサーバー(ConohaWING)のコンパネからサイトを追加
  • DNSの設定(ConohaWingのネームサーバーをGoogleDomainsに登録)
  • QiitaAPIのアクセストークンを取得
  • POSTMANでAPIを叩く
  • HTMLでフォームを作成
  • フォームから受け取ったデータを使いPHPでAPIを叩く
  • 取得したデータを整形して表示
  • Tailwind CSSでデザインを整える
  • SSL有効化
  • URL正規化
  • ファビコンを作成→設置
  • TwitterOGPの設定
  • GoogleAnalyticsの設定

実装はシンプルでもやることが意外と多くて、結局半日かかりました。
超久々にPHPを書きました。(普段はRubyを書いています)

ソースコード

Formから受け取った値を使ってQiitaのAPIを叩くだけのシンプルな実装です。
Tailwind CSSをCDNから読み込んでいます。

画像ファイル等も含めたソース全体はGitHubからご確認ください。

index.php
<!DOCTYPE html>
<html lang='ja'>
<head>
  <meta charset='UTF-8'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
  <meta name="twitter:card" content="summary" />
  <meta name="twitter:site" content="@d0ne1s" />
  <meta property="og:url" content="https://qiigle.com" />
  <meta property="og:title" content="Qiigle - qiita記事検索サービス" />
  <meta property="og:description" content="Qiitaの記事を検索するサービスです。ユーザー名、タイトル、本文、タグの複数条件で検索することができます。" />
  <meta property="og:image" content="https://qiigle.com/ogp.png" />
  <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
  <link rel="icon" href="favicon.svg" type="image/svg+xml">
  <title>Qiigle - qiita記事検索サービス</title>
</head>
<body>
  <style>
    .green {color: #5bad10;}
    .light-green {color: #54c000;}
    .blue {color: #4092d4;}
    .pink {color: #eadad1;}
    .black {color: #3b3e3e;}
  </style>
  <div class='max-w-3xl mx-auto px-4'>
    <div class='text-center mb-4'>
      <a href='/'>
        <h1>
          <span class='green text-6xl'>Q</span>
          <span class='blue text-6xl'>i</span>
          <span class='pink text-6xl'>i</span>
          <span class='green text-6xl'>g</span>
          <span class='black text-6xl'>l</span>
          <span class='blue text-6xl'>e</span>
          <span class='text-xs text-gray-800'>by <a href='https://twitter.com/d0ne1s' class='text-gray-600 text-sm'>d0ne1s</a></span>
        </h1>
      </a>
    </div>
    <div class='mb-6'>
      <p class='text-sm text-center text-gray-800'>Qiitaの記事を検索します</p>
    </div>
    <form action='search_result.php'>
      <div class="bg-white rounded-lg">
        <div class="grid lg:grid-cols-2 gap-6">
          <div class="border focus-within:border-blue-500 focus-within:text-blue-500 transition-all duration-500 relative rounded p-1">
            <div class="-mt-4 absolute tracking-wider px-1 uppercase text-xs">
              <p>
                <label for="user" class="bg-white text-gray-600 px-1">ユーザー名</label>
              </p>
            </div>
            <p>
              <input id="user" name='user' autocomplete="false" tabindex="0" type="text" placeholder='d0ne1s' class="py-1 px-1 text-gray-900 outline-none block h-full w-full">
            </p>
          </div>
          <div class="border focus-within:border-blue-500 focus-within:text-blue-500 transition-all duration-500 relative rounded p-1">
            <div class="-mt-4 absolute tracking-wider px-1 uppercase text-xs">
              <p>
                <label for="title" class="bg-white text-gray-600 px-1">タイトル</label>
              </p>
            </div>
            <p>
              <input id="title" name='title' autocomplete="false" tabindex="0" type="text" placeholder='Rubocopを使ってみた' class="py-1 px-1 outline-none block h-full w-full">
            </p>
          </div>
          <div class="border focus-within:border-blue-500 focus-within:text-blue-500 transition-all duration-500 relative rounded p-1">
            <div class="-mt-4 absolute tracking-wider px-1 uppercase text-xs">
              <p>
                <label for="body" class="bg-white text-gray-600 px-1">本文</label>
              </p>
            </div>
            <p>
              <input id="body" name='body' autocomplete="false" tabindex="0" type="text" placeholder='.rubocop.yml' class="py-1 px-1 outline-none block h-full w-full">
            </p>
          </div>
          <div class="border focus-within:border-blue-500 focus-within:text-blue-500 transition-all duration-500 relative rounded p-1">
            <div class="-mt-4 absolute tracking-wider px-1 uppercase text-xs">
              <p>
                <label for="tag" class="bg-white text-gray-600 px-1">タグ</label>
              </p>
            </div>
            <p>
              <input id="tag" name='tag' autocomplete="false" tabindex="0" type="text" placeholder='Ruby' class="py-1 px-1 outline-none block h-full w-full">
            </p>
          </div>
        </div>
        <div class="mt-3 pt-3 text-center">
          <button class="rounded text-gray-100 px-8 py-2 hover:shadow-inner hover:bg-blue-700 transition-all duration-300 hover:opacity-75" style='background: #54c000'>
            検索
          </button>
        </div>
      </div>
    </form>
  </div>
</body>
</html>
search_result.php
<?php
if($_GET['user']) {
  $q_user = urlencode("user:{$_GET['user']} ");
}
if($_GET['title']) {
  $q_title = urlencode("title:{$_GET['title']} ");
}
if($_GET['body']) {
  $q_body = urlencode("body:{$_GET['body']} ");
}
if($_GET['tag']) {
  $q_tag = urlencode("tag:{$_GET['tag']} ");
}
function esc($s){
  return htmlspecialchars($s, ENT_QUOTES, 'utf-8');
}
$base_url = "https://qiita.com/api/v2/items";
$url = "{$base_url}?per_page=100&query={$q_user}{$q_title}{$q_body}{$q_tag}";
$curl = curl_init();
$option = [
  CURLOPT_URL => $url,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_SSL_VERIFYPEER => false,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER =>  ['Authorization: Bearer MY_ACCESS_TOKEN'],
];
curl_setopt_array($curl, $option);
$response = curl_exec($curl);
$articles = json_decode($response, true);
curl_close($curl);
?>
<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
  <meta name="twitter:card" content="summary" />
  <meta name="twitter:site" content="@d0ne1s" />
  <meta property="og:url" content="https://qiigle.com" />
  <meta property="og:title" content="Qiigle - qiita記事検索サービス" />
  <meta property="og:description" content="Qiitaの記事を検索するサービスです。ユーザー名、タイトル、本文、タグの複数条件で検索することができます。" />
  <meta property="og:image" content="https://qiigle.com/ogp.png" />
  <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
  <link rel="icon" href="images/favicon.svg" type="image/svg+xml">
  <title>検索結果 | Qiigle - qiita記事検索サービス</title>
</head>
<body>
 <style>
    .green {color: #5bad10;}
    .light-green {color: #54c000;}
    .blue {color: #4092d4;}
    .pink {color: #eadad1;}
    .black {color: #3b3e3e;}
  </style>
  <div class='max-w-3xl mx-auto px-4 pb-8'>
    <div class='text-center mb-4'>
      <a href='/'>
        <h1>
          <span class='green text-6xl'>Q</span>
          <span class='blue text-6xl'>i</span>
          <span class='pink text-6xl'>i</span>
          <span class='green text-6xl'>g</span>
          <span class='black text-6xl'>l</span>
          <span class='blue text-6xl'>e</span>
          <span class='text-xs text-gray-800'>by <a href='https://twitter.com/d0ne1s' class='text-gray-600 text-sm'>d0ne1s</a></span>
        </h1>
      </a>
    </div>
    <div>
      <p>
        <span class='mr-1'>検索結果</span>
        <span class='text-gray-800 text-xs'>(ユーザー名: <?= esc($_GET['user']); ?>, タイトル: <?= esc($_GET['title']); ?>, 本文: <?= esc($_GET['body']); ?>, タグ: <?= esc($_GET['tag']); ?>)
</span>
      </p>
    </div>
    <div>
      <?php foreach($articles as $a){ ?>
        <div class='py-4'>
          <a href='https://qiita.com/<?= esc($a['user']['id']); ?>' class='block text-xs' target='_blank'><?= esc($a['user']['name']); ?> (@<?= esc($a['user']['id']); ?>)</a>
          <a href='<?= esc($a['url']); ?>' class='block hover:underline' target='_blank'>
            <h3 class='text-lg' style='color: #1a0dab;'><?= esc($a['title']); ?></h3>
            <p class='break-all text-xs text-gray-800'><?= substr(esc($a['body']), 0, 200); ?></p>
          </a>
        </div>
      <?php } ?>
    </div>
  </div>
</body>
</html>

機能追加(08/03)

  • 検索結果画面にLGTM、投稿日、更新日を追加
  • ユーザー名を記憶する機能を実装

参考

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