Symfony2初心者がバンドルを作ってみた

この記事はSymfony Advent Calendar 2011の21日目の記事になります。
Advent Calendarとはなんぞや?という方は下記をどうぞ。

http://gihyo.jp/news/info/2011/12/0101


まず始めに、自分はSymfonyをバージョン1系を使ったことがなく、バージョン2からの初心者です
しかもSymfony2もまだ触り始めて1ヶ月ちょっとになります。
こんな若輩者の私ですので、突っ込みどころがあるかもしれませんので、もし気になるところがありましたら突っ込んでいただければ幸いです。

Symfony2を使ってみて思ったこと

以前までEthnaを使っていたのですが、以前からSymfonyがいいよ!と色々な方から言われていたので触ってみようと思ったところ既にバージョン2が出ている状態だったので、せっかくなので今更バージョン1系を覚えるのもあれかなと思い、バージョン2に的を当ててみました。

Symfony2を触ってみた最初の感想としてはかなり複雑!この一言しか出てきませんでした。
やりたい事の都合上、いきなり触ったのがSecurityコンポーネントだったってのもある気がしますが…
さすがSymfonyは大規模向けで複雑と一般的に言われてるだけあるなぁと言ったのが最初の感想です。

まずはひたすらドキュメントを読み漁るところから始めました。
章によってはドキュメントだけでは足りない部分も多かったですが、そこはGoogle先生を便りに色々読みあさって少しは理解を深めることが出来ました。

ある程度理解が深まった時点での感想はかなり良く出来てるなぁといった感じです。
Symfony2ではDIコンテナとEventDispatcherが採用されており、こいつらのお陰で各々のコンポーネントがほぼ独立していることと自分で作ったコンポーネントを簡単に組み込めるところがすごいです。

もう一点思ったことといえば各々のコンポーネントが独立しすぎていて、素のままのSymfony2では少し入り組んだロジックを組む際には面倒くさいなと思うところもありましたが、ここは先人の方々が色々なバンドルを公開しているので結構カバー出来ました。
後述しますが、自分もSymfony2を勉強していってるうえで、何かバンドルを作ってみようと思い作ってみました。

オレオレバンドルを作ってみた

Symfony2を使って開発していたのですが、情報の登録画面で少し複雑なフォームを作った際に、確認画面を出すときにめんどくさいなぁと思いました。
昔からPHPを触っている身としてはHTML_QuickFormのfreezeみたいなことが出来たら楽だなぁと思い、バンドルを作ってみた次第です。

GitHub - ryster/HaouFormBundle: HaouFormBundle

インストール手順などはREADMEを読んで頂ければと思いますが、機能としては下記の通りです。

  • フォームの凍結機能(HTML_QuickFormのfreezingのようなもの)
  • フォームへのヘルプメッセージ機能


実際のコードを記述していきます。
Formコンポーネントにfreezeオプションを指定するだけでフォームが凍結されます。

<?php
// ...
$form = $this->get('form.factory')->create(new UserType(), new User(), array('freeze' => true));

freezeオプションが指定されたフォームは強制的にフォームタイプがhiddenになります。
現実的に使うとなるとバリデーションが完了した後などにfreezeオプションを指定し直す必要がありますので、下記のようなコードになります。

<?php
    // ...

    /**
     * @Template()
     */
    public function registAction()
    {
        $request = $this->get('request');
        
        $builder = $this->get('form.factory')->createBuilder(new UserType(), new User());
        $form = $builder->getForm();

        // 検証
        if ('POST' === $request->getMethod()) {
            $params = $request->request->get($form->getName());
            $form->bindRequest($request);
            if ($form->isValid()) {
                // 確認画面へ
                if (false == isset($params['_confirmation']) || false == $params['_confirmation']) {
                    // freezeオプションを指定する為に、フォームを生成し直して、bindし直す
                    $form = $builder->setAttribute('freeze', true)->getForm();
                    $form->bindRequest($request)->get('_confirmation')->bind(true);

                    return array('form' => $form->createView());
                }

                // データベースへ保存
                $em = $this->getDoctrine()->getEntityManager();
                $em->persist($form->getData());
                $em->flush();

                return new Response('登録完了');
            }
        }

        return array('form' => $form->createView());
    }

上記のコードで[入力] -> [確認] -> [登録完了]までのページ実装が完了となります。
パーツ毎の凍結も可能です、その際はパーツのFormクラスにfreezeオプションを設定してください。
上記のコードで下記のような画面実装となります。

【入力画面】

【確認画面】


また、フォームのヘルプメッセージを表示したい場合は下記のようにFormBuilderでフォームを生成する際のオプションにexplainを指定してください。

<?php
// ...
$formBuilder->add('name', 'text', array(
    'label'      => '名前',
    'explain'  => '名前は50文字以内で入力してください。')
);

サンプルプログラムの一式をgithubにあげておきますので参考にどうぞ。

https://github.com/ryster/HaouDemoBundle

バンドルを作ってみて思ったこと

簡単なバンドルですが、最初は少し手こずるかなと思っていましたが、実際作った後考えると思ったより案外簡単に作ることができたなといった感じでした。
公開されている他人のバンドルを参考にすると良いです。
自分でバンドルを作ってみると更にSymfony2の理解を深めることができるのでオススメです!
皆さんも便利なバンドルをどんどん作って公開していきましょう!

以上でSymfony Advent Calendar 2011の記事を終わりにします。
明日は@yandoさんの記事になります。

Symfony2の認証を色々試してみる(後編)

前編の続きです。
前編では下記の認証方法を説明しました。

  • Basic認証
    • security.ymlで設定されたUser情報によるBasic認証
  • フォーム認証(User情報は固定)
    • security.ymlで設定されたUser情報によるフォーム認証
  • フォーム認証(Doctrineと連携)
    • データベースに設定されたUser情報によるフォーム認証

後編では下記を説明していこうと思います。

  • フォーム認証(DBで権限管理 ManyToOne)
    • データベースに設定されているUserと権限情報によるフォーム認証(Userに対する権限は1つ)
  • フォーム認証(DBで権限管理 ManyToMany)
    • データベースに設定されているUserと権限情報によるフォーム認証(Userに対する権限は複数)

では早速説明していきます。

フォーム認証(DBで権限管理 ManyToOne)

前編のほうで書いたフォーム認証(Doctrineと連携)のほうでは特に権限の説明をしませんでした。
Symfony2のSecurityコンポーネントではユーザーには必ず最低でも1つの権限を割り当てなくてはいけません。
権限はその名の通り閲覧権限(アクションを実行出来る権限)だと思って頂ければ良いと思います。

前編のフォーム認証(Doctrineと連携)では説明していませんが、下記の部分で権限の割り振りはしてあります。

  • src/App/TestBundle/Entity/User.php
<?php
    /**
     * 権限を返す(今回は権限管理はしない為、固定でROLE_ADMINを返す)
     *
     * @return array
     */
    public function getRoles()
    {
        return array('ROLE_ADMIN');
    }

上記の場合、ユーザーには固定でROLE_ADMINの権限が返されるようになっています。
この権限で操作可能なページはsecurity.ymlに設定されてあります。

  • app/config/security.yml
    # アクセス権限の設定
    access_control:
        # /から始まるコンテンツにアクセスするにはROLE_ADMIN権限が必要
        - { path: ^/, roles: ROLE_ADMIN }

今回の例ではaccess_controlの設定を理解する為にも、権限も分けてみます。
また権限が分かりやすいようにデバッグツールバーも表示してみます。
では、まずsecurity.ymlを下記のように変更します。

  • src/App/TestBundle/Entity/User.php
security:
    # エンコーダの設定
    encoders:
        # エンコードと対象となるユーザーモデルを指定(作成したUserモデルを指定)
        App\TestBundle\Entity\User: plaintext
    
    # 権限継承
    role_hierarchy:
        # ROLE_ADMINはROLE_USERの権限も持つ
        ROLE_ADMIN: ROLE_USER

    # ユーザー情報の設定(ユーザープロバイダ)
    providers:
        # 作成したUserモデルをユーザー情報とする
        my_users:
            # Entityクラスの指定とユーザー名となるプロパティを指定
            entity: { class: App\TestBundle\Entity\User, property: username }

    # ファイアーウォールの設定
    firewalls:
        # デバッグツールバーのセキュリティを無効
        dev:
            pattern:  ^/(_(profiler|wdt))/
            security: false
        
        # ログイン画面は認証エリア外
        login:
            # エリア範囲(正規表現)
            pattern: ^/login$
            # セキュリティ設定を無効
            security: false
            #anonymous: true
            
        # 認証エリアの設定
        secured_area:
            # エリア範囲(正規表現)
            pattern: ^/
            # ログインフォームの設定
            form_login:
                # ログインフォームのパス
                login_path: /login
                # ログイン状態を確認するパス
                check_path: /login/check
            # ログアウトの設定
            logout:
                # ログアウト用のURLのパス
                path: /logout
                # ログアウト後に移行するページ
                target: /login

    # アクセス権限の設定
    access_control:
        # /testにはROLE_USERとROLE_ADMINがアクセス可能
        - { path: ^/test, roles: ROLE_USER }
        # /から始まるコンテンツにアクセスするにはROLE_ADMIN権限が必要
        - { path: ^/, roles: ROLE_ADMIN }

追記・変更した部分は下記の部分です。

    # 権限継承
    role_hierarchy:
        # ROLE_ADMINはROLE_USERの権限も持つ
        ROLE_ADMIN: ROLE_USER

上記は権限の継承?設定です。
上記の設定の場合、ROLE_ADMINはROLE_USERの権限も引き継ぎます。

    # アクセス権限の設定
    access_control:
        # /testにはROLE_USERとROLE_ADMINがアクセス可能
        - { path: ^/test, roles: ROLE_USER }
        # /から始まるコンテンツにアクセスするにはROLE_ADMIN権限が必要
        - { path: ^/, roles: ROLE_ADMIN }

最後にデバッグツールバーが表示されるようにデバッグツールバーにセキュリティ設定が適用されないように設定。

        dev:
            pattern:  ^/(_(profiler|wdt))/
            security: false

access_controlの部分にも追記しました。
/testへのアクセスはROLE_USER権限でもアクセスが可能という設定です。
ROLE_ADMINはROLE_USERの権限も継承している為、ROLE_ADMINでもアクセスが可能です。

次はEntityとなるRoleクラスを作成します。
下記のコマンドを打って、Entityを作成してください。

$ php app/console doctrine:generate:entity --entity="AppTestBundle:Role" --fields="name:string(255)"

作成したRoleクラスを下記のように編集します。

  • src/App/TestBundle/Entity/Role.php
<?php
// ...

use Symfony\Component\Security\Core\Role\RoleInterface;
use \Doctrine\Common\Collections\ArrayCollection;

/**
 * App\TestBundle\Entity\Role
 *
 * @ORM\Table()
 * @ORM\Entity
 */
class Role implements RoleInterface
{
    // ...
    
    /**
     * @ORM\OneToMany(targetEntity="User", mappedBy="role")
     */
    protected $users;

    
    /**
     * construct
     */
    function __construct()
    {
        $this->users = new ArrayCollection();
    }

    // ...
    
    /**
     * Get Role
     *
     * @return string
     */
    public function getRole()
    {
        return $this->getName();
    }

    /**
     * Get users
     *
     * @return Doctrine\Common\Collections\Collection 
     */
    public function getUsers()
    {
        return $this->users;
    }
}

まずは利用するクラスとインターフェースを呼び出し、RoleInterfaceを継承します。
次に$usersプロパティとコンストラクタを定義します、ここでは$usersプロパティはUserクラスのroleプロパティとの関連付けをしています。
最後に必要なメソッドを追加します。

次にUserクラスの方も編集します。
Userクラスのほうは$roleプロパティを追加し、それに対応するメソッドを追加するだけです。

  • src/App/TestBundle/Entity/User.php
<?php
class User implements UserInterface
{
    // ...
    
    /**
     * @var integer $role
     * 
     * @ORM\ManyToOne(targetEntity="Role", inversedBy="users")
     * @ORM\JoinColumn(name="role_id", referencedColumnName="id")
     */
    protected $role;
    
    // ...
    
    /**
     * Set role
     *
     * @param integer $role
     */
    public function setRole($role)
    {
        $this->role = $role;
    }

    /**
     * Get role
     *
     * @return integer 
     */
    public function getRole()
    {
        return $this->role;
    }
    
    /**
     * @return array
     */
    public function getRoles()
    {
        return array($this->role->getName());
    }

肝となる部分は$roleプロパティのアノテーションの部分です。
アノテーションを利用して、Roleクラスと結合しています。

ここまで出来たら後は適当に/testになるコンテンツを作成してみます。
routing.ymlに下記を追記します。

  • src/App/TestBundle/Resources/config/routing.yml
test_test:
    pattern: /test
    defaults: { _controller: AppTestBundle:Default:test }

次にアクションの作成です。
DefaultController.phpに下記を追記します。

  • src/App/TestBundle/Controller/DefaultController.php
<?php
    public function testAction()
    {
        return $this->render('AppTestBundle:Default:test.html.twig');
    }

次にテンプレートを作成します。
デバッグツールバーを表示する為にbase.html.twigを継承します。

  • src/App/TestBundle/Resources/views/Default/test.html.twig
{% extends "::base.html.twig" %}

{% block body %}
ここはROLE_ADMINとROLE_TESTがアクセス可能です。<br />
<a href="{{ path('test_logout') }}">ログアウト</a>
{% endblock %}

ここまで出来たらデータベースにアクセスし、RoleテーブルにROLE_USERを登録し、UserテーブルにROLE_USER権限を持つユーザーを登録しましょう。
自分は以下のように登録してあります。

mysql> select * from Role \G
*************************** 1. row ***************************
  id: 1
name: ROLE_ADMIN
*************************** 2. row ***************************
  id: 2
name: ROLE_USER
2 rows in set (0.01 sec)


mysql> select * from User \G
*************************** 1. row ***************************
      id: 1
username: user1
password: user1
 role_id: 1
*************************** 2. row ***************************
      id: 2
username: user2
password: user2
 role_id: 2
2 rows in set (0.00 sec)

この状態でuser2でログインしてみてください。
ログインすると下記のような画面になるかと思います。

/はROLE_ADMIN権限がないと閲覧出来ないため、上記のようなエラーが発生します。
ではapp_dev.php/testにアクセスしてみてください。
下記のような画面になれば成功です。

デバッグツールバーを見ると、user2でログインしているのが分かります。
プロファイラを開いてユーザー情報を見てみるとROLE_USER権限を持っているのが分かります。

一度ログアウトし、user1でもログインして、app_dev.php/testにアクセスしてみて下さい。
user1でもアクセス出来れば問題ありません。

フォーム認証(DBで権限管理 ManyToMany)

次は1ユーザーに対して複数の権限を割り当てます。
フォーム認証(DBで権限管理 ManyToOne)をもとにいじっていくと結構簡単に出来てしまいます。
変更するのはEntityクラスのUser.phpとRole.phpのみです。
では早速いじっていきます。

  • src/App/TestBundle/Entity/User.php
<?php
// ...
use \Doctrine\Common\Collections\ArrayCollection;

class User implements UserInterface
{
    // ...

    /**
     * @var ArrayCollection $userRoles
     *
     * @ORM\ManyToMany(targetEntity="Role")
     * @ORM\JoinTable(name="UserRole",
     *     joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")},
     *     inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")}
     * )
     */
    protected $userRoles;

    /**
     * construct
     */
    public function __construct()
    {
        $this->userRoles = new ArrayCollection();
    }

    // ...

    /**
     * @return ArrayCollection|\Doctrine\Common\Collections\ArrayCollection
     */
    public function getUserRoles()
    {
        return $this->userRoles;
    }

    /**
     * @return array
     */
    public function getRoles()
    {
        return $this->getUserRoles()->toArray();
    }
}

変更点としては$roleプロパティをまず削除し、それに対応するsetRole, getRoleメソッドを削除します。
その後、$userRolesプロパティを追加し、アノテーションを設定します。(アノテーションの内容についてはもう少し調べてみる)
後は各メソッドを追加し、終了。

次にRoleクラスの方を変更します。

  • src/App/TestBundle/Entity/Role.php
<?php
namespace App\TestBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\Role\RoleInterface;

/**
 * App\TestBundle\Entity\Role
 *
 * @ORM\Table()
 * @ORM\Entity
 */
class Role implements RoleInterface
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var string $name
     *
     * @ORM\Column(name="name", type="string", length=100)
     */
    protected $name;

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     */
    public function setName($name)
    {
        $this->name = $name;
    }

    /**
     * Get name
     *
     * @return string 
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Get Role
     *
     * @return string
     */
    public function getRole()
    {
        return $this->getName();
    }
}

先ほどの$usersプロパティを削除し、それに関連するメソッドを削除したくらいです。
非常にシンプルなクラスとなりました。

ここまで完了した後は下記のコマンドを実行してください。

$ php app/console doctrine:schema:update --force

上記コマンドが完了するとデータベースにUserRoleテーブルが作成されているはずです。
ユーザーに権限を複数登録してみてください。
自分は下記のように登録してみました。

mysql> select * from User\G;
*************************** 1. row ***************************
      id: 1
username: user1
password: user1
    salt: 
*************************** 2. row ***************************
      id: 2
username: user2
password: user2
    salt: 
2 rows in set (0.00 sec)


mysql> select * from Role\G;
*************************** 1. row ***************************
  id: 1
name: ROLE_ADMIN
*************************** 2. row ***************************
  id: 2
name: ROLE_USER
*************************** 3. row ***************************
  id: 3
name: ROLE_TEST
3 rows in set (0.00 sec)


mysql> select * from UserRole\G
*************************** 1. row ***************************
user_id: 1
role_id: 1
*************************** 2. row ***************************
user_id: 1
role_id: 2
*************************** 3. row ***************************
user_id: 1
role_id: 3
*************************** 4. row ***************************
user_id: 2
role_id: 2
4 rows in set (0.00 sec)

ここまで完了したら、ログインしてみてプロファイラから権限を確認してみてください。
下記のように複数権限が割り当てられていれば成功です!

以上で、認証関係のメモは終わりです。
パスワードのエンコード方式もやろうとしましたが、それは別途やりたいと思います。
メモってみて再認識したけど、Securityコンポーネントは半端ない!
ちなみに自分が取りあげた機能はSecurityコンポーネントの一部に過ぎません。
Securityコンポーネントではもっと色々出来るみたいですね。

Symfony2の認証を色々試してみる(前編)

最近はもっぱらSymfony2の勉強に明け暮れているので、成果をメモ代わりに説明していこうかと。
今回はSecurityコンポーネントについて説明していきます。

Securityコンポーネントでは認証処理を自動で行ってくれます。
各ユーザーには権限(role)が最低でも1つ必要みたいです。
とりあえず今回はBasic認証とフォームからの認証の例を下記に記述したいと思います。
Symfony2は既に設置されているものと考え、インストール方法などは省きます。

Securityコンポーネントの概要については下記を参考にどうぞ。

http://docs.symfony.gr.jp/symfony2/book/security.html


実行環境として、とりあえず下記のコマンドを実行してAppTestBundleの作成済みとして後述します。

$ php app/console generate:bundle --namespace=App/TestBundle --format=yml

Basic認証

Symfony2で認証設定を行うにはまずはsecurity.ymlを設定しなければいけません。
config.ymlにも記述出来ますが、設定内容も多いので自分は別ファイルに分けて使おうと思います。
まずはsecurity.ymlを下記のように設定します。

  • app/config/security.yml
# Securityコンポーネントの設定
security:
    # エンコーダの設定
    encoders:
        # パスワードのエンコード方式を設定
        # plaintextは生のパスワードでの認証方式、他にもsha1とかsha512とかがある
        # Symfony\Component\Security\Core\User\Userはユーザーモデルを使わない際に利用されるデフォルトのユーザーモデル?
        Symfony\Component\Security\Core\User\User: plaintext

    # ユーザー情報の設定(ユーザープロバイダ)
    providers:
        # ユーザー情報の固定設定(インメモリユーザープロバイダ)
        in_memory:
            # ユーザー情報
            users:
                # ユーザー名はadmin, パスワードはadminpass, このユーザーの権限はROLE_ADMIN
                admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }

    # ファイアーウォールの設定
    firewalls:
        # 認証エリアの設定
        # secured_areaは勝手に自分で決めたエリア
        # 認証エリアは複数設定可能です
        secured_area:
            # エリア範囲(正規表現)
            pattern: ^/
            # Basic認証を利用する設定
            http_basic:
                # Basic認証時に表示される文言
                realm: "Secured Area"

    # アクセス権限の設定
    access_control:
        # /から始まるコンテンツにアクセスするにはROLE_ADMIN権限が必要
        - { path: ^/, roles: ROLE_ADMIN }

説明はコメントアウトの通り
上記を設定した後、ブラウザで/以下にアクセスするとBasic認証が表示されます。

たかだかBasic認証をかけるのにこんなに設定が必要なのかと最初は思うかもしれません…(自分は思いました)
ですが、これはBasic認証に限らず全ての認証の設定ファイルになりますので、上記のようにページ全体にBasic認証をかけるような例だと少し面倒のように見えるかもしれないですね。
後述しますが、データベースを絡ませた認証などを利用してみるとかなりSecurityコンポーネントの凄さが分かってくるかと思います。

フォーム認証(ユーザー情報は固定)

Basic認証の設定方法を理解したら、次はフォーム認証です。
フォーム認証で利用される認証情報はsecurity.ymlに定義される固定の情報となります。
まずはsecurity.ymlを設定します。

  • app/config/security.yml
# Securityコンポーネントの設定
security:
    # エンコーダの設定
    encoders:
        # パスワードのエンコード方式を設定
        # plaintextは生のパスワードでの認証方式、他にもsha1とかsha512とかがある
        # Symfony\Component\Security\Core\User\Userはユーザーモデルを使わない際に利用されるデフォルトのユーザーモデル?
        Symfony\Component\Security\Core\User\User: plaintext

    # ユーザー情報の設定(ユーザープロバイダ)
    providers:
        # ユーザー情報の固定設定(インメモリユーザープロバイダ)
        in_memory:
            # ユーザー情報
            users:
                # ユーザー名はadmin, パスワードはadminpass, このユーザーの権限はROLE_ADMIN
                admin: { password: admin, roles: [ 'ROLE_ADMIN' ] }

    # ファイアーウォールの設定
    firewalls:
        # ログイン画面は認証エリア外
        login:
            # エリア範囲(正規表現)
            pattern: ^/login$
            # セキュリティ設定を無効
            security: false
            
        # 認証エリアの設定
        secured_area:
            # エリア範囲(正規表現)
            pattern: ^/
            # ログインフォームの設定
            form_login:
                # ログインフォームのパス
                login_path: /login
                # ログイン状態を確認するパス
                check_path: /login/check
            # ログアウトの設定
            logout:
                # ログアウト用のURLのパス
                path: /logout
                # ログアウト後に移行するページ
                target: /login

    # アクセス権限の設定
    access_control:
        # /から始まるコンテンツにアクセスするにはROLE_ADMIN権限が必要
        - { path: ^/, roles: ROLE_ADMIN }

Basic認証の設定とかわった部分はfirewallsセクション内です。
secured_areaの他にloginセクションを追加しました。
secured_areaセクションが認証エリアの設定になります。
secred_area内のpathを見ると分かるように全てのページが認証エリア範囲内と設定されていますので、ログインページ(/login)も認証エリア内に入ってしまいます。
これではログインページにアクセス出来ない為、別途loginセクションを定義し、ログインページは非認証エリアとして設定してあります。
認証エリア(loginやsecured_area)セクション内でsecurity: falseを指定するとそのpathに適用される範囲はセキュリティ設定が無効となります。
この際の注意点としてはsecured_areaセクションの前にloginセクションを定義しなければいけないという点です。
loginセクションをsecured_areaセクションの下に持っていった場合、ログインページもsecured_areaの設定が適用されてしまう為、これもまたログインページにアクセス出来なくなってしまいます。


ではsecurity.ymlを設定したら次はAppTestBundle内のrouting.ymlを設定します。

  • src/App/TestBundle/Resources/config/routing.yml
# index
test_index:
    pattern:  /
    defaults: { _controller: AppTestBundle:Default:index }

# ログイン
test_login:
    pattern: /login
    defaults: { _controller: AppTestBundle:Default:login }

# ログイン確認
login_check:
    pattern: /login/check

# ログアウト
logout:
    pattern: /logout

security.yml内で定義した、login_path(/login/check)とlogoutセクション内のpath(/logout)はパスを設定するだけでいいです。コントローラは特に指定しなくて問題ありません、Securityコンポーネントが自動で処理してくれます。

では次にログイン処理の部分をコントローラに定義します。
下記のDefaultControllerにloginActionを追加して、下記のように記述してください。

  • src/App/TestBundle/Controller/DefaultController.php
<?php
namespace App\TestBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;

class DefaultController extends Controller
{
    public function indexAction()
    {
        return $this->render('AppTestBundle:Default:index.html.twig');
    }
    
    public function loginAction()
    {
        $request = $this->getRequest();
        $session = $request->getSession();
        
        $error = null;
        
        // ログインエラーがあれば、ここで取得
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        // Sessionにエラー情報があるか確認
        } elseif ($session->has(SecurityContext::AUTHENTICATION_ERROR)) {
            // Sessionからエラー情報を取得
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
            // 一度表示したらSessionからは削除する
            $session->remove(SecurityContext::AUTHENTICATION_ERROR);
        }
        
        return $this->render('AppTestBundle:Default:login.html.twig', array(
            // ユーザによって前回入力された username
            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
            'error' => $error,
        ));
    }
}

コントローラを上記のように設定した後は次はテンプレートの定義です。
ログインページとなる部分を作成します。
下記のように記述してください。

  • src/App/TestBundle/Resources/views/Default/login.html.twig
{% if error %}
 <div>{{ error.message }}</div>
{% endif %}

<form action="{{ path('test_login_check') }}" method="post">
 <label for="username">ログインID:</label>
 <input type="text" id="username" name="_username" value="{{ last_username }}" />

 <label for="password">パスワード:</label>
 <input type="password" id="password" name="_password" />

  {#
  認証成功した際のリダイレクト URL を制御したい場合は_target_pathにパスを指定する
  <input type="hidden" name="_target_path" value="/" />
  #}

 <input type="submit" name="login" />
</form>

これで準備は完了です、ブラウザでアクセスするとログインページ(/login)に飛ばされると思います。
下記のような画面になれば成功です、ログイン出来るか試してみてください。

これでフォームによるログイン認証が可能になりました。
ですが、今のままだとログイン情報が固定になってしまいます。
ユーザー情報は基データベースで管理したいことが多いですよね、なので次はユーザー情報をデータベースから取ってきて認証してみましょう。

フォーム認証(Doctrineと連携)

ここでは認証に用いるユーザー情報はDoctrineを通して取得し、利用する方法を記述したいと思います。
データベースのセットアップ方法などは省きます。

では最初にユーザー情報となるEntityクラスを作成します、下記のコマンドからEntityクラスを作成してください。

$ php app/console doctrine:generate:entity --entity="AppTestBundle:User" --fields="username:string(255) password:string(255)"

Entityクラスを作成した後はSecurityコンポーネントでこのEntityクラスが使えるようにしなくてはいけません。
Entityクラスが下記ようになるように追記してください。

  • src/App/TestBundle/Entity/User.php
<?php
namespace App\TestBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * App\TestBundle\Entity\User
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="App\TestBundle\Repository\UserRepository")
 */
class User implements UserInterface
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string $username
     *
     * @ORM\Column(name="username", type="string", length=255)
     */
    private $username;

    /**
     * @var string $password
     *
     * @ORM\Column(name="password", type="string", length=255)
     */
    private $password;


    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set username
     *
     * @param string $username
     */
    public function setUsername($username)
    {
        $this->username = $username;
    }

    /**
     * Get username
     *
     * @return string 
     */
    public function getUsername()
    {
        return $this->username;
    }

    /**
     * Set password
     *
     * @param string $password
     */
    public function setPassword($password)
    {
        $this->password = $password;
    }

    /**
     * Get password
     *
     * @return string 
     */
    public function getPassword()
    {
        return $this->password;
    }
    
    /**
     * 権限を返す(今回は権限管理はしない為、固定でROLE_ADMINを返す)
     *
     * @return array
     */
    public function getRoles()
    {
        return array('ROLE_ADMIN');
    }
    
    /**
     * Saltを返すメソッド、パスワードのエンコード方式がplaintextの場合
     * Saltを定義しても意味がないので、これも空文字が返るようにする
     *
     * @return string
     */
    public function getSalt()
    {
        return '';
    }
    
    /**
     * 取得されたくないようなユーザーデータとかを削除するメソッドらしい
     */
    public function eraseCredentials()
    {
        
    }
    
    /**
     * 同一ユーザーであるかの判定
     *
     * @return boolean
     */
    public function equals(UserInterface $user)
    {
        return $this->getUsername() == $user->getUsername();
    }
}

use Symfony\Component\Security\Core\User\UserInterface;を追記した後、
UserIntefaceをimplementsし、UserInterfaceに沿って下記のメソッドを実装する

  • function getRoles()
  • function getSalt()
  • function eraseCredentials()
  • function equals()

各メソッドの説明についてはコメントアウトに記述されている通りです。
また、下記の行でリポジトリクラスの拡張も宣言しています。
@ORM\Entity(repositoryClass="App\TestBundle\Repository\UserRepository")

ここまで実装したら、次はリポジトリクラスの実装です。
下記のコマンドを実行すると自動でリポジトリクラスを作成してくれます。

$ php app/console doctrine:generate:entities App/TestBundle

作成されたリポジトリクラスを下記のように編集します。

  • src/App/TestBundle/Repository/UserRepository.php
<?php
namespace App\TestBundle\Repository;

use Doctrine\ORM\EntityRepository;
use \Symfony\Component\Security\Core\User\UserProviderInterface;
use \Symfony\Component\Security\Core\User\UserInterface;

/**
 * UserRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class UserRepository extends EntityRepository implements UserProviderInterface
{
    /**
     * ユーザー名からユーザー情報を取得する
     *
     * @param string $username
     * @return object
     */
    public function loadUserByUsername($username)
    {
        return $this->findOneBy(array('username' => $username));
    }

    /**
     * ユーザーのリロード
     *
     * @param \Symfony\Component\Security\Core\User\UserInterface $user
     * @return object
     */
    public function refreshUser(UserInterface $user)
    {
        return $this->loadUserByUsername($user->getUsername());
    }

    /**
     * TODO:要確認
     *
     * @param $class
     * @return bool
     */
    public function supportsClass($class)
    {
        return true;
    }
}

まず、Userリポジトリクラスでは下記のインターフェースを利用します。

  • use \Symfony\Component\Security\Core\User\UserProviderInterface;
  • use \Symfony\Component\Security\Core\User\UserInterface;

上記のインターフェースを呼び出した後、UserRepositoryクラスは呼び出した、UserProviderInterfaceを継承します。

  • class UserRepository extends EntityRepository implements UserProviderInterface

その後、UserProviderInterfaceに沿い下記のメソッドを実装します。

  • function loadUserByUsername
  • function refreshUser
  • function supportsClass

各メソッドについてはコメントアウトの説明通りかと思います。
supportsClassメソッドについては自分もよく分かってません…


ここまで実装したら次はsecurity.ymlの設定です。
作成したUserモデルを認証に利用するように変更します。

  • app/config/security.yml
security:
    # エンコーダの設定
    encoders:
        # エンコードと対象となるユーザーモデルを指定(作成したUserモデルを指定)
        App\TestBundle\Entity\User: plaintext

    # ユーザー情報の設定(ユーザープロバイダ)
    providers:
        # 作成したUserモデルをユーザー情報とする
        my_users:
            # Entityクラスの指定とユーザー名となるプロパティを指定
            entity: { class: App\TestBundle\Entity\User, property: username }

    # ファイアーウォールの設定
    firewalls:
        # ログイン画面は認証エリア外
        login:
            # エリア範囲(正規表現)
            pattern: ^/login$
            # セキュリティ設定を無効
            security: false
            
        # 認証エリアの設定
        secured_area:
            # エリア範囲(正規表現)
            pattern: ^/
            # ログインフォームの設定
            form_login:
                # ログインフォームのパス
                login_path: /login
                # ログイン状態を確認するパス
                check_path: /login/check
            # ログアウトの設定
            logout:
                # ログアウト用のURLのパス
                path: /logout
                # ログアウト後に移行するページ
                target: /login

    # アクセス権限の設定
    access_control:
        # /から始まるコンテンツにアクセスするにはROLE_ADMIN権限が必要
        - { path: ^/, roles: ROLE_ADMIN }

先ほどのフォーム認証(ユーザー情報は固定)で利用したsecurity.ymlとの違いは、encodersセクションで指定するモデルクラスの変更とproviders内の変更です。
encodersセクション内は作成したUserクラスを指定します。
providersセクション内はin_memoryセクションを削除し、代わりにmy_usersセクションを追加してあります。
my_usersセクション内には作成したUserクラスとユーザー名となるプロパティ名を指定します。

ここまで完了したら最後にデータベースにテーブルを作成し、最初のユーザーを作成します。
テーブルの作成は下記のコマンドから作成可能です。

$ php app/console doctrine:schema:update --force

テーブルの作成が完了したら、データベースに直接ユーザーデータを登録し、そのユーザーでログインが出来るかどうか確認してください。

一気に全部書こうとしたけどさすがに疲れたので後編に持ち越します。
後編では権限もDBで管理するのとパスワードのエンコードあたりを説明していこうかと。

自分もSymfony2は初心者なので、変なところがあれば突っ込んでいただけると有り難いです!

Xenのメモ

Xenを利用する際のただの覚書き

ゲストOSのインストール

# virt-install --nographics --prompt
Name                              ID VCPUs   CPU State   Time(s) CPU Affinity
Domain-0                           0     0     0   r--      32.7 0
Domain-0                           0     1     1   -b-       3.4 1
Domain-0                           0     2     -   --p       0.7 2
Domain-0                           0     3     -   --p       0.6 3
Domain-0                           0     4     -   --p       0.6 4
Domain-0                           0     5     -   --p       0.6 5
Domain-0                           0     6     -   --p       0.5 6
Domain-0                           0     7     -   --p       0.4 7
db.srv                             3     0     2   -b-       1.7 any cpu
db.srv                             3     1     1   -b-       0.4 any cpu
db.srv                             3     2     7   -b-       0.5 any cpu
db.srv                             3     3     4   -b-       0.3 any cpu
web.srv                            4     0     6   -b-       1.8 any cpu
web.srv                            4     1     5   -b-       0.7 any cpu

上記コマンドを打つと、色々聞かれるので答えていく

割当CPU数の確認

# xm vcpu-list

ゲストOSの割当CPU数の変更

# vim /etc/xen/Domain-U
-----------------------------------
vcpus = 2 ← ここを変更
-----------------------------------

Domain-0の割当CPU数の変更

# vim /etc/xen/xend-config.sxp
-----------------------------------
(dom0-cpus 0 ← ここを変更)
-----------------------------------

Domain-Uにコンソールの切り替え

# xen console domain-u

SSLの場合でも静的コンテンツをキャッシュしたい場合

SSLで保護されているページの場合、FirefoxSafariなどでは静的コンテンツもキャッシュをしないようだ。
毎回、画像とかをリクエストされると描画も遅いので、下記のようにすればSSLの場合でもキャッシュされるようになるみたい。

<IfModule expires_module>
    ExpiresActive On
    <FilesMatch ".(jpeg|jpg|gif|png|gz|css|js)$">
        ExpiresDefault "access plus 10 days"
        Header onsuccess append Cache-Control public
    </FilesMatch>
</IfModule>

重要なのは下記の1行。

Header onsuccess append Cache-Control public

これでちゃんとキャッシュされるようになりました、めでたしめでたし。

cURLの挙動について

PHPcURLを使う時のCURLINFO_HEADER_OUTとCURLOPT_VERBOSEの挙動についてのメモ。

とりあえず最低限のオプションのみで実行してみる。

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://www.google.co.jp/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

echo '<pre>';
print_r(curl_getinfo($ch));
?>

送信ヘッダの取得は出来ない
Array
(
    [url] => http://www.google.co.jp/
    [content_type] => text/html; charset=Shift_JIS
    [http_code] => 200
    [header_size] => 609
    [request_size] => 55
    [filetime] => -1
    [ssl_verify_result] => 0
    [redirect_count] => 0
    [total_time] => 0.17429
    [namelookup_time] => 0.019786
    [connect_time] => 0.058394
    [pretransfer_time] => 0.058422
    [size_upload] => 0
    [size_download] => 29089
    [speed_download] => 166899
    [speed_upload] => 0
    [download_content_length] => 0
    [upload_content_length] => 0
    [starttransfer_time] => 0.102573
    [redirect_time] => 0
)

次にCURLINFO_HEADER_OUTをtrueに設定し、再度実行

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://www.google.co.jp/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
$response = curl_exec($ch);

echo '<pre>';
print_r(curl_getinfo($ch));
?>

正常に送信ヘッダの取得は出来た。

Array
(
    [url] => http://www.google.co.jp/
    [content_type] => text/html; charset=Shift_JIS
    [http_code] => 200
    [header_size] => 609
    [request_size] => 55
    [filetime] => -1
    [ssl_verify_result] => 0
    [redirect_count] => 0
    [total_time] => 0.169215
    [namelookup_time] => 0.019161
    [connect_time] => 0.056915
    [pretransfer_time] => 0.056952
    [size_upload] => 0
    [size_download] => 28910
    [speed_download] => 170847
    [speed_upload] => 0
    [download_content_length] => 0
    [upload_content_length] => 0
    [starttransfer_time] => 0.099661
    [redirect_time] => 0
    [request_header] => GET / HTTP/1.1
Host: www.google.co.jp
Accept: */*


)

次にCURLOPT_VERBOSEだけを有効にしてみる

<?php
$fp = tmpfile();

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://www.google.co.jp/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_STDERR, $fp);
$response = curl_exec($ch);

echo '<pre>';
print_r(curl_getinfo($ch));

fseek($fp, 0);
$verbose = '';
while (($line = fgets($fp)) !== false) {
    $verbose .= $line;
}
fclose($fp);

echo $verbose;
?>

リクエストの詳細の取得は正常に出来た、詳細には送信・受信ヘッダ共に記述されている。

Array
(
    [url] => http://www.google.co.jp/
    [content_type] => text/html; charset=Shift_JIS
    [http_code] => 200
    [header_size] => 609
    [request_size] => 55
    [filetime] => -1
    [ssl_verify_result] => 0
    [redirect_count] => 0
    [total_time] => 0.174437
    [namelookup_time] => 0.017929
    [connect_time] => 0.056555
    [pretransfer_time] => 0.056586
    [size_upload] => 0
    [size_download] => 29107
    [speed_download] => 166862
    [speed_upload] => 0
    [download_content_length] => 0
    [upload_content_length] => 0
    [starttransfer_time] => 0.101525
    [redirect_time] => 0
)
* About to connect() to www.google.co.jp port 80
*   Trying 64.233.183.106... * connected
* Connected to www.google.co.jp (64.233.183.106) port 80
> GET / HTTP/1.1
Host: www.google.co.jp
Accept: */*

< HTTP/1.1 200 OK
< Date: Thu, 28 Jul 2011 11:40:11 GMT
< Expires: -1
< Cache-Control: private, max-age=0
< Content-Type: text/html; charset=Shift_JIS
< Set-Cookie: PREF=ID=f594bcbff899f3b1:FF=0:TM=1311853211:LM=1311853211:S=I6qM4ZKHRVYZOZ4_; expires=Sat, 27-Jul-2013 11:40:11 GMT; path=/; domain=.google.co.jp
< Set-Cookie: NID=49=n-u1vQyrvh8AEIaqRZAgHDdPIHnpkhcADuAyIngubbzP3aPNranWFSvBih4eABLZTaucHxVoLWnec_avcsWfjg0mE7ZOmK904Ne7-J7wwMPHhkQqV6Og8UTL1MS4KJra; expires=Fri, 27-Jan-2012 11:40:11 GMT; path=/; domain=.google.co.jp; HttpOnly
< Server: gws
< X-XSS-Protection: 1; mode=block
< Transfer-Encoding: chunked
* Connection #0 to host www.google.co.jp left intact

次にこの状態で、CURLINFO_HEADER_OUTを有効にしてみる

<?php
$fp = tmpfile();

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://www.google.co.jp/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_STDERR, $fp);
$response = curl_exec($ch);

echo '<pre>';
print_r(curl_getinfo($ch));

fseek($fp, 0);
$verbose = '';
while (($line = fgets($fp)) !== false) {
    $verbose .= $line;
}
fclose($fp);

echo $verbose;
?>

CURLINFO_HEADER_OUTが優先されて、リクエストの詳細は取得出来なかった。
ちなみにCURLINFO_HEADER_OUTをCURLOPT_VERBOSEより下に移動してもダメだったので、CURLINFO_HEADER_OUTが設定されると
CURLOPT_VERBOSEは強制的に無効になるようだ。

Array
(
    [url] => http://www.google.co.jp/
    [content_type] => text/html; charset=Shift_JIS
    [http_code] => 200
    [header_size] => 609
    [request_size] => 55
    [filetime] => -1
    [ssl_verify_result] => 0
    [redirect_count] => 0
    [total_time] => 0.17543
    [namelookup_time] => 0.016662
    [connect_time] => 0.056478
    [pretransfer_time] => 0.056505
    [size_upload] => 0
    [size_download] => 28910
    [speed_download] => 164795
    [speed_upload] => 0
    [download_content_length] => 0
    [upload_content_length] => 0
    [starttransfer_time] => 0.101421
    [redirect_time] => 0
    [request_header] => GET / HTTP/1.1
Host: www.google.co.jp
Accept: */*


)

ちなみに、CURLOPT_VERBOSEは有効にしたくないけど、受信・送信ヘッダ共に取得したい場合は
CURLOPT_HEADERを有効にして、レスポンス(ボディ部含む)からヘッダとボディを分割するしかないっぽい