2011年11月7日月曜日

[symfony2]ユーザーとかのフォームとかコントローラーとか

少し間が開きましたが、認証関連のフォームを作ってみました。

1.UserとRoleのフォームを作る
それぞれ以下のコマンドを実行します。
php app/console doctrine:generate:crud --entity=AcmeSecurityBundle:User --route-prefix=AcmeSecrityBundle_user --with-write
php app/console doctrine:generate:crud --entity=AcmeSecurityBundle:Role --route-prefix=AcmeSecrityBundle_role --with-write

「doctrine:generate:crud」は指定したエンティティに対して、新規追加、一覧、編集、詳細のフォームを作るコマンドみたいです。
--entity 作成するフォームのエンティティを指定
--route-prefix ルーターの接頭辞
--with-write 一緒にnewとdeleteアクションを作ってくれる?
フォーマットの指定はアノテーションでやりました。

するとsymfonyが自動で入力フォームをサクッと作ってくれました。
できたものを確認すると、多分以下ファイルが出来てました。
src/Acme/SecurityBundle/Controller/UserController.php
src/Acme/SecurityBundle/Form/UserType.php
src/Acme/SecurityBundle/Resources/views/User/edit.html.twig
src/Acme/SecurityBundle/Resources/views/User/index.html.twig
src/Acme/SecurityBundle/Resources/views/User/new.html.twig
src/Acme/SecurityBundle/Resources/views/User/show.html.twig
*Roleも同じものが出来てました。

早速ブラウザで「ect.com/web/app_dev.php/user/」にアクセスすると一覧が出てました。
2.Role.phpの改造
そのまま新規追加してみようと思いまして、新規追加するリンクをクリックしたらエラーが出ました。
Roleエンティティに__toString()メソッドを作ってくれない?みたいな感じです。
多分ユーザーの権限の入力フォームがselectタグで、そこに表示する用の文字列を吐き出せば良いかなと当たりをつけて以下のコードを追加してみました。
# src/Acme/SecurityBundle/Entity/Role.php
public function __tostring()
 {
  return $this->name;
 }

再読込みをしてみると無事表示されました。

3.パスワードをハッシュ化する用のユーティリティクラス
このままだと、パスワードは平文のままデータベースに保存されます。それだと色々とまずいのでハッシュ化する機能を追加する。
パスワードをハッシュ化するようなユーティリティクラスを作る。前に機能拡張した時のコメントと日本symfonyユーザー会のセキュリティ パスワードのエンコードを参考に以下の様に作ってみました。新しくUtilディレクトリを作って、クラス名はまあ適当な感じです。
# src/Acme/SecurityBundle/Util/PasswordUtil.php
namespace Acme\SecurityBundle\Util;

class PasswordUtil
{
 /**
  *
  * パスワードをハッシュ化する一連の処理を行う。
  * @param From $form
  * @param Controller $controller
  * @return Form $form
  */
 public static function encodePasswordUtil($form, $controller)
 {
  //まず下準備
  $factory = $controller->get('security.encoder_factory');
  //$formからエンティティを取り出す
  $data = $form->getData();
  //下準備2
  $encoder = $factory->getEncoder($data);
  //パスワードをハッシュ値にする
  $hashedPassword = $encoder->encodePassword($data->getPassword(), $data->getSalt());
  //エンティティにハッシュ化したパスワードをセットする。
  $data->setPassword($hashedPassword);
  //$formにエンティティをセットする
  $form->setData($data);
  return $form;
 }

 // 文字列がハッシュか確認する。
 // @param $password フォームの内容が入っているリクエスト。
 // @return bool 長さ40で英数字の場合はtrue
 public static function isHashUtil($password)
 {
  $pattern = '/[a-zA-z0-9]{40}/i';
  if(strlen($password)===40 && preg_match($pattern, $password)){
   return true;
  }else{
   return false;
  }
 }
}


4.パスワードをハッシュ化するために変更したユーザーコントローラー
続いてユーザーコントローラー
# src/Acme/SecurityBundle/Controller/UserController.php

...
use Acme\SecurityBundle\Util\PasswordUtil;

/**
* User controller.
*
* @Route("/admin/user")
*/
class UserController extends Controller
{
....
 /**
  * Creates a new User entity.
  *
  * @Route("/create", name="AcmeSecurityBundle_user_create")
  * @Method("post")
  * @Template("AcmeSecurityBundle:User:new.html.twig")
  */
 public function createAction()
 {
  $entity = new User();
  $request = $this->getRequest();
  $form = $this->createForm(new UserType(), $entity);
  $form->bindRequest($request);

  if ($form->isValid()) {
   //パスワードをハッシュ化した$formを取得
   $form = PasswordUtil::encodePasswordUtil($form, $this);
   $em = $this->getDoctrine()->getEntityManager();
   $em->persist($entity);
   $em->flush();
   return $this->redirect($this->generateUrl('AcmeSecurityBundle_user_show', array('id' => $entity->getId())));
  }
  return array(
   'entity' => $entity,
   'form' => $form->createView()
  );
 }
...
 /**
  * Edits an existing User entity.
  *
  * @Route("/{id}/update", name="AcmeSecurityBundle_user_update")
  * @Method("post")
  * @Template("AcmeSecurityBundle:User:edit.html.twig")
  */
 public function updateAction($id)
 {
  $em = $this->getDoctrine()->getEntityManager();
  $entity = $em->getRepository('AcmeSecurityBundle:User')->find($id);
  if (!$entity) {
   throw $this->createNotFoundException('Unable to find User entity.');
  }
  $editForm = $this->createForm(new UserType(), $entity);
  $deleteForm = $this->createDeleteForm($id);
  $request = $this->getRequest();
  $editForm->bindRequest($request);
  if ($editForm->isValid()) {
   //パスワードがハッシュ値ならそのままupdate、
   //ハッシュ値で無ければ恐らく変更があったということなので、ハッシュ化してupdate
   //そんな訳で取り敢えずフォームのパスワードを取得
   $data = $editForm->getData();
   $password = $data->getPassword();
   //パスワードがハッシュ化されているか調べる
   if(!PasswordUtil::isHashUtil($password)){
    //パスワードをハッシュ化したFormを受け取る
    $editForm = PasswordUtil::encodePasswordUtil($editForm, $this);
   }
   $em->persist($entity);
   $em->flush();
   return $this->redirect($this->generateUrl('AcmeSecurityBundle_user_edit', array('id' => $id)));
  }

  return array(
   'entity'  => $entity,
   'edit_form' => $editForm->createView(),
   'delete_form' => $deleteForm->createView(),
  );
 }
}


5.ちょっとした解説
PasswordUtil::encodePasswordUtil()関数の引数$formはUserコントローラー内で出てくる以下の2つみたいなヤツです。
$form = $this->createForm(new UserType(), $entity);
$editForm = $this->createForm(new UserType(), $entity);

もう一つの引数$controllerはUserコントローラー自身($this)です。
PasswordUtil::encodePasswordUtil()関数の流れを少し説明すると、「$data = $form->getData();」でエンティティが取得できます。つまりUser.phpです。なので「$data->getPassword()」とか「$data->getSalt()」でフォームに入力された値が取得できます。
これを順調にハッシュ化していって、「$form->setData($data);」でフォームに設定し直して返します。

もう一つのPasswordUtil::isHashUtil()関数はupdateアクションの時に利用しています。ハッシュ値がどうかを検証してます。

UserControllerは自動で出来たコードから変更した箇所は
  1. use Acme\SecurityBundle\Util\PasswordUtil;を追加
  2. createAction()にパスワードがハッシュ化するように色々追加
  3. updateAction()にフォームのパスワードがハッシュ値ではない場合にハッシュ化する処理を色々と追加
updateAction()の方法はあまり良くないですが、一応これでおいておく。
これでひと通りはOKかなと思います。

次はフォームの表示を変更していく予定です。

2011年11月6日日曜日

[symfony2][mac]データベース関連のコンソールでエラー

普段は会社でしかsymfony2をいじってなかったのだが、散々遅れたので家のmacでよやろうと思ったらsymfonyのコンソールでエラーが出ました。

コンソールで実行したコマンドは
php app/console doctrine:schema:update --force

出てきたエラーは
[PDOException]
SQLSTATE[HY000] [2002] No such file or directory
[ErrorException]
Warning: PDO::__construct(): [2002] No such file or directory
(trying to connect via unix:///var/mysql/mysql.sock) in /Applications/
MAMP/htdocs/ext/vendor/doctrine-dbal/lib/Doctrine/DBAL/Driver/
PDOConnection.php line 36

でした。どうして良いか分からず途方に暮れてましたが、以前に「googleグループの日本symfonyユーザー会に気軽に質問して下さい。」とメールを頂いていたのでディスカッションに投稿してみました。ものの15分ぐらいで返信ありました。感謝感謝。

その投稿を引用すると
...
エラーを見るに、UNIXソケットを利用してMySQLの接続を行おうとして、
接続に失敗しているものと思われます。

http://jp2.php.net/manual/ja/ref.pdo-mysql.connection.php

app/config/config.yml にてUNIXソケットの指定をすれば接続できるかと思います。
設定については下記のドキュメントを参考にしてください。
# unix_socket という項目名です

http://symfony.com/doc/2.0/reference/configuration/doctrine.html
...
との事でした。

早速リンク先のConfiguration Referenceを見ると
unix_socket: /tmp/mysql.sock
ふむふむ、この「mysql.sock」を探せば良いのか…とmacのspotlightで検索するも出て来ません。
ググれ俺。
って事で「いろいろあって仕事に手が付かない、、」というブログの記事に(感謝感謝)。
mysqladmin version
で調べればわかるとあったのですが、分からず(この理由は後でわかります。)
諦めかけた時にMAMPのスタートページをふと見ると、「
ソケット /Applications/MAMP/tmp/mysql/mysql.sock」ってある…

今日の教訓「灯台下暗し」

そんな訳でファイルを修正
# app/config/config.yml
doctrine:
 dbal:
   ...
   unix_socket:  /Applications/MAMP/tmp/mysql/mysql.sock

で意気込んで「php app/console doctrine:schema:update --force」を実行…

また違うエラー出たー!
今度のエラーは
Exception]
DateTime::__construct(): It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected 'Asia/Tokyo' for 'JST/9.0/no DST' instead

ん?デフォルトのタイムゾーンが違うぞっていう話??
おかしいとおもいいつつMAMPのphp.iniを開く。
やっぱし「date.timezone = "Asia/Tokyo"」になってました。そりゃそうだ、前に直したもんな。なんぞや?...
やっぱしググれ俺。
って事で、それっぽい記事を見つけました。tech tech -テクテクブログ-の記事(感謝感謝)を参考にバックアップとコピーする。

今度こそと「php app/console doctrine:schema:update --force」を実行…

無事実行されました。
なるほど、だからさっきの「mysqladmin version」も分からなかったんだなと一人納得。

ということで今日はちっとも作業がすすみませんでした。
ではでは。

2011年11月5日土曜日

[Symfony2]ユニーク バリテーション

Userを追加する時に、usernameが既に使用されていないかのチェックをするバリテーションをしたかった。
日本Symfonyユーザー会の方では見つからなかったので、本家のValidation Constraints ReferenceUniqueEntityが怪しいと調べてみる。

結局このページ通りにやるとユニークか調べてくれます。ついでにメッセージを変更してみました。
# src/Acme/SecurityBundle/Entity/User.php
...
use Symfony\Bridge\Doctrine\Validator\Constraints as DoctrineAssert; ←追加
/**
* @orm\Entity(repositoryClass="Manyo\SecurityBundle\Repository\UserRepository")
* @orm\Table(name="user")                 
* @DoctrineAssert\UniqueEntity(                   ←追加
* fields="username",
* message="このユーザー名は既に使用されています"
* )            
*/
class User implements UserInterface{
...

2011年11月4日金曜日

[symfony2]認証(ファイアーウォール )と承認(アクセス制御)

日本Symfonyユーザー会のsymfony2ガイドブックのセキュリティデータベースと Doctrine (“The Model”)辺りを読みながらユーザー認証とアクセス制御の仕組みを作ってみました。
それだけでは難しかったので、他にもVOYAGE GROUPさんのブログにもお世話になりました。感謝感謝です。

作りたいものの整理

今回、作りたかったのは
  1. /loginでログイン
  2. /logoutでログアウト
  3. ログインフォームでログイン
  4. データベースからユーザーを取得
  5. 簡単にアクセス出来るように、security.ymlでの設定
  6. アクセス制御はsecurity.yml
  7. 「/」はアクセスフリー、「page/home」はユーザー権限でログインした時にアクセス可。

はまった箇所

何箇所かハマりました。
  1. security.ymlで「anonymous: ~」の「~」は多分tureの意
  2. 「UserInterface」でオーバーライドするメソッド
  3. Userテーブルとは別にRoleテーブルを作る

作業手順

最終的にはこの順序で作れば良い?という手順。
1.securityBundleを作る。
php app/console generate:bundle --namespace=Acme/SecurityBundle

取り敢えず何はともあれSecurityBundleを作る。ガイドブックには特に解説がありませんが、Symfonyのお作法的にはSecurityBundleを作ってその中に認証と承認のコードを書くのが良いみたいです。この時formatはアノテーションを選択します。これはデータベースの設定にアノテーションを使って説明が多いからです。.ymlでも指定できるならそっちでもいいのですが。

2.security.ymlを設定する。
ガイドブックだと、security.ymlとconfig.ymlでごちゃ混ぜになってますが、どうやらsecurity.ymlに書くのが正しいようです。
# app/config/security.yml
security:
 encoders:
  main1:
   class: Symfony\Component\Security\Core\User\User
   algorithm: plaintext
  main2:
   class: Acme\SecutityBundle\Entity\User
   algorithm: sha1
   iterations: 1
   encode_as_base64: false

 role_hierarchy:
  ROLE_ADMIN: ROLE_USER
  ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

 providers:
  chain_provider:
   providers: [in_memory,user_db]
  in_memory:
   users:
    user: { password: userpass, roles: [ 'ROLE_USER' ] }
    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
  user_db:
   entity: { class: Acme\SecurityBundle\Entity\User, property: username}

 firewalls:
  login:
   pattern: ^/login$
   security: false

  secured_area:
   pattern: ^/
   anonymous: ~
   form_login:
    login_path: /login
    check_path: /login_check
   logout:
    path: /logout
    target: /

 access_control:
  - { path: ^/admin/user, role: ROLE_ADMIN }
  - { path: ^/admin/role, role: ROLE_ADMIN }
  - { path: ^/page/home, role: ROLE_USER }
  - { path: ^/page/information, role: ROLE_USER }

encodersはパスワード暗号化の為の設定で、上記の様に複数指定出来るようです。
role_hierarchyは権限の階層。
providersは要はユーザー情報の設定の事で、security.ymlに直接書く設定とデータベースから引っ張ってくる方法を設定しています。
firewallsは認証の設定。
firewallsの「anonymous: ~」とすると、認証はするけれどもアクセス制御が空なら全部見れるよっていう設定っぽい。
access_controlはアクセス制御の設定。

3.ルーティングの設定
ログイン、ログアウトのルーティング設定をします。

# app/config/routing.yml
login:
 pattern: /login
 defaults: { _controller: AcmeSecurityBundle:Security:login }

login_check:
 pattern: /login_check

logout:
 pattern: /logout

ログイン、ログインチェック、ログアウトのルーティングを設定します。ログインチェックとログアウトは特にアクション、テンプレートを用意する必要はないそうです。


4.コントローラーを作る

# src/Acme/SecurityBundle/Controller/SecurityController.php
namespace Acme\SecurityBundle\Controller;

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

class SecurityController extends Controller
{
 public function loginAction()
 {
  $request = $this->getRequest();
  $session = $request->getSession();

  // ログインエラーがあれば、ここで取得
  if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
   $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
  } else {
   $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
  }

  return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
   // ユーザによって前回入力された username
   'last_username' => $session->get(SecurityContext::LAST_USERNAME),
   'error' => $error,
  ));
 }
}


5.ログインフォームのテンプレートを作る

# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig
{% if error %}
 <div>{{ error.message }}</div>
{% endif %}

<form action="{{ path('login_check') }}" method="post">
 <label for="username">Username:</label>
 <input type="text" id="username" name="_username" value="{{ last_username }}" />

 <label for="password">Password:</label>
 <input type="password" id="password" name="_password" />

 {#
  認証成功した際のリダイレクト URL を制御したい場合(詳細は以下に説明する)
  <input type="hidden" name="_target_path" value="/account" />
 #}

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


6.Userエンティティを作る。
取り敢えず「UserInterface」は無視する。メソッドをオーバーライドしておかないと、コンソールが動かないので。
# src/Acme/SecurityBundle/Entity/User.php
namespace Acme\SecurityBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* @orm\Entity(repositoryClass="Acme\SecurityBundle\Repository\UserRepository")
* @orm\Table(name="user")
*/
class User //implements UserInterface
{
  /**
  * @orm\Id
  * @orm\Column(type="integer")
  * @orm\GeneratedValue(strategy="AUTO")
  */
 protected $id;
 /**
  * @orm\Column(name="username",unique="true")
  */
 protected $username;
 /**
  * @orm\Column(name="password",length="255",)
  */
 protected $password;
  /**
  * @orm\Column(name="salt", length="16")
  */
 protected $salt;
  /**
  * @orm\Column(name="role_id", type="integer", nullable="true")
  */
 protected $role;

}

$roleはユーザーのロールエンティティが入るイメージだと思います。

ドクトリンにゲッター、セッターを作ってもらいます。
php app/console doctrine:generate:entities Acme/SecurityBundle/Entity/User

「UserInterface」をインプリメントして以下のメソッドを追加。
# src/Acme/SecurityBundle/Entity/User.php
...
 public function getRoles(){
  return array($this->role->getName());
 }
 public function eraseCredentials(){
 }
 public function equals(UserInterface $user){
  return $this->getUsername() == $user->getUsername();
 }

テーブルは後でまとめて作るのでここではスルーする。

7.Roleエンティティを作る

# src/Acme/SecurityBundle/Entity/Role.php
namespace Acme\SecurityBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @orm\Entity(repositoryClass="Acme\SecurityBundle\Repository\RoleRepository")
* @orm\Table(name="role")
*/
class Role
{
 /**
  * @orm\Id
  * @orm\Column(type="integer")
  * @orm\GeneratedValue(strategy="AUTO")
  */
 protected $id;
 /**
  * @orm\Column(name="name", unique="true", nullable="true")
  */
 protected $name;
 /**
  * @ORM\Column(name="description", type="text")
  */
 protected $description;

 /**
  * @ORM\OneToMany(targetEntity="User", mappedBy="role")
  */
 protected $users;

 public function __construct()
 {
  $this->users = new ArrayCollection();
 }
}


ドクトリンにゲッター、セッターを作ってもらいます。
php app/console doctrine:generate:entities Acme/SecurityBundle/Entity/Role


8.データベースにテーブルを作る
php app/console doctrine:schema:update --force


9.レポジトリを作る

php app/console doctrine:generate:entities Acme

ユーザーレポジトリは「UserProviderInterface」を「implements」する必要があるので、追加します。
最終的には以下のようになってます。
namespace Acme\SecurityBundle\Repository;

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

/**
* UserRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class UserRepository extends EntityRepository implements UserProviderInterface
{
 public function loadUserByUsername($username)
 {
  return $this->findOneBy(array('username' => $username));
 }
 public function supportsClass($class)
 {
  return true;
 }
 public function refreshUser(UserInterface $user)
 {
  if(!$user->getUsername()){
   throw new Exception(sprintf('Intances of "%s" are not supported.', get_class($user)));
  }
   return $this->loadUserByUsername($user->getUsername());
 }
}


一応これでブラウザでアクセスするとそれっぽい事になっています。
次回はこれに続いてUserの管理する辺りを作っていきます。

2011年11月2日水曜日

Symfony2でxml表示

Symfony2でhtmlではなく、xmlを表示するように作りたいので調べてみました。

助けてもらってばかりで足を向けて寝れない、日本symfonyユーザー会クイックツアー コントローラーにそれっぽい記述があるので試してみました。

# src/Acme/PagesBundle/Controller/PagesController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

...

 /**
 * @Route("/xml", defaults={"_format"="xml"}, name="_xml")
 * @Template()
 */
 public function xmlAction()
 {
  return $this->render('AcmePagesBundle:Pages:xml.xml.twig');
 }

# src/Acme/PagesBundle/Resources/views/Pages/
pc_home.html.twig
...
<a href="{{ path('_xml') }}">XML test</a>
...

# src/Acme/PagesBundle/Resources/views/Pages/xml.xml.twig
<test>
 xml test
</test>


これでOKかなと、ブラウザでアクセスするとエラーが表示される。
An exception has been thrown during the rendering of a template ("Route "_xml" does not exist.") in "AcmePagesBundle:Pages:pc_home.html.twig" at line 15.

「_xml」っていう名前のルートは設定されてないよ。って意味かな?多分。...速く英語勉強しないとダメだなぁ。

そんなこんなで色々と試して結局以下のように書いたら動いた。
# src/Acme/PagesBundle/Controller/PagesController.php
 public function xmlAction()
 {
  return $this->render('AcmePagesBundle:Pages:xml.xml.twig');
 }
useとアノテーションを消す。

# src/Acme/PagesBundle/Resources/views/Pages/
pc_home.html.twig
...
<a href="{{ path('AcmePagesBundle_xml') }}">XML test</a>
...
このルーティングの名前を変更。

# src/Acme/PagesBundle/Resources/views/Pages/xml.xml.twig
<test>
 xml test
</test>
このファイルは変更なし。

# src/Acme/PagesBundle/Resources/config/routing.yml
...
AcmePagesBundle_xml:
 pattern: /xml
 defaults: { _controller: AcmePagesBundle:Pages:xml, _format: xml }
通常のルーティングの設定に「_format: xml」を追加。

これで何とかxmlで表示されるように動いた。

FirefoxのFirebugでコンテンツタイプを見てみても
Content-Type text/xml; charset=UTF-8
って出てる。ヨシヨシ。

...ただ、アノテーション使う方式は何がダメだったのかなぁ。謎は残ったままです。

Symfony2で機能拡張 訂正

昨日、Symfony2で機能拡張という記事を投稿したらコメント頂きました。Hidenori GOTO さん有難うございます。

以下コメント抜粋
まずドキュメントのサービスコンテナのページで説明しているMailerクラスの配置場所ですが、バンドルのディレクトリ直下に配置しているようですね。

また、今回作られているようにコントローラ内で直接利用するライブラリクラスは、DependencyInjectionディレクトリではなくて、単純にバンドルディレクトリ直下や、Util、Request等ライブラリに合わせたディレクトリを作って配置すればOKです。
バンドルディレクトリ直下で良かったのか。なるほど、なるほど。

という訳で、Symfony2のお作法に従って書き直しする。

#\src\Acme\PagesBundle\Carrier.php
namespace Acme\PagesBundle;

use Symfony\Component\HttpFoundation\Request;

class Carrier
{
}
保存場所を変更して名前空間も変更。

#\src\Acme\PagesBundle\Controller\PagesContller.php
use Acme\PagesBundle\Carrier;
...
 public function indexAction()
 {
  $carrier = new Carrier();
 }
useの設定変更

とこんな感じでした。ちゃんちゃん。

2011年11月1日火曜日

Symfony2のRequestクラスのserver値の取り出し

コントローラー内でRequestクラスを取得するのは

$request = $this->getRequest();

でOKですが、その後でつまづきました。

今回欲しい値は$_SERVER['HTTP_USER_AGENT']だったのですが、

$sever = $request->server;

で$_SERVER変数の値は取れているようですが、

$agent = $request->server->parameters['HTTP_USER_AGENT'];

で取得しようとするとエラーが出ました。
なんかparametersはprotectedだからダメって感じです。

でvendor¥symfony¥src¥Symfony¥Component¥HttpFoundation¥Request.phpを開いてのぞく。
 ↓
Request->serverはコメントから¥vendor¥symfony¥src¥Symfony¥Component¥HttpFoundation¥ServerBag.phpらしいのでを開いてのぞく。
 ↓
メソッドが1つポツンとあるだけなので、親クラスの¥vendor¥symfony¥src¥Symfony¥Component¥ParameterBag.phpを開いてのぞく。

メソッドがたくさん。
all()でparametersが返すらしい。試しに使ってみるが全部はいらないんだよね。
get($path, $default, $deep)でparametersの値を返すっぽい。

$agent = $request->server->get('HTTP_USER_AGENT');

で無事値を拾えた。フゥ〜、一件落着です。

Symfony2でviewsのファイル名を変更するとでるエラー

symfony2でバンドル内のビューのファイル名を
index.html.twig

pc_index.html.twig
に変更したら

Warning: filemtime() [<a href='function.filemtime'>function.filemtime</a>]: stat failed for C:\...\src\Acme\PagesBundle/Resources/views/Pages/index.html.twig in C:\...\vendor\symfony\src\Symfony\Bundle\FrameworkBundle\Templating\Loader\FilesystemLoader.php line 68

っていうエラーが出ました。

意味がわからず途方に暮れてましたが、コマンドコンソールで

php app/console cache:clear

を実行したらエラーがなくなりました。
キャッシュが悪さしていたようです。
良かった、良かった。

それにしても、cakephpと比べてsymfony2のエラーメッセージは分かりづらいなぁ...

symfony2でキャリアの判別

キャリアというかuser_agentというかですけれど、とにかくリクエストからユーザーエージェントをを調べて利用している端末を調べてガラパゴス携帯なら文字のエンコードをshift-jisで吐き出す処理。

関連するphpファイル

#\src\Acme\PagesBundle\DependencyInjection\Carrier.php

namespace Acme\PagesBundle\DependencyInjection;

use Symfony\Component\HttpFoundation\Request;

class Carrier
{
 protected $agent;
 protected $carrier_type;

 public function __construct(Request $request)
 {
  $this->agent = $request->server->get('HTTP_USER_AGENT');
  $this->initialize();
 }

 public function initialize()
 {
  $this->carrier_type = "pc";

  if(preg_match("/^DoCoMo/i", $this->agent)){
   $this->carrier_type = "docomo";
  }else if(preg_match("/^(J\-PHONE|Vodafone|MOT\-[CV]|SoftBank)/i", $this->agent)){
   $this->carrier_type = "softbank";
  }else if(preg_match("/^KDDI\-/i", $this->agent) || preg_match("/UP\.Browser/i", $this->agent)){
   $this->carrier_type = "au";
  }else if(preg_match("/iPad/i",$this->agent)){
   $this->carrier_type = "ipad";
  }else if(preg_match("/iPhone/i",$this->agent)){
   $this->carrier_type = "iphone";
  }else if(preg_match("/Galaxy/i",$this->agent)){
   $this->carrier_type = "iphone";
  }else if(preg_match("/Android/i",$this->agent)){
   $this->carrier_type = "android";
  }
 }

 public function setCarrierType($type)
 {
  $this->carrier_type = $type;
 }

 public function isDocomo()
 {
  if($this->carrier_type === "docomo"){
   return true;
  }else{
   return false;
  }
 }

 public function isSoftbank()
 {
  if($this->carrier_type === "softbank"){
   return true;
  }else{
   return false;
  }
 }

 public function isAu()
 {
  if($this->carrier_type === "au"){
   return true;
  }else{
   return false;
  }
 }

 public function isIpad()
 {
  if($this->carrier_type === "ipad"){
   return true;
  }else{
   return false;
  }
 }

 public function isIphone()
 {
  if($this->carrier_type === "iphone"){
   return true;
  }else{
   return false;
  }
 }

 public function isAndroid()
 {
  if($this->carrier_type === "android"){
   return true;
  }else{
   return false;
  }
 }

 public function isPc()
 {
  if($this->carrier_type === "pc"){
   return true;
  }else{
   return false;
  }
 }

 public function isLegacy()
 {
  if($this->isDocomo() || $this->isSoftbank() || $this->isAu()){
   return true;
  }else{
   return false;
  }
 }

 public function isSmartphone()
 {
  if($this->isIphone() || $this->isAndroid()){
   return true;
  }else{
   return false;
  }
 }

 public function isSmartpad()
 {
  if($this->isIpad()){
   return true;
  }else{
   return false;
  }
 }
}


#\src\Acme\PagesBundle\Controller\PagesContller.php

namespace Acme\PagesBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Acme\PagesBundle\DependencyInjection\Carrier;


class PagesController extends Controller
{

 public function indexAction()
 {
  //リクエストを取得
  $request = $this->getRequest();
  //キャリアのインスタンスを生成
  $carrier = new Carrier($request);
  //ガラパゴス携帯か判断する
  if($carrier->isLegacy()){
   $content = $this->renderView('AcmePagesBundle:Pages:legacy_index.html.twig');
   $content = mb_convert_encoding($content, "SJIS", "auto");
   return new Response($content);
  }
  return $this->render('AcmePagesBundle:Pages:pc_index.html.twig');


あと当たり前ですが、これとは別にビューを用意します。

...っていうか本当にshift-jisに変わってるのかな?今更ながら不安になってきた。

Symfony2で機能拡張

この記事は訂正されました。Symfony2で機能拡張  訂正も御覧下さい。
symfony2で機能拡張はどうすれば良いのだろうか。
cakePHPで言うところのコンポーネント、ビヘイビア辺りをイメージしているのですがいまいち不明。

やっぱり日本symfonyユーザー会のsymfony2日本語ドキュメントを読む。
サービスコンテナを取り敢えず読んでみる。

ふむふむ。
  1. app/config/config.ymlにserviceを追加して
  2. コントローラーに $mailer = $this->get('my_mailer');って書く

...あれ?Mailerクラスはどこ書くの?

違うとこも読む。
セマンティック...
???Acme/HelloBundle/DependencyInjection/HelloExtension.phpをMailerとして書くの?

意味不明すぎる。英語が読めたらなぁ...


取り敢えず自力で頑張ってみる。
以下一応できたのでその結果。symfony2の規約として正しいかどうか不明。でも出来た。
#\src\Acme\PagesBundle\DependencyInjection\Carrier.php

namespace Acme\PagesBundle\DependencyInjection;

use Symfony\Component\HttpFoundation\Request;

class Carrier
{
}


#\src\Acme\PagesBundle\Controller\PagesContller.php

use Acme\PagesBundle\DependencyInjection\Carrier;
...
public function indexAction()
{
$carrier = new Carrier();
}


DependencyInjectionは依存性の注入って意味らしいです。なのでこの直下に作りたいクラスを書く。
コントローラーでuseで書いたクラスを呼び出せるように設定。new Carrier();でインスタンス生成。
って感じです。


これで良いのか不明。でも出来たし、よしとする。

Symfony2でshift-jis表示

次の仕事の案件でSymfony2を使って、主にモバイルをターゲットにしたサイトを作る事になった。
ちなみにSymfony自体初心者で分からない事だらけ...

Symfony日本ユーザー会の日本語ドキュメントを読みあさって何とか簡単なページを使えるようにはなった。
感謝感謝。

で当然モバイル用なのでShift-jisのページを表示する必要があるのだが、これが中々分からなかったので防備録を兼ねて投稿。

通常コントローラー内でレスポンス返すには

return $this->render('AcmePagesBundle:Pages:index.html.twig');

とするのだがこれだと当然保存したエンコード形式になってしまう。
PC用のページも作るし、eclipse使っているのでファイル自体はUTF-8にして、htmlを吐き出す時だけshift-jisにしたい。
なんだかんだで

use Symfony\Component\HttpFoundation\Response;

...

$content = $this->renderView('AcmePagesBundle:Pages:index.html.twig');
$content = mb_convert_encoding($content, "SJIS", "auto");
return new Response($content);

で出来るようになったっぽい。

...これで本当にいいのか?なにせ始めたばかりで自信がない。
もし他に良い方法知っている方いらっしゃたらコメントお願いします。m(_ _)m