OrePhalconとOrePuxでPhalconとPuxのルーティングをベンチマークしてみた

(2014-01-15 13:30 追記) この記事のPuxのベンチマーク結果は、Cで書かれたPuxのPHP機能拡張が使われておらず、PHPで書かれたPuxの結果のようです。修正記事を後ほどアップします。

(2014-01-15 16:20 追記) PHP機能拡張が使われていないわけではないことがわかりました。後日、詳細な記事をアップします。

(2014-01-16 追記) 【完全版】Pux - A High Performance PHP Routerのルーティング性能をベンチマークしてみたをアップしました。

GitHubにPuxというPHPのルータがありました。

Pux is 48.5x faster than symfony router in static route dispatching, 31x faster in regular expression dispatching. (with pux extension installed)

ということで、symfony routerより静的なルーティングで48.5倍、正規表現のルーティングでも31倍速い(pux機能拡張をインストールした場合)とのことです。

ということで、brtRiverさんのOrePhalconを参考にベンチマークしてみました。

ベンチマーク環境

OS: Ubuntu 12.04
Apache: 2.4.7
PHP: 5.5.6 (32bit)
Phalcon: 1.2.5
Pux: 1.1.2 (C extension), GitHub master f42393f9d174872b48884363acb1344355aa43da (Tue Jan 14 02:22:26 2014 +0800)

OrePhalcon

まず、OrePhalconをPhalcon 1.2.5で動作するように移植します。

index.php:

<?php

ini_set('display_errors', 1);
error_reporting(-1);

$router = new \Phalcon\Mvc\Router();
$router->add("/hello/:action", array(
    "controller" => "hello",
    "action"     => "say",
    "name"       => 1,
));
$router->handle();

$controller = ucfirst($router->getControllerName());
$action     = $router->getActionName();
$params     = $router->getParams();

try {
    $controllerFilePath = __DIR__ . '/../app/controllers/' . $controller . ".php";
    if (! file_exists($controllerFilePath)) {
         throw new Exception("controller file is not found");
    }
    require $controllerFilePath;
} catch (Exception $e) {
    echo $e->getMessage();
    exit;
}

echo $controller::$action($params);

.htaccess:

AddDefaultCharset UTF-8

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ index.php?_url=/$1 [QSA,L]
</IfModule>

OrePux

次に、OrePhalconをPuxに移植したOrePuxを作成します。

index.php:

<?php

ini_set('display_errors', 1);
error_reporting(-1);

require __DIR__ . '/../vendor/autoload.php';

$mux = new \Pux\Mux();
$mux->get('/hello/:name', ['hello','say']);
$route = $mux->dispatch($_SERVER['PATH_INFO']);

$controller = ucfirst($route[2][0]);
$action     = $route[2][1];
$params     = $route[3]['vars'];

try {
  $controllerFilePath = __DIR__ . '/../app/controllers/' . $controller . ".php";
  if (! file_exists($controllerFilePath)) {
    throw new Exception("controller file is not found");
  }
  require $controllerFilePath;
} catch (Exception $e) {
  echo $e->getMessage();
  exit;
}

echo $controller::$action($params);

.htaccess:

AddDefaultCharset UTF-8

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ index.php/$1 [L]
</IfModule>

Hello controller

Helloコントローラを用意します。

../app/controllers/Hello.php:

<?php

class Hello
{
    public static function say($params)
    {
        return "Hello " . $params['name'];
    }
}

Phalcon Routing vs Pux Routing

ab -n 1000 http://localhost/OrePhalcon/web/hello/xxxのようにベンチマークしてみました。

Framework Requests per second Relative
OrePhalcon 808.64 220%
OrePux 367.21 100%

Phalconの圧勝でした。Phalconほんと速いですね。

(2014-01-15 07:07 追記) さらにPuxのルーティング性能をベンチマークしてみたを書きました。

(2014-01-15 13:30 追記) この記事のPuxのベンチマーク結果は、Cで書かれたPuxのPHP機能拡張が使われておらず、PHPで書かれたPuxの結果のようです。修正記事を後ほどアップします。

(2014-01-15 16:20 追記) PHP機能拡張が使われていないわけではないことがわかりました。後日、詳細な記事をアップします。

(2014-01-16 追記) 【完全版】Pux - A High Performance PHP Routerのルーティング性能をベンチマークしてみたをアップしました。

参考

Tags: php, phalcon, pux, benchmark

Windows版PHPのstrftime()関数が文字化けする問題

Windows版PHP 5.5のstrftime()関数だと文字化けが起こるという問題、『PHP逆引きレシピ 第2版』にも記載されています。

サンプルで説明すると、以下になります。

<?php
function p($str)
{
  echo $str, '<br>';
  echo strftime($str), '<br>';
  echo bin2hex($str), '<br>';
  echo bin2hex(strftime($str)), '<hr>';
}

setlocale(LC_ALL, 'C');
p('あ');

期待される結果:

あ
あ
e38182
e38182

実際の結果:

あ
ぁE
e38182
e3818145

この問題、以下でバグ報告してあるのですが、再現できないと言われてしまいました。

https://bugs.php.net/bug.php?id=65371

手許では、PHP 5.5.1、5.5.6(XAMPP for Windows)で問題が生じることを確認しています。

上記の問題が再現しない環境やバグの原因について心当たりのある方は、お知らせいただけるとありがたいです。 上記のバグ報告にコメントしてもらうのが一番助かります。

回避策

(20:32 追記) なんか、VC11のstrftimeの問題なので、PHPではどうしようもないらしいです。

ということで、ロケールをjapaneseに指定して、以下のような関数(スクリプトの文字エンコーディングがUTF-8の場合)を用意することで回避できることを確認しました。

function strftime_win($str)
{
  $str = mb_convert_encoding($str, 'CP932', 'UTF-8');
  return mb_convert_encoding(strftime($str), 'UTF-8', 'CP932');
}

Tags: php, windows

How to write Unit Tests of FuelPHP Application with AspectMock

This post explains how to integrate ApectMock with FuelPHP, and make your FuelPHP applications more testable with replacing static methods with test doubles (mocks).

"Testability" should not be used as argument deciding what design pattern is right to use and what is not.

Requirements:

  • FuelPHP 1.7.1 (or 1.8/develop)
  • AspectMock master branch cc2be6945a705e65a2a4a12df7e35de82d0129f7 (2013-09-09)

Prepare

Install AspectMock and a required package with Composer.

diff --git a/composer.json b/composer.json
index e1b21ea..006ac5b 100644
--- a/composer.json
+++ b/composer.json
@@ -16,10 +16,14 @@
         "forum": "http://fuelphp.com/forums"
     },
     "require": {
-        "php": ">=5.3.3",
+        "php": ">=5.4",
         "monolog/monolog": "1.5.*",
        "fuelphp/upload": "2.0.1"
     },
+    "require-dev": {
+        "codeception/aspect-mock": "*",
+        "symfony/finder":"*"
+    },
     "suggest": {
         "mustache/mustache": "Allow Mustache templating with the Parser package",
         "smarty/smarty": "Allow Smarty templating with the Parser package",

Install them with composer.

$ php composer.phar update

Change FuelPHP 1.7.1

Change FuelPHP to use AspectMock on testing.

diff --git a/fuel/app/bootstrap.php b/fuel/app/bootstrap.php
index a6213d5..b491688 100644
--- a/fuel/app/bootstrap.php
+++ b/fuel/app/bootstrap.php
@@ -1,9 +1,5 @@
 <?php

-// Load in the Autoloader
-require COREPATH.'classes'.DIRECTORY_SEPARATOR.'autoloader.php';
-class_alias('Fuel\\Core\\Autoloader', 'Autoloader');
-
 // Bootstrap the framework DO NOT edit this
 require COREPATH.'bootstrap.php';

diff --git a/oil b/oil
index 62033d6..4a21f80 100644
--- a/oil
+++ b/oil
@@ -48,6 +48,10 @@ define('COREPATH', realpath(__DIR__.'/fuel/core/').DIRECTORY_SEPARATOR);
 defined('FUEL_START_TIME') or define('FUEL_START_TIME', microtime(true));
 defined('FUEL_START_MEM') or define('FUEL_START_MEM', memory_get_usage());

+// Load in the Autoloader
+require COREPATH.'classes'.DIRECTORY_SEPARATOR.'autoloader.php';
+class_alias('Fuel\\Core\\Autoloader', 'Autoloader');
+
 // Boot the app
 require APPPATH.'bootstrap.php';

diff --git a/public/index.php b/public/index.php
index e01d3a4..9cb90d3 100644
--- a/public/index.php
+++ b/public/index.php
@@ -40,6 +40,10 @@ define('COREPATH', realpath(__DIR__.'/../fuel/core/').DIRECTORY_SEPARATOR);
 defined('FUEL_START_TIME') or define('FUEL_START_TIME', microtime(true));
 defined('FUEL_START_MEM') or define('FUEL_START_MEM', memory_get_usage());

+// Load in the Autoloader
+require COREPATH.'classes'.DIRECTORY_SEPARATOR.'autoloader.php';
+class_alias('Fuel\\Core\\Autoloader', 'Autoloader');
+
 // Boot the app
 require APPPATH.'bootstrap.php';

Configure AspectMock to load AspectMock ($kernel->init()). If the configuration is wrong, AspectMock does not work fine.

diff --git a/bootstrap_phpunit.php b/bootstrap_phpunit.php
index 3b5b851..2605850 100644
--- a/bootstrap_phpunit.php
+++ b/bootstrap_phpunit.php
@@ -32,6 +32,34 @@ unset($app_path, $core_path, $package_path, $_SERVER['app_path'], $_SERVER['core
 defined('FUEL_START_TIME') or define('FUEL_START_TIME', microtime(true));
 defined('FUEL_START_MEM') or define('FUEL_START_MEM', memory_get_usage());

+/**
+ * Load the Composer autoloader if present
+ */
+defined('VENDORPATH') or define('VENDORPATH', realpath(COREPATH.'..'.DS.'vendor').DS);
+if ( ! is_file(VENDORPATH.'autoload.php'))
+{
+   die('Composer is not installed. Please run "php composer.phar update" in the root to install Composer');
+}
+require VENDORPATH.'autoload.php';
+
+// Add AspectMock
+$kernel = \AspectMock\Kernel::getInstance();
+$kernel->init([
+   'debug' => true,
+   'appDir'    => __DIR__ . '/../',
+   'includePaths' => [
+       __DIR__.'/../app', __DIR__.'/../core', __DIR__.'/../packages',
+   ],
+   'excludePaths' => [
+       __DIR__.'/../app/tests', __DIR__.'/../core/tests',
+   ],
+]);
+
+// Load in the Autoloader
+//require COREPATH.'classes'.DIRECTORY_SEPARATOR.'autoloader.php';
+$kernel->loadFile(COREPATH.'classes'.DIRECTORY_SEPARATOR.'autoloader.php'); // path to your autoloader
+class_alias('Fuel\\Core\\Autoloader', 'Autoloader');
+
 // Boot the app
 require_once APPPATH.'bootstrap.php';

(2014/03/21 Addition) I change the part of $kernel->init() as below, because the latest AspeckMock creates cache folders in current directory.

$kernel->init([
    'debug' => true,
    'appDir' => __DIR__ . '/../',
    'includePaths' => [
        APPPATH, COREPATH, PKGPATH,
    ],
    'excludePaths' => [
        APPPATH.'tests', COREPATH.'tests'
    ],
    'cacheDir'     => APPPATH.'tmp/AspectMock',
]);

(End of 2014/03/21 Addition)

To work AspectMock fine, copy fuel/core/phpunit.xml to fuel/app/phpunit.xml, and change backupGlobals to false.

--- fuel/core/phpunit.xml   2013-12-02 19:56:52.847375706 +0900
+++ fuel/app/phpunit.xml    2013-12-02 19:56:38.910935143 +0900
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>

-<phpunit colors="true" stopOnFailure="false" bootstrap="../core/bootstrap_phpunit.php">
+<phpunit colors="true" stopOnFailure="false" bootstrap="../core/bootstrap_phpunit.php" backupGlobals="false">
    <php>
        <server name="doc_root" value="../../"/>
        <server name="app_path" value="fuel/app"/>

In case of FuelPHP 1.8/develop (1.7.2 or later)

On 1.8/develop branch, changes above has been merged. So only change fuel/core/bootstrap_phpunit.php,

diff --git a/bootstrap_phpunit.php b/bootstrap_phpunit.php
index 50b9c88..20678e7 100644
--- a/bootstrap_phpunit.php
+++ b/bootstrap_phpunit.php
@@ -40,8 +40,22 @@ if ( ! is_file(VENDORPATH.'autoload.php'))
 }
 require VENDORPATH.'autoload.php';

+// Add AspectMock
+$kernel = \AspectMock\Kernel::getInstance();
+$kernel->init([
+       'debug' => true,
+       'appDir' => __DIR__ . '/../',
+       'includePaths' => [
+               __DIR__.'/../app', __DIR__.'/../core', __DIR__.'/../packages',
+       ],
+       'excludePaths' => [
+               __DIR__.'/../app/tests', __DIR__.'/../core/tests',
+       ],
+]);
+
 // Load in the Fuel autoloader
-require COREPATH.'classes'.DIRECTORY_SEPARATOR.'autoloader.php';
+//require COREPATH.'classes'.DIRECTORY_SEPARATOR.'autoloader.php';
+$kernel->loadFile(COREPATH.'classes'.DIRECTORY_SEPARATOR.'autoloader.php'); // path to your autoloader
 class_alias('Fuel\\Core\\Autoloader', 'Autoloader');

 // Boot the app

(2014/03/21 Addition) I change the part of $kernel->init() as below, because the latest AspeckMock creates cache folders in current directory.

$kernel->init([
    'debug' => true,
    'appDir' => __DIR__ . '/../',
    'includePaths' => [
        APPPATH, COREPATH, PKGPATH,
    ],
    'excludePaths' => [
        APPPATH.'tests', COREPATH.'tests'
    ],
    'cacheDir'     => APPPATH.'tmp/AspectMock',
]);

(End of 2014/03/21 Addition)

and create fuel/app/phpunit.xml. That's it.

--- fuel/core/phpunit.xml   2013-12-02 19:56:52.847375706 +0900
+++ fuel/app/phpunit.xml    2013-12-02 19:56:38.910935143 +0900
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>

-<phpunit colors="true" stopOnFailure="false" bootstrap="../core/bootstrap_phpunit.php">
+<phpunit colors="true" stopOnFailure="false" bootstrap="../core/bootstrap_phpunit.php" backupGlobals="false">
    <php>
        <server name="doc_root" value="../../"/>
        <server name="app_path" value="fuel/app"/>

How to write tests

Let's create a test for a controller with Response::redirect().

Response::redirect() will exit() for safty, phpunit exits and tests also stop. So normally you can't test it. But, replacing it with a test double, make it testable.

First, let's create a controller below.

<?php

class Controller_Test extends Controller
{
    public function action_redirect()
    {
        return Response::redirect('welcome/404', 'location', 404);
    }
}

Then, let's create a test.

<?php

// import Test class of AspectMock as test
use AspectMock\Test as test;

/**
 * Tests for Controller_Test
 *
 * @group App
 * @group Controller
 */
class Test_Controller_Test extends TestCase
{
    protected function tearDown()
    {
        test::clean(); // remove all registered test doubles
    }

    public function test_redirect()
    {
        // replace Response::redirect() with a test double which only returns true
        $req = test::double('Fuel\Core\Response', ['redirect' => true]);

        // generate a request to 'test/redirect'
        $response = Request::forge('test/redirect')
                        ->set_method('GET')->execute()->response();

        // confirm Response::redirect() was invoked with arguments below
        $req->verifyInvoked('redirect', ['welcome/404', 'location', 404]);
    }
}

Run test.

$ oil test --group=App
Tests Running...This may take a few moments.
PHPUnit 3.7.28 by Sebastian Bergmann.

Configuration read from /mnt/fuelphp/fuel/app/phpunit.xml

.

Time: 8.08 seconds, Memory: 48.50Mb

OK (1 test, 0 assertions)

Passed! It seems strange that "0 assertinos", but it can't be helped because we don't use assertion methods of PHPUnit.

Try to change the third argument from 404 to 405 in verifyInvoked().

$req->verifyInvoked('redirect', ['welcome/404', 'location', 405]);
$ oil test --group=App
Tests Running...This may take a few moments.
PHPUnit 3.7.28 by Sebastian Bergmann.

Configuration read from /mnt/fuelphp/fuel/app/phpunit.xml

F

Time: 7.72 seconds, Memory: 48.50Mb

There was 1 failure:

1) Test_Controller_Test::test_redirect
Expected Fuel\Core\Response::redirect('welcome/404','location',405) to be invoked but it never occur.

/mnt/fuelphp/fuel/vendor/codeception/aspect-mock/src/AspectMock/Proxy/Verifier.php:73
/mnt/fuelphp/fuel/app/tests/controller/test.php:28

FAILURES!
Tests: 1, Assertions: 0, Failures: 1.

Failed as expected.

You see that AspectMock can replace static methods with test doubles. Or it could redefine methods dynamically. So you can easily write applications tests. You don't need to use DI only for testability.

But ApsectMock is "Stability: alpha". So there might be still something wrong.

References

Tags: english