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

いろんなレイヤーに居ます

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

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で使用するディレクトリを作る
$ 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辺りを上手く使って行こうと思う。