Symfony

【AngularJS/Symfony2.3】AngularJS用のXSRFトークン発行/チェックサービス。

前回のエントリで若干触れたのでペタリ。

<?php

namespace Hoge\FugaBundle\Services;

use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\SessionCsrfProvider;

class XsrfTokenManager
{
    private $request;
    
    private $provider;
    
    protected $_cookieName = 'XSRF-TOKEN';
    
    protected $_sessionName = 'S-XSRF-TOKEN';
    
    const RECEIVED_HEADER_NAME = 'X-XSRF-TOKEN';
    
    /**
     * Construct
     */
    public function __construct(Request $request, SessionCsrfProvider $provider)
    {
        $this->request = $request;
        $this->provider = $provider;
    }
    
    /**
     * check xsrf token matches
     * 
     * @return boolean
     */
    public function check()
    {
        $_header = $this->request->headers->get(self::RECEIVED_HEADER_NAME);
        
        if (empty($_header) || $_header != $this->create()) {
            
            throw new \Symfony\Component\Security\Core\Exception\AccessDeniedException();
        }
        
        return true;
    }
    
    /**
     * generate strings of xsrf token
     * 
     * @param string $salt
     * @return \Hoge\FugaBundle\Services\XsrfTokenManager
     */
    private function create($salt = 'csrf_token')
    {
        $request = $this->request;
        
        $token = null;
        
        if ($request->getSession() && $request->getSession()->has($this->_sessionName)) {
            $token = $request->getSession()->get($this->_sessionName);
        } else {
            $token =  $this->provider->generateCsrfToken($salt);
            if ($request->getSession()) {
                $request->getSession()->set($this->_sessionName, $token);
            }
        }
        
        return $token;
    }

    /**
     * set cookie for client
     * 
     * @return boolean|\Symfony\Component\HttpFoundation\Cookie
     */
    public function set()
    {
        $cookie = new Cookie($this->_cookieName, $this->create(), 0, '/', null, false, false);
        
        $response = new Response();
        $response->headers->setCookie($cookie);
        $response->send();
        
        return $cookie;
    }
    
    /**
     * setter for cookie name
     * 
     * @param unknown $name
     * @return \Hoge\FugaBundle\Services\XsrfTokenManager
     */
    public function setCookieName($name)
    {
        $this->_cookieName = $name;
        
        return $this;
    }
    
    /**
     * getter for cookie name
     * 
     * @return string
     */
    public function getCookieName()
    {
        return $this->_cookieName;
    }
    
    /**
     * setter for session name
     * 
     * @param unknown $name
     * @return \Hoge\FugaBundle\Services\XsrfTokenManager
     */
    public function setSessionName($name)
    {
        $this->_sessionName = $name;
        
        return $this;
    }
    
    /**
     * getter for session name
     * 
     * @return string
     */
    public function getSessionName()
    {
        return $this->_sessionName;
    }
    
}

これをService.ymlで下記のように定義してやり
※前回のエントリそのまんま。

services:
    xsrf.token.manager:
        class: Hoge\FugaBundle\Services\XsrfTokenManager
        arguments: [@request, @form.csrf_provider]
        scope: request
        tags:
            - { name: xsrf.token.manager }

レスポンスリスナあたりでset()メソッドを実行してやればOK。

services:
    listener.before.filter:
        class: Hoge\FugaBundle\EventListener\ResponseListener
        arguments: [@xsrf.token.manager]
        scope: request
        tags:
            - { name: kernel.event_listener, event: kernel.response, method: createToken }

ResponseListenerは下記の通り。

\Hoge\FugaBundle\EventListener\ResponseListener.php

<?php
namespace Hoge\FugaBundle\EventListener;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Hoge\FugaBundle\Services\XsrfTokenManager;

class ResponseListener
{
    
    protected $manager;
    
    /**
     * Construct
     */
    public function __construct(XsrfTokenManager $manager)
    {
        $this->manager = $manager;
    }
    
    /**
     * before filter event
     */
    public function createToken()
    {
        $this->manager->set();
    }
    
}

あとはトークンチェックが必要なコントローラーのアクション先頭でcheck()メソッドを呼んでやればOK。
※本来はここもリスナーでやったほうがなおベター。

 

【Symfony2.3】自作サービスにRequestオブジェクトを注入したい場合はスコープにリクエストを指定してやること。

標題の通り。

こんな感じ。

services:
    xsrf.token.manager:
        class: Hoge\FugaBundle\Services\XsrfTokenManager
        arguments: [@request, @form.csrf_provider]
        scope: request
        tags:
            - { name: xsrf.token.manager }

ちなみにXsrfTokenManagerはAngularJSがサーバーにリクエストを送る際にくっつけてくるTokenの正当性を評価するサービス。

次のエントリにでも貼っつけとこう。

 

【Symfony2.3】DoctrineのprePersistもしくはpreUpdateイベントをトリガーとするリスナーに対してSecurityContextを注入すると循環参照例外が発生する。

ので注意。

StackOverflowで解決策は唯一containerを注入してそこからSecurityContextを取得するようにするしかないと回答されていたが、公式でも問題として認識していた模様。

http://stackoverflow.com/questions/7561013/injecting-securitycontext-into-a-listener-prepersist-or-preupdate-in-symfony2-to

下記のような投稿が確認出来る。

I had similar problems and the only workaround was to pass the whole container in the constructor

As of Symfony 2.6 this issue should be fixed.

どうやらSymfony2.6でこの問題は解決されるとのこと。公式でも記事を発見。

http://symfony.com/blog/new-in-symfony-2-6-security-component-improvements

ということで2.3環境の自分はおとなしくコンテナを注入してやることにしたのであった。

 

【Symfony2.3】FormTypeからContainerを参照する。

やりたいことそのまんまQiitaで見つけたのでペタリ。

http://qiita.com/adarah_g/items/53c88baef65da915adb8

めちゃめちゃイイ感じ。

 

【Symfony2.3】StofDoctrineExtensionsBundleのSoftdeleteableとJMSSerializerを併せて用いている場合の注意。

結構ハマったのでメモ。

例えばニュースとカテゴリがMany-to-Oneで紐付いていたとする。(ニュースは一つのカテゴリを持つ)
この場合、ニューステーブルにカテゴリIDを保持するが、紐付いているカテゴリが論理削除済みであると、シリアライズ時に解決できないエラーに遭遇してしまうことが判明。

カテゴリを作成した後、そのカテゴリを紐付けてニュースを作成。その後紐付けていたカテゴリ自体を論理削除すると、ニューステーブルにはカテゴリIDが残ったままになる。
この状態でニュースエンティティをシリアライズしようとするとEntityNotFoundExceptionが発生し、処理が止まってしまう。
当然といえば当然だが、無ければ無いでnullをセットしておいて欲しいものである。

下記サイトのやりとりを見て判明。

https://github.com/schmittjoh/serializer/issues/101

 

【Symfony】のベスト・プラクティス。

が公式に纏められていたのでペタリ。

http://symfony.com/doc/current/best_practices/index.html

設計時の参考になるね。

 

【Symfony2.3】ParamConverterがくっそ便利な件。

これ使うとコントローラーをかなり圧縮できる。

例)

namespace Hoge\FugaBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

// 例えばFOSRestBundleを道入してRestAPIを作成していたとする。
use FOS\RestBundle\Controller\Annotations\View;

class PiyoController extends Controller
{
    /**
     * @Route("/user/{id}.{_format}", name="hoge_fuga_piyo_show", defaults={"_format" = "json"})
     * @View(serializerEnableMaxDepthChecks=true, serializerGroups={"piyoShow"})
     * @ParamConverter("entity", class="HogeFugaBundle:User")
     * @Method("GET")
     */
    public function showAction(User $entity)
    {
        return $entity;
    }

}

見て分かる通り、URLよりIDを取得後対応するエンティティをjson文字列として返却するメソッドは上記の通り1行で済むようになる。

アノテーションでParamConverterを指定してやると、自動でDoctrineから習得したエンティティを注入してくれているのである。

基本的に指定がなければリポジトリのfind()メソッドにRouteアノテーションから受け取ったidを渡してエンティティ習得を試みてくれるが、リポジトリ内のメソッドを指定することも出来る

/**
 * @ParamConverter("entity", class="HogeFugaBundle:User", options={"repository_method"="customGet"})
 */

といった具合である。

更に便利な機能もあるのであとは公式を参照されたし。

http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html

これはまじで便利。

 

【Symfony2.3】ユニットテストでファイルアップロードをする際にtempファイルを生成する。

テスト用のダミーファイルを生成したかった時にやってみたことをメモ。

use Symfony\Component\HttpFoundation\File\UploadedFile;

$tempFile = tempnam(sys_get_temp_dir(), '__');
$fp = fopen($tempFile, 'a+');
fwrite($fp, hash('sha512', md5(uniqid(mt_rand(), true))));
fclose($fp);

$file = new UploadedFile(
    $tempFile,
    basename($tempFile),
    null,
    filesize($tempFile)
);

var_dump($file);

意外と便利。