2012年6月21日木曜日

symfony2を読む(3)

さて、前回に引き続きsymfony2を読み解く。今日は以下の箇所について調べる。

./web/app_dev.php


[php]
Request::createFromGlobals()
[/php]

vendor/symfony/src/Symfony/Component/HttpFoundation/Request.php


[php]
static public function createFromGlobals()
{
$request = new static($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER);

if (0 === strpos($request->server->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
&& in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE'))
) {
parse_str($request->getContent(), $data);
$request->request = new ParameterBag($data);
}

return $request;
}
[/php]
1行目, new static ってなんだ!? new self と new static の違い - Sarabande.jp によると、記述元のクラスでメソッド呼び出された風になるのが、new self, 呼び出し元のクラスでメソッド呼び出し風になるのが new static とのことだが、今回は記述元も呼び出し元も同じである。例えばJavaでstatic int といえばすべてのインスタンスで共有したいintを指すが、今回は他のクラスからRequestインスタンスを呼び出すときに同一のものを扱いたい、ということなのだろうか?そういうことにしておこう。何はともあれ、コンストラクタの呼び出しがあり、そこで初期化のための関数が呼ばれる。
[php]
public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null)
{
$this->initialize($query, $request, $attributes, $cookies, $files, $server, $content);
}
public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null)
{
$this->request = new ParameterBag($request);
$this->query = new ParameterBag($query);
$this->attributes = new ParameterBag($attributes);
$this->cookies = new ParameterBag($cookies);
$this->files = new FileBag($files);
$this->server = new ServerBag($server);
$this->headers = new HeaderBag($this->server->getHeaders());

$this->content = $content;
$this->languages = null;
$this->charsets = null;
$this->acceptableContentTypes = null;
$this->pathInfo = null;
$this->requestUri = null;
$this->baseUrl = null;
$this->basePath = null;
$this->method = null;
$this->format = null;
}
[/php]
ここではキーと値を格納するクラスParameterBagのインスタンスが内部変数に格納される。createFromGlobalsに戻ろう。
[php]
if (0 === strpos($request->server->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
&& in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE'))
) {
parse_str($request->getContent(), $data);
$request->request = new ParameterBag($data);
}
[/php]
if文の中には

  1. ファイルアップロードで使われるようなCONTENT_TYPEではない。

  2. PUT OR DELETE メソッド


の2つを満たす時にだけ入る。このとき、リクエストの内容はrequestBodyに書き換えられる。

最後に Requestクラスのstaticインスタンスを返して終了だ。

2012年6月18日月曜日

symfony2を読む(2)

昨日に引き続き、symfony2を読む。カレントディレクトリをsymfony2のインストールディレクトリとする。

web/app_dev.php


[php]
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('dev', true);
$kernel->loadClassCache();
$kernel->handle(Request::createFromGlobals())->send();
[/php]

useにより、$SYMFONY2_DIR/vendor/symfony/src/Symfony/Component/HttpFoundation/Request.php を読み出している。
この $SYMFONY2_DIR/vendor/symfony/src の位置はどうやって伝えられたのだろうか?それは bootstrap.php.cache 内で読み込まれた autoload.php に秘密があった。

app/autoload.php


[php]
$loader = new UniversalClassLoader();
$loader->registerNamespaces(array(
'Symfony' => array(__DIR__.'/../vendor/symfony/src', __DIR__.'/../vendor/bundles'),
[/php]

loadClassCache(), handle()はともにAppKernelの親クラスであるKernelクラスの下記メソッドが呼ばれている。

vendor/symfony/src/Symfony/Component/HttpKernel/Kernel.php


[php]
public function loadClassCache($name = 'classes', $extension = '.php')
{
if (!$this->booted && file_exists($this->getCacheDir().'/classes.map')) {
ClassCollectionLoader::load(include($this->getCacheDir().'/classes.map'), $this->getCacheDir(), $name, $this->debug, false, $extension);
}
}
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
if (false === $this->booted) {
$this->boot();
}

return $this->getHttpKernel()->handle($request, $type, $catch);
}
[/php]
今日は loadClassCache()の挙動をよく調べてみよう。まず
[php]
$this->booted # false
$this->getCacheDir().'/classes.map' # app/cache/dev/classes.map
[/php]
となっている。bootedの値ついては__constructメソッドを読めば分かる。
[php]
ClassCollectionLoader::load
[/php]
では、下記クラスのloadメソッドが呼ばれている。

vendor/symfony/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php


[php]
static public function load($classes, $cacheDir, $name, $autoReload, $adaptive = false, $extension = '.php')
{
// each $name can only be loaded once per PHP process
if (isset(self::$loaded[$name])) {
return;
}

self::$loaded[$name] = true;
[/php]
$nameという名前で、$classesに渡されたクラス群を読み込むらしい。$nameと$classesは一対一対応となっている。
[php]
if ($adaptive) {
// don't include already declared classes
$classes = array_diff($classes, get_declared_classes(), get_declared_interfaces());

// the cache is different depending on which classes are already declared
$name = $name.'-'.substr(md5(implode('|', $classes)), 0, 5);
}
[/php]
ここでは $adaptive = false なのでif文の中には入らない。

[php]
// auto-reload
$reload = false;
if ($autoReload) {
$metadata = $cacheDir.'/'.$name.$extension.'.meta';
if (!file_exists($metadata) || !file_exists($cache)) {
$reload = true;
} else {
$time = filemtime($cache);
$meta = unserialize(file_get_contents($metadata));

if ($meta[1] != $classes) {
$reload = true;
} else {
foreach ($meta[0] as $resource) {
if (!file_exists($resource) || filemtime($resource) > $time) {
$reload = true;

break;
}
}
}
}
}

if (!$reload && file_exists($cache)) {
require_once $cache;

return;
}
[/php]
$autoReload変数には今回true(AppKernel->debug)が渡されているのでif文の中に入る。ここでは$classesを再度読み込み直すか?それともキャッシュされたクラスをそのまま使うのかが指定されている。再読み込みする条件は3つ。

  1. そもそもキャッシュが存在しない

  2. キャッシュされているクラス群と読み込もうとしているクラス群が異なる

  3. キャッシュ元のクラスが更新されている


[php]

$files = array();
$content = '';
foreach ($classes as $class) {
if (!class_exists($class) && !interface_exists($class) && (!function_exists('trait_exists') || !trait_exists($class))) {
throw new \InvalidArgumentException(sprintf('Unable to load class "%s"', $class));
}

$r = new \ReflectionClass($class);
$files[] = $r->getFileName();

$c = preg_replace(array('/^\s*<\?php/', '/\?>\s*$/'), '', file_get_contents($r->getFileName()));

// add namespace declaration for global code
if (!$r->inNamespace()) {
$c = "\nnamespace\n{\n".self::stripComments($c)."\n}\n";
} else {
$c = self::fixNamespaceDeclarations(' $c = preg_replace('/^\s*<\?php/', '', $c);
}

$content .= $c;
}

// cache the core classes
if (!is_dir(dirname($cache))) {
mkdir(dirname($cache), 0777, true);
}
self::writeCacheFile($cache, '[/php]
$classesを1つずつ読み込み、最終的にすべてのクラスを1つのファイル($cacheDir.'/'.$name.$extension)にまとめている。ここで$nameと$classesを一対一対応にしたことが効いてくる。

[php]
if ($autoReload) {
// save the resources
self::writeCacheFile($metadata, serialize(array($files, $classes)));
}
[/php]
オブジェクトを直列化してファイルを保存する。

dev環境、prod環境でのautoReloadの違い


prod環境では$autoReload=falseとなり、キャッシュを消すまで古いクラスが使われ続ける。これは安定した速い運用が求められるprod環境に適している。対してdev環境では$autoReload=trueとなり、常に最新のソースコードでSymfonyを動かすことになる。多少ブラウザ表示が遅くなるとはいえ、開発効率を上げるための工夫である。

2012年6月17日日曜日

symfony2を読む(1)

はじめに


環境: Amazon EC2, Amazon Linux AMI x86_64 EBS
ソースコード:Symfony2.0.15

まずはコードを動かしてみる


参考:The Big Picture (current) - Symfony

ソースコードの配置


[bash]
~$wget http://symfony.com/get/Symfony_Standard_Vendors_2.0.15.tgz
~$tar xvf Symfony_Standard_Vendors_2.0.15.tgz
~$mv Symfony /var/www/html/symfony2
[/bash]

バーチャルホストの設定


参考:バーチャルホスト設定 - CentOSで自宅サーバー構築
DocumentRoot /var/www/html/symfony2/web
に対してバーチャルホストを設定。

config.phpを表示


アクセス情報【使用中のIPアドレス確認】にて自分の現在のIPアドレスを確認。
/var/www/html/symfony2/web/ を開いて
[php]
if (!in_array(@$_SERVER['REMOTE_ADDR'], array(
'127.0.0.1',
'::1',
'Your.Current.Ip.Address',
))) {
header('HTTP/1.0 403 Forbidden');
exit('This script is only accessible from localhost.');
}
[/php]
のように編集。その後 http://symfony2.yourdomain.com/config.php アクセス。表示に従い必要なライブラリのインストールや、フォルダの作成などを行う。

app_dev.phpを表示


config.phpと同様に
[php]
if (isset($_SERVER['HTTP_CLIENT_IP'])
|| isset($_SERVER['HTTP_X_FORWARDED_FOR'])
|| !in_array(@$_SERVER['REMOTE_ADDR'], array(
'127.0.0.1',
'::1',
'Your.Current.Ip.Address',
))
) {
header('HTTP/1.0 403 Forbidden');
exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
}
[/php]
と編集。その後 http://symfony2.yourdomain.com/app_dev.php にアクセスすると以下のような画面が表示される。
app_dev