GANCHIKU.com

Entity の カラム定義に unsigned int の指定

Symfony2 で Entity のカラムの定義を unsigned int にするときにちょっと迷ったのでメモ。
type ではなく、 columnDefinition を使う。

    /**
     * @var integer $point
     *
     * @ORM\Column(name="point", columnDefinition="integer unsigned")
     */
    private $point;

小菅村でオータサンとハワイ大学ヒロ校の本田先生のコンサートがあります

奥多摩なんですね。行くのがすごく大変そうですが、自然が好きな人はぜひ!おそらくここ以外にも広島やいろんな所を周ると伺っています。

この本田先生には、5年ほど前にハワイ島でお世話になってから、関係が続いています。日本に帰っていらっしゃるときにいつも連絡をいただいており、これからも末永く良い関係を続けていきたいと思っています。

オータサンは、私は面識はないのですが、素晴らしいウクレレの演奏者です。2009年時に来たときに少しだけ抜粋をネットに上げていましたので、見てみてください。他にも YouTube に上がっているみたいですし(許しを得ているものかどうかは知らないですが。。。)、 amazon でも高い評価を得ていますね。ちょっと amazon へのリンクを貼ってみました。

そして、本田先生とオータサンは、年に何度か一緒に小学校などの学校施設を回っているようです。本田先生のハワイに渡った日系人の話はとても興味深いです。基本的にはあまり学校で学ぶ機会がないことなので、こういう歴史もあったんだな、と勉強になります。

小菅村体育館こけらおとし記念で申し込みができるようです。時間がある方、ウクレレが好きな方、ハワイ日系人について調べたい方、ぜひぜひどうぞー。日にちは、5月26日です。

ベーシック認証の htpasswd の作成方法

2012年5月11日

単なるメモ。
いつも忘れてしまうので。。。。 apache がインストールしていなくて、 htpasswd が使えない時用。nginx のみしかインストールしていないときなど。

echo -e "username:`perl -le 'print crypt("password","salt")'`" > htpasswd

FOSUserBundle で ユーザ名を削る

Symfony2 のバンドルで一番使われているものは、 FOSUserBundle のようですね。たくさんの人に使われているということは、つまり、それだけ完成度が高いということでもあると思います。私自身も最近になり FOSUserBundle を使っており、その柔軟性と拡張性の恩恵を受けています。

FOSUserBundle はそのまま使っても確かに使えるのですが、よりアプリのニーズにあった柔軟な開発への適用も可能となっています。ユーザ管理でユーザ名を使用しないときなどもちょっとがんばれば可能になります。多くのウェブサービスでユーザ名でアイデンティファイするサービスの場合は確かにユニークな名前が必要になりますが、全てのウェブサービスがそうとは限りません。メールアドレスがユニークであれば、それで足りることもあると思います。となると、 FOSUserBundle のデフォルトの仕様であるユーザ名とメールアドレス、そしてパスワードを入力する、というのはやりすぎな感じがします。また、ユーザ名を毎回決めさせるのも、どうなんだろ。。。と思えます。

そんなときは拡張しましょう。

まず、フォームタイプを拡張し、username を取ってしまいましょう。

src/Ganchiku/UserBundle/Form/Type/RegistrationFormType.php

namespace Ganchiku\UserBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseFormType;

class RegistrationFormType extends BaseFormType
{   

    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('email', 'email')
            ->add('plainPassword', 'repeated', array('type' => 'password'));
    }

    public function getName()
    {
        return 'ganchiku_user_registration';
    }
}

ここで変更した後には、app/config/config.yml と Ganchiku/UserBundle/Resources/config/services.xml を修正してみましょう。

app/config/config.yml

fos_user:
    ... snip ....
    registration:
        form:
            type: ganchiku_user_registration

Ganchiku/UserBundle/Resources/config/serivces.xml


      
        
        %fos_user.model.user.class%
    

ちょっと省略しましたが、これで username のない FormType を表示することができます。しかし、これではバリデーションに引っかかってしまいます。username は一意でなくてはいけないですし、空ではダメなんです。そこで FormHandler も変更して、username に email の値を格納するようにしてみましょう。

src/Ganchiku/UserBundle/Form/Handler/RegistrationFormHandler.php

namespace Ganchiku\UserBundle\Form\Handler;

use FOS\UserBundle\Form\Handler\RegistrationFormHandler as BaseFormHandler;

class RegistrationFormHandler extends BaseFormHandler
{
    public function process($confirmation = false)
    {
        $user = $this->userManager->createUser();
        $this->form->setData($user);

        if ('POST' === $this->request->getMethod()) {
            $this->form->bindRequest($this->request);

            // username に email の内容をコピーする
            $appData = $this->form->getData();
            $appData->setUsername($appData->getEmail());
            $appData->setUsernameCanonical($appData->getEmailCanonical());
            $this->form->setData($appData);
            $this->form->bindRequest($this->request);

            if ($this->form->isValid()) {
                $this->onSuccess($user, $confirmation);

                return true;
            }
        }
        return false;
    }
}

これでコピーされました。そして、config.yml で FormHandler に今の作成した Handler を指定します。

app/config/config.yml

fos_user:
    ...snip...
    registration:
        form:
            type: ganchiku_user_registration
            handler: ganchiku_user.form.handler.registration

Ganchiku/UserBundle/Resources/config/serivces.xml

      
        
        
        
        
      

これで、今作成したFormHandler を使用することになり、バリデーションをする前に email の値を username にコピーすることができるようになりました。同じ内容のカラムを2つ持つのはどうなんだ!という意見もあると思います。内部のコードを書き換えずにそこまでできたら、いいのですが、今はできないんじゃないかなぁ。

最終的な app/config/config.yml と src/Ganchiku/UserBundle/Resources/config/services.xml は次のようになりました。

fos_user:
    db_driver:      orm
    firewall_name:  main
    user_class:     Application\Sonata\UserBundle\Entity\User
    group:
        group_class: Application\Sonata\UserBundle\Entity\Group
    registration:
        form:
            type: ganchiku_user_registration
            handler: ganchiku_user.form.handler.registration
        confirmation:
            enabled:    true
    from_email:
        address:        example@gmail.com
        sender_name:    Acme Demo App

user_class と group_class は SonataUserBundle を使用しているので、上のようになっています。

src/Ganchiku/UserBundle/Resources/config/services.xml





    
      
        
        %fos_user.model.user.class%
      

      
        
        
        
        
      
    


もっとスマートな方法がある!と知っている方は連絡をください。まだまだ私も勉強不足なので学ばせてもらいたいと思っています。

Symfony2 でフォームの確認画面を作る

個人的には編集時に、プレビューを見ながらフォーム記入をする方が好きなのですが、確認画面が欲しい、というニーズはよくありますよね。
ちょっとやってみたので、メモとして書いておきます。

フォームの確認画面なのですが、特に画像のアップロードが入ると結構厄介なんですよね。つまり、ファイルをどこに置くの?ってことになるので。と言うのも、確認画面の時点でアップロードが完了していないといけないんですよね。もし、そこで「やーめた」って思ったら、ファイルがゴミとして残ってしまうんですわ。また、「やっぱ他の画像にしよー」と思って戻られても、同様にファイルがゴミとして残ってしまうんですわ。ここは結構悩ましいところでいくつか方法があるのかな、と思います。私がパッと思いついた方法は、一時ディレクトリに保存しておいて、確認ができたら、ファイルを本番ディレクトリに移動って感じかな、と。

また、確認画面ってデータの保持ってどうするの?っていう問題があります。セッションに持っておいたり、hiddenで渡したり、一時的にデータベースに保存したり、とかいったことがよくされる方法ですかね。私はめんどくさがりやさんだったので、hiddenで渡しました。一時的にデータベースに保存した方が画像の保存のことも考えると楽かもしれません。

というわけで、ここでやろうとしていることを整理すると、次のようになります。

  • 登録フォームがあり、画像もアップロードができるようにフォームをレンダーする
  • バリデーションに引っかかったら、登録フォームに戻る
  • バリデーションに通ったら、確認画面に行く
  • 確認画面では、各値は hidden で渡す
  • 確認画面のフォームで本登録をする
  • 確認画面のフォームで、修正ボタンが押されたら、値をセットして、フォームに戻る
  • 本登録されたら、hidden に入っているデータをデータベースに登録し、画像も正しい場所に移動する

さて、ここで厄介そうなのは、 hidden のフォームってどうするの、ってことかな、と。おそらく方法としては、新しく FormType を作る方法と、twig のフォーム周りの widget を修正して hidden で出す、ってくらいかな、と。私は後者でやってみました。

confirm.html.twig

{% form_theme form _self %}

{% block field_widget %}

    {% if type is not defined or type != 'hidden' %}{{ value }}{% endif %}
{% endblock field_widget %}

{% block textarea_widget %}
  {% set type = 'hidden' %}
{{ value }}
{% endblock textarea_widget %}

{% block checkbox_widget %}
{% spaceless %}

{% endspaceless %}
{% endblock checkbox_widget %}

{% block radio_widget %}
{% spaceless %}

{% endspaceless %}
{% endblock radio_widget %}

{% block choice_widget %}
{% spaceless %}
    {% if expanded %}
{% for child in form %} {% if child.vars.checked %} {{ form_label(child) }} {{ form_widget(child) }} {% endif %} {% endfor %}
{% else %} {% for choice, label in choices %} {% if _form_is_choice_selected(form, choice) %} {{label|trans}} {% endif %} {% endfor %} {% endif %} {% endspaceless %} {% endblock choice_widget %}

とかやって、hidden で出力するように変更してあげます。choice の辺りが結構面倒でした。あとは、普通に扱ってあげれば大丈夫です。画像に関しては、確認画面では表示させてあげます。


{{ form_label(form.file) }}

      {% if entity.path is not null %}
      
      {% endif %}
      {{ form_widget(form.path, {'value': entity.path}) }}
      {{ form_widget(form.file) }}
      {{ form_widget(form.file_delete, {'value': null}) }}
      


...snip...


      {{ form_rest(form) }}
        
        
    

...snip...

確認画面では、file を使用しませんし、file_delete も使用しません。ただ、path を FormType に加えて、一時保存先へのパスを入れておいてます。
「修正する」ボタンを付けておきました。back が入っていれば、データを保持しつつフォームを表示するようにしています。

DocumentType.php

namespace Ganchiku\TestBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class DocumentType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('title', null, array(
                'label' => 'タイトル',
                'required' => true,
            ))
            ->add('file', null, array(
               'label' => '画像ファイル',
               'required' => false,
            ))
            ->add('file_delete', 'checkbox', array(
                'label' => '画像ファイル削除',
                'required' => false,
            ))
            ->add('path', 'hidden', array(
                'required' => false,
            ))
            ;
    public function getName()
    {
        return 'ganchiku_testbundle_documenttype';
    }
}

前回のポストの延長です。こんな感じで、path も追加しておいてあげます。hidden フィールドで。あとは Entity の方でゴニョゴニョします。Action でも少しだけセットしてあげたりしています。

Document.php

namespace Ganchiku\TestBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\File\File;

/**
 * Ganchiku\TestBundle\Entity\Document
 * @ORM\Table(name="document")
 * @ORM\HasLifecycleCallbacks
 */
class Document
{

    /**
     * @Assert\File(maxSize="6000000")
     */
    public $file;
    public $file_delete = false;
    public $file_tmp = false;
    public $file_confirmed = false;
    public $path_old = null;

    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

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

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

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

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

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

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

    public function getAbsolutePath()
    {
        return null === $this->path ? null : $this->getUploadRootDir().'/' . $this->path;
    }

    public function getWebPath()
    {
        return null === $this->path ? null : $this->getUploadDir().'/' . $this->path;
    }

   /**
     * @ORM\PrePersist()
     * @ORM\PreUpdate()
     */
    public function preUpload()
    {
        if (null !== $this->path and $this->file_delete) {
            $this->path_old = $this->getAbsolutePath();
            $this->path = null;
        }

        if (null !== $this->file) {
            $this->path = md5_file($this->file->getPathname()) . '.' . $this->file->guessExtension();
        }
    }

    /**
     * @ORM\PostPersist()
     * @ORM\PostUpdate()
     */
    public function upload()
    {
        if ($this->path_old) {
            unlink($this->path_old);
            return;
        }
        if ($this->file_confirmed) {
            $tmp_file = new File($this->getUploadRootDir(true) . '/' . $this->path);
            $tmp_file->move($this->getUploadRootDir(), $this->path);
            return;
        }

        if (null === $this->file) {
            return;
        }

        $this->file->move($this->getUploadRootDir(), $this->path);

        unset($this->file);
    }

    /**
     * @ORM\PostRemove()
     */
    public function removeUpload()
    {
        if ($file = $this->getAbsolutePath()) {
            unlink($file);
        }
    }

    protected function getUploadRootDir($file_tmp = false)
    {
        return __DIR__.'/../../../../web' . $this->getUploadDir($file_tmp);
    }

    protected function getUploadDir($file_tmp = false)
    {
        if ($this->file_tmp or $file_tmp) {
            return '/uploads/tmp';
        }
        return '/uploads/documents';
    }

}

なんか public な変数が増えて少し嫌な感じですが、まぁ、許容範囲でしょう。ちょっとだけ説明しておきます。

$file は、実際のアップロードファイルが保存されます。 SplFileInfo の孫クラスの Symfony\Component\HttpFoundation\File\UploadedFile インスタンスが入ることになります。アップロードファイルがあれば。なければ、放置です。

$file_delete は、今回はあまり関係がありません。前回のポストを参照してください。ファイル削除のフラグを見ているだけです。

$file_tmp は、一時ファイルを使用するかどうかを見るフラグです。このフラグは、isser にした方がスマートかも。コントローラーの方で、confirmAction 内でセットしてあげます。getUploadDir() の保存先を変更するのに使用しています。

$file_confirmed は、一時ファイルから本番ディレクトリに移動する際に使用します。コントローラーの方で、createAction 内でセットしてあげます。upload()内の動作を変更しています。

$path_old は、今回はあまり関係がありません。前回のポストを参照してください。ファイルの削除の際のカラムの変更のために使用しています。

confirmAction はこんな感じになりました。

    /**
     * Displays a form confirmation to create a new Document entity.
     *
     * @Route("/confirm", name="document_confirm")
     * @Secure(roles="ROLE_USER")
     * @Method("post|get")
     * @Template()
     */
    public function confirmAction(Request $request)
    {
        if ($request->getMethod() == "GET") {
            return $this->redirect($this->generateUrl('document_new'));
        }
        $context = $this->get('security.context');
        $user = $context->getToken()->getUser();

        $entity  = new Document();
        $entity->file_tmp = true;
        $request = $this->getRequest();
        $form    = $this->createForm(new DocumentType(), $entity);
        $form->bindRequest($request);
        if ($form->isValid()) {
            $entity->preUpload();
            $entity->upload();
            // todo 本来は、 redirect すること
            // ここでは confirm ページを表示させる
            return array(
                'entity' => $entity,
                'form'   => $form->createView()
            );
        }

        $response = $this->render('GanchikuTestBundle:Document:new.html.twig', array(
            'entity' => $entity,
            'form'   => $form->createView()
        ));
        return $response;
    }

実際に保存をするわけではないので、preUpload()やupload()は呼び出されないので、アップロードのために直接書いておいてあげます。また、その前に file_tmp に true をセットしてファイルの保存先を変更してあげています。Method で GET を受け付けているのは、個人的な趣向です。redirect 先が confirm とかに来られると 405 エラーを出すので、嫌だなー、と。リダイレクトしちゃった方が実際はいいんじゃないかなぁ、と。 todo はしておいた方がいいですが、とりあえず。。。

    /**
     * Creates a new Document entity.
     *
     * @Route("/create", name="document_create")
     * @Secure(roles="ROLE_USER")
     * @Method("post")
     * @Template("GanchikuTestBundle:Document:new.html.twig")
     */
    public function createAction()
    {
        $context = $this->get('security.context');
        $user = $context->getToken()->getUser();

        $entity  = new Document();
        $entity->setUser($user);
        $entity->file_confirmed = true;
        $request = $this->getRequest();
        $form    = $this->createForm(new DocumentType(), $entity);
        $form->bindRequest($request);

        if ($request->request->get('back', null) != 'back') {
            if ($form->isValid()) {
                $em = $this->getDoctrine()->getEntityManager();
                $em->persist($entity);
                $em->flush();

                return $this->redirect($this->generateUrl('document_show', array('id' => $entity->getId())));
            }
        }
        return array(
            'entity' => $entity,
            'form'   => $form->createView()
        );
    }

ここで実際に保存(永続化)します。ただ、その前に file_confirmed に true をセットして、ファイルを移動してあげましょう。backは、修正する際に使用しており、フォームをデータを持ったまま表示させています。

なんか抜け落ちているような感じがしないでもないですが、こんな感じですかね。

あとは、一時ファイルを cron 等で定期的にお掃除してあげればいいかな、と。

Shin Ohno 2003-2012