ab(apache bench)の使い方メモ

JSONリクエストを受け取るAPIベンチマークを採りたいと思います。

  • BASIC認証が必要な場合は、「-A ユーザ名:パスワード」オプションで。
  • Content-Typeの指定は「-T」オプションで。
  • POSTデータは「-p」オプションにファイルパスを渡します。
    • xamppにバンドルされてるabはwindows版(ab.exe)なので、cygwin上でもパスはwindows形式で指定します。
  • 拡張ヘッダは「-H "ヘッダ名: 値"」オプションで。
  • その他の主要なオプション
    • -n リクエスト回数
    • -c 同時リクエスト数
    • -w 結果をHTML形式で出力
$ ab.exe -n 5 -A "user1:passwd1" http://localhost/authz/

$ ab.exe -n 5 -p "C:\...\xxxx.json" -T "application/json" http://localhost/json/
  • 結果のポイント
    • Document Length : 想定通りの長さ?(エラードキュメントが返っていないか?)
    • Failed requests
    • Non-2xx responses
    • Requests per second
    • Time per request

JSONスキーマバリデータとオリジナルのエラーメッセージ

justinrainbow/json-schema は大変便利なんですが、どの制約(Constraints)に引っかかったのか分かりません。
オリジナルのエラーメッセージの出し分けに'message'をヒントにしようと思います。

制約キーワード

分類 キーワード
必須系 require, defined
無効系 'value found, but a', valid, format, missing, 'must be', allow, known, match, regex

注意

データ型の制約エラーの場合、messageにキーワード「required」が含まれます。

Type.php:  gettype($value) value found, but a $type is required

制約エラーの場合にどんなmessageが設定される?

$ find vendor/justinrainbow/json-schema -type f -name "*.php" | xargs grep "this->addError(" | grep ...

Collection.php: There must be a minimum of $schema->minItems in the array
Collection.php: There must be a maximum of $schema->maxItems in the array
Collection.php: There are no duplicates allowed in the array
Collection.php: The item ' $i '[' $k '] is not defined and the definition does not allow additional items

Enum.php:       does not have a value in the enumeration print_r($schema->enum, true)

Format.php:     Invalid date %s, expected format YYYY-MM-DD, json_encode($element)
Format.php:     Invalid time %s, expected format hh:mm:ss, json_encode($element)
Format.php:     Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm, json_encode($element)
Format.php:     Invalid time %s, expected integer of milliseconds since Epoch, json_encode($element)
Format.php:     Invalid regex format $element
Format.php:     Invalid color
Format.php:     Invalid style
Format.php:     Invalid phone number
Format.php:     Invalid URL format
Format.php:     Invalid email
Format.php:     Invalid IP address
Format.php:     Invalid IP address
Format.php:     Invalid hostname
Format.php:     Unknown format: json_encode($schema->format)

Number.php:     must have a minimum value greater than boundary value of $schema->minimum
Number.php:     must have a minimum value of $schema->minimum
Number.php:     use of exclusive Minimum requires presence of minimum
Number.php:     must have a minimum value of $schema->minimum
Number.php:     must have a maximum value less than boundary value of $schema->maximum
Number.php:     must have a maximum value of $schema->maximum
Number.php:     use of exclusive Maximum requires presence of maximum
Number.php:     must have a maximum value of $schema->maximum
Number.php:     is not divisible by $schema->divisibleBy
Number.php:     must be a multiple of $schema->multipleOf

Object.php:     The pattern $pregex is invalid
Object.php:     The property $i is not defined and the definition does not allow additional properties
Object.php:     the presence of the property $i requires that $require also be present

String.php:     must be at most $schema->maxLength characters long
String.php:     must be at least $schema->minLength characters long
String.php:     does not match the regex pattern $schema->pattern

Type.php:       gettype($value) value found, but a $type is required

Undefined.php:  the property $required is required
Undefined.php:  is missing and it is required
Undefined.php:  disallowed value was matched
Undefined.php:  matched a schema which it should not
Undefined.php:  must contain a minimum of $schema->minProperties properties
Undefined.php:  must contain no more than $schema->maxProperties properties
Undefined.php:  failed to match all schemas
Undefined.php:  failed to match at least one schema
Undefined.php:  $key depends on $dependency and $dependency is missing
Undefined.php:  $key depends on $d and $d is missing

Doctrine2 DBAL フェッチのサンプル

実装コード

class TestDoctrineDbalFetchCommand extends ContainerAwareCommand
{
    const EXIT_SUCCESS = 0;
    const EXIT_FAILURE = 1;

    const THROUGHPUT = 5;

    protected function configure()
    {
        $this->setName('sample:dbal-fetch');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $sql = <<< SQL
SELECT
  id, DATE_FORMAT(record_date, '%Y/%m/%d %H:%i:%S') as r_date, temperature
FROM
  mercury2
WHERE
  record_date > :record_date
ORDER BY
  record_date DESC
SQL;

        $output->writeln("-- 処理単位件数 = " . self::THROUGHPUT);

        try {

            $conn = $this->getContainer()->get('doctrine')->getManager()->getConnection();
            $conn->connect();   // 接続エラーの場合はここで例外が発生します

            $date = new \DateTime("2014-09-01");

            $stmt = $conn->prepare($sql);
            $stmt->bindValue(':record_date', $date, 'datetime');
            if (!$stmt->execute()) {
                throw new \RuntimeException("execute() returned false.");
            }

            $count = 0;
            while ($row = $stmt->fetch()) {
                if ($count === 0) {
                    $output->writeln("-- フェッチを開始します");
                }
                $count++;

                $output->writeln(sprintf("%02d : %s, %s, %s",
                    $count, $row['id'], $row['r_date'], $row['temperature']));

                if ($count % self::THROUGHPUT === 0) {
                    $output->writeln("-- 現在 ${count} 件目です");
                }
            }

            $conn->close();
            $output->writeln("-- 合計 ${count} 件を処理しました");

            return self::EXIT_SUCCESS;

        } catch (\Exception $e) {
            $conn->close();
            $output->writeln("-- 例外が発生しました : " . $e->getMessage());
            return self::EXIT_FAILURE;
        }

    }
}

実行結果

$ php app/console sample:dbal-fetch
-- 処理単位件数 = 5
-- フェッチを開始します
01 : 554, 2014/09/12 00:00:00, 21
02 : 553, 2014/09/11 00:00:00, 20
03 : 552, 2014/09/10 00:00:00, 21
04 : 551, 2014/09/09 00:00:00, 22
05 : 550, 2014/09/08 00:00:00, 20
-- 現在 5 件目です
06 : 549, 2014/09/07 00:00:00, 20
07 : 548, 2014/09/06 00:00:00, 26
08 : 547, 2014/09/05 00:00:00, 26
09 : 546, 2014/09/04 00:00:00, 23
10 : 545, 2014/09/03 00:00:00, 23
-- 現在 10 件目です
11 : 544, 2014/09/02 00:00:00, 23
-- 合計 11 件を処理しました

Symfony2 Console Table Helper を試してみます

実装コード

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ConsoleTableHelperCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this->setName('sample:table-helper');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $em = $this->getContainer()->get('doctrine')->getManager();
        $repo = $em->getRepository('XxxxBundle:Mercury');

        $dql = <<<DQL
SELECT
  m.mercury, COUNT(m.mercury) cnt
FROM
  XxxxBundle:Mercury m
GROUP BY
  m.mercury
DQL;

        $rows = $em->createQuery($dql)->getResult();

        $table = $this->getHelper('table');
        $table->setHeaders(array('Mercury', 'Count'));
        $table->setRows($rows);
        $table->render($output);
    }
}

実行結果

$ php app/console sample:table-helper
+---------+-------+
| Mercury | Count |
+---------+-------+
| 15.00   | 89    |
| 16.00   | 79    |
| 17.00   | 84    |
...
| 33.00   | 68    |
| 34.00   | 93    |
| 35.00   | 80    |
+---------+-------+

これは助かりますね!

Symfony2 バンドル固有のパラメータ定義

バンドル固有のパラメータは、Resources/config/parameters.yml に定義するのが一般的の様です。

app/config/config.yml

imports:
    - { resource: parameters.yml }
    - { resource: @FooBarBundle/Resources/config/parameters.yml }

src/Foo/BarBundle/Resources/config/parameters.yml

パラメータ名には、名前空間として「バンドルエイリアス」を接頭辞として付けるのがオススメだそう。

parameters:
    foo_bar.parameter: "Hello"

src/Foo/BarBundle/Controller/XxxxController.php

パラメータはコンテナから取得することができます。

$value = $this->container->getParameter('foo_bar.parameter')

Goutte でスクレイピング

Goutte は a screen scraping and web crawling library for PHP だそうです。
私は twilog をブックマーク代わりに使っているのですが、 Goutte を使ってツイートをスクレイピングしてみようと思います。

なお

Goutte depends on PHP 5.4+ and Guzzle 4+.
If you need support for PHP 5.3 or Guzzle 3, use Goutte 1.0.6.

との事なので、ご注意ください。

インストール

  • composer.json

    { "require": { "fabpot/goutte": "~2.0" },

  • composer 実行
$ php composer.phar update fabpot/goutte

実装コード

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Goutte\Client;

class ScrapingByGoutteCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this->setName('sample:goutte');
        $this->addArgument('url', InputArgument::REQUIRED);
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $url = $input->getArgument('url');

        $client = new Client();
        $crawler = $client->request('GET', $url);

        $crawler->filter(
                'body > div#container > div#content > section.tl-tweets > article > p.tl-text'
            )->each(function($node) {
            echo trim($node->text()) . PHP_EOL;
        });
    }
}

これはお手軽ですね!

Symfony2 phpunitでJSON-APIの機能テストをします

JSONテキストを返すAPIをSymfony2で実装して、実際にリクエストを投げてその応答をテストしてみようと思います。

テスト対象のAPIコントローラ

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

class ApiController extends Controller
{
    // $ curl http://localhost/Xxxx/web/app_dev.php/api/ -d id=123 --data-urlencode name="Foo bar" -i
    public function indexAction(Request $request)
    {
        $params = $request->request->all();

        return new JsonResponse($params);
    }
}

テストコード

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;

// WebTestCase を継承すると、WebTestCase クラスがカーネルを準備してくれます
class JsonServerFunctionalTest extends WebTestCase
{
    private $client;

    public function setUp()
    {
        // ブラウザをシミュレートするクライアントを取得します
        $this->client = static::createClient();
    }

    public function testJsonUrl()
    {
        /*
         * リクエストを送信して、Crawler オブジェクトを得ます
         * Crawlerは、HTML/XML形式のレスポンスをパースして
         * DOMで操作できるメソッドを提供します(JSONは未対応?)
         */
        $crawler = $this->client->request('POST', '/api/',
            array('id' => 123, 'name' => 'Foo Bar'),        // parameters
            array(),                                        // files
            array(
                'Content-Type' => 'x-www-form-urlencoded'   // server
            )
        );

        $response = $this->client->getResponse();

        $this->assertEquals(200, $response->getStatusCode(),
            "レスポンスのステータスコードが 200 であること");

        $this->assertTrue(
            $response->headers->contains('Content-Type', 'application/json'),
            "レスポンスヘッダの Content-Type が 'application/json' であること"
        );

        // レスポンスボディを取得します
        $content = $response->getContent();

        $json = json_decode($content, true);
        $this->assertNotNull($json,
            'レスポンスボディがJSONテキスト(=JSONデコード成功)であること');

        // 以下、応答パラメータの検証など
    }
}

Symfony2 一時ファイルに関して

Symfony2アプリケーション内で一時的なファイルを作成する場合、こちらによれば、キャッシュディレクトリ内がオススメの様です。

  • Kernel::getCacheDir() でキャッシュディレクトリのパスを取得できます。
  • ExtensionInterface::getAlias() でバンドル名のエイリアスを得ることができます。
    エイリアスは、小文字+アンダースコアで構成されるので、パスに使うのに都合が良さそうです。
  • ファイル/ディレクトリ操作は、Symfony2のFilesystemコンポーネントが便利そうです。
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Exception\IOException;

class TestWorkFileCommand extends ContainerAwareCommand
{

    protected function configure()
    {
        $this->setName('sample:work-file');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $kernel = $this->getContainer()->get('kernel');
        $bundle = $kernel->getBundle('XxxxSampleBundle');

        $bundleAlias = $bundle->getContainerExtension()->getAlias();
        $cacheDirPath = $kernel->getCacheDir();
        $workDirPath = $cacheDirPath . DIRECTORY_SEPARATOR . $bundleAlias;

        echo "bundleAlias  = ${bundleAlias}" . PHP_EOL;
        echo "cacheDirPath = ${cacheDirPath}" . PHP_EOL;
        echo "workDirPath  = ${workDirPath}" . PHP_EOL;

        $fs = new Filesystem();

        if (!$fs->exists($workDirPath)) {
            echo "${workDirPath} が存在しないので作ります". PHP_EOL;

            try {
                // 既に存在する場合は、例外は発生しないみたいです
                $fs->mkdir($workDirPath, 744);
            } catch (IOException $e) {
                echo $e->getMessage() . PHP_EOL;
            }
        }

        $workDir = new \SplFileInfo($workDirPath);

        if ($workDir->isDir() and $workDir->isWritable()) {
            echo 'ファイルをタッチします' . PHP_EOL;

            try {
                $fs->touch($workDirPath . DIRECTORY_SEPARATOR . 'dummy.file');
            } catch (IOException $e) {
                echo $e->getMessage() . PHP_EOL;
            }
        }
    }
}

Symfony2 カーネルが保持している情報について

簡易コードを書いて調べてみました。

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ShowConfigCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this->setName('sample:show-config');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $kernel = $this->getContainer()->get('kernel');
        $bundle = $kernel->getBundle('XxxxSampleBundle');

        print <<< BUF
kernel:
  env:      {$kernel->getEnvironment()}
  RootDir:  {$kernel->getRootDir()}
  CacheDir: {$kernel->getCacheDir()}
  LogDir:   {$kernel->getLogDir()}

bundle:
  Name:      {$bundle->getName()}
  Namespace: {$bundle->getNamespace()}
  Alias:     {$bundle->getContainerExtension()->getAlias()}

BUF;
    }
}

実行結果

$ php app/console sample:show-config
kernel:
  env:      dev
  RootDir:  .../app
  CacheDir: .../app/cache/dev
  LogDir:   .../app/logs

bundle:
  Name:      XxxxSampleBundle
  Namespace: Xxxx\SampleBundle
  Alias:     xxxx_sample

Doctrine2 Mapping Type を確認します

Entity

以下の様なエンティティを用意して、 $ php app/console doctrine:schema:update --force で、DBスキーマを作ってみます。

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="foobar")
 */
class FooBar
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     */
    protected $field_string;

    /**
     * @ORM\Column(type="integer")
     */
    protected $field_integer;

    /**
     * @ORM\Column(type="smallint")
     */
    protected $field_smallint;

    /**
     * @ORM\Column(type="bigint")
     */
    protected $field_bigint;

    /**
     * @ORM\Column(type="boolean")
     */
    protected $field_boolean;

    /**
     * @ORM\Column(type="decimal")
     */
    protected $field_decimal;

    /**
     * @ORM\Column(type="date")
     */
    protected $field_date;

    /**
     * @ORM\Column(type="time")
     */
    protected $field_time;

    /**
     * @ORM\Column(type="datetime")
     */
    protected $field_datetime;

    /**
     * @ORM\Column(type="text")
     */
    protected $field_text;

    /**
     * @ORM\Column(type="object")
     */
    protected $field_object;

    /**
     * @ORM\Column(type="array")
     */
    protected $field_array;

    /**
     * @ORM\Column(type="float")
     */
    protected $field_float;
}

MySQL スキーマ

mysql> desc foobar;
+----------------+---------------+------+-----+---------+-------+
| Field          | Type          | Null | Key | Default | Extra |
+----------------+---------------+------+-----+---------+-------+
| id             | int(11)       | NO   | PRI | NULL    |       |
| field_string   | varchar(255)  | NO   |     | NULL    |       |
| field_integer  | int(11)       | NO   |     | NULL    |       |
| field_smallint | smallint(6)   | NO   |     | NULL    |       |
| field_bigint   | bigint(20)    | NO   |     | NULL    |       |
| field_boolean  | tinyint(1)    | NO   |     | NULL    |       |
| field_decimal  | decimal(10,0) | NO   |     | NULL    |       |
| field_date     | date          | NO   |     | NULL    |       |
| field_time     | time          | NO   |     | NULL    |       |
| field_datetime | datetime      | NO   |     | NULL    |       |
| field_text     | longtext      | NO   |     | NULL    |       |
| field_object   | longtext      | NO   |     | NULL    |       |
| field_array    | longtext      | NO   |     | NULL    |       |
| field_float    | double        | NO   |     | NULL    |       |
+----------------+---------------+------+-----+---------+-------+
14 rows in set (0.01 sec)