k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

SqaleのPHPアプリケーションでSilexのサービスプロバイダ用Traitを使ってみたメモ

クラウドホスティングサービス Sqale では国内ホスティングサービスとしては珍しく(?)、動作環境にPHP5.4を選択できます。

せっかくなので、以前に書いたSilexアプリケーションのコードをSilex付属のサービスプロバイダ用Traitを利用したものに書き換えるとともに、マイクロフレームワークをつくろう - Pimpleの上に(Pimple拡張編) の成果を元に、Pimpleコンテナへのプロパティアクセスとメソッドアクセスを実装しました。

(内容的にはSqaleは関係なく、Silexの話です)

app.php (アプリケーションオブジェクト生成)

<?php

$loader = include __DIR__ . '/../vendor/autoload.php';

use Acme\Application;

use Silex\Provider\SessionServiceProvider;
use Silex\Provider\UrlGeneratorServiceProvider;
use Silex\Provider\MonologServiceProvider;
use Silex\Provider\SwiftmailerServiceProvider;
use Silex\Provider\TwigServiceProvider;

use Monolog\Logger;

$app = new Application();

// アプリケーション設定
$app->config = $app->share(function($app) {
    $config = [];
    $file = __DIR__ . '/config/config.json';
    if (file_exists($file)) {
        $config_json = json_decode(file_get_contents($file), true);
        if ($config_json === null) {
            switch (json_last_error()) {
            case JSON_ERROR_DEPTH:
                throw new \RuntimeException(sprintf('config file "%s" Maximum stack depth exceeded.', $file));
            case JSON_ERROR_STATE_MISMATCH:
                throw new \RuntimeException(sprintf('config file "%s" Underflow or the modes mismatch.', $file));
            case JSON_ERROR_CTRL_CHAR:
                throw new \RuntimeException(sprintf('config file "%s" Unexpected control character found.', $file));
            case JSON_ERROR_SYNTAX:
                throw new \RuntimeException(sprintf('config file "%s" Syntax error, malformed JSON.', $file));
            case JSON_ERROR_UTF8:
                throw new \RuntimeException(sprintf('config file "%s" Malformed UTF-8 characters, possibly incorrectly encoded.', $file));
            default:
                throw new \RuntimeException(sprintf('config file "%s" Unknown error.', $file));
            }
        }
        return array_replace($config, $config_json);
    }
    return $config;
});

$app->register(new SessionServiceProvider());

$app->register(new UrlGeneratorServiceProvider());

$app->register(new MonologServiceProvider(), [
    'monolog.name'  => 'demo',
    'monolog.level' => $app->share(function($app) {
        return ($app['debug']) ? Logger::DEBUG : Logger::NOTICE;
    }),
    'monolog.logfile' => __DIR__ . '/log/application.log',
]);

$app->register(new SwiftmailerServiceProvider(), [
    'swiftmailer.options' => $app->share(function($app) {
        return (isset($app['config']['mail'])) ? $app['config']['mail'] : [];
    }),
]);

$app->register(new TwigServiceProvider(), [
    'twig.path' => __DIR__ . '/views',
]);

// リクエスト変数フィルタ
$app->filterVar = $app->protect(function($value) {
    $filters = [
        function($val) {
            // HT,LF,CR,SP以外の制御コード(00-08,11,12,14-31,127,128-159)を除去
            // ※参考 http://en.wikipedia.org/wiki/C0_and_C1_control_codes
            return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]|\xC2[\x80-\x9F]/S', '', $val);
        },
        function($val) {
            // 改行コードを統一
            return str_replace("\r", "\n", str_replace("\r\n", "\n", $val));
        },
    ];
    foreach ($filters as $filter) {
        $value = (is_array($value)) ? array_map($filter, $value) : $filter($value);
    }
    return $value;
});

return $app;

以下のサービスプロバイダ用Traitを利用します。

  • Silex\Provider\UrlGeneratorServiceProvider → Silex\Application\UrlGeneratorTrait
  • Silex\Provider\MonologServiceProvider → Silex\Application\MonologTrait
  • Silex\Provider\SwiftmailerServiceProvider → Silex\Application\SwiftmailerTrait
  • Silex\Provider\TwigServiceProvider → Silex\Application\TwigTrait

Traitの利用にはクラスの定義が必要なので、以下のようにAcme名前空間でApplicationクラスを定義しました。

src/Acme/Application.php (アプリケーションクラス)

<?php
/**
 * @copyright 2013 k-holy <k.holy74@gmail.com>
 * @license The MIT License (MIT)
 */

namespace Acme;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;

/**
 * アプリケーションクラス
 *
 * @author k.holy74@gmail.com
 */
class Application extends \Silex\Application
{
    use \Silex\Application\UrlGeneratorTrait;
    use \Silex\Application\MonologTrait;
    use \Silex\Application\SwiftmailerTrait;
    use \Silex\Application\TwigTrait;

    /**
     * __get()
     *
     * @param string
     * @return mixed
     */
    public function __get($name)
    {
        return parent::offsetGet($name);
    }

    /**
     * __set()
     *
     * @param string
     * @param mixed
     */
    public function __set($name, $value)
    {
        if (method_exists($this, $name)) {
            throw new \InvalidArgumentException(
                sprintf('The property "%s" is already defined as a method.', $name)
            );
        }
        parent::offsetSet($name, $value);
    }

    /**
     * __call()
     *
     * @param string
     * @param array
     * @return mixed
     */
    public function __call($name, $args)
    {
        if (parent::offsetExists($name)) {
            $value = parent::offsetGet($name);
            if (is_callable($value)) {
                return call_user_func_array($value, $args);
            }
            return $value;
        }
        throw new \BadMethodCallException(
            sprintf('Undefined Method "%s" called.', $name)
        );
    }

}

利用側のスクリプトはTraitによってこんな感じになります。

public/index.php (アプリケーション利用スクリプト)

<?php
$app = include __DIR__ . '/../app.php';

use Silex\Application;
use Symfony\Component\HttpFoundation\Request;

// 前処理
$app->before(function(Request $request) use ($app) {

    $app['session']->start();

});

// ホーム
$app->get('/', function(Application $app, Request $request) {

    return $app->render('index.html', [
        'title'  => 'ホーム',
    ]);

})->bind('home');

// phpinfo()
$app->get('/phpinfo', function(Application $app, Request $request) {

    ob_start();
    phpinfo();
    $phpinfo = ob_get_contents();
    ob_end_clean();
    if (preg_match('~<body[^>]*>(.*)</body>~is', $phpinfo, $matches)){
        $phpinfo = $matches[1];
    }
    return $app->render('phpinfo.html', [
        'title'   => 'phpinfo',
        'phpinfo' => $phpinfo,
    ]);

})->bind('phpinfo');

// メール送信フォーム
$app->match('/mail', function(Application $app, Request $request) {

    $errors = [];
    $subject = null;
    $body = null;

    if ($request->getMethod() === 'POST') {

        $subject = $app->filterVar($request->request->get('subject'));
        $body = $app->filterVar($request->request->get('body'));

        if (!isset($subject) || strlen($subject) === 0) {
            $errors['subject'] = '件名を入力してください';
        }

        if (mb_strlen($subject) > 20) {
            $errors['subject'] = '件名は20文字以内で入力してください';
        }

        if (!isset($body) || strlen($body) === 0) {
            $errors['body'] = '本文を入力してください';
        }

        if (mb_strlen($body) > 50) {
            $errors['body'] = '本文は50文字以内で入力してください';
        }

        if (empty($errors)) {

            if ($app->mail(\Swift_Message::newInstance()
                ->setSubject($subject)
                ->setFrom(['k.holy74@gmail.com' => 'k-holy'])
                ->setTo(['k.holy74@gmail.com' => 'k-holy'])
                ->setBody($body, 'text/plain')
            )) {

                $app['session']->getFlashBag()->add('success', 'メールを送信しました');

                return $app->redirect(
                    $app->path('mail'),
                    303
                );

            }
        }
    }

    return $app->render('mail.html', [
        'title'   => 'メール送信フォーム',
        'errors'  => $errors,
        'subject' => $subject,
        'body'    => $body,
    ]);

})->method('GET|POST')->bind('mail');

$app->run();

Traitによって以下のメソッドがApplicationクラスに実装されています。

  • Silex\Application\UrlGeneratorTrait → $app->path(), $app->url()
  • Silex\Application\MonologTrait → $app->log()
  • Silex\Application\SwiftmailerTrait → $app->mail()
  • Silex\Application\TwigTrait → $app->render(), $app->renderView()

便利なのは、TwigとURLジェネレータの併用時に、上記のようにコントローラで $app->get('/', function(){...中略...})->bind('home') としておくと、テンプレートでは <a href="{{app.path('home')}}">ホーム</a> などと書けばURLが展開されるところです。

特にサブディレクトリでの運用時には便利に使えるんじゃないでしょうか。(参考:Silexアプリケーションをサブディレクトリで運用する(mod_rewriteとUrlGenerator)

その他のTraitは特に手数的に有利になるということはありませんが、利用側がコンテナ内のオブジェクトのキーを意識しなくて済むのは良いですね。

(個人的には $app->mail() などは Swift_Message::newInstance() とか利用側が呼ばなくても中でやってくれる方が好みですが…)

過去のSqale関連記事