nazolabo

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

UnityでVoiceText Web APIを使って喋らせる

モヤさまのアレがWebAPIになったらしいので使ってみる

     IEnumerator SayVoiceText(AudioSource source, string apiKey, string text) {
          string url = "https://api.voicetext.jp/v1/tts";
          WWWForm form = new WWWForm();
          form.AddField("speaker", "show");
          form.AddField("text", text);
          Hashtable headers = form.headers;
          headers["Authorization"] = "Basic " + System.Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(apiKey + ":"));
          WWW www = new WWW(url, form.data, headers);
          while( !www.isDone ) {
               yield return www;
          }

          if (www.error != null) {
               Debug.Log (www.error);
          } else {
               source.clip = www.GetAudioClip(false, false, AudioType.WAV);
               source.Play();
          }
     }

こんなのを作って

StartCoroutine(SayVoiceText(audioSource, "your api key", "こんにちは、なぞさんです"));

Unityで日本を登山する(3D地図データを読み込む方法)

先日公開された国土地理院の3D地図データを使います。なお利用規約に注意してください。

  1. ダウンロードしたファイルの中にある*.stlファイルをBlenderでImport
  2. Blenderで*.fbxファイルとしてExport
  3. Unityの新規プロジェクトに突っ込む(とりあえず名前をmapとする)
  4. mapをHierarchyに配置
  5. 縦になってしまっているのでMeshの角度を合わせる(mapそのものではなく、その子要素のMesh(多分複数ある)をそれぞれxを90度曲げてあげると丁度良くなります。Blenderでやっておくのが一番いいです。)
  6. mapを適当に拡大(できればBlenderでやったほうがいい)
  7. mapに対し、Meshの数だけMesh Colliderを設置、Mesh ColliderのMeshにmapのMeshを割り当て、Convexにチェックを入れる(入れなくてもいいが入れたほうが自然な判定になる)
  8. First Person Controllerを配置して適当に座標を合わせる
  9. 実行

f:id:nazone:20140320102743p:plain

多分実用性はないです

@propertyの属性の挙動の違い

@propertyの属性にcopyとかstrongとかweakとかあるけど、概念的な説明はどこでもあるけど、具体的な説明をしている記事が少ないので調査した。

Xcode4.3.2時点での内容です。

nonatomic

非スレッドセーフにする。マルチスレッドのものを使わない限りはどんどん指定しよう。
ちなみにatomicな場合は内部的にはgetterは以下のような処理になる模様。ロックが入ってしまう。

[_internal lock];
id result = [[value retain] autorelease];
[_internal unlock];
return result;

getter/setter

getterとsetterのメソッド名はデフォルトで、getterは変数名と同じ、setterはset[Capitalize変数名]だが、それを任意のものに設定することができる。
readonly時にsetterは指定できない(当たり前だが)
通常変更する必要はないが、isなんたら とかってgetterにしたい場合に使うといいらしい。

readwrite/readonly

readwriteはデフォルトなので、単品で指定することはまずない。readonlyはその名の通りsetterが生成されなくなる。self.prop = 1;みたいなドットでのアクセスすらコンパイルエラーになるので、このままだと何もできない変数になる気がする。
.hではreadonlyにして、.mでreadwriteにして定義を上書きすることで、クラス内部では読み書きができ、外部では読み出ししかできないプロパティが出来上がる。

.h

#import <Foundation/Foundation.h>

@interface TestData : NSObject

@property (nonatomic, readonly) NSString* title;

-(void)action;

@end

.m

#import "TestData.h"

// ここがないとactionではエラー
@interface TestData ()
@property (nonatomic, readwrite) NSString* title;
@end

@implementationTestData

@synthesize title;

-(void)action {
    self.title = @"hello";
}

@end

strong/copy/retain/assign

  • ARCを使わない場合はassignがデフォルト、使う場合はstrongがデフォルト。
  • copyとreleaseは、以前の値に対しreleaseが呼ばれる。
  • retainは参照カウントが保持されて、assignは保持されないらしい。
  • copyはNSCopyingプロトコルが実装されていないオブジェクトに対して設定すると落ちる。NSObjectだけだと実装されていないので、使用する際は注意。
  • ARCを無効にしてテスト

TestString.h

#import <Foundation/Foundation.h>

@interface TestString : NSObject <NSCopying>
@property (nonatomic, copy) NSString* string;
-(id)initWithString:(NSString*)string;
@end

TestString.m

#import "TestString.h"

@implementation TestString
@synthesize string;

-(id)initWithString:(NSString*)newString {
    self = [super init];
    self.string = newString;
    return self;
}

- (void)dealloc {
    NSLog(@"dealloc %@", self.string);
    [super dealloc];
}

- (id)copyWithZone:(NSZone *)zone
{
    NSLog(@"duplicate %@", self.string);
    TestString* clone = [[TestString alloc] init];
    [clone setString:self.string];
    return clone;
}

@end

TestData.h

#import <Foundation/Foundation.h>

#import "TestString.h"

@interface TestData : NSObject
@property (nonatomic, [ここを任意に変更]) TestString* title;
@end

TestData.m

#import "TestData.h"

@implementation TestData

@synthesize title;

@end

上記のようなコードがあった場合、[ここを任意に変更]が以下を指定した場合に、

    TestData* data = [[TestData alloc] init];
    TestString* string = [[TestString alloc] initWithString:@"abc"];
    TestString* string2 = [[TestString alloc] initWithString:@"def"];
    [data setTitle:string];
    [data setTitle:string2];
    NSLog(@"end");
    [string release];
    [string2 release];   
    [data release];

というコードを動かした場合、以下のような結果になる。(コメントはその行を通過直後に発生されるログ)
strongは、最後に代入したstring2の所有権がdataに移っているので、外からstring2をreleaseしてもデストラクタが呼ばれない。
copyはcopyなので、外でreleaseするとデストラクタが呼ばれる。またcopyは、自分自身が入れ替わると、入れ替わる前の値はreleaseされるので、setTitle:string2でもデストラクタが呼ばれている。
assignは内部でどう扱おうが知ったことではないという感じ。外でreleaseするとdataのtitleもreleaseされているので、テストコードで言うところの、[data release];の直前でdataのtitleを扱おうとするとEXC_BAD_ACCESSとなる。参照だけを単純に代入すると考えるといい。あとスカラー型はassignでもコピー。
retainは基本的にstrongと同じ。内部的にretainというメソッドがあり、それを使って、「hoge = [fuga retain]」のように代入すると、strongと同じような効果が得られるため、そういうものがあるらしい。

copy
    TestData* data = [[TestData alloc] init];
    TestString* string = [[TestString alloc] initWithString:@"abc"];
    TestString* string2 = [[TestString alloc] initWithString:@"def"];
    [data setTitle:string];             // duplicate abc
    [data setTitle:string2];          // duplicate def, dealloc abc
    NSLog(@"end");                    // end
    [string release];               // dealloc abc
    [string2 release];               // dealloc def
    [data release];
strong
    TestData* data = [[TestData alloc] init];
    TestString* string = [[TestString alloc] initWithString:@"abc"];
    TestString* string2 = [[TestString alloc] initWithString:@"def"];
    [data setTitle:string];
    [data setTitle:string2];
    NSLog(@"end");               // end
    [string release];               // dealloc abc
    [string2 release];   
    [data release];

assign

    TestData* data = [[TestData alloc] init];
    TestString* string = [[TestString alloc] initWithString:@"abc"];
    TestString* string2 = [[TestString alloc] initWithString:@"def"];
    [data setTitle:string];
    [data setTitle:string2];
    NSLog(@"end");               // end
    [string release];                // dealloc abc
    [string2 release];             // dealloc def
    [data release];
retain
    TestData* data = [[TestData alloc] init];
    TestString* string = [[TestString alloc] initWithString:@"abc"];
    TestString* string2 = [[TestString alloc] initWithString:@"def"];
    [data setTitle:string];
    [data setTitle:string2];
    NSLog(@"end");               // end
    [string release];               // dealloc abc
    [string2 release];   
    [data release];

weak

weakはARCを有効にしないと使えない。assignと同等。

PHPカンファレンス北海道を開催しました #phpcondo

2012年4月21日(土)に、北海道では初めてとなる、PHPカンファレンス北海道が開催されました。
今回、私は実行委員長ということで、立ち上げから全ての面でイベントを作っていきました。

何で開催したの?


元々「北海道でもPHPカンファレンスやるといいんじゃないかなー関西でも開催したしなー」と、関西の開催あたりからぼんやりと思っていたのですが、直接のきっかけは、しまださんから、
「札幌でPHPの大きいイベントやらないの?」
みたいなことを言われたことでした。

とは言っても、言われたからやったわけではないです。
普段から、LOCAL PHP部での発表者不足や、PHP技術者がそもそもどこにいるかわからない、という悩みを抱えており、大きいイベントをすることで、そういう人達が集まることができるのではないか、と思っていました。
また、実績がある「PHPカンファレンス」という名前を使うことにより、普段の「勉強会」という名前よりは、「カンファレンスっていうくらいだからそれなりにまともなイベントなんじゃね」ということで、普段勉強会とかには出てこない層も出てくるのではないか、と思っていました。
普段の勉強会にこんなに人数が集まる必要はないと思っていますが、「PHPの技術者が集まるイベントがある」ということはもっと認知されていいと思っています。

初めてなのによく開催できたね?


北海道(特に札幌)には、LOCALという団体があり、こういうイベントをやると言えば、自然にスタッフが集まってくるのはいいところです。
私はほとんど大枠を決めただけで、実際は他のスタッフが動いてくれたおかげで、イベントが出来上がりました。
結果として、のべ150人を超えた参加人数となり、とても盛り上がったイベントになったと思います。
スタッフの皆様には本当に感謝しております。

あのプログラム構成はどうなの?


北海道は、SaCSSの影響などもあって、デザイナーやコーダーなどの、「PHPはほんの少し使うか使わないか程度」の人が結構多いです。そこで、そういう人達もフォローアップしようという目的で、午前中は初心者枠にする、というのは、最初からの案でした。
実際は、柏岡さんの午後一のセッションが、初心者から上級者まで幅広く楽しめる内容になってくれたので、多くの方が楽しめる内容になったと思います。
また、ほぼ飛び入りで、yandoさんから「ハンズオンをやりたい」という提案があったため、午前中のセッションに興味ない人や、午後からのセッションはよくわからない人、というのを、そちらに流せたと思います。
とは言っても、全員がハンズオンに流れたわけでもなく、懇親会では「後半は何を言っているのかわからなかった」という会話をちらほら見かけました。まあ1トラックでは限界かなと思います。
1トラックで収容人数を増やすか、並行トラック数を増やすかどうかは悩ましいところです。

また、最初のほうのセッションで、訓練されたPHP技術者の皆様から「それはいかんだろ」みたいな意見が飛んで、その議論が盛り上がったのは、予想外でしたが、結果として面白い展開になって、とてもよかったと思っています。

来年もやるの?


アンケート結果や、私の予定などにもよりますが、できる限り開催したいと思っております。

クロージングの時にも話しましたが、来年開催する場合は、発表者をもっと広く募集したいと思っています。今回は道外からの講演者が多くなってしまったため、来年は道内の技術者がもっと活躍していただければと思っています。せっかくのPHPカンファレンス北海道なのに、道外から呼ぶだけ、だともったいないですよね。

皆様本当にありがとうございました!

おまけ


イベントのまとめ等は公式サイトやLOCALのサイトで公開していきますが、皆様の感想や今後の動きについては、#phpcondoハッシュタグを追っていただければと思います。

CakePHP2.0ハンズオン@札幌で使用した資料を公開します

2011年11月26日に開催した、CakePHP2.0ハンズオン@札幌の資料をHTMLにしたものを公開します。
解説は現場で行ったため、説明不足の資料になっています。基本的には公式のチュートリアルと同一です。なので、以前の記事とも内容が重複しています。

1. 作業の注意・この資料について


プログラムを保存する場合、文字コードは「UTF-8」にして保存してください。改行コードは何でもいいです(LFのみ推奨)。
ファイル名の大文字小文字は間違えないように入力してください。
この資料は、http://book.cakephp.org/2.0/en/ を元に作成されています。

2. CakePHPのインストール


ダウンロード


http://cakephp.org/から、2.0.3 Stable(現時点での最新バージョン)のパッケージをダウンロードします。解凍し、Apacheが見える場所(htdocs直下など)に展開してください。
ここでは仮に、htdocs/cakeblog/に展開します。
XAMPP(Windows)環境の場合は、 ダウンロードして展開して作られた「cakephp-cakephp-2b55a02」(ファイル名はダウンロードしたバージョンによって違います)というフォルダを、「C:¥xampp¥htdocs」に移動します。その後、「 cakephp-cakephp-2b55a02」を「cakeblog」にリネームします。
MAMP(MacOSX)環境の場合は、ダウンロードして展開して作られた「cakephp-cakephp-2b55a02」(ファイル名はダウンロードしたバージョンによって違います)というフォルダを、「/Application/MAMP/htdocs」(あるいは「 アプリケーション -> MAMP -> htdocs」)に移動します。その後、「 cakephp-cakephp-2b55a02」を「cakeblog」にリネームします。

起動と設定


http://localhost/cakeblog/でアクセスできると思うので、アクセスしてみてください。(環境によっては変わるかもしれません)
エラーがいろいろ出ると思うので、確認していきます。(環境によって出るものと出ないものがあります)
「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.」と出た場合

次項で説明します。
「Cake is NOT able to connect to the database.」と出た場合

DBの設定は書けていますが、設定した通りのDBに接続ができません。次項を読みなおしてください。
「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」で、いろいろエラーが消えます。
「Warning: /Application/MAMP/htdocs/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. 」と出た場合

Apacheでmod_rewriteを有効にしてください。できない場合は、app/Config/core.phpの、

//Configure::write(’App.baseUrl’, env(’SCRIPT_NAME’));

の行を、

Configure::write(’App.baseUrl’, env(’SCRIPT_NAME’));

と変更した上で、今後の説明の「http://localhost/cakeblog/」を、「http://localhost/cakeblog/index.php/」に読み替えてください。
(例:「http://localhost/cakeblog/posts」→「http://localhost/cakeblog/index.php/posts

3. DBの設定


「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’,
);

のように変更します。(独自のDB接続設定がある場合は、それに合わせて修正してください)
phpMyAdmin等で、「cakeblog」というデータベースを作成します。phpMyAdminの場合は、「新規データベースを作成する」の箇所に「cakeblog」と入力し、「照合順序」を、「utf8_bin」にして、「作成」を押してください。
その後、以下のSQLを入力します。phpMyAdminの場合は、「cakeblog」のデータベースを選択し、「SQL」タブで、以下を貼りつけて、「実行する」を押します。

CREATE TABLE posts (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(50),
    body TEXT,
    created DATETIME DEFAULT NULL,
    modified DATETIME DEFAULT NULL
) Engine=InnoDB;

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());

4. モデルの作成


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

<?php

class Post extends AppModel {
}

5. コントローラーの作成


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

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

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

6. ビューの作成


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

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

    <?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>

7. 詳細画面の作成


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

<?php
class PostsController extends AppController {
    public $helpers = array(’Html’, ‘Form’);

    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を作成します。

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

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

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

8. 新規作成画面の作成


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

<?php
class PostsController extends AppController {
    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を作成します。

<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’)); ?>

9. バリデーション


app/Model/Post.phpを修正します。

<?php
class Post extends AppModel {
    public $validate = array(
        ‘title’ => array(
            ‘rule’ => ‘notEmpty’
        ),
        ‘body’ => array(
            ‘rule’ => ‘notEmpty’
        )
    );
}

10. 編集画面の作成


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を作成します。

<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に、編集リンクを追加します。

<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>

<?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>

11. 削除機能の作成


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’));
    }
}
…
}

12. ルーティング


ルーティングを変更する場合は、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’));

のようにします。

13. 見た目の変更


app/View/Layouts/default.ctpを作成することによって、外観を自由に変更することができます。
以下の内容をサンプルに、自由に書き換えてみましょう。

<!DOCTYPE html>
<html>
<head>
        <?php echo $this->Html->charset(); ?>
        <title>
                <?php echo $cakeDescription ?>:
                <?php echo $title_for_layout; ?>
        </title>
        <?php
                echo $this->Html->meta(’icon’);

                echo $this->Html->css(’cake.generic’);

                echo $scripts_for_layout;
        ?>
</head>
<body>
        <div id="container">
                <div id="header">
                        <h1>CakePHP 2.0 Sample</h1>
                </div>
                <div id="content">

                        <?php echo $this->Session->flash(); ?>

                        <?php echo $content_for_layout; ?>

                </div>
                <div id="footer">
                        Powered by CakePHP
                </div>
        </div>
        <?php echo $this->element(’sql_dump’); ?>
</body>
</html>

14. コメント機能


ここまでの実装を元に、コメント機能を作成してみましょう。(時間が余った人向け)

15. 管理画面の作成


CakePHPには、管理画面を簡単に作成できる機能が入っています。
仮に、ユーザーというテーブルがあり、それを管理できる画面を簡単に作ります。
データベースに、以下のSQLを投入します。

CREATE TABLE users (
  id int(11) NOT NULL AUTO_INCREMENT,
  username varchar(255) NOT NULL,
  password varchar(255) NOT NULL,
  created datetime DEFAULT NULL,
  modified datetime DEFAULT NULL,
  PRIMARY KEY (id)
) Engine=InnoDB;

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

<?php

class User extends AppModel {
}

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

<?php

class UsersController extends AppController {
        public $scaffold;
}

http://localhost/cakeblog/users にアクセスしてみましょう。

16. 認証


一覧・表示以外は、認証が必要にします。
ここでは、先程作成した、Usersモデル及びコントローラーを使用します。
パスワードを暗号化しなければいけないので、自動で保存時に暗号化されるようにします。app/Model/User.phpを変更します。

<?php
App::uses(’AuthComponent’, ‘Controller/Component’);
class User extends AppModel {

// ...

public function beforeSave() {
    if (isset($this->data[$this->alias][’password’])) {
        $this->data[$this->alias][’password’] = AuthComponent::password($this->data[$this->alias][’password’]);
    }
    return true;
}

// ...

変更したら、http://localhost/cakeblog/usersから、適当にユーザーを作成してみてください。
作成したら、ログインとログアウトができるようにします。
ログイン関係はUsersControllerに追加します。app/Controller/UsersController.phpを編集します。

<?php

class UsersController extends AppController {
        public $scaffold;
        public $components = array('Session', 'Auth');

        public function beforeFilter() {
                parent::beforeFilter();
                $this->Auth->allow('login');
        }

        public function login() {
                if ($this->request->is('post')) {
                        if ($this->Auth->login()) {
                                $this->redirect($this->Auth->redirect());
                        } else {
                                $this->Session->setFlash(__('Invalid username or password, try again'));
                        }
                }
        }

        public function logout() {
                $this->redirect($this->Auth->logout());
        }
}

ログイン画面が必要なので、app/View/Users/login.ctpを作成します。

<div class="users form">
<?php echo $this->Session->flash('auth'); ?>
<?php echo $this->Form->create('User');?>
        <fieldset>
<legend><?php echo __('Please enter your username and password'); ?></legend>
        <?php
                echo $this->Form->input('username');
                echo $this->Form->input('password');
        ?>
        </fieldset>
<?php echo $this->Form->end(__('Login'));?>
</div>

これでhttp://localhost/cakeblog/users/loginにアクセスするとログインができるようになります。
ログインできているのかどうかよくわからないので、ブログの投稿・編集・削除は、ログインが必要にします。
app/Controller/PostsController.phpを編集します。

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

        public function beforeFilter() {
                parent::beforeFilter();
                $this->Auth->allow('index', 'view');
        }

        // .....

ついでに、ログインとログアウトのリンクを追加します。app/View/Layouts/default.ctpの適当な場所に、以下を追加します。

<?php
if ($login_user_id !== null) {
    echo "Welcome ".h($login_user_name)." !!<br />";
    echo $this->Html->link('Logout', array('controller' => 'users', 'action' => 'logout'));
} else {
    echo $this->Html->link('Login', array('controller' => 'users', 'action' => 'login'));
}
?>

このままではエラーになるので、$login_user_idと$login_user_nameが常に入るようにします。app/Controller/AppController.phpを作成します。

<?php

class AppController extends Controller
{
        public $components = array('Session', 'Auth');

        public function beforeFilter() {
                parent::beforeFilter();
                $this->set('login_user_id', $this->Auth->user('id'));
                $this->set('login_user_name', $this->Auth->user('username'));
        }
}

ブログの投稿時にユーザーIDを記録するようにします。
まず、postsテーブルにuser_idを追加します。phpMyAdmin等で、以下のSQLを入力してください。
ALTER TABLE posts ADD COLUMN user_id INT(11);
これに合わせて、PostsControllerのaddの際に、user_idを自動入力させます。app/Controller/PostsController.phpを編集します。

<?php
// app/Controller/PostsController.php
public function add() {
    if ($this->request->is(’post’)) {
        $this->request->data[’Post’][’user_id’] = $this->Auth->user(’id’); //Added this line
        if ($this->Post->save($this->request->data)) {
            $this->Session->setFlash(’Your post has been saved.’);
            $this->redirect(array(’action’ => ‘index’));
        }
    }
}

Symfony2の認証(access_controlとrole_hierarchy編)

Symfony2のユーザーには、権限のレベルを細かく設定できます。

ここでの説明には、Entity経由でログイン編のソースを使います。

権限を確認するために、アクションを1つ追加します。

Controller/DefaultController.php

...

    /**
     * @Route("/user/sugoi")
     * @Template()
     */
    public function sugoiAction()
    {
        return array(
        );
    }

...

Resources/views/Default/sugoi.html.twig

sugoi

当たり前ですが、この時点では、ログインすれば app_dev.php/user/sugoi にはアクセスできます。

では、アクセスできなくしてみましょう。

app/config/security.yml

…

    access_control:
        - { path: ^/user/sugoi, roles: [ROLE_ADMIN] }

これで、 app_dev.php/user/sugoi にアクセスすると、403になるようになりました。

では、一度ログアウトして、UserクラスのRoleをROLE_ADMINにしましょう。

Entity/User.php

    private $roles = array('ROLE_ADMIN');

これでログインすると、 app_dev.php/user/sugoi にアクセスできます。

role_hierarchyの設定は、権限の上下関係を表しています。現在は、

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER

となっていますので、security.ymlを、

…

    access_control:
        - { path: ^/user/sugoi, roles: [ROLE_USER] }

としても、ROLE_ADMINの人は、 app_dev.php/user/sugoi にアクセスすることができます。

access_controlの設定は、rolesの他にも、


  • requires_channel : アクセス方法の設定(https と指定すると強制的にhttps接続になります)

  • ip : 接続元IPアドレスの設定("192.168.0.1/24"とか指定可)


などで縛ることができます。(あとhostとかmethodsとかあるようですが確認できませんでした)
rolesには特殊な権限を3種類設定することができます。(ここから引用)

  • IS_AUTHENTICATED_ANONYMOUSLY : 実際にログインしていないユーザが、サイト上のファイアーウォールの保護された場所にアクセスしたユーザに自動的に割り当てるロール。これは匿名アクセスを許しているときのみ使用可能です。

  • IS_AUTHENTICATED_REMEMBERED : remember me クッキーを介して認証されたユーザに自動的に割り当てるロール。

  • IS_AUTHENTICATED_FULLY : 現在のセッション中にログインをしたユーザに自動的に割り当てるロール。


早い話が、ANONYMOUSLYは、firewallsで設定されていて、anonymousがfalseでない箇所に、ログインしていないユーザーが入ったら設定される、REMEMBEREDは、「remember me」機能を使う場合の権限、FULLYは、ログイン中の全ユーザーが対象になります。REMEMBEREDについては、引用元URLを参照してください。

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 から、ユーザーを作り、そのユーザーでログインができるようになりました。