Do You PHP はてな このページをアンテナに追加 RSSフィード Twitter

2012-02-02

[][]generate:bundleのformatパラメータとDependencyInjection下のExtension.phpの中身

前のエントリの続きです。

追記にも書きましたが、@hidenorigotoさんからpullリクエストをもらい、原因が判りました。ありがとうございます:-)

直接の原因

問題となった部分ですが、

を見るとsrc/Acme/AndRoleVoterBundle/DependencyInjection/AcmeAndRoleVoterExtension.phpに以下のコードが追加されています。

<?php$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));

        $loader->load('services.yml');

「services.ymlを設定ファイルとしてロードするよ」ということですが、まあ言われてみれば当たり前っちゃ当たり前ですね。。。

何でこのような違いが出てきたのか?

ちょっと試したところ、generate:bundleのformatパラメータで指定した内容によってDependencyInjection下のExtension.phpの中身が変わるようです。

以下、formatパラメータを"yml"と"xml"でBundleを作って差分を取った結果です。

$ php app/console generate:bundle --namespace=Acme/YamlBundle --dir=src --format=yml --no-interaction
$ php app/console generate:bundle --namespace=Acme/XmlBundle --dir=src --format=xml --no-interaction
$ diff src/Acme/YamlBundle/DependencyInjection/AcmeYamlExtension.php src/Acme/XmlBundle/DependencyInjection/AcmeXmlExtension.php
3c3
< namespace Acme\YamlBundle\DependencyInjection;
---
> namespace Acme\XmlBundle\DependencyInjection;
15c15
< class AcmeYamlExtension extends Extension
---
> class AcmeXmlExtension extends Extension
25,26c25,26
<         $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
<         $loader->load('services.yml');
---
>         $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
>         $loader->load('services.xml');
$ 

前のエントリでも「別Bundleだとservices.ymlでうまくいってる」と書きましたが、確かにインタラクティブモードでymlを指定してました。

ちなみに、"--format=php"の場合も同様に

  • FileLoaderはLoader\PhpFileLoader
  • 設定ファイルの拡張子は".php"(services.php)

になりました。

ということで

「それぞれのBundleのExtensionでどのフォーマットをサポートしているのか?」を最初っから統一するのが一番面倒がなさそうですね。

2012-02-01

[][]Symfony2で権限の組み合わせを満たす場合のみアクセスを許可したい

Symfony2では権限によるアクセス制御はapp/config/security.ymlなどにある"access_control"で指定しますが、直下のrolesには複数の権限が設定できます。

security:
    access_control:
        - { path: ^/foo/bar/, roles: [ROLE_A, ROLE_B]}

ただし、このrolesって"OR条件"なので、上の例の場合だとROLE_AかROLE_Bを持っていれば"/foo/bar/"にアクセスできます。

で、rolesをAND条件にする方法はないかと

security.ymlのsecurity.access_control.rolesに複数書いた時はOR条件なのか。AND条件にならんのかな?

Twitter / @shimooka: security.ymlのsecurity.acce ...

ツイートしたところ、id:Fivestarさんから

@shimooka そしたらsecurity.access.simple_role_voterサービスを上書きするとかですかねw

Twitter / @fivestr: @shimooka そしたらsecurity.acc ...

というリプライをもらってから試行錯誤していたところ、

@shimooka こんな感じでクラス定義して、DIコンテナパラメータ書き換えるだけでできると思います(実装は確認してないですが...) gist.github.com/1709098

Twitter / @fivestr: @shimooka こんな感じでクラス定義して、DI ...

というサンプルを提示してもらいました。で、

@fivestr おお、似たようなことやってる途中でしたw うまくいったらまとめときます

Twitter / @shimooka: @fivestr おお、似たようなことやってる途中で ...

とりあえず、期待する動作をするようになったので一度まとめておきます。間違いなどあれば指摘してください:-)

前提

  • ユーザーは1つ以上の権限を持つ場合がある
  • 特定のURLに対して、権限の組み合わせを満たす場合のみアクセスを許可したい

環境

  • PHP5.3.9
  • Symfony2.0.9 without venders
  • ソースはgithubに上げてあります
    • Acme/AndRoleVoterBundle
    • app/config/parameters.iniがありませんので、試す場合は別途作成してください

手順1:security.ymlのaccess_controlを定義する

冒頭にも書きましたが、security.access_controlのrolesに複数指定します。

security:access_control:
        - { path: ^/AndRoleVoterBundle/user/, roles: [ROLE_A]}
        - { path: ^/AndRoleVoterBundle/private/, roles: [ROLE_A, ROLE_B]}

また、サンプルでは以下のユーザーも併せて追加してます。

ユーザー名権限
role_aROLE_A
role_bothROLE_A、ROLE_B

github上のソースは以下のとおりです。

手順2:RoleHierarchyVoter#voteのロジックを変更する

id:Fivestarさんからはsecurity.access_control.roles(Symfony\Component\Security\Core\Authorization\Voter\RoleVoter.php)という情報をもらいましたが、このクラスには権限があるかどうかを決定するvoteメソッドがあります。で、実際にはRoleVoterクラスを変更するのではなく、そのサブクラスであるsecurity.access.role_hierarchy_voter.class(Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter.php)が利用されているようでした。RoleHierarchyVoterクラスはvoteメソッドをオーバーライドしていません。

ということで、次のような感じでRoleHierarchyVoterクラスのサブクラスを作り、voteメソッドをオーバーライドします。

<?php

namespace Acme\AndRoleVoterBundle\Component\Security\Authorization\Voter;

use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class AndRoleHierarchyVoter extends RoleHierarchyVoter
{
    /**
     * {@inheritdoc}
     */
    public function vote(TokenInterface $token, $object, array $attributes)
    {
        $satisfied_roles = true;
        $roles = $this->extractRoles($token);
        foreach ($attributes as $attribute) {
            if (!$this->supportsAttribute($attribute)) {
                continue;
            }
            $has_role = false;
            foreach ($roles as $role) {
                if ($attribute === $role->getRole()) {
                    $has_role = true;
                }
            }
            $satisfied_roles &= $has_role;
        }
        return ($satisfied_roles ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED);
    }
}

github上のソースは以下のとおりです。

手順3:サービスのパラメータの上書き設定を追加する

アクセス制限をかけるBundleのResources/config/services.xmlに追加します。

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="security.access.role_hierarchy_voter.class">Acme\AndRoleVoterBundle\Component\Security\Authorization\Voter\AndRoleHierarchyVoter</parameter>
    </parameters>
</container>

github上のソースは以下のとおりです。実際にはservices.xmlとなります。

で。。。

最後のservices.xmlですが、services.ymlでも動作するハズと思い試してみたのですがxml形式でないと動作しませんでした。。。

@shimooka バンドルのResources/configにあるサービス定義ファイルは、バンドルのDependencyInjection/**Extension.php内で読み込んでいます。形式ごとのローダーでファイルを読み込んでいるだけなので、app側とは独立しています

Twitter / @hidenorigoto: @shimooka バンドルのResources/c ...:

ちなみに、Entityを使った認証するBundle環境だとservices.xmlでもservices.ymlファイルでもちゃんと動作してるんですよねぇ。。。なんだろ。。。

追記(2012/02/02 11:35)

@hidenorigotoさんからもらったpullリクエストをマージしました。ありがとうございます:-)

現在のソースではymlファイルでもDIコンテナパラメータ(security.access.role_hierarchy_voter.class)を上書きできます。

なぜこのような違いになったかは次のエントリで

2012-01-30

[][]No encoder has been configured for account

自分用メモ。データベースを使った認証処理を試し中に出てきたエラー。

まさにエラーメッセージの通り"アカウント用のエンコーダが設定されていない"ということなんですが、この「account」って何や?状態。。。

ここで言う「account」はsecurity.ymlのencodersで定義した認証情報を格納するためのクラスということでした。

security:
    encoders:
        Sandbox\SecurityBundle\Entity\Customer:  # <=== これ
            algorithm: sha1
            iterations: 1
            encode_as_base64: false

で、このクラス名の名前空間が間違っていたというガックリなオチでした><

ここまで話が繋がれば確かに「No encoder has been configured for account」ですな。。。

[][]Symfony2で複数EntityManager利用時にBundleを新規追加した時の注意点

自分用メモ。

Symfony2で複数のEntityManagerを利用している場合、generate:bundleで新しくBundleを追加してもconfig.ymlのdoctrine.orm.entity_managers.*.mappingsには追加されない。手動でBundleを追加する必要がある。

以下、config.ymlの例。

doctrine:
              :
    orm:
        auto_generate_proxy_classes: %kernel.debug%
        default_entity_manager:   em1
        entity_managers:
            em1:
                connection: %database_name%
                mappings:
                    MyFirstBundle: ~
                    MySecondBundle: ~  # 追加したBundleを追記する
            em2:
                connection: database2
                mappings:
                    MyFirstBundle: ~
                    MySecondBundle: ~  # 追加したBundleを追記する
              :

ちなみに、新しいBundleに対してgenerate:doctrine:entityでEntityを生成してもdoctrine:schema:createできないし、EntityManager#getRepositoryもできない。

たとえば、MyFirstBundle

$ # 最初のBundleを作成
$ #
$ php app/console generate:bundle --namespace=My/FirstBundle --dir=src --no-interaction
$ 
$ #
$ # Entity"Customer"を作成
$ #
$ php app/console generate:doctrine:entity --entity=MyFirstBundle:Customer --format=annotation --fields="name:string(255) memo:text createdAt:datetime updatedAt:datetime" --no-interaction
$ 
$ #
$ # ここでconfig.ymlのentity_managersの設定を行い、
$ # mappingsにMyFirstBundleを指定する
$ #
$ 
$ #
$ # DBに反映(EntityManagerはem1)
$ #
$ php app/console doctrine:schema:create
$ 
$ #
$ # 2つ目のBundleを作成
$ #
$ php app/console generate:bundle --namespace=My/SecondBundle --dir=src --no-interaction
$ 
$ #
$ # Entity"Admin"を作成
$ #
$ php app/console generate:doctrine:entity --entity=MySecondBundle:Admin --format=annotation --fields="name:string(255) memo:text createdAt:datetime updatedAt:datetime" --no-interaction
$ 
$ #
$ # DBに反映(EntityManagerはem1)
$ #
$ php app/console doctrine:schema:update --force
Nothing to update - your database is already in sync with the current entity metadata.
$ 

ここで冒頭のようにconfig.ymlのmappingsにMySecondBundleを追記してやるとadminテーブルが正しく作成される。

$ php app/console doctrine:schema:update --force
Updating database schema...
Database schema updated successfully! "2" queries were executed
$ 

2012-01-24

[][]PDO_PGSQL利用時にclient_encodingを指定する

ちょっと悩んでましたが難しく考えすぎでした。。。

とりあえず、"postgresql.conf内にclient_encodingを定義しておく"というのを除いて、2パターンはありますね。

環境変数"PGCLIENTENCODING"を利用する

httpd起動時に環境変数"PGCLIENTENCODING"が設定されていればOKです。通常はこちらでしょうか。

たとえば、apachectlを実行する前にPGCLIENTENCODINGをexportしておくとか。また、/usr/local/apache2/bin/envversに定義する場合は以下のような感じ。

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
# envvars-std - default environment variables for apachectl
#
# This file is generated from envvars-std.in
#
LD_LIBRARY_PATH="/usr/local/apache2/lib:$LD_LIBRARY_PATH"
export LD_LIBRARY_PATH
#
export PGCLIENTENCODING=UTF8

ただし、Webアプリから複数DBを使う、かつ、エンコーディングが変わる(既存のDBと新規DBが混ざってるとか)場合は使えませんね。

config.yml(もしくはparameters.ini)に定義する

接続情報のhost、port、dbnameのいずれかにclient_encodingのオプションを付けてやればOKです。以下の例は昨日のエントリにあるconfig.ymlのdbnameにオプションを付けたものです。

doctrine:
    dbal:
        default_connection: %database_name_0%
        connections:
            %database_name_0%:
                driver:   %database_driver%
                host:     %database_host%
                port:     %database_port%
                # 次のようにシングルクオートがなくてもOK(っぽい)
                # %database_name_0% options=--client_encoding=UTF8
                dbname:   %database_name_0% options='--client_encoding=UTF8'
                user:     %database_user%
                password: %database_password%
            %database_name_1%:
                driver:   %database_driver%
                    :

このoptionsの書き方は、PHP: pg_connect - Manualの例にあります。

config.ymlのDB接続情報は、Symfony/vendor/doctrine-dbal/lib/Doctrine/DBAL/Driver以下にある各DBに対応したドライバ内でDSNに変換されますが、その際に利用される項目は先程のhost、port、dbnameになります。ですので、これらのいずれかにオプションを付ける必要があります。

ちなみに、PDO_PGSQL用のDoctrineドライバ(Symfony/vendor/doctrine-dbal/lib/Doctrine/DBAL/Driver/PDOPgSql/Driver.php)の場合、Configuration Reference - Symfonyにある"charset"は無視されるので注意が必要です。

こちらの方法なら昨日のエントリと組み合わせて、エンコーディングが混ざった複数DB環境でも大丈夫そうです。

って、そんな環境あるんかな。。。歴代のレガシーシステムDBに接続しなきゃならないWebアプリとか?あー。。。orz

2012-01-23

[][]Symfony2で複数DBに接続する

最近になってようやくSymfony2を始めました。現在blogチュートリアル写経中です(MySQLではなくPostgreSQL使ってますが。。。)。

で、DBへの接続・CRUDを一通り確認した後、複数DBへの接続を検証してみました。とりあえず、同一ホスト上の別DBですが、接続し分けることが確認できたので備忘録としてまとめておきます。

環境

設定ファイル

まずはSymfony/app/config/parameters.ini。"database_name"の部分を別途追記。まあ、config.ymlで使用する変数を定義しているだけなので必須ではありません。

[parameters]
    database_driver   = pdo_pgsql
    database_port     = 5432
    database_name     = blogsymfony2
    database_user     = blogsymfony2
    database_password = blogsymfony2

    database_name_0   = blogsymfony2
    database_name_1   = blogsymfony2_1
    database_name_2   = blogsymfony2_2
              :

続いてSymfony/app/config/config.yml。記述方法については以下を参照。

にあるとおり。

doctrine:
    dbal:
        # デフォルトの接続先DBは"blogsymfony2"
        default_connection: %database_name_0%
        # 複数定義する場合は、"connections"の下に記述する
        connections:
            %database_name_0%:
                driver:   %database_driver%
                host:     %database_host%
                port:     %database_port%
                dbname:   %database_name_0%
                user:     %database_user%
                password: %database_password%
            %database_name_1%:
                driver:   %database_driver%
                host:     %database_host%
                port:     %database_port%
                dbname:   %database_name_1%
                user:     %database_user%
                password: %database_password%
            %database_name_2%:
                driver:   %database_driver%
                host:     %database_host%
                port:     %database_port%
                dbname:   %database_name_2%
                user:     %database_user%
                password: %database_password%
    orm:
        auto_generate_proxy_classes: %kernel.debug%
#        auto_mapping: true
        # それぞれの接続に対してEntityManagerを定義
        # 便宜上、DB名と同じにしている
        entity_managers:
            default:
                # "blogsymfony2"に接続
                connection: %database_name%
                mappings: # Required
                    MyBlogBundle: ~
            %database_name_1%:
                # "blogsymfony2_1"に接続
                connection: %database_name_1%
                mappings: # Required
                    MyBlogBundle: ~
            %database_name_2%:
                # "blogsymfony2_2"に接続
                connection: %database_name_2%
                mappings: # Required
                    MyBlogBundle: ~
              :

PHPスクリプト(Controllerクラス)

とりあえずblogチュートリアルで作成したコントローラ(Symfony/src/My/BlogBundle/Controller/DefaultController.php)で確認。Registry::getEntityManagerメソッドを呼び出す際に、EntityManager名を第1引数に指定します。ちなみに"%〜%"ではなくparameters.iniで定義した名称を指定します。指定しない場合は、デフォルトのEntityManager名(config.ymlのentity_managers直下の"default")が指定されたことになります。

以下は、entity_managers下2つ目の"%database_name_1%"(つまり"blogsymfony2_1")を指定した例です。この場合、接続先DBは"blogsymfony2_1"になります。

<?php
namespace My\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use My\BlogBundle\Entity\Post;

class DefaultController extends Controller
{
    public function indexAction()
    {
        /**
         * 引数なしの場合、デフォルトのEntityManagerである
         * %database_name_0%("blogsymfony2")に接続
         */
//        $em = $this->getDoctrine()->getEntityManager();
        $em = $this->getDoctrine()->getEntityManager('blogsymfony2_1');
//        $em = $this->getDoctrine()->getEntityManager('blogsymfony2_2');

        /**
         * 接続先情報の表示
         */
        var_dump($em->getConnection()->getParams());
              :

ちなみに、DB接続だけ取得する場合は、次のような感じで。

<?php
$conn = $this->getDoctrine()->getConnection('blogsymfony2_1')->connect();

もっといい方法があれば教えて下さい:-)

2012-01-19

[][]503: Stop SOPA/PIPA

2012/01/19 12:45(JST)現在、www.php.netアクセスすると

f:id:shimooka:20120119124832p:image

のような表示になってますね。レスポンスヘッダも。。。

HTTP/1.1 503 Stop SOPA/PIPA
Date: Thu, 19 Jan 2012 03:45:06 GMT
Server: Apache/1.3.41 (Unix) PHP/5.2.17
X-Powered-By: PHP/5.2.17
Content-Language: en
Status: 503 Stop SOPA/PIPA
Retry-After: 7200
Last-Modified: Wed, 18 Jan 2012 14:40:05 GMT
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8

ちなみに、トップページだけのようで、ダウンロードページやサブディレクトリなどには直接アクセスできるようです。

追記(2012/01/09 17:00)

現時点では元に戻っているようです。

2012-01-18

[][]File_AndroidManifest作ってみた

2012年ももう20日近く経とうとしていますが、今年最初のエントリです:-)

昨年の10月にKlabさんの開発者Blogに面白そうなエントリが上がりました。

AndroidアプリにはAndroidManifest.xmlというマニフェストファイルが含まれていますが、プレーンなXMLファイルではなく独自のバイナリ形式となっているため通常は読むことができません。Klabさんのエントリではバイナリデータの解析を行ってデータフォーマットを類推し、Cによるパーサを作成しています。

で、「おお、これ面白い」ということでPHPでも実装してみました。まあ、特に何かをするという訳ではないんですが(汗)

インストールpearコマンド一発です。

$ sudo pear channel-discover openpear.org
$ sudo pear install openpear/File_AndroidManifest-alpha
$ 

ざっとした使い方は以下のとおりです。

<?php
error_reporting(E_ALL);
require_once 'File/AndroidManifest.class.php';

$manifest = new AndroidManifest(file_get_contents('/path/to/AndroidManifest.xml'));
$element = $manifest->getSimpleXMLElement();
          :

AndroidManifestクラスをrequireしてコンストラクタにAndroidManifest.xmlの中身を丸ごと渡し、結果をgetXMLメソッドもしくはgetSimpleXMLElementメソッドで受け取ります。前者は文字列で、後者はSimpleXMLElementオブジェクトを返します。

Klabさんのエントリで使用しているエンコードずみAndroidManifest.xmlを読み込ませた場合、getXMLメソッドで以下のようなXMLが取得できます。実際は整形されておらず、1行で返されます。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    package="jp.klab.sample.myapp" >
    <uses-sdk android:minSdkVersion="4" ></uses-sdk>
    <application android:label="@0x7F050001" android:icon="@0x7F020000" >
        <activity
            android:label="@0x7F050001"
            android:name=".MyApp"
            android:excludeFromRecents="false"
            android:launchMode="2"
            android:configChanges="0x000000A0" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" ></action>
                <category android:name="android.intent.category.LAUNCHER" ></category>
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" ></uses-permission>
</manifest>

ホントは年末年始の宿題として作ってあったのですが、ソースのコメントを書くのが億劫になってしまって伸び伸びになってしまいました。。。orz