読者です 読者をやめる 読者になる 読者になる

PHPでSQSのFIFOキューを使った時のメモ

SQSのFIFOキューを使用する時に調べた事を記述します。

みんな大好きSQSは昔から存在する手軽に利用できるキューシステムですが、メッセージの順序保障と二重送信などはアプリ側で考慮する必要がありました。
しかし何時の間にやらFIFOキューとして機能が強化されていましたので調べてみた次第です

Amazon Simple Queue Service の新機能 – 1 回だけの処理と重複排除機能を備えた FIFO キュー | Amazon Web Services ブログ
※ただし、現在はUS East (Ohio) および US West (Oregon)でのみ利用可能な機能のようです。

まずはaws上で「オレゴン」リージョンにSQSのキューを作成します。
FIFOキューを選択肢します。

f:id:taka512:20170427190730p:plain

既にcomposerで以下のバージョンのawssdkを導入済とします

"aws/aws-sdk-php": "3.26.2"

以下のように送信処理を実装します。

// send.php
$sqs = SqsClient::factory([
    'credentials' => [
      'key' => YOUR_KEY,
      'secret' => YOUR_SECRET_KEY,
    ],
    'region' => 'us-west-2',
    'version' => '2012-11-05',
]);

$time = time();
for($i =0; $i < 10; $i++){
    $sqs->sendMessage([
        'QueueUrl'    => 'https://sqs.us-west-2.amazonaws.com/xxx/xxxxx.fifo',
        'MessageBody' => 'test body'.$i,
        'MessageGroupId' => 'group',
        'MessageDeduplicationId' => hash('sha256', $time.$i),
    ]);
}

受信処理は以下のように実装します。

// recv.php
$sqs = SqsClient::factory([
    'credentials' => [
        'key' => YOUR_KEY,
        'secret' => YOUR_SECRET_KEY,
    ],
    'region' => 'us-west-2',
    'version' => '2012-11-05',
]);
$result = $sqs->receiveMessage([
    'MaxNumberOfMessages' => 10,
    'QueueUrl' =>  'https://sqs.us-west-2.amazonaws.com/xxx/xxxxx.fifo',
]);
foreach ($result->search('Messages[]') as $message) {
    $queueHandle = $message['ReceiptHandle'];
    $messageBody = $message['Body'];
    echo $messageBody."\n";
    $sqs->deleteMessage([
        'QueueUrl' => 'https://sqs.us-west-2.amazonaws.com/xxx/xxxxx.fifo',
        'ReceiptHandle' => $queueHandle,
    ]);
}

実行結果

$ php send.php
$ php recv.php
test body0
test body1
test body2
test body3
test body4
test body5
test body6
test body7
test body8
test body9

ちなみにFIFOでない標準のキューを使用すると以下のようになります

$ php send.php
$ php recv.php
test body0
test body4
test body7
$ php recv.php
test body5
test body8
~略~


次に送信処理を以下のようにMessageDeduplicationIdが重複するように書き換えます

'MessageDeduplicationId' => hash('sha256', $time.$i),
↓
'MessageDeduplicationId' => hash('sha256', $time),

実行結果は以下のように最初の1行のみキューイングされます

$ php send.php
$ php recv.php
test body0

但し永遠に重複idは受付しないわけではなく、300秒間のインターバルで重複メッセージを排除するようです。

FIFO (First-In-First-Out) Queues - Amazon Simple Queue Service

キューの設定に以下のように「コンテンツに基づく重複排除」の選択肢があり、これをチェックすると「sendMessage」で「MessageDeduplicationId」の指定を省略できます。
MessageBodyの内容をsha256して自動で「MessageDeduplicationId」に設定してくれるようです。

f:id:taka512:20170427191537p:plain

「MessageGroupId」は同じグループでの順序性を保証するための値なので適当な文字列を設定するとよさげでした。

PHPのslim3でEloquent\Modelを使用したモデルの実装

illuminate/databaseはEloquentというORM機能も利用できるのでslim3での利用法を記述

github.com

以下のようにモデルクラスを作成します。

# src/Sample/Model/User.php
<?php
namespace Sample\Model;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
    protected $table = 'user';
    public function getAttribute($key) {
        return parent::getAttribute(\snake_case($key));
    }
    public function setAttribute($key, $value){
        return parent::setAttribute(\snake_case($key), $value);
    }
}

dependencies.phpに「$container->get('db');」の初期化の1行を追加します。

# src/dependencies.php
$container['db'] = function ($c) {
~略~
};
$container->get('db');

コントローラで以下のようにクエリービルダーの形式で記述する事でDBからデータを取得できます

<?php
namespace Sample\Controller;
use Psr\Container\ContainerInterface;
use Sample\Model\User;
class UserController {
    protected $container;
    public function __construct(ContainerInterface $container) {
        $this->container = $container;
    }
    public function index($request, $response, $args) {
        $users = User::all();
        return $this->container['renderer']->render($response, 'user/index.twig', ['users' => $users]);
    }
}

その他Eloquentのモデルの利用法を以下を参照

Eloquent: Getting Started - Laravel - The PHP Framework For Web Artisans

illuminate/databaseを使用したデータベース操作

素で「illuminate/database」を使用したデータベース操作のまとめ

github.com

前回の記事で以下のような感じにdbの接続設定を追加したので、それを使用します。

$container['db'] = function ($c) {
    $capsule = new \Illuminate\Database\Capsule\Manager;
    $capsule->addConnection($c['settings']['db']);
    $capsule->setAsGlobal();
    $capsule->bootEloquent();
    return $capsule;
};

素のSQLを使用したデータベース操作

# insert
$container['db']->getConnection()->insert('INSERT INTO user (name, created_at, updated_at) VALUES(:name, now(), now())',['name' => 'test1']);

# update
$container['db']->getConnection()->update('UPDATE user SET name = :name, updated_at = now() WHERE id = 3',['name' => 'test10']);

# delete
$container['db']->getConnection()->delete('DELETE FROM user WHERE name = :name',['name' => 'test4']);

# select
$container['db']->getConnection()->select('SELECT * FROM user WHERE id > :id', ['id' => 3]);

# 戻り値が返ってこないようなデータベース操作のSQL
$container['db']->getConnection()->statement('DROP TABLE user');

# トランザクション
$container['db']->getConnection()->beginTransaction();
try {
    $container['db']->getConnection()->insert('INSERT INTO user (name, created_at, updated_at) VALUES(:name, now(), now())',['name' => 'test']);
    $container['db']->getConnection()->commit();
} catch (\Exception $e) {
    $this->container['db']->getConnection()->rollback();
}

クエリビルダーを使用したデータベースの操作

# insert
$this->container['db']->table('user')->insert(['id' => 8, 'name' => 'test8', 'created_at' =>  \Carbon\Carbon::now(), 'updated_at' =>  \Carbon\Carbon::now()]);

# update
$this->container['db']->table('user')->where('id', '=', 2)->update(['name' => 'test12']);

# delete
$this->container['db']->table('user')->where('id', '=', 7)->delete();

# select
$users = $this->container['db']->table('user')->where('id', '>', 1)->get();


その他、クエリービルダーの記述法

laravel.com

PHPのslim3でデータベースに接続する

今回はデータベースに接続する設定を追加します。

DBに接続するライブラリは以下を使います。

github.com

composer.jsonに以下の設定を追加してcomposer updateします。

composer.json
@@ -15,6 +15,7 @@
         "php": ">=5.5.0",
         "slim/slim": "^3.1",
         "slim/twig-view": "2.2.0",
+        "illuminate/database": "v5.4.13",

MySQLに接続する環境はできてるとしてMySQLに以下のコマンドでテーブルを追加します

CREATE DATABASE sample default character set utf8;
GRANT ALL ON sample.* TO sample@'%' IDENTIFIED BY 'password';
CREATE TABLE `user` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ユーザID',
      `name` varchar(256) NOT NULL COMMENT 'ユーザ名',
      `created_at` datetime NOT NULL COMMENT '登録日',
      `updated_at` datetime NOT NULL COMMENT '更新日',
      PRIMARY KEY (`id`)
);
INSERT INTO user VALUES (1,'test_user',now(),now());

settings.phpMySQL用の接続設定を追加します

src/settings.php
     'settings' => [
         'displayErrorDetails' => true, // set to false in production
         'addContentLengthHeader' => false, // Allow the web server to send the content-length header
+        'determineRouteBeforeAppMiddleware' => false,
+        'displayErrorDetails' => true,
+
+        'db' => [
+            'driver' => 'mysql',
+            'host' => 'mysql-server',
+            'database' => 'sample',
+            'username' => 'sample',
+            'password' => 'password',
+            'charset'   => 'utf8',
+            'collation' => 'utf8_unicode_ci',
+            'prefix'    => '',
+        ],

dependencies.phpにdb用のコンテナ設定を追加します

src/dependencies.php
@@ -3,6 +3,16 @@

 $container = $app->getContainer();

+$container['db'] = function ($c) {
+    $capsule = new \Illuminate\Database\Capsule\Manager;
+    $capsule->addConnection($c['settings']['db']);
+    $capsule->setAsGlobal();
+    $capsule->bootEloquent();
+    return $capsule;
+};

最後にコントローラに以下のようにDBにアクセスするコードを記述します。

src/Sample/Controller/TopController.php
@@ -15,6 +15,7 @@ class TopController
     public function index($request, $response, $args)    {
+        $user = $this->container['db']->table('user')->find(1);
+        return $this->container['renderer']->render($response, 'top/index.twig', ['message' => 'Hello '.$user->name]);
     }
 }

これでアクセスするとDBの内容が表示されるようになります

PHPのslim3にtwigの設定を追加

前回TOPページのコントローラの追加を行ったので、今回はテンプレートの追加をします。
テンプレートにはtwigを使用します。

composer.jsonを更新してtwigのパッケージをインストールします

$ vi composer.json
    "require": {
        "php": ">=5.5.0",
        "slim/slim": "^3.1",
        "slim/twig-view": "2.2.0",
$ php composer.phar update

rendererの設定にtwig用の項目を設けます。

$ vi src/settings.php
        // Renderer settings
        'renderer' => [
            'template_path' => __DIR__ . '/../templates/',
            'twig_settings' => [
                'cache' => __DIR__ . '/../cache',
            ],
        ],

コンテナのrendererにはphp-viewの設定がありますが、twigの設定に書き換えます。

$ vi src/dependencies.php
$container['renderer'] = function ($c) {
    $settings = $c->get('settings')['renderer'];
    $view = new \Slim\Views\Twig($settings['template_path'], $settings['twig_settings']);
    $basePath = rtrim(str_ireplace('index.php', '', $c['request']->getUri()->getBasePath()), '/');
    $view->addExtension(new \Slim\Views\TwigExtension($c['router'], $basePath));
    return $view;
};

コントローラでtwigのテンプレートを使用するように書き換えます。

    public function index($request, $response, $args){
        return $this->container['renderer']->render($response, 'top/index.twig', ['message' => '<test>']);
    }

twigテンプレートを追加します

$ vi templates/top/index.twig
{{ message }}

これでtwig用の設定の追加は完了です

PHPのslim3で環境構築

久しぶりにslim3で素のフォームを作るまでを勉強しなおしたので筆をとりました。
この記事ではコントローラの追加までを記述します。

slimはマイクロフレームワークですが、公式でスケルトン機能を用意されているのでそれを利用します。

github.com

ケルトンをチェックアウト

$ php composer.phar create-project slim/slim-skeleton sample
Installing slim/slim-skeleton (3.1.2)
  - Installing slim/slim-skeleton (3.1.2): Downloading (100%)
~(略)~
Writing lock file
Generating autoload files
$

すると以下のような感じのディレクトリ構成で環境が作成されます。

sample
|-- CONTRIBUTING.md
|-- Makefile
|-- README.md
|-- composer.json
|-- composer.lock
|-- composer.phar
|-- logs
|-- phpunit.xml
|-- public
|   `-- index.php
|-- src
|   |-- dependencies.php
|   |-- middleware.php
|   |-- routes.php
|   `-- settings.php
|-- templates
|   `-- index.phtml
|-- tests
|   `-- Functional
`-- vendor

これでpublicディレクトリをドキュメントルートに設定すると簡単な画面が表示されます。

f:id:taka512:20170401231509p:plain
ルーティングにコントローラを追加する前にクラスのオートロード設定を追加します。

具体的にはcomposer.jsonに以下の記述を追加して更新します。

$ vi composer.json
    "autoload": {
        "psr-4": {
            "Sample\\": "src/Sample/"
        }
    },
$ php composer.phar update

ルーティングのファイルを編集します

$ vi src/routes.php
<?php
// Routes
$app->get('/', \Sample\Controller\TopController::class . ':index');

コントローラのファイルを追加します

$ vi src/Sample/Controller/TopController.php
<?php

namespace Sample\Controller;

use Psr\Container\ContainerInterface;

class TopController
{
    protected $container;
    public function __construct(ContainerInterface $container){
        $this->container = $container;
    }
    public function index($request, $response, $args){
        echo 'Hello World';
        return $response;
    }
}

これで再度トップページにアクセスすると「Hello World」と表示されるようになります。

pyramidのjinjaテンプレートで独自filterを定義

pyramidのテンプレートで「nl2br」のような独自フィルターを挟みたいと思った時にメモです。
今回は「hoge2hage」という「hoge」を「hage」に変換するフィルターを定義してみます。

1. フィルター処理を作成
適当なところに処理を作成します。

$ vi myapp/filters.py
def hage2hoge(value):
    return re.sub(r'hage', 'hoge', value)

2. フィルターを定義
定義をコンフィグファイルに記載します。

$ vi development.ini
[app:main]
use = egg:myapp

pyramid.reload_templates = true
pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
pyramid.includes =
    pyramid_debugtoolbar
jinja2.filters =
    hoge2hage = myapp.filters:hoge2hage

3. テンプレートでフィルターを使用
あとは以下のように使用すればjinjaテンプレートのフィルターとして動作します。

$ vi myapp/templates/hello.jinja2
hello {{data|hoge2hage}}