それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

MVCに基づいて設計する時に思う自分なりのベストプラクティス

はじめに

最近、ヴァイオレット・エヴァーガーデンスペシャルをみてダメになったけんつです。

割とよく「MVCってなんだ」とか「MVCを採用している開発でどう切り分けたらいいかわからん」と聞かれる事が多いので
自分なりに考え、「こうなっていると最高に嬉しかった、良かった」というベストプラクティスをまとめてみたくなったのでまとめてみます。

MVCは様々なケースで使われていますが、今回は特にWebサーバサイドに限定して紹介していきます。

MVC とは

特にWebサーバサイドの開発に携わってきた人にとっては古くからお馴染みな Model View Controller という
UIをもつアプリケーションソフトウェアを実装するためのデザインパターン

特徴として、内部で扱うデータをユーザが直接参照・編集する情報から分離する構成を取ることが多い。

そして、これはwikipediaにも書いている。
Model View Controller - Wikipedia

次に、一般的に言われているMVCそれぞれの役割についてまとめる。

Controller の役割

一般的にControllerの役割は、ユーザの入力情報に応じて
ModelとViewを制御する役割を持っている。

Model の役割

一般的にModelは、ビジネスロジックに関するものをもたせることになっている。
ビジネスロジック、つまり何らかの値やそれに関連したアルゴリズムを適用した処理などを指す。

View の役割

一般的にViewは表示や、ユーザの入出力が行われるものを制御する。
特に表示、レンダリングに関する事を行う場合はModelを受け取って行うことが多い。

MVCのメリットとデメリット

メリット

MVCを使うことで、ロジック部分とレンダリング部分、それらを制御する部分という
役割がはっきりとしている状態に分離することができるので開発も効率的に行うことができる。

また、MVCはそれぞれModel View Controllerという分類もされるがModelならModelの中でもわけられていることが多く、それぞれに独立性が高くなるため
仕様が変更されても比較的柔軟に対応できる。

デメリット

プロダクトの規模が大きくなると抱える問題も増えてくる。

例えば、規模が大きくなるとビジネスロジックも多くなってしまうため
モデルがFat Modelになってしまいがちで、管理が大変。

これはControllerにおいても言えることで、必要な機能を増やしていくと
Controllerで行うべき処理も増えてしまうため、Fat Controller になってしまう。
この状況を作ると、ControllerとViewの依存性が始めからある程度強いMVCでは変更が難しくなってしまう。

また、ControllerとView、Modelの依存性が強くなってしまいがちで
修正が困難になってしまう場合がある。

MVCを使うためのベストプラクティス


上記に上げた、概要やメリットとデメリットを考慮した上でいくつかポイントを絞って
自分の思うWebサーバサイドに適用できる、MVCのベストプラクティスについてまとめていく。

Controller の使い方

まず、大事なのがModel View Controller で、特に Controller をどう切り出すか。
つまり、どのような処理を Controller にまとめるかということだと思っている。

これを意識することで、Model と Controller の境界をハッキリさせ必要以上に Model にロジックを集約させてしまうことを防ぐ。

では、どのように分けるのがよいかというと以下の一点を考えるとうまく行く。

  • HTTP に関する処理を Controller でのみ扱い、 Model に含めない。


これに尽きると思っている。

確かにビジネスロジックを Model にまとめるのは重要な話だが、Webサーバサイドにおいて
それらと HTTP では、DBなどから取ってくるという処理や、その結果として現れるデータと
Webサーバサイドの根幹にある通信技術というように、そもそもデータとしての属性が大きく異なる。


そのため、Controller では Cookieやセッションに関する処理、Query String や Json POST で渡ってきたデータのバリデーション、それに伴うエラーハンドリングを行う。


このようにすると、ModelにHTTP関連の処理を負わせないため比較的すっきりとまとまる上に
Controller と Model を確実に分離することができるため必要以上に Controller と Model を依存させないという利点をもたらしてくれる。

イメージは下記の画像の感じで
f:id:RabbitFoot141:20181016193500p:plain

URL設計を見なおしてみる。


これは、「MVCと関係無いのでは?」と思うかもしれないが実は上の Controller にHTTP関連の処理をまとめることにも関連している重要な点だと思っている。


上の記述で、HTTP関連のバリデーションを追加すると辛くなってくるのが
Query String を多用する場合で、そのような場合に Controller で実装するメソッドは規模が大きくなってしまいがちになる。
また、それらを多用するとそもそもにそのクエリがリクエストに存在するかどうかから確かめないと行けない場合もあって冗長になりやすい。


それを未然にルーティングで吸収してしまうことで、必要のないHTTPリクエストの時点でエラーをはいてしまうようなリクエストを Controller で扱うことがなくなる。


特に以下のような場合で有効。

ユーザプロフィールを表示するためのルーティングを前提とした場合

/profile?id=xxx&x=a&y=b  <= 一意に情報が定まるものもQuery Stringにしている


/profile/xxx?x=a&y=b <= 一意に定まるものはURLのパスに含めてしまう

この様に、必要となる一意で定まるものをパスに含めてしまうことで
それが確実に存在するように保証する、そしてそれの形式が異なる場合や存在しない場合は
その先、Controllerなどにリクエストを飛ばさないようにする。

これで必要のないバリデーションを行わないようにする。

Controller と Model でやりとりを増やし過ぎない

Controller はその名前の通り、View と Model の流れを管理しているわけだが
Model が増えてくると Controller で呼び出すべき Model が増えて依存性が高くなってしまう。


こうなってくると、後々の修正や変更が大変になってくる上にMVCのメリットである独立性を維持できていない状況が生まれてしまう。

そのような状況を増やさないためにも、ControllerからViewに渡すべき Model が多いと感じた場合はそもそも Model の設計が適切かどうかを見なおすか

Facade パターンを採用することが解決策として挙げられる。

Facade パターンを採用する

複数のModelを参照する必要がある場合、それらをひとつの処理と見なして Facede パターンを適用するのが得策の場合がある。

f:id:RabbitFoot141:20181016194118p:plain

Facede パターンとは、Modelの動作順を守ったうえでControllerで行っていた処理をひとまとめにしているだけだが
Model を多く呼び出す必要がある場合に Fat Controller になるのを防ぐことができる。

おわりに

MVC を Web サーバサイドに適用する場合の、自分が思うベストプラクティスに関してまとめてみたが
これが必ずしも正しいわけでもなく、あくまでも自分が経験してきた中でこれがよいのではという一つの提案程度に見てもらえると嬉しい

【PHPのお話】PSRって知ってますか?

はじめに

どうも、最近ヴァイオレット・エヴァーガーデンの5話にメンタルブレイクされたけんつです。

最近、さらなるスキルアップを目指してサーバサイドで別の経験を積もうとしている。
それでGoとか調べていると、PHPが盛大にディスられていて悲しい気持ちになっている。

確かにPHPはバグとかを生み出しやすい感じは自分で書いていても思うが、そういう記事ほどPSRについて触れていない。
なので、こういう便利なものが標準であるんだよということを今回は書いていきたい。

PSRとは

PSRとは、PHP Standards Recommendations の事で PHP-FIG(PHP Framework Interop Group = PHPフレームワーク相互運用性グループ)が策定しているPHPコーディング規約のことを指す。
The PHP League とか、PHPで使えるOSSをリリースしているところではPSRを厳守する様に書かれていて、これにはいくつかの種類がある。

現状で承認されているPSRは以下の通り

  • PSR-1 Basic Coding Standard(基本的なコーディング標準
  • PSR-2 Coding Style Guide(コーディングスタイルガイド)
  • PSR-3 Logger Interface(ロガーインタフェース)
  • PSR-4 Autoloader(オートローダー)
  • PSR-6 Caching Interface(キャッシングインターフェイス
  • PSR-7 HTTP Message Interface(HTTPメッセージインターフェイス
  • PSR-11 Container Interface(コンテナインタフェース)
  • PSR-13 Hypermedia Links(ハイパーメディアリンク)
  • PSR-15 HTTP Handlers(HTTPハンドラ)
  • PSR-16 Simple Cache(シンプル・キャッシュ)
  • PSR-17 HTTP Factories(HTTPファクトリー)


これらをざっくりと、ところどころ重要なものはある程度詳細に書いていく。

PSR-1 Basic Coding Standard

自分はあまり意識することが少ないが PSR-1 は最低限準拠することが求められるコーディング規約を示している。
といっても、本当に基本的なことなので次にくる PSR-2 を満たす過程で自動的に満たしている事が多い。

詳しいことは以下のドキュメントを参照して欲しい。

www.php-fig.org

PSR-2 Coding Style Guide

これはPHPで何かOSSを書くときや、その開発に参入するときや業務で使うときなど
ありとあらゆる場合で絶対といってもいいぐらいに使う最高に重要なコーディングおよびコーディングスタイル規約。

公式DocにあるOverviewを一部、紹介すると

  • 一行は80文字以下が望ましい
  • namespace のあとには空行をいれる
  • クラスを宣言している中括弧はclass キーワードを含む行におく必要がある

などなど、結構色々ある。

公式DocにあるPSR-2を満たしたコーディングを以下に引用する。

<?php
namespace Vendor\Package;

use FooInterface;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

class Foo extends Bar implements FooInterface
{
    public function sampleMethod($a, $b = null)
    {
        if ($a === $b) {
            bar();
        } elseif ($a > $b) {
            $foo->bar($arg1);
        } else {
            BazClass::bar($arg2, $arg3);
        }
    }

    final public static function bar()
    {
        // method body
    }
}

ただ、こんなこと毎回意識していられないので自動でfixしてくれるツールをいつも使っている。

github.com

基本的にPHPを用いた開発ではcomposerを使うので、requireしたあとはコミットする前に

$ ./vendor/bin/php-cs-fixer fix . --verbose

を走らせる。


これは、多くの PHP 製の OSS で満たされている。

以下が公式Doc

www.php-fig.org

PSR-3 Logger Interface

これも普段あまり意識することがない。
なぜなら、大抵error_log関数にまかせてしまうからだ。

PSR-3 はロギングに関する規約となっている。

これを満たすのはOSSではあまり見たことないので詳しいことは割愛。

以下が公式Doc
www.php-fig.org

PSR-4 Autoloader

これは、最高に重要な規約の一つでファイルパスからクラスをオートロードするための規約。
PSR-0っていう削除予定の規約もあるが、こっちは現役バリバリで使える。

むしろ、これを満たさないと本当にuse を使うのがしんどかったり
composerを使っている場合なら、そもそもオートロードされないとかっていう最高にヤバい問題が起き始める。

  • 完全修飾クラス名は、「ベンダーの名前空間」として知られているトップレベルの名前空間名を持っている必要がある
  • 完全修飾クラス名は、1つ以上のサブ名前空間名を持つことがある
  • 完全修飾クラス名は終端クラス名を持つ必要がある
  • アンダースコアは、完全修飾クラス名のいずれの部分にも特別な意味を持たない。英字は小文字と大文字の任意の組み合わせで構わない。
  • すべてのクラス名は大文字と小文字を区別して参照する必要がある。
\<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>


例を以下にしめす。

Abraham\TwitterOAuth\TwitterOAuth;

vendor name\(sub namespace)\class name となっているものをよくみる。

composerの設定を書くときもオートロードの設定はこれがベースになっているから
知らないとオートロードではまりがちなポイントが出来上がる。

公式Docは以下の通り
www.php-fig.org

PSR-6 Caching Interface

割と基本的な機能が完成された場合はまず意識することがなくライブラリで自動的に対処されている場合があるのでここもさくっと書く。

PSR-6はキャッシュに関する規約がまとまっている。
キャッシュを扱う際にこれだけは最低限用意して欲しいというメソッドが固まった interface を指す。

公式Docは以下

www.php-fig.org

PSR-7 HTTP Message Interface


これは、色々なプロダクトを触るのに最高に重要なHTTPのリクエスト、レスポンスに関する規約。
HTTPリクエスト、レスポンスを扱う上で絶対にこれだけは用意してくれっていう関数群がinterfaceとなっている。

これは、Laravelでも標準のリクエスト、レスポンスクラスをPSR-7のInterfaceに置き換えることが出来たりするぐらいWebフレームワークでは対応していることが多い。

あと、HTTPが絡んだOSSをテストする際にこれらのInterfaceをモックする場合もある。

書いていることはそんな大した量じゃないが最高に重要な役割を持つ規約の一つである。


これがベースになっていることが多い

github.com


公式Docは以下
www.php-fig.org

PSR-11 Container Interface

これは、あまり注目されていないけど実は重要でOSSではよく使われている。
というのもPSR-11はDIコンテナに関する規約がまとまっているからだ。

これを満たしている、利用しているPHP製のOSSは意外と多い。

うだうだ語るよりも以下の記事を見れば大体わかる。

rabbitfoot141.hatenablog.com

ルーティングやバッチ系のライブラリで使われているのをよく見る。

公式Docは以下
www.php-fig.org



PSR-13、普段はめったに考えることないけどハイパーメディアリンクに関する規約。
これもハイパーメディアリンクを扱う上で必要な関数群を定義しているがこのあたりのライブラリを見たり実装をすることがないから本当にわからない

なので公式Docだけ晒しておく
www.php-fig.org

PSR-15 HTTP Handlers


これは、PSR-7と関連して満たされる場合が多いためさくっとまとめる。
PSR-15はHTTPハンドラーの規約になっている。

HTTPハンドラーと言っても、リクエストハンドラとミドルウェアに関するコンポーネントが含まれている。

が、これを自前で実装することはなくLaravelなどのFWで最初から組み込まれていることが多い。

それなりに重要なはずなんだけど、よく見えないところに隠されていることが多いため
意識しなくても使っている場合がある

以下が公式Doc
www.php-fig.org

PSR-16 Simple Cache

これはキャッシュに関する規約になっている。
キャッシュ系のライブラリを作ることは少ないと思うが使う際には、この規約にあるインターフェースを利用することになる。

中にはメソッドからキャッシュとして保持する必要のあるデータ型などが書いてある。

以下が公式Doc
www.php-fig.org

PSR-17 HTTP Factories

PSR-17はHTTPファクトリ、つまりPSR-7準拠のHTTPオブジェクトの作成方法についての規約となる。
これができるまでは、HTTPメッセージに関しての規約(PSR-7)しかなく、そのオブジェクトについてはバラバラだったため
それらの規格を統一するためにある。
これも同様にインターフェースなどが定められている。

こういった部分はよくFWの内部に埋もれがちになってしまっている。

以下が公式Doc
www.php-fig.org

おわりに


途中、FWに隠れがちで実例を示すのが難しい部分はかなり説明を端折ってきたが
特に有名なOSSやプロダクトの裏ではこのように制定された規約の上に成り立っていることが多く
これらを満たすことで、快適に開発することができる。

oEmbed を叩いてメディア共有サイトのコンテンツ情報を簡単に取得する

はじめに

どうも、よく訓練された PHPer のけんつです。
突然ですが、みなさん YoutubeInstagram などのメディア共有サイトの情報で特にコンテンツの埋め込みに必要な情報だけ欲しいときに
わざわざデベロッパー登録して、コンシューマーキー等を取得してAPIを叩くのって面倒じゃね?と思うことありませんか?

今日はそんなあれこれを手軽に便利にしてしまう「oEmbed」というものについて書こうと思います。

oEmbed とは

oEembed については以下のサイトがドキュメントを公開している。

https://oembed.com/

じゃあ、oEmbed とは何かというと「コンテンツ共有サイトの情報を埋め込み可能な形式で提供するAPI」のことを指していて。
これは開発者登録等をしなくとも使用することができるためコンテンツ情報だけを使いたい時に非常に便利なもの。

この後、詳しい規格について記述していこうと思いますが「そんなんしらん、使えればok」という人は以下の例を見てください。

Quick Start

今回は Youtube を例に説明していく。
www.youtube.com
上記の動画を題材にする。

まず、oEmbed で必要になるのはそのコンテンツのURL。
今回で言えば

https://www.youtube.com/watch?v=SX_ViT4Ra7k


これ。
youtube の url に /watch というルーティングがあり、 クエリストリングで動画IDが付与されているこれ。


コンテンツのURLがわかれば次にやることは youtube の oEmbed エンドポイントにリクエストを投げるだけ。
Youtube の oEmbed エンドポイントは

https://www.youtube.com/oembed

↑これ。

それのエンドポイントに url 情報と format 形式をクエリストリングで指定してリクエストを送ると json が返ってくる。
※本当はクエリストリングの部分は url encode が必要。

https://www.youtube.com/oembed?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSX_ViT4Ra7k&format=json

{
	"width": 480,
	"version": "1.0",
	"author_url": "https://www.youtube.com/user/08yakari",
	"provider_name": "YouTube",
	"type": "video",
	"provider_url": "https://www.youtube.com/",
	"html": "<iframe width=\"480\" height=\"270\" src=\"https://www.youtube.com/embed/SX_ViT4Ra7k?feature=oembed\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>",
	"thumbnail_url": "https://i.ytimg.com/vi/SX_ViT4Ra7k/hqdefault.jpg",
	"thumbnail_width": 480,
	"title": "米津玄師  MV「Lemon」",
	"thumbnail_height": 360,
	"author_name": "米津玄師",
	"height": 270
}

あとは json を煮るなり焼くなり好きにするといい感じにできる。
ひとつ言うなら、 html メンバーの値は直接埋め込み可能な iframe 形式となっている。
なんならサムネイルのURLもついてきているのでスゴク便利。

oEmbed の規格

使うだけならあれでいいんだけど、それじゃ気がすまねぇ!!っていう人のためにしっかりと規格についてもまとめていきたい。

Consumer Request


oEmbed のエンドポイントに送るリクエストには必要なパラメータと形式が存在する。

まず大前提としてリクエストはエンドポイントに対してGETリクエストである必要があり、全てのパラメータは RFC 1738 に準拠した形式で URL Encode された状態で無いといけない。

その大前提の元に以下のパラメータをリクエストに含めることができる。

  • url (require)

これは情報を取得したいコンテンツのURL。

  • maxwidth (optional)

埋め込みリソースの最大幅。一部のリソースタイプ(以下に記載)にのみ適用される

  • maxheigth (optional)

埋め込みリソースの最大高さ。一部のリソースタイプ(以下に記載)にのみ適用される。

  • format (optional)

レスポンス形式を指定する。

この中で max*** 系 はあまり使わないが、注意が必要なものが format になる。
これは大抵 json or xml を指定するが存在しないものを選択するとエラーが返ってくる。
なので、どちらかを指定してリクエストを投げるのが良い。

Provider Response

ここからはレスポンスに関すること。

Response Format

これは大抵、 jsonxml になっている。
どちらかしかサポートしてないサービスも結構あるが、今回は json についてのみ書いていく。

Response parameters

json に含まれるパラメータについて。

  • type (required)

コンテンツの種別を表す、video, image などがある

  • version (required)

oEmbed のバージョンを表す、今は1.0

  • title (optional)

コンテンツのタイトル

  • author_name (optional)

コンテンツの作者等。

  • author_url (optional)

コンテンツの作者のURL

  • provider_name (optional)

コンテンツを提供しているヤツの名前、youtube などサービス名がおおい

  • provider_url (optional)

これもコンテンツを提供しているサービスのURL

  • cache_age (optional)

推奨されるコンテンツのキャッシュ有効期間。

  • thumbnail_url (optional)

サムネイルを管理しているURL

  • thumbnail_width (optional)

サムネイルのサイズ

  • thumbnail_height (optional)

サムネイルのサイズ


この中で特に注意が必要なのが type である。
これによって更に require なパラメータが増えたり、iframe の埋め込みコードがあったりなかったりしてしまう。

Errors

渡したurlパラメータの値となるurlにコンテンツが無い場合に返る

レスポンスを要求された形式で返せない場合に返る。

  • 401 Unauthorized

指定されたURLには、プライベート(非公開)リソースが含まれている場合に返る。

Security considerations

oEmbed 対応のAPIを提供する場合に対応するURLについて。
URL スキームを http, https, mailto などに限定しないと、javascript:... などXSSの攻撃に利用されてしまう点に注意

おわりに

oEmbed は、コンテンツ情報をひっぱってくる際にかなり便利なのでぜひ使っていきたい。
ただ、コンテンツの説明などは手に入らない場合があるのでそういうときは黙って開発者登録などしてAPIを使うしかない。

oauth2-mastodon をつくった話

はじめに

いろいろあって、mastodon の Streaming API を叩くことになったが
PHP で使えるいい感じの OAuth2 クライアントがなかったので自作した。

レポジトリは以下のリンクから
github.com

Packagist にも追加してあるのでcomposer を使ってインストールすることができる。
packagist.org

パッケージ概要

対応バージョンは

PHPは現行の安定版(ver 1.0.1)で

  • 7.0
  • 7.1
  • 7.2

に対応している。

基本的な構成は
github.com
みんな大好き、The PHP League にある oauth2-client パッケージの AbstractProvider を継承したマストドン用のクラスを作ることで割とあっさり実現できた。

そのほか Unit Test などは全て The PHP League にある skeleton レポジトリに準拠したものとなっている。
github.com

Mastodon の OAuth2.0 周りは以下のドキュメントを参考にして必要なことを行っているだけ
github.com

最低限実装するべき色々は以下のドキュメントにあるものに基づいている。
Implementing Your Own Provider - OAuth 2 Client



使い方


このライブラリをローカル環境でphpコマンドを叩いてさくっと使うというのは
もとからあるライブラリの構成上難しいので -S オプションをつけて起動して欲しい。

というのもセッション等を使っているのと、認証後は手元の環境にリダイレクトさせる必要があるから。


簡単な使い方を以下に示す。

まずはcomposer を使ってパッケージをインストールする。

$ composer require lrf141/oauth2-mastodon

その後の使い方は非常に簡単で

<?php

use Lrf141\OAuth2\Client\Provider\Mastodon;

session_start();

$provider = new Mastodon([
    'clientId' => '',
    'clientSecret' => '',
    'redirectUri' => 'redirect url',
    'instance' => 'https://mstdn.jp',
    'scope' => 'read write follow',
]);


if (!isset($_GET['code'])) {

    $authUrl = $provider->getAuthorizationUrl();

    $_SESSION['oauth2state'] = $provider->getState();
    header('Location: '.$authUrl);
    exit;

// Check given state against previously stored one to mitigate CSRF attack
} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {

    unset($_SESSION['oauth2state']);
    exit('Invalid state');

} else {

    // Try to get an access token (using the authorization code grant)
    $token = $provider->getAccessToken('authorization_code', [
        'code' => $_GET['code']
    ]);
    
    // Optional: Now you have a token you can look up a users profile data
    try {
    
        $user = $provider->getResourceOwner($token);
       
        echo $user->getName();
        
    } catch(Exception $e) {
       
       
        exit('Oh dear...');
    }


    echo $token->getToken();
}

とするだけ。

後半にあるユーザ関連の情報は扱わなくてもよいが、諸々の認証をすると
Access Token が取れるようになっているのであとは煮るなり焼くなり好きにして欲しい。


ただし、 oauth2-client やその他 third-party 製の類似ライブラリに見られる使用方法と oauth2-mastodon はいくつか異なる部分がある。
それは特に Provider インスタンスを作成する最初の部分で、分散型であるため対象となるマストドンインスタンスを指定することと

先にマストドン側でアプリケーションを作成し、該当する scope 、つまりアクセス権限について空白スペース区切りで指定する必要がある。

またマストドン側でアプリケーションを作成した場合、リダイレクトURLを複数指定できるがデフォルトのリダイレクトURLを指定すると
どこにもリダイレクトされないためwebページ上にアクセストークンが表示されてしまうだけなので注意が必要。


今回の手元で自身がテストした際には
上記の利用例を test.php に全て書き

$ php -S localhost:8000 -t ./

というコマンドで起動し

リダイレクトURLには

http://localhost:8000/test.php

を指定していた。

さいごに

基本的なクラスを継承して、必要事項などを受け取れるように多少機能を追加しただけで6時間ほどあれば諸々の実装はできるので
PHP で oauth2-client を使って独自の third-party 製ライブラリを作る必要が出てくればぜひともトライしてみて欲しい。

AWS SDK for PHP から AWS SQSを使ってみた

はじめに

最近話題のマストドンのStreaming APIを叩いて、特定のトゥートに含まれる文字列を別な何かに提供したい!!
という要望を叶えるために AWS SQS を使うことになった。
ここでは、メモも含めてここにまとめていく。

意外と文献がなかったので、実践では標準キュー、FIFOキューの両方についてコードを書く。

AWS Simple Queue Service とは

aws.amazon.com

SQS とは、これ。

メッセージキューをクラウド化したもの、キューをいくつも作る事ができてそれぞれ独立して管理できる。
一時的にデータを蓄えて置くのにはかなり最適で、非同期サービス対サービスのような今回の状況にかなり適している。
またキュー自体にも2種類あり、標準のキューとFIFOキューがある。
多分キューと聞いて思い浮かべるのは後者だと思う。

スタンダードキュー

スタンダードキューには大きく3つの特徴がある。

メッセージの順序

スタンダードキューにおけるメッセージの順序は特徴的な部分がある。通常キューと言われるとFIFO(First-In First-Out)を考えると思うがこのキューでは順番がなるべく保持されるだけで必ずしもFIFOを実現できるものではないと言うこと。
そのため、各メッセージに順序をつけてDBなどに突っ込むなどしてメッセージを受け取ったときにソートする必要がある

一回以上の配信

ドキュメントにもあるが、SQSは冗長性と可用性を確保するために、メッセージが複数のサーバに保存される。
それが影響して、メッセージの受信と削除のときにまれに同一のメッセージのコピーを処理してしまい想定される挙動と異なる挙動を取る場合がある。
そのため、それらを考慮して処理を設計する必要がある。

ショートポーリングを使用したメッセージ処理

SQSはデフォルトでショートポーリングを使用する。ショートポーリングとはポーリングされたキューが空であろうとなかろうとすぐに応答を返すことを指す。
反対にロングポーリングはキューに達するかロングポーリングがタイムアウトになるまで応答を返すことがない。

ショートポーリングを使用した場合、サーバのサブセットがサンプリングされるためそれらのサーバにあるメッセージだけ返される。
そのため、受信リクエストによっては全てのメッセージを受け取ることができない、というかそもそもサーバがわが持っていいない場合がある。
これは、サーバにデータをコピーしていることとそれらのサーバが分散システムとなっていることが要因となっている。

AWS SDK for PHP による実践

まず共通する環境について。
PHPは以下のバージョンを使う。

$ php -v
PHP 7.2.8-1+ubuntu16.04.1+deb.sury.org+1 (cli) (built: Jul 25 2018 10:51:50) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.2.8-1+ubuntu16.04.1+deb.sury.org+1, Copyright (c) 1999-2018, by Zend Technologies
with Xdebug v2.6.0, Copyright (c) 2002-2018, by Derick Rethans

composer を使って、以下のパッケージを持ってくる。

$ composer require aws/aws-sdk-php

また以下のディレクトリにcredential、つまり認証情報を以下の形式でいれる必要がある。
このあたりは、次回のIAM関連についての記事と同じタイミングで書く。

$ cat $HOME/.aws/credential
[default]
aws_access_key_id = ACCESS_KEY
aws_secret_access_key = SECRET_KEY

標準キューとFIFOキューの作り方に関してはコンソールから作るだけなので省略。


標準キュー(スタンダードキューの実践)
メッセージの送信
<?php

require_once 'vendor/autoload.php';

use Aws\Sqs\SqsClient;
use Aws\Exception\AwsException;

define('QUEUE_URL', 'url');


try {

    $client = new SqsClient([
        'profile' => 'default',
        'region' => 'us-east-1',
        'version' => 'latest',
    ]);

    $post_params = [
        'DelaySeconds' => 0, //ここに入れた秒数だけキューから値を取り出せない
        'MessageAttributes' => [
            'Title' => [
                'DataType' => 'String', //binaryを指定するとbase64 encodeでバイナリを与える
                'StringValue' => 'Amazon SQS Test Message',
            ]
        ],
        'MessageBody' => 'This is AWS SDK for PHP.', //メッセージ本文
        'QueueUrl' => QUEUE_URL,
    ];

    $result = $client->sendMessage($post_params);

} catch (SqsException $error) {
    $e->getMessage();
}

メッセージの受信と削除
<?php

require_once 'vendor/autoload.php';

use Aws\Sqs\SqsClient;
use Aws\Exception\AwsException;

define('QUEUE_URL', 'url');


try {

    $client = new SqsClient([
        'profile' => 'default',
        'region' => 'us-east-1',
        'version' => 'latest',
    ]);

    $get_params = [
        'AttributeNames' => ['All'],
        'MessageAttributeNames' => ['All'], //All指定部分は取得したい属性を設定する
        'MaxNumberOfMessages' => 5, //一度に受け取るメッセージの最大数
        'QueueUrl' => QUEUE_URL,
        'WaitTimeSeconds' => 20, 
        'VisibilityTimeout' => 60,
    ];

    $result = $client->receiveMessage($get_params);
    var_dump($result);
    $data = $result->get('Messages');

    if ($data)
    {
        foreach($data as $item)
        {
            echo $item['Body']."\n";

            //受け取ったら削除しないとデータは残る
            $client->deleteMessage([
                'QueueUrl' => QUEUE_URL,
                'ReceiptHandle' => $item['ReceiptHandle'],
            ]);
        }
    }

} catch (SqsException $error) {
    $e->getMessage();
}

FIFO キュー

FIFOキューはイベントの順番が重要になる場合や重複不可な時に使うと良いらしい・
というのもメッセージ順序が大きく関係している。

メッセージの順序

FIFOキューでは配信は一回だけの処理とされる点が標準キューと大きな違いがある。
メッセージの送信、受信の順序は厳密に守られコンシューマが削除するまで使用可能状態で残り続ける。
また、キューの状態で重複は導入されていない・

重要な用語

いくつか、SQS FIFOを使ううえで重要な用語がある。(メッセージグループなど)
それは以下のサイトで詳解されていたので省略。
Amazon SQS FIFO (先入れ先出し) キュー - Amazon Simple Queue Service

メッセージの送信
<?php

require_once 'vendor/autoload.php';

use Aws\Sqs\SqsClient;
use Aws\Exception\AwsException;

define('QUEUE_URL', 'url');

try {

    $client = new SqsClient([
        'profile' => 'default',
        'region' => 'us-east-1',
        'version' => 'latest',
    ]);

    $client->sendMessage([
        'QueueUrl' => QUEUE_URL,
        'MessageBody' => 'hello,SQS!!',
        'MessageGroupId' => 'test',
        'MessageDeduplicationId' => time(),
    ]);

} catch (SqsException $error) {
    $e->getMessage();
}
メッセージの受信と削除
<?php

require_once 'vendor/autoload.php';

use Aws\Sqs\SqsClient;
use Aws\Exception\AwsException;

define('QUEUE_URL', 'url');


try {

    $client = new SqsClient([
        'profile' => 'default',
        'region' => 'us-east-1',
        'version' => 'latest',
    ]);

    $result = $client->receiveMessage([
        'MaxNumberOfMessages' => 1,
        'QueueUrl' => QUEUE_URL,
    ]);

    foreach ($result->search('Messages[]') as $message) 
    {
        $queueHandle = $message['ReceiptHandle'];
        echo $message['Body'];
        $client->deleteMessage([
            'QueueUrl' => QUEUE_URL,
            'ReceiptHandle' => $queueHandle,
        ]);
    }

} catch (SqsException $error) {
    $e->getMessage();
}

おわりに

SQSはシンプルなメッセージをアプリケーション間で共有する際に便利で
特にFIFOを様々な配信サーバの一つの機能として利用できる。

Laravelの動く環境をdocker-compose(PHP 7.2 + nginx + MySQL)でいい感じにする

はじめに

Laravelを

$ php artisan serve

で動かすのにすごくモヤモヤしていたのでdocker-compose をつかってLaravelのプロジェクトが動くインフラ(PHP 7.2 + nginx + MySQL)を構築してみたからまとめる。

ディレクトリ構成は以下の感じになった。

$ tree -L 1
.
├── LICENSE
├── README.md
├── app
├── artisan
├── bootstrap
├── composer.json
├── composer.lock
├── config
├── database
├── docker
├── docker-compose.yml
├── package.json
├── phpunit.xml
├── public
├── readme.md
├── resources
├── routes
├── server.php
├── storage
├── tests
└── webpack.mix.js

完全に好みである。

docker-compose

例のファイル(docker-compose.yml)を書く。

version: '3'
services:
    nginx:
        build:
            context: ./docker/nginx
        depends_on:
            - php
        ports:
            - 80:80
        volumes:
            - ./:/src

    php:
        build:
            context: ./docker/php
        environment:
            DB_HOST: mysql
        volumes:
            - ./:/src

    mysql:
        image: mysql:5.7
        volumes:
            - ./docker/mysql:/var/lib/mysql
        environment:
            - MYSQL_ROOT_PASSWORD=root
            - MYSQL_USER=sample
            - MYSQL_PASSWORD=sample
            - MYSQL_DATABASE=sample
        ports:
            - 3306:3306

見ればわかるようにプロジェクトルートを/srcとして設定している。
コンテナ側では基本的に/srcに入るようになっていてそれを公開する形になっている。

またこの設定ファイルにあるように、docker-composeで使用するコンテナの設定ファイル群は./dockerディレクトリ以下にある。
PHPのDockerfileは以下の通り

FROM php:7.2.8-fpm
RUN docker-php-ext-install pdo_mysql mysqli mbstring
WORKDIR /src

PHP 7.2を持ってきている。


そしてこれがnginxのDockerfile (./docker/nginx/Dockerfile)

FROM nginx:latest
COPY ./default.conf /etc/nginx/conf.d/default.conf

こっちが一番重要なnginxの設定ファイル (./docker/nginx/default.conf)。
今回は、https化まではしていないから結構シンプルになっている。

server {
    
    listen 80;
    server_name _;

    root /src/public;
    index index.php;

    access_log /src/docker/nginx/logs/access.log;
    error_log  /src/docker/nginx/logs/error.log;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;    
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
        
}

諸々のパスはコンテナ側に準拠している。
そして一番重要なのがrootを設定すること。

Laravelではpublicディレクトリ以下のindex.phpにアクセスが飛ぶためここをrootに設定する。

環境を立ち上げる

ログファイルを格納するディレクトリを作る
$ mkdir ./docker/nginx/logs

MySQLで使用するディレクトリを作る、このディレクトリはignoreしないとgit add できなくなる
$ mkdir ./docker/mysql

githubで管理した時にignoreされたファイルをなかったら作る
$ echo '' >> ./storage/logs/laravel.log

権限を与える
$ sudo chmod 777 -R ./storage/ ./bootstrap/

$ composer install

Laravelのおまじない
$ cp .env.example .env
$ php artisan key:generate

$ docker-compose up -d

これでLaravelが動く最低限の環境ができた

yarn + webpack を感覚で使っていたので勉強がてら手元に環境を構築してみる

はじめに

今までJavaScala、Cなど型が割としっかりしている言語を使って開発を続けてきた。
しかし、その経験がweb系の開発を行うときに思わぬ障害になった。
PHPとJSがわからない、あいつら抽象的過ぎてわからないという現象にぶち当たった。

そしてなんだ、こいつらwebpackとyarnってなんや!!ってなったからしっかりやっていく。

yarnとは

yarn はパッケージマネージャの npm の上位互換のようなツールと言われている。
だったら npm でいいじゃん!と思うかも知れないけど npm は色々な問題を抱えているため、 yarn が使われることが多いみたい。

yarn ではダウンロードしたパッケージをキャッシュしているので、一度使ったパッケージはすぐに再利用することができる。
このあたりは composer に似ている。

yarn は npm で管理されているけど、その方法でUbuntuにインストールするのは推奨されていない。

また package.json に対応しているから npm で package.json を使っている場合はすぐに移行できるらしい。

webpackとは

webpack はWebアプリケーションで使用しているリソースの依存関係を解決し、js や css などのアセットを生成するビルドツール。
つまり、1つ以上のモジュールをひとつにまとめたファイルを吐ける。

yarn の導入

Ubuntu 16.04に突っ込むことを前提にする。
しかし apt では入らないので、公式にかいてあるようにコマンドを叩いて突っ込む

$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
$ sudo apt-get update
$ sudo apt-get install yarn

version 1.7.0の安定版を入れるので

$ yarn --version
1.7.0

と、でればok

webpack の導入

をやろうとしたら、nodejsのバージョンが低いっぽいので先に nvm を入れる。

nvm の導入

次のコマンドを叩いて導入する

$ git clone https://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh

それが終わったら、雑に node のバージョンを指定して突っ込む。

$ nvm install 8.11.3
Downloading and installing node v8.11.3...
Downloading https://nodejs.org/dist/v8.11.3/node-v8.11.3-linux-x64.tar.xz...
######################################################################## 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v8.11.3 (npm v5.6.0)
Creating default alias: default -> 8.11.3 (-> v8.11.3)

webpackの導入をやる

適当にディレクトリを作ってそれをプロジェクトとする。
その前に、yarn を使って以下のことをする必要がある。

$ yarn init

これで例の package.json が生成される。

{
  "name": "SampleWebpack",
  "version": "1.0.0",
  "main": "index.js",
  "author": "lrf141",
  "license": "MIT"
}


ここまできたら、いよいよ webpack を導入する。

$ yarn add webpack webpack-dev-server --dev
yarn add v1.7.0
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.4: The platform "linux" is incompatible with this module.
info "fsevents@1.2.4" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Saved 2 new dependencies.
info Direct dependencies
├─ webpack-dev-server@3.1.4
└─ webpack@4.12.0
info All dependencies
├─ webpack-dev-server@3.1.4
└─ webpack@4.12.0
Done in 3.31s.

これで導入は完了。

{
  "name": "SampleWebpack",
  "version": "1.0.0",
  "main": "index.js",
  "author": "lrf141",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^4.12.0",
    "webpack-dev-server": "^3.1.4"
  }
}

package.json も上記の様に変化している。

次に webpack.config.js という webpack の設定ファイルを書く。

module.exports = {
    entry: "./src/index.js",
    output: {
        filename: "index.bundle.js",
        path: __dirname + "/build"
    }
}

サンプルのjsファイルを作る。

window.onload = function()
{
    var elem = document.getElementById('sample');
    elem.innerHTML = 'I love Jason Statham.';
}
<script src="index.bundle.js"></script>
<body>
    <div id="sample"></div>
</body>

実際にビルドしてみる。

$ yarn run webpack-dev-server -- --inline

と、失敗するので yarn を使って webpack-cli を導入する

$ yarn add webpack-cli
{
  "name": "SampleWebpack",
  "version": "1.0.0",
  "main": "index.js",
  "author": "lrf141",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^4.12.0",
    "webpack-dev-server": "^3.1.4"
  },
  "dependencies": {
    "webpack-cli": "^3.0.8"
  }
}

もう一回やってみる。

$ yarn run webpack-dev-server -- --inline
yarn run v1.7.0
warning From Yarn 1.0 onwards, scripts don't require "--" for options to be forwarded. In a future version, any explicit "--" will be forwarded as-is to the scripts.
$ /home/rabbitfoot/webProject/SampleWebpack/node_modules/.bin/webpack-dev-server --inline
ℹ 「wds」: Project is running at http://localhost:8080/
ℹ 「wds」: webpack output is served from /
⚠ 「wdm」: Hash: 485e1d2511df65a321f5
Version: webpack 4.12.0
Time: 427ms
Built at: 2018-06-24 01:12:22
          Asset     Size  Chunks             Chunk Names
index.bundle.js  139 KiB       0  [emitted]  main
Entrypoint main = index.bundle.js
 [2] ./src/index.js 127 bytes {0} [built]
 [4] (webpack)/hot/emitter.js 77 bytes {0} [built]
 [6] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {0} [built]
 [9] ./node_modules/html-entities/index.js 231 bytes {0} [built]
[10] ./node_modules/ansi-html/index.js 4.16 KiB {0} [built]
[11] (webpack)-dev-server/client/overlay.js 3.58 KiB {0} [built]
[12] ./node_modules/sockjs-client/dist/sockjs.js 176 KiB {0} [built]
[13] (webpack)-dev-server/client/socket.js 1.05 KiB {0} [built]
[14] ./node_modules/loglevel/lib/loglevel.js 7.68 KiB {0} [built]
[15] ./node_modules/ansi-regex/index.js 135 bytes {0} [built]
[16] ./node_modules/strip-ansi/index.js 161 bytes {0} [built]
[19] ./node_modules/querystring-es3/index.js 127 bytes {0} [built]
[23] ./node_modules/url/url.js 22.8 KiB {0} [built]
[24] (webpack)-dev-server/client?http://localhost:8080 7.75 KiB {0} [built]
[25] multi (webpack)-dev-server/client?http://localhost:8080 ./src/index.js 40 bytes {0} [built]
    + 11 hidden modules

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/
ℹ 「wdm」: Compiled with warnings.

これで画面に文字列が無事に表示された。

このあと、webpack の設定をいじってビルド周りのコマンドを登録したりして
効率化するべきなんだろうけど、そこら辺は必要になったときに追ってやっていく