nazolabo

フリーランスのWebエンジニアが近況や思ったことを発信しています。

Symfony2の認証(Entity経由でログイン編)

おそらくほとんどの人が実現したいと思う、Userテーブルの情報からログインをする方法を考えてみましょう。

今回は、NazoUserBundleというサンプルバンドルを作り、そこで作業します。

chmod -R 777 app/cache app/logs
php app/console generate:bundle --namespace=Nazo/UserBundle

とりあえず基本設定をします。
app/config/parameters.iniで、DBの接続設定を書きます。(省略)

DBを作成します。

php app/console doctrine:database:create

以下、「src/Nazo/UserBundle/」で作業します。

Entity/User.phpを作成します。

<?php

namespace Nazo\UserBundle\Entity;

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

/**
 * Nazo\UserBundle\Entity\User
 *
 * @ORM\Table(name="users")
 * @ORM\Entity
 */
class User implements AdvancedUserInterface
{
    /**
     * @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;

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

    private $accountNonExpired = true;
    private $credentialsNonExpired = true;
    private $accountNonLocked = true;
    private $enabled = true;
    private $roles = array('ROLE_USER');


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

    /**
     * {@inheritdoc}
     */
    public function getPassword()
    {
        return $this->password;
    }

    /**
     * {@inheritdoc}
     */
    public function getSalt()
    {
        return $this->salt;
    }

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

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

    /**

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

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

    /**
     * {@inheritdoc}
     */
    public function getRoles()
    {
        return $this->roles;
    }
    /**
     * {@inheritdoc}
     */
    public function isAccountNonExpired()
    {
        return $this->accountNonExpired;
    }

    /**
     * {@inheritdoc}
     */
    public function isAccountNonLocked()
    {
        return $this->accountNonLocked;
    }

    /**
     * {@inheritdoc}
     */
    public function isCredentialsNonExpired()
    {
        return $this->credentialsNonExpired;
    }

    /**
     * {@inheritdoc}
     */
    public function isEnabled()
    {


        return $this->enabled;
    }

    /**
     * {@inheritdoc}
     */
    public function eraseCredentials()
    {
    }

    /**
     * {@inheritDoc}
     */
    public function equals(UserInterface $user)
    {
        if (!$user instanceof User) {
            return false;
        }

        if ($this->password !== $user->getPassword()) {
            return false;
        }

        if ($this->getSalt() !== $user->getSalt()) {
            return false;
        }

        if ($this->getUsername() !== $user->getUsername()) {

            return false;
        }

        if ($this->accountNonExpired !== $user->isAccountNonExpired()) {
            return false;
        }

        if ($this->accountNonLocked !== $user->isAccountNonLocked()) {
            return false;
        }

        if ($this->credentialsNonExpired !== $user->isCredentialsNonExpired()) {
            return false;
        }

        if ($this->enabled !== $user->isEnabled()) {
            return false;
        }

        return true;
    }
}

Controller/DefaultController.phpを編集します。

<?php

namespace Nazo\UserBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContext;

class DefaultController extends Controller
{
    /**
     * @Route("/user/home")
     * @Template()
     */
    public function indexAction()
    {
        return array(
        );
    }

    /**
     * @Route("/user/login")
     * @Template()
     */
    public function loginAction(Request $request)
    {
        $session = $request->getSession();

        // get the login error if there is one
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
        }

        return array(
            // last username entered by the user
            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
            'error'         => $error,
        );
    }

    /**
     * @Route("/user/login_check", name="login_check")
     * @Template()
     */
    public function login_checkAction()
    {
        return array();
    }
}

Resources/views/Default/index.html.twigを編集します。

Hello !!

Resources/views/Default/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" />

    {#
        If you want to control the URL the user is redirected to on success (more details below)
        <input type="hidden" name="_target_path" value="/account" />
    #}

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

ルートに戻って、DBにテーブルを作成します。

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

app/config/security.ymlを編集します。

security:
    encoders:
        Nazo\UserBundle\Entity\User: plaintext

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

    providers:
        users:
            entity: { class: Nazo\UserBundle\Entity\User, property: username }

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

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

        secured_area:
            pattern:    ^/user/.*
            form_login:
                check_path: /user/login_check
                login_path: /user/login
            logout:
                path:   /user/logout
                target: /

            #anonymous: ~
            #http_basic:
            #    realm: "Secured Demo Area"

    access_control:
        #- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
        #- { path: ^/_internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }

ログインは app_dev.php/user/login から試せます。 app_dev.php/user/logout にアクセスするとログアウトします。(見た目に変化はありませんが)
ログイン中かどうかは、 app_dev.php/user/home にアクセスするとわかると思います。
これでログインすることとができるようになりましたが、パスワードが平文です。よろしくないですね。

パスワードを暗号化できるように、app/config/security.ymlを編集します。

security:
    encoders:
        Nazo\UserBundle\Entity\User:
            algorithm: sha1
            encode_as_base64: false

            iterations: 1
...

しかしこれではユーザーデータを人力で入れることができません。(がんばれば入れれますが)

人力で入れるために、管理画面を作ります。

Controller/UserAdminController.php

<?php

namespace Nazo\UserBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContext;
use Nazo\UserBundle\Entity\User;

class UserAdminController extends Controller
{
    /**
     * @Route("/admin/user/index", name="admin_user_index")
     * @Template()
     */
    public function indexAction()
    {
        $em = $this->getDoctrine()->getEntityManager();
        $users = $em->getRepository('NazoUserBundle:User')->findAll();
        return array(
            'users' => $users,
        );
    }

    /**
     * @Route("/admin/user/create", name="admin_user_create")
     * @Template()
     */
    public function createAction()
    {

        return $this->onEdit(null);
    }

    /**
     * @Route("/admin/user/edit", name="admin_user_edit")
     * @Template()
     */
    public function editAction(Request $request)
    {
        $id = $request->get('id');

        return $this->onEdit($id);
    }

    /**
     * protected
     *
     */
    protected function onEdit($id = null)
    {
        $em = $this->getDoctrine()->getEntityManager();
        $request = $this->get('request');

        if ($id === null) {
            $user = new User();
        } else {
            $user = $em->getRepository('NazoUserBundle:User')->find($id);
        }

        $form = $this->createFormBuilder($user)

            ->add('username', 'text')
            ->add('password', 'password')
            ->getForm();

        if ($request->getMethod() == 'POST') {
            $form->bindRequest($request);
            if ($form->isValid()) {
                // encrypt password
                $salt = sha1(uniqid($user->getUsername(), true));
                $password_raw = $user->getPassword();
                $user->setSalt($salt);

                $ef = $this->get('security.encoder_factory');
                $password = $ef->getEncoder($user)->encodePassword($password_raw, $salt);
                $user->setPassword($password);

                $em->persist($user);
                $em->flush();
            }
        }

        return $this->render('NazoUserBundle:UserAdmin:edit.html.twig', array(
            'user' => $user,
            'form' => $form->createView(),
        ));
    }

}

Resources/views/UserAdmin/index.html.twig

<h1>User admin</h1>

<div>
<a href="{{ path('admin_user_create') }}">create new user</a>
</div>

<table>
    <tr>
        <th>ID</th>
        <th>name</th>
        <th>action</th>
    </tr>
{% for user in users %}
<tr>
    <td>{{ user.id }}</td>
    <td>{{ user.username }}</td>
    <td>
        <a href="{{ path('admin_user_edit', {'id':user.id}) }}">edit</a>
    </td>
</tr>
{% endfor %}
</table>

Resources/views/UserAdmin/edit.html.twig

<h1>Edit User</h1>

<div>ID : {{ user.id }}</div>

<form action="{{ path('admin_user_edit') }}" method="post" {{ form_enctype(form) }}>
    <input type="hidden" name="id" value="{{ user.id }}" />
    {{ form_widget(form) }}
    <input type="submit" value="save" />
</form>

<div>
<a href="{{ path('admin_user_index') }}">back to list</a>
</div>

これで、 app_dev.php/admin/user/index から、ユーザーを作り、そのユーザーでログインができるようになりました。

Symfony2でAnnotationを書くときの注意

Symfony2で、EntityなどをAnnotationで手書きする際、

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

これは反映されません。

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

これは反映されます。

違いは、コメントが「/**」で始まっているか「/*」で始まっているか、です。「/**」が正解です。

RefectionClass::getDocComment()の仕様らしいですが、手書きするときは気をつけましょう。

Symfony2で、Entityと連動しない場合にFormの値を取得する方法

createFormの第二引数にEntityを入れると、そのEntityに値が勝手に入るけど、それじゃEntityを使わない場合にFormの値ってどうやって取るの?という話。

まあとても簡単で、$form->getData()で、全データがarrayで返ってきます。

$data = $form->getData();
$comment = $data['comment'];

みたいな感じ。

一度代入するのめんどい!個別に取りたい!って場合は、

$comment = $form['comment']->getData();

という手もあります。このほうが楽かもしれません。

CakePHP2.0のブログチュートリアル

CakePHP2.0がリリースされました

とりあえず英語でブログチュートリアルがあるのでやってみます。

インストール


まずgithubから、2.0のtarballをダウンロードします。zipでもいいです。

適当に展開して、展開したところにWebからアクセスします。(webrootではなく、展開したところ。index.phpがあって、appとかcakeとかってフォルダが見えるところ)
ここでは、/Users/nazo/public_html/cakeblogに展開して、http://localhost/cakeblog/にアクセスする、とします。

いろいろエラーが出ます。

「Warning: _cake_core_ cache was unable to write 'cake_dev_ja' to Apc cache in 〜」と出た場合


APCが無効になっています。APCを有効にするか、apc.soを読み込まないようにしてください。どちらもphp.iniで設定できます。

「Warning: _cake_core_ cache was unable to write 'cake_dev_ja' to File cache in 〜」と出た場合


APCは無効ですが、ファイルキャッシュへの書き込みができません。「chmod -R 777 app/tmp」で、いろいろエラーが消えます。(Apacheに権限が入れば何でもいいです。以下同様。)

「Warning: /Users/nazo/public_html/cakeblog/app/tmp/cache/ is not writable in 〜」と出た場合


「chmod -R 777 app/tmp」で解決します。

「Your tmp directory is NOT writable.」と出た場合


「chmod -R 777 app/tmp」で解決します。

「URL rewriting is not properly configured on your server. 」と出た場合


Apachemod_rewriteを有効にしてください。できない場合はどうにかなります。(未調査)

「Please change the value of 'Security.salt' in app/Config/core.php to a salt value specific to your application」と出た場合


「app/Config/core.php」の、

Configure::write('Security.salt', 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi');

の行の、よくわからない文字列の部分を、適当なよくわからない文字列に変更してください。

「Please change the value of 'Security.cipherSeed' in app/Config/core.php to a numeric (digits only) seed value 〜」と出た場合


「app/Config/core.php」の、

Configure::write('Security.cipherSeed', '76859309657453542496749683645');

の行の、よくわからない数字の部分を、適当なよくわからない数字に変更してください。

「Your database configuration file is NOT present.」と出た場合


「app/Config/database.php.default」を、「app/Config/database.php」にリネーム(あるいはコピー)し、

        public $default = array(
                'datasource' => 'Database/Mysql',
                'persistent' => false,
                'host' => 'localhost',
                'login' => 'user',
                'password' => 'password',
                'database' => 'database_name',
                'prefix' => '',
                //'encoding' => 'utf8',
        );

の部分を適当に変更してください。とりあえず今回は、

        public $default = array(
                'datasource' => 'Database/Mysql',
                'persistent' => false,
                'host' => 'localhost',
                'login' => 'root',
                'password' => '',
                'database' => 'cakeblog',
                'prefix' => '',
                'encoding' => 'utf8',
        );

とします。MySQLで、この設定のデータベースも作ってください。

DBの作成


チュートリアル通りに、以下のデータを入れます。

/* First, create our posts table: */
CREATE TABLE posts (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(50),
    body TEXT,
    created DATETIME DEFAULT NULL,
    modified DATETIME DEFAULT NULL
);

/* Then insert some posts for testing: */
INSERT INTO posts (title,body,created)
    VALUES ('The title', 'This is the post body.', NOW());
INSERT INTO posts (title,body,created)
    VALUES ('A title once again', 'And the post body follows.', NOW());
INSERT INTO posts (title,body,created)
    VALUES ('Title strikes back', 'This is really exciting! Not.', NOW());

モデルの作成


app/Model/Post.phpを作成します。

<?php

class Post extends AppModel {
    public $name = 'Post';
}

?>

コントローラーの作成


app/Controller/PostsController.phpを作成します。

<?php
class PostsController extends AppController {
    public $helpers = array ('Html','Form');
    public $name = 'Posts';

    public function index() {
        $this->set('posts', $this->Post->find('all'));
    }
}
?>

ビューの作成


app/View/Posts/index.ctpを作成します。

<!-- File: /app/View/Posts/index.ctp -->

<h1>Blog posts</h1>
<table>
    <tr>
        <th>Id</th>
        <th>Title</th>
        <th>Created</th>
    </tr>

    <!-- Here is where we loop through our $posts array, printing out post info -->

    <?php foreach ($posts as $post): ?>
    <tr>
        <td><?php echo $post['Post']['id']; ?></td>
        <td>
            <?php echo $this->Html->link($post['Post']['title'],
array('controller' => 'posts', 'action' => 'view', $post['Post']['id'])); ?>
        </td>
        <td><?php echo $post['Post']['created']; ?></td>
    </tr>
    <?php endforeach; ?>

</table>

これで、 http://localhost/cakeblog/posts にアクセスすると、

一覧画面が表示されます。

詳細画面の作成


app/Controller/PostsController.phpを修正します。

<?php
class PostsController extends AppController {
    public $helpers = array('Html', 'Form');
    public $name = 'Posts';

    public function index() {
         $this->set('posts', $this->Post->find('all'));
    }

    public function view($id = null) {
        $this->Post->id = $id;
        $this->set('post', $this->Post->read());
    }
}
?>

app/View/Posts/view.ctpを作成します。

<!-- File: /app/View/Posts/view.ctp -->

<h1><?php echo $post['Post']['title']?></h1>

<p><small>Created: <?php echo $post['Post']['created']?></small></p>

<p><?php echo $post['Post']['body']?></p>

新規作成画面の作成


app/Controller/AppController.phpを修正します。

<?php
class PostsController extends AppController {
    public $name = 'Posts';
    public $components = array('Session');

    public function index() {
        $this->set('posts', $this->Post->find('all'));
    }

    public function view($id) {
        $this->Post->id = $id;
        $this->set('post', $this->Post->read());

    }

    public function add() {
        if ($this->request->is('post')) {
            if ($this->Post->save($this->request->data)) {
                $this->Session->setFlash('Your post has been saved.');
                $this->redirect(array('action' => 'index'));
            }
        }
    }
}
?>

app/View/Posts/add.ctpを作成します。

<!-- File: /app/View/Posts/add.ctp -->

<h1>Add Post</h1>
<?php
echo $this->Form->create('Post');
echo $this->Form->input('title');
echo $this->Form->input('body', array('rows' => '3'));
echo $this->Form->end('Save Post');
?>

app/View/Post/index.ctpの末尾に、addへのリンクを追加します。

<?php echo $this->Html->link('Add Post', array('controller' => 'posts', 'action' => 'add')); ?>

バリデーション


空データの入力を拒否します。
app/Model/Post.phpを修正します。

<?php
class Post extends AppModel {
    public $name = 'Post';

    public $validate = array(
        'title' => array(
            'rule' => 'notEmpty'
        ),
        'body' => array(
            'rule' => 'notEmpty'
        )
    );
}
?>

編集画面の作成


app/Controller/PostsController.phpに、以下の処理を追加します。

<?php
class PostsController extends AppController {
...
public function edit($id = null) {
    $this->Post->id = $id;
    if ($this->request->is('get')) {
        $this->request->data = $this->Post->read();
    } else {
        if ($this->Post->save($this->request->data)) {
            $this->Session->setFlash('Your post has been updated.');
            $this->redirect(array('action' => 'index'));
        }
    }
}
…
}
?>

app/View/Posts/edit.ctpを作成します。

<!-- File: /app/View/Posts/edit.ctp -->

<h1>Edit Post</h1>
<?php
    echo $this->Form->create('Post', array('action' => 'edit'));
    echo $this->Form->input('title');
    echo $this->Form->input('body', array('rows' => '3'));
    echo $this->Form->input('id', array('type' => 'hidden'));
    echo $this->Form->end('Save Post');
?>

app/View/Posts/index.ctpに、編集リンクを追加します。

<!-- File: /app/View/Posts/index.ctp  (edit links added) -->

<h1>Blog posts</h1>
<p><?php echo $this->Html->link("Add Post", array('action' => 'add')); ?></p>
<table>
    <tr>
        <th>Id</th>
        <th>Title</th>
                <th>Action</th>
        <th>Created</th>
    </tr>

<!-- Here's where we loop through our $posts array, printing out post info -->

<?php foreach ($posts as $post): ?>
    <tr>
        <td><?php echo $post['Post']['id']; ?></td>
        <td>
            <?php echo $this->Html->link($post['Post']['title'], array('action' => 'view', $post['Post']['id']));?>
                </td>
                <td>
            <?php echo $this->Form->postLink(
                'Delete',
                array('action' => 'delete', $post['Post']['id']),
                array('confirm' => 'Are you sure?')
            )?>
            <?php echo $this->Html->link('Edit', array('action' => 'edit', $post['Post']['id']));?>
        </td>
        <td><?php echo $post['Post']['created']; ?></td>
    </tr>
<?php endforeach; ?>

</table>

削除リンクもあります。(使うのはこの後)

削除機能の作成

app/Controller/PostsController.phpに、以下の処理を追加します。

<?php
class PostsController extends AppController {
...

public function delete($id) {
    if (!$this->request->is('post')) {
        throw new MethodNotAllowedException();
    }
    if ($this->Post->delete($id)) {
        $this->Session->setFlash('The post with id: ' . $id . ' has been deleted.');
        $this->redirect(array('action' => 'index'));
    }
}
…
}
?>

ルーティング


ルーティングを変更する場合は、app/Config/routes.phpを編集します。

現在は http://localhost/cakeblog/posts からアクセスしていますが、これを http://localhost/cakeblog/ でアクセスできるようにするには、

        Router::connect('/', array('controller' => 'posts', 'action' => 'index'));
        //Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));

のようにします。

Symfony2で、created_at/updated_atを自動で使う方法

Symfony2で、Entityにcreated_at/updated_at(作成日時/更新日時)を自動で入れる方法

普通にやるとLifecycle Callbackを使うことになるのですが、こんなの毎回書くのは面倒なので、一発でどうにかしたいです。
Doctrine Extensions(DoctrineExtensionsBundle)を使います。

インストール


deps

[gedmo-doctrine-extensions]
    git=git://github.com/l3pp4rd/DoctrineExtensions.git

[DoctrineExtensionsBundle]
    git=git://github.com/stof/StofDoctrineExtensionsBundle.git
    target=/bundles/Stof/DoctrineExtensionsBundle
    version=1.0.0

で、

bin/vendors install

app/AppKernel.php

$bundles = array(
…
new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
…
);

app/autoload.php

$loader->registerNamespaces(array(
    // ...
    'Stof'  => __DIR__.'/../vendor/bundles',
    'Gedmo' => __DIR__.'/../vendor/gedmo-doctrine-extensions/lib',
    // ...
));

使い方


app/config/config.yml

...
stof_doctrine_extensions:
    orm:
        default:
            timestampable: true
…

で、作成日時/更新日時を入れたいEntityの定義ファイルに

...
  fields:
...
     created_at:
         type: datetime
         gedmo:
           timestampable:
             on: create
     updated_at:
         type: datetime
         gedmo:
           timestampable:
             on: update

を追加して、あとはEntityを再生成してデータを投入

php app/console doctrine:generate:entities Nantoka
php app/console doctrine:schema:update --force

でOK

気になる点



  • createとupdateで秒がずれることがある

参考


Symfony2でアップロードされたファイルを設置する方法

いやファイルアップロードは前回の説明を見れば大体わかるんだけど、データの保存先をどうするの?的な話。

アップロードされたファイルは


  • DBに入れる

  • どこかに移動する


の2パターンしかなくて、前者はまあどうでもいいんだけど、問題は後者。

symfony1だと、sfConfig::get('sf_upload_dir')で取得できたんだけどーみたいな話。

で、$this->get('request')->getBasePath()で、URLのルートからの相対パスhttp://example.com/なら""、http://example.com/hoge/なら"/hoge")が取得できて、$this->get('request')->server->get('DOCUMENT_ROOT')で、"/home/nazo/hoge/web"みたいなのが取得できます。
ちなみに$this->get('request')->getBaseUrl()で、"/app_dev.php"みたいなのが取得できます。使わないだろうけど。

ドキュメントルートの取得方法が他にないのかなー

Symfony2のフォーム(Form)の使い方

Symfony2のフォームは、かなり使いやすくなっています。
1はModelとの依存が激しくて、凝ったことをしようとすると意味不明になりましたが、2は完全に切り離されており、かつ依存させることも可能になっています。

パッと使う


action側

$this->createFormBuilder()
        ->add('hoge', 'text')
        ->add('fuga', 'date')
        ->getForm();

if ($request->getMethod() == 'POST') {
  $form->bindRequest($request);
  if ($form->isValid()) {
    // ここに保存処理とかを書く
  }
}

return $this->render('HogeBundle:Fuga:index.html.twig', array('form' => $form->createView()));

template側

<form action="{{ path('post_page') }}" method="post" {{ form_enctype(form) }}>
    {{ form_widget(form) }}
    <input type="submit" />
</form>

パッと使うだけなら、フォーム用のクラスを作らなくても、actionに内蔵されているcreateFormBuilderで、その場限りのフォームを作成することができます。テスト中は便利ですね。

パッと使う(Entity連動)


action側

$user = new User();
$this->createFormBuilder($user)
        ->add('name', 'text')
        ->add('registered', 'date')
        ->getForm();
if ($request->getMethod() == 'POST') {
  $form->bindRequest($request);
  if ($form->isValid()) {
    // ここに保存処理とかを書く
  }
}

bindした時点で、$userの、フォームのフィールド名に対応するメンバに、フォームの値が入っています。
簡単なフォームであれば、このままsave()するだけです。

ちゃんとクラスを作る


このままでもいいのですが、クラスを作ったほうが使い回しができます。同じフォームが複数箇所で出てくる時はもちろん、本開発ではフォームクラスに機能を入れたい場合もあるので、クラスを作ったほうがいいです。
クラスは一般的には、BundleのForm/Type以下に作るようです。Typeという名の通り、正確にはこれはフォームクラスではありません。フォームの種類を定義したクラスです。

<?php
// src/Hoge/FugaBundle/Form/Type/UserType.php

namespace Hoge\FugaBundle\Form\Type;

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

class UserType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('name', 'text');
        $builder->add('registered', 'date');
    }

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

buildFormの、$builderが、createFormBuilderで返されるものと同じです。(FormBuilderクラス)

フォームの種類


addの第一引数がフィールド名、第二引数が型ですが、この型の種類は、
text, textarea, email, integer, money, number, password, percent, search, url, choice, entity, country, language, locale, timezone, date, datetime, time, birthday, checkbox, file, radio, collection, repeated, hidden, csrf, field, form
があります。

テンプレート側が全部まとめて出ちゃうんだけど、これだとデザインと合わせられない!個別に出したい!

{{ form_errors(form) }}

{{ form_row(form.name) }}
{{ form_row(form.registered) }}

{{ form_rest(form) }}

これで分離できます。errorsはエラー、rowは各フィールド、restはhiddenのもの(トークンなど)が出ます。
rowはラベルとかもくっついて出てくるので、それも分離したい場合は、

{{ form_label(form.name) }}
{{ form_errors(form.name) }}
{{ form_widget(form.name) }}

で、さらに分離できます。

分離できるのはわかった!でも入力フォームのサイズとかはどうやって変えるの?


CSSでやれ

あ、class指定したいなら

$this->add('name', 'text', array('attr' => array('class' => 'nantoka')));

で指定できますよ。他のHTMLオプションも指定できますよ。

えっそこで何か指定できるの?他に何あるの?


しらん。ここ見て。(多すぎる)

多いのはわかるけど、よく使うのくらい教えてよ!







attrHTMLのオプションをarrayで指定
required必須指定
max_length最大文字数
labelラベル
read_only読み込み専用

デフォルト値はどこで指定するの?


Entityに値が入っていれば、勝手にそれになるよ。
手動で入れたければ、
$form->get('name')->setData('デフォルトの文字列');
という感じ。

ところで複数選択のやつとかどうやって指定するの?

$builder->add('blood_type', 'choice', array(
  'choices' => array(
    1 => 'A',
    2 => 'B',
    3 => 'O',
    4 => 'AB',
  ), 'empty_value' => '選択してね!'));

こんな感じ。empty_valueを省略すると、全部選択肢になる。

ファイルは?


'file'で指定すると、UploadedFileクラスが入るので、あとはご自由に

バリデーションは?


(続く)(かもしれない)