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

僕と MySQL と時々 MariaDB

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

Laravelを使いたかったので雑に入門した

はじめに

なぜか急にwebシステムを作る必要がでてきたため、Laravelを使ってみようとおもった。
だが使ったことないので備忘録も兼ねてまとめていこうと思う。

対象

  • webアプリケーションを構築したことがある
  • 他のフレームワークを使用した経験がある
  • PHPを書いたことがある
  • PHPでなくてもなんかコードは書いたことある

などなど、何が言いたいかというと初心者に向けた記事じゃなくて
自分用のメモ兼ある程度のサーバサイド経験者に共有したい内容となっている

Laravelとは

Laravel は、MVCのWebアプリケーション開発用の無料・オープンソースPHPで書かれたWebアプリケーションフレームワークのこと。
PHPのwebアプリケーションフレームワークといえば、CakePHPFuelPHPあたりを想像するだろうが、最近はLaravelが熱いらしい。

Laravelの環境を構築する


composerが入っていることを前提にする。
その上で最初にComposerを使用し、Laravelインストーラをダウンロードする。

composer global require "laravel/installer"

またはcomposerを使ってプロジェクトを作る

composer create-project --prefer-dist laravel/laravel project-name

プロジェクトを作成したらそのディレクトリに移動して、ローカル開発サーバを起動する。
その前にドキュメントによるとアプリケーションキーの設定と設定ファイルのあれこれを行う。

設定ファイルをリネームする

cp -p .env.example .env

そしてアプリケーションキーを作成する。

php artisan key:generate


ここまで完了した段階で、以下のコマンドを実行することで最初のHello,World的なページが表示される。

$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>

ブラウザからlocalhost:8000にアクセスすると…
f:id:RabbitFoot141:20180607104430p:plain
ここまででとりあえず最低限起動するところまでは完了した。


ちなみにLaravelではpublicディレクトリにあるindex.phpがフロントコントローラとなっている。

ルーティング

webアプリケーションを作る上でまずやらないといけないのがルーティング
普段はLeague\RouteをつかってやっているがLaravelではどうおこなうかわからなかったのでまとめてみる。

まずルーティングがどこにあるかというとroutesディレクトリにおいてある。
そうするといくつかのファイルがあると思うが、 web.php がwebインターフェースのルーティングを定義する。

Laravelではルーティング内で毎回App\Http\Controllerを指定しなくて済むようにデフォルトでRouteServiceProviderが設定している。

Laravelで利用できるルート定義メソッドは以下の通り

Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);

HTTPのリクエストメソッドに対応している。
引数のタイプは、URIクロージャーになっている。

複数のHTTPリクエストメソッドを利用したい場合はmatchメソッドを使用して

<?php
Route::match(['get', 'post'], '/', function () {
    //
});

Route::any('foo', function () {
    //
});

のように書くこともできる
anyはすべてのHTTPリクエストメソッドに対してルーティングを登録する場合にもちいられる。

また一般的なGETリクエストを使用するルーティングは次のように書かれる

<?php
Route::get('/hello', 'HogeController@index');

第一引数については言う必要はないと思うが、問題は第二引数。
呼び出すコントローラのメソッドを@で指定している。

また、単純にViewを表示するだけなら以下のようにも書ける。

<?php
Route::view('/welcome', 'welcome');
Route::view('/welcome', 'welcome', ['name' => 'Taylor']);


シンプルなルーティングはこれでかけるがもう少し詳しく書きたい場合がある。
そういう時にルートパラメータは次のように書ける。

<?php
Route::get('user/{id}', function ($id) {
    return 'User '.$id;
});

Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
    //
});


パラメータ名の後ろに?をつけると任意パラメータになる。

<?php
Route::get('user/{name?}', function ($name = null) {
    return $name;
});

Route::get('user/{name?}', function ($name = 'John') {
    return $name;
});

さらに詳しいことは公式ドキュメントに書いてある。
ルーティング 5.6 Laravel

リクエスト周り

LaravelでHTTPリクエストのインスタンスを利用するには
Illuminate\Http\Requestクラスをコントローラのメソッドに指定する必要がある

ひとまず、以下のクラスとルーティングを前提に説明していく

<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;

class SampleController extends BaseController
{

    public function index()
    {
        return 'hello,world';
    }

    public function sampleRequest(Request $request)
    {
        $name = $request->name;
        return $name;
    }
    
    public function sampleRequestWithParam(Request $request, $val)
    {
        return $id;
    }

}
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::get('hello', function()
{
    return 'Hello,World';
});

Route::get('/sample', 'SampleController@index');
Route::get('/sample2/{name}', 'SampleController@sampleRequest');
Route::get('/sample3/{val}', 'SampleController@sampleRequestWithParam');

このコードにおいて、 パラメータの値を取得する方法は2つある。
ひとつ目が、

<?php
$name = $request->name;

上記の部分。
こちらは動的プロパティを参照する方法となる。

ふたつ目が

<?php
public function sampleRequestWithParam(Request $request, $val)
    {
        return $id;
    }

の引数部分。
プロパティ名を変数として宣言するとそちらに直接代入される。


プロパティだけでなく、URL全体を取得するのは次のように行う。

<?php
// クエリストリングなし
$url = $request->url();

// クエリストリング付き
$url = $request->fullUrl();

クエリストリングから値を取り出したい場合は以下のようにする

<?php
$name = $request->query('name');


またこのRequestクラスは、PSR7を満たしている以下のパッケージで置き換えることができる

composer require symfony/psr-http-message-bridge
composer require zendframework/zend-diactoros

その他、リクエストまわりは先程と同様に公式に全てまとまっている
HTTPリクエスト 5.6 Laravel

ビュー

LaravelのViewはBladeを使用している。
Bladeは .blade.php という拡張子でテンプレートを用意し
サーバサイドから呼び出しとデータを渡して表示する。という手順で利用する

//sample_view.blade.php
<html>
    <body>
        <h1>Hello, {{ $name }}</h1>
    </body>
</html>

ルーティングでは以下のように直接viewを呼び出す方式を今回は取ってみる。

<?php
Route::get('/sample4', function (){
    return view('sample_view', ['name' => 'scala']);
    //view('sample_view')->with('name', 'scala');でも同様に行える
});

雑感だけど、League/Plateと似ている
諸々のテンプレートエンジンとほとんど使い方自体は同じ

コントローラ

コントローラはapp/Http/Controllers以下のディレクトリに配置する。
namespaceはPSR準拠のものを設定する。

ドキュメントに乗っているサンプルコントローラは以下の通り

<?php

namespace App\Http\Controllers;

use App\User;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
    /**
     * 指定ユーザーのプロフィール表示
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        return view('user.profile', ['user' => User::findOrFail($id)]);
    }
}

このように、コントローラクラスを継承し拡張することでコントローラは作成できる
メソッドではURLパラメータを受け取ることも、PSR7準拠のリクエストを受け取ることも出来る。

ルーティングと関連するところはコントローラの名前空間で、ルーティングではApp\Http\Controllerまでは自動で設定されているので
それより深いnamespaceを持つ場合は適宜指定しないと行けない。


シングルアクションコントローラでは__invokeメソッドを使用することができる。
この場合のルーティングは クラス名@アクションメソッド ではなく、 クラス名 となる。

モデル

モデル関連は公式ドキュメントに情報が少ない。
それもそれで何故かわかった。

そもそも、MVCのモデルは開発者やベンダーで割と作り方がバラバラでそれに対応するためにLaravelでも自由に作れるようになっているとあった。
たしかに、コントローラやビューほどある一定の形式を保つ必要が無いからその理由に同意できる。

Laravelではモデルはartisanコマンドをつかって作る
それらは以下のサイトが参考になった。
udemy.benesse.co.jp

おわりに

題名にあるように相当雑な入門記事が出来上がった。
特にモデルなんかはひどい。

しかし、思った以上にLaravelのドキュメントが整っていて
入門記事とうたって機能の大半を紹介するには分量が最高に多くなってしまうため
MVCとルーティング、リクエストというWebアプリを構築する上でこれだけわかっておけばどうにかなりそうなあれこれを最高に雑にまとめてみた

このあと、気合で勉強してその都度、機能を深く掘り下げていきたい

PHPでheaders already sentを解消するためだけに雑にテンプレートエンジンを作った話

はじめに

どうもよく訓練されたJavaer、けんつです。
PHPの勉強がてらオレオレTwitterクライアントを作ろうとしていたら次のエラーにぶち当たった。

Warning: Cannot modify header information - headers already sent by
(output started at /some/file.php:12) in /some/file.php on line 23

調べると

このエラーは HTTP ヘッダーを変更する関数(後述)の呼び出しより前に、すでに何かがアウトプットされているために発生します

とのことで、ならMVCモデルのViewでレンダリングするときはどうすれば良いのだろうかと疑問に思った。
今回はそれを理解するためだけに、最小構成オレオレテンプレートエンジンを雑に作ってみたのでそれについて書いて見ようと思う。

github.com

2018/05/13の段階でとりあえずレンダリングはできる状態にあるが、細かい所や設計面を作りこめていないので今後改良していく。
composerをつかっている環境であれば以下のコマンドで導入できる。

$ composer require lrf141/rook

packagist.org

エラーの原因と解決策

まず原因はHTTPヘッダーを変更する関数をコールするまえに、何かしらを出力していることである。
なのでよくある解決策としてはレンダリングしたいものを

<?
ob_start();

などのように内部バッファに溜め込んで最終的にヘッダーに追加することが挙げられる。

もしくは、サーバ側の設定でアウトプットバッファリングを有効にすることでも回避できる。
今回は前者でやってみる。

テンプレートエンジンの実装

今回自作したテンプレートエンジンのソースは次のような構成になっている。

./src/
├── Directory.php
├── Engine.php
├── FileExtension.php
├── Name.php
└── Template.php

何をやったかというと、雑に説明するなら
テンプレート内で使用したいデータ群を配列として受け取ったらそれを変数として展開し
テンプレートとして利用するファイル(.php)をテンプレートエンジン内でincludeしてレンダリングするというもの。
その過程で、 バッファを利用している。

まず基礎となるEngineクラスは次のように実装した。

<?php
namespace Lrf141\Rook;
class Engine
{

    /**
     * Default template directory
     * @var Directory
     */
    private $directory;

    /**
     * Template file extension
     * @var FileExtension
     */
    private $fileExtension;

    /**
     * create new Engine Instance
     * @param string $base_dir
     * @param string $ext file extension
     */
    public function __construct(string $base_dir, string $ext = 'php')
    {
        $this->directory = new Directory($base_dir);
        $this->fileExtension = new FileExtension($ext);
    }

    /**
     * generate Template
     * 
     * @param string $name template name
     * @return Template
     *
     */
    public function make(string $name): Template
    {
        return new Template($this, $name);
    }

    /**
     * render based on template
     *
     * @param string $template template name
     * @param array $data wanna use in template
     * @return string rendering result
     */
    public function render(string $template, array $data = []): string
    {
        return $this->make($template)->render($data);
    }

    /**
     * get path to templates Directory
     * @return Directory
     */
    public function getDirectory(): Directory
    {
        return $this->directory;
    }

    /**
     * get templates file extension
     * @return FileExtension
     */
    public function getFileExtension(): FileExtension
    {
        return $this->fileExtension;
    }
}

このクラスのインスタンスを生成した時点で基準となるディレクトリと拡張子(ここではphp)を指定してそれらを管理するクラスのインスタンスを保持している。
Engineクラスのインスタンスからrenderメソッドをコールするとテンプレートが生成されて、レンダリング結果のhtmlがstringで返ってくる仕組みとなっている。

諸々の関連クラスのあれこれは省略するとして、一番重要なのがTemplateクラス。

<?php
namespace Lrf141\Rook;
use LogicException;
use Throwable;
use Exception;
class Template
{

    /**
     * @var Engine
     */
    private $engine;

    /**
     * @var Name
     */
    private $name;

    /**
     * @var array
     */
    private $sections = array();

    /**
     * @var array
     */
    private $data = array();

    /**
     * generate new Template Instance
     * @param Engine $engine
     * @param string $name
     */
    public function __construct(Engine $engine, string $name)
    {
        $this->engine = $engine;
        $this->name = new Name($name);
    }

    /**
     * rendering based on template 
     * @param array $data
     * @return string
     */
    public function render(array $data = []): string
    {
        //expands array as var
        $this->data($data);
        unset($data);
        extract($this->data);
        try {
            $level = ob_get_level();
            //dump buffer
            ob_start();
            include $this->path();
            
            $content = ob_get_clean();
            
            return $content;
        } catch (Throwable $e) {
            while (ob_get_level() > $level) {
                ob_end_clean();
            }
            throw $e;
        } catch (Exception $e) {
            while (ob_get_level() > $level) {
                ob_end_clean();
            }
            throw $e;
        }
    }

    /**
     * add data
     * @param array $data
     * @return array|none
     */
    public function data(array $data = null)
    {
        if (is_null($data)) {
            return $this->data;
        }
        $this->data = array_merge($this->data, $data);
    }

    /**
     * generate path
     * @return string
     */
    public function path(): string
    {
        $dir = $this->engine->getDirectory()->get();
        $ext = $this->engine->getFileExtension()->get();
        $name = $this->name->get();
        $path = $dir . '/' . $name . '.' . $ext;
        if (!$this->isExist($path)) {
            throw LogicException(
                `the template path "` . $path . '" does not exist.'
            );
        }
        return $path;
    }

    /**
     * check the path is true
     * @param string $path
     * @return bool
     */
    public function isExist(string $path): bool
    {
        return file_exists($path);
    }
}

この中で重要なのがrenderメソッドで、次の部分で配列を変数として展開している。

<?php

//expands array as var
$this->data($data);
unset($data);
extract($this->data);

特にextract関数がそれをやっている。

その後に

<?php

ob_start();

で、出力で内部バッファの利用を開始する。
その後に

<?php
 include $this->path();
 $content = ob_get_clean();

指定したパスにあるphpファイルをincludeして
ob_get_clean();でバッファの内容をstringで取得して、クリアしている。
これによって、直接出力しなくてもバッファに溜め込んで取得できるため上記のエラーが起きなくなる。
ただ最終的には取得した内容を吐き出す必要が出てくるのでその段階で工夫がまた必要にはなってくる。

それはPSR-7で制定されているHTTPメッセージ辺りの扱いを上手くやっていく必要があるのだと思う。
ただ出力を乱立させないで、テンプレートエンジンを利用することである程度まとめられるのでエラーは起こしにくくなるだろう。

おわりに

エラーを潰すためにこれだけやったがやったことをざっくりとまとめると内部バッファを使っただけなので
その辺り、PSR-7辺りを上手く使って行こうと思う。

PHPでLeague/Routeを使ってルーティングを構成する

はじめに

どうもよく訓練されたJVM教のけんつです。
JVM環境でしか使い物にならないと言われてきた自分がWebサーバサイドのコードをPHPで書く機会がここ半年ほどで多くなってきた。
それでも、Webやったことないしわからんぞ!という感じなので素のPHPでオレオレTwitterクライアントを書く事にした。
とりあえず、イカしたdocker-composeを適当に引っ張ってきてイマドキのなうでヤングな環境を構築するところまではやった。

結構順調だったので、このまま最小構成を作ってしまおうとした矢先に自分を苦しめる存在が出てきた。
そうそれこそが

「素のPHPでルーティングどうやんの?」

という疑問である。


とりあえず、「PHP ルーティング」でぐぐってみると

  • apache 環境では…。いや使ってるのnginxだし!!
  • Cakeでルーティングはこうする…。 いや素のPHPで書きたいんだ!!
  • Laravelではこうする…。 だからフレームワークは使わんと言った!!
  • なかったから自作したお。 わけわからん!!

といった調子になった。

それでもいくつか、FastRouteのような使えそうなライブラリを見つけた。
しかし、以前Play Frameworkで色々やっていた自分に取ってみれば「書く事が多すぎてだるい!!」に尽きる。
それでも探し続けて、やっと「League/Route」なるものを見つけた。

色々とサンプルをみて、「これええやん!」となったが問題はPHPに浸かっている人間でなかったのでわかりにくいというか何をやっているかわからないという個人的な点にあった。
なので、ここでは自分なりに頑張って色々調べてみて「League/Route」を使うための公式にあるサンプルが何をやっているのかまとめてみようと思う。

route.thephpleague.com

環境

まずPHPのバージョンだが、Routeは5.4以降に対応しているので大抵の環境で動作するだろう。

Routeを使うにはcomposerかautoloadで持ってくるのを前提にしている。
自分はPHPを書くときはcomposerを使うのでそちらを参考にする。

$ composer require league/route

またこのライブラリはPSR-7を前提にしているのでそちらも解決する。

composer require zendframework/zend-diactoros

あとの文章に出てくるLeague\Containerでは2.x系のものを使用している。
こちらはバージョンによって仕様がかなり違うので注意が必要。

と環境はここまでで構築できるはず。

ルーティングを構成する基本要素

環境を構築したら、次のようにコードを書けばルーティングが実装できるよ。と公式には書いているが

<?php

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

$container = new League\Container\Container;

$container->share('response', Zend\Diactoros\Response::class);
$container->share('request', function () {
    return Zend\Diactoros\ServerRequestFactory::fromGlobals(
        $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
    );
});

$container->share('emitter', Zend\Diactoros\Response\SapiEmitter::class);

$route = new League\Route\RouteCollection($container);

$route->map('GET', '/', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('<h1>Hello, World!</h1>');

    return $response;
});

$response = $route->dispatch($container->get('request'), $container->get('response'));

$container->get('emitter')->emit($response);

ここらへんを全てフレームワークに投げていた人にはよく分からない。
なので、該当するPSRなどを読んで理解していく。

まず、先頭にあるこのコード

$container = new League\Container\Container;

League\Containerを呼び出しているのだが、ここで指すコンテナってなんぞ?とおもったので調べてみた。

Dependency Injection Container (PSR-11)

DIコンテナと言われるものでPSR-11内で決定されている。
DIコンテナと言われても分からない。なので調べた。
PSR-11については以下のページが
qiita.com

DIについては以下のページが個人的に分かりやすかった。
qiita.com

そもそもDIとは

つまりこれは何かと言われると、例えば以下の処理をしようとする。

<?php
class Sample
{
    private $engine;

    public function __construct()
    {
        $this->engine = new Engine();
    }
}

ここではnew Engineとしたがこれはテンプレートエンジンでもいいし、エンジンでなくてもいい。
依存性を直接コンストラクタやメソッドのスコープ内で生成してしまうコードを指す。

これらは上記のqiita記事でも書かれているが
上のコードだと何が嬉しくないというと

  • Engineクラスを別のクラスに変えたりするとSampleクラスを直接変更しなければならない。
  • Engineが上手く動作してくれないとそもそも、このクラスのテストが出来ない。

という点にある。

それを次のようにすると嬉しいことがある。

<?php
class Car
{
    /**
     * @var EngineInterface
     */
    private $engine;

    /**
     * @param EngineInterface $engine
     */
    public function __construct(EngineInterface $engine)
    {
        $this->engine = $engine;
    }
}

何が嬉しいかというと、依存性が外部で完結しこちらのクラスではそれを受け取っているだけなので変更も容易でテストもしやすい。

しかしこれも残念な点を抱えている。
何かというと色んな依存関係を解消しようとすると引数がめちゃくちゃ長くなりやすいという点。
それと結局依存を注入する手続きは手動で行っており,抽象ではなく具象に対して依存するコードを書くことになるという点。
それらの問題を解決するためにDIコンテナが存在する。

DIコンテナとは

DIコンテナについては以下のサイトがわかりやすくまとまっていた。
qiita.com

つまり、今まで抽象っぽくなってたけど結局具象な上に手動で行っていたものを別な場所(DIコンテナ)に突っ込んで管理しよう。というのを実現するためにある。(らしい)

今回の環境ではLeague/Containerを使っているので公式から使用例を引用してくる。
container.thephpleague.com

まずは前提として以下のようなコードがあるとする。

<?php

namespace Acme\Service;

class SomeService
{
    // ...
}

これに対してDIコンテナを次のように使うことができる

<?php

$container = new League\Container\Container;

// register the service as a prototype against an alias
$container->add('service', 'Acme\Service\SomeService');

// now to retrieve this service we can just retrieve the alias
// each time we `get` the service it will be a new instance
$service1 = $container->get('service');
$service2 = $container->get('service');

var_dump($service1 instanceof Acme\Service\SomeService); // true
var_dump($service1 === $service2); // false

使い方としてはコンテナにDIしたいサービスのクラスを渡すこと。
それをgetすることで実際にそれらを利用することができる。
しかし、add/getではgetするたびに新しいインスタンスになるため

$service1 === $service2

という、評価式がfalseを返す。この点には注意したい。

実際にルーティングを組むときにはgetではなくshareメソッドが呼ばれているが実装は以下のようになっている。

<?php

    /**
     * {@inheritdoc}
     */
    public function share($alias, $concrete = null)
    {
        return $this->add($alias, $concrete, true);
    }

addメソッドに$shareというフラグをtrueにして呼び出しているだけである。
公式ドキュメントでは$shareがtrueになっているもの関しては先程の評価式がtrueになる。
つまり同一のインスタンスが返ることになる。
同一インスタンスが返る場合としては、addメソッドをコールした場合でもクラスインスタンスを直接渡す場合もなると公式に書いていた。


ここまでの内容を理解すれば、ルーティング設定時のこの部分を理解できる。

<?php

$container = new League\Container\Container;

$container->share('response', Zend\Diactoros\Response::class);
$container->share('request', function () {
    return Zend\Diactoros\ServerRequestFactory::fromGlobals(
        $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
    );
});

$container->share('emitter', Zend\Diactoros\Response\SapiEmitter::class);

まずDIコンテナを作り、そこにサービスをshareメソッドを使って追加している。
具体的にはPSR-7に準拠したサービスである、ResponseとRequestにEmitterを追加している。

が、しかしPSR-7とは何者か知らなかったので次はそれをまとめる。

HTTP message interfaces (PSR-7)

PSR-7は何を決めているかというと、題名の通りPHPでHTTP通信を扱うときのリクエストとレスポンスの標準となる仕様を決定している。
これに関しては公式と以下のサイトが役に立った。

www.php-fig.org
qiita.com

全てのHTTPリクエストには特定の形式が存在しRequestは以下のような

POST /path HTTP/1.1
Host: example.com

foo=bar&baz=bat

Responseは次のような形式をとる。

HTTP/1.1 200 OK
Content-Type: text/plain

This is the response body

これをPSR-7では次の2つのインターフェースを使って宣言している

Psr\Http\Message\RequestInterface
Psr\Http\Message\ResponseInterface

これらを使うことで、自分でああだこうだしないでHTTPリクエストをPHPで扱うことができる。
ここをまとめようと思うと、DIコンテナの比にならないくらい文章量が多くなってしまうのでここでは割愛。後日別記事にする。

しかし、何故これが必要なのかというとルーティングを構成する次の部分で使っているからである。

<?php
$route->map('GET', '/', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('<h1>Hello, World!</h1>');

    return $response;
});

そう、この部分をみてわかるようにルーティングを構成して呼び出すメソッドはPSR-7準拠のインターフェースを引数に持ち$responseを返すことになっているからだ。
今上記のコードだと、HTTPレスポンスのボディにHTMLを書き込みそれを返しているのでHello,World!が表示されるといった状況になっている。
これはこの後、クラスを指定する場合でも同様に呼び出すメソッドには関係してくる。

Dispatchから先

残りはこの部分。

<?php

$response = $route->dispatch($container->get('request'), $container->get('response'));

$container->get('emitter')->emit($response);

ここではdispatchに先ほど設定したDIコンテナの値を使っている。
それをdispatchメソッドに渡している。dispatchメソッドの実装は次の通り。

<?php
    /**
     * Dispatch the route based on the request.
     *
     * @param \Psr\Http\Message\ServerRequestInterface $request
     * @param \Psr\Http\Message\ResponseInterface      $response
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    public function dispatch(ServerRequestInterface $request, ResponseInterface $response)
    {
        $dispatcher = $this->getDispatcher($request);
        $execChain  = $dispatcher->handle($request);
        foreach ($this->getMiddlewareStack() as $middleware) {
            $execChain->middleware($middleware);
        }
        try {
            return $execChain->execute($request, $response);
        } catch (Exception $exception) {
            $middleware = $this->getStrategy()->getExceptionDecorator($exception);
            return (new ExecutionChain)->middleware($middleware)->execute($request, $response);
        }
    }

dispatcherを取得した後にDispatcherクラスのhandleメソッドをコールしてミドルウェアを適用しそれらを実行している。
最後に今までのあれこれをemit(うまく表現できる日本語がわからなかった)することでルーティングで設定したあれこれが実行される。

ルーティング周りをもう少し頑張ってみる

これまでまとめたところが実は公式で言うところのHello Worldでしかない。
PHPをやったこともない人間が雑に触ろうとした結果PSR-7,11を調べたりしないと行けないのですごく大変だった。
ただ、ここまでやった段階だけでは無名関数を毎回書いて呼び出す必要があったり、もしルーティング内でワイルドカードを使用したい場合などどうするんだとなるのでそのあたりを公式サイトをみてまとめていく。

ワイルドカードを使いたい場合

これが自分自身このライブラリを使いたかった理由のひとつでもあるが、ルーティングとしてpathを設定するときにワイルドカードは次のように使える。

<?php

$router = new League\Route\RouteCollection;

$router->map('GET', '/user/{id}/{name}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
    // $args = [
    //     'id'   => {id},  // the actual value of {id}
    //     'name' => {name} // the actual value of {name}
    // ];

    return $response;
});

もっと値を限定して使うなら次のようにする。

<?php

$router = new League\Route\RouteCollection;

// this route will only match if {id} is numeric and {name} is a alpha
$router->map('GET', '/user/{id:number}/{name:word}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
    // $args = [
    //     'id'   => {id},  // the actual value of {id}
    //     'name' => {name} // the actual value of {name}
    // ];

    return $response;
});

ここで指定できる種別には以下のものがある。

  • number
  • word
  • alphanum_dash
  • slug
  • uuid

ここまででも十分便利なのに正規表現を適用することもできる。

<?php

$router = new League\Route\RouteCollection;

$router->addPatternMatcher('wordStartsWithM', '(m|M)[a-zA-Z]+');

$router->map('GET', 'user/mTeam/{name:wordStartsWithM}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
    // $args = [
    //     'id'   => {id},  // the actual value of {id}
    //     'name' => {name} // the actual value of {name}
    // ];

    return $response;
});

HTTPメソッドでルーティングを設定

今まではmapを使った例が多かったがもう少し便利なものがある。

<?php
$route = new League\Route\RouteCollection;

$route->get('/acme/route', 'Acme\Controller::getMethod');
$route->post('/acme/route', 'Acme\Controller::postMethod');
$route->put('/acme/route', 'Acme\Controller::putMethod');
$route->patch('/acme/route', 'Acme\Controller::patchMethod');
$route->delete('/acme/route', 'Acme\Controller::deleteMethod');
$route->head('/acme/route', 'Acme\Controller::headMethod');
$route->options('/acme/route', 'Acme\Controller::optionsMethod');

直接指定することもできるし、HTTPメソッドごとに処理を分けることもできる。

ホストやスキームを指定する

なんてこともできる。

<?php

$route = new League\Route\RouteCollection;

// this route will respond to http://example.com/acme/route
// or https://example.com/acme/route
$route->map('GET', '/acme/route', 'AcmeController::method')->setHost('example.com');

// this route will only respond to https://example.com/acme/route
$route->map('GET', '/acme/route', 'AcmeController::method')->setScheme('https')->setHost('example.com');

ルーティングをグループとしてまとめる

これが一番使いたかった機能だと思う。
ルーティングをグループ化して追加できるのはスゴク便利。

<?php

$route = new League\Route\RouteCollection;

$route->group('/admin', function ($route) {
    $route->map('GET', '/acme/route1', 'AcmeController::actionOne');
    $route->map('GET', '/acme/route2', 'AcmeController::actionTwo');
    $route->map('GET', '/acme/route3', 'AcmeController::actionThree');
});

もちろんこれらも個別にホストやスキームを設定できる。

<?php

$route = new League\Route\RouteCollection;

$route->group('/admin', function ($route) {
    $route->map('GET', '/acme/route1', 'AcmeController::actionOne');
    $route->map('GET', '/acme/route2', 'AcmeController::actionTwo')->setScheme('https');
    $route->map('GET', '/acme/route3', 'AcmeController::actionThree');
})
    ->setScheme('http')
    ->setHost('example.com')
;

そして実際のリクエストは次のようになる

GET http://example.com/admin/acme/route1
GET https://example.com/admin/acme/route2
GET http://example.com/admin/acme/route3

これスゴク便利。

さいごに

とりあえず、ここまでが自分の使いたかった機能を一通り網羅した。
ルーティングについて雑に書くはずがDIコンテナやPSRについてまで触れるとは思わなかったけどかなり理解が深まった。
関連するPSRやその他についてもこの後まとめていきたい。

あと自分はWebあたりの知識が割と不足していてわからないことだらけだったので、単純に語句とか調べてなるほどなとなるより
コードをみてこういうふうになっているのかと理解する方がよいということがわかったのは収穫だった