nazolabo

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

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クラスが入るので、あとはご自由に

バリデーションは?


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


PHP5.4 alpha1を使ってみた その2(変更点1)

前回:http://nazo.hatenablog.com/entry/2011/06/29/000000

前回でインストールが完了したので、次は実際に新機能を試してみます。

変更点:http://www.php.net/releases/NEWS_5_4_0_alpha1.txt
type hintingでscalar値が指定できるのは採用されなかったようです。

break $var;の廃止

$i=2;
$j=2;
$k=2;
while($i--) {
  while($j --) {
    echo "test", PHP_EOL;
    break $k;
  }
}

PHP5.3までだと動きますが、5.4だと
Fatal error: 'break' operator with non-constant operand is no longer supported in php shell code on line 4
となります。
ちなみに定数であれば問題ないです。(break 2;とかはOK)

array_combineの変更

PHP5.3だと

php > var_dump(array_combine(array(), array()));

Warning: array_combine(): Both parameters should have at least 1 element in php shell code on line 1
bool(false)

ですが、PHP5.4では、

php > var_dump(array_combine(array(), array()));
array(0) {
}

となります。

preg_match_allの変更


第三引数がoptionalになりました。

PHP5.3

php > preg_match_all("/a/", "aaa");

Warning: preg_match_all() expects at least 3 parameters, 2 given in php shell code on line 1

PHP5.4

php > preg_match_all("/a/", "aaa");
php >

null等に対するObjectとしての扱いの変更


null、空文字列、falseの変数に対して、オブジェクトのプロパティ代入のような操作をすると、従来は勝手にstdClassのオブジェクトになりましたが、今回はwarningになるようになりました。(stdClassのオブジェクトとしては動作します)

PHP5.3

php > $null->hoge = 1;
php >

PHP5.4

php > $null->hoge = 1;

Warning: Creating default object from empty value in php shell code on line 1
php > var_dump($null);
object(stdClass)#1 (1) {
  ["hoge"]=>
  int(1)
}

<?=が常に有効に


<?でPHPコードを始めるshort_open_tagsは、iniで設定しない限り有効になりませんでしたが、<?=の表記(echoする)に限り、常に有効になりました。

register_globals、safe_modeなどの廃止


deprecatedじゃなくて根本的に使えなくなっています。定義するとFatalで即終了します。(Offで定義してある分には問題ない)
ちなみにmagic_quotes_gpcは生き残ってます。

まだまだ続く(予定)