nazolabo

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

Laravel5を試す

Laravelとか初めて使うよ!出たばっかりなので試す。

この記事はLaravel5.0が出たばかりに書いた内容です。最新の内容と必ず比較してください。

インストール

PHP5.5以上とmcrypt/openssl/mbstringとcomposerが入ってる前提。 まずインストーラのインストール

php composer.phar global require "laravel/installer=~1.1"

プロジェクトの作成

laravel new blog

とりあえず

$ php artisan -v
Laravel Framework version 5.0

Usage:
 [options] command [arguments]

Options:
 --help (-h)           Display this help message
 --quiet (-q)          Do not output any message
 --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
 --version (-V)        Display this application version
 --ansi                Force ANSI output
 --no-ansi             Disable ANSI output
 --no-interaction (-n) Do not ask any interactive question
 --env                 The environment the command should run under.

Available commands:
 clear-compiled       Remove the compiled class file
 down                 Put the application into maintenance mode
 env                  Display the current framework environment
 help                 Displays help for a command
 inspire              Display an inspiring quote
 list                 Lists commands
 migrate              Run the database migrations
 optimize             Optimize the framework for better performance
 serve                Serve the application on the PHP development server
 tinker               Interact with your application
 up                   Bring the application out of maintenance mode
app
 app:name             Set the application namespace
auth
 auth:clear-resets    Flush expired password reset tokens
cache
 cache:clear          Flush the application cache
 cache:table          Create a migration for the cache database table
config
 config:cache         Create a cache file for faster configuration loading
 config:clear         Remove the configuration cache file
db
 db:seed              Seed the database with records
event
 event:generate       Generate the missing events and handlers based on registration
handler
 handler:command      Create a new command handler class
 handler:event        Create a new event handler class
key
 key:generate         Set the application key
make
 make:command         Create a new command class
 make:console         Create a new Artisan command
 make:controller      Create a new resource controller class
 make:event           Create a new event class
 make:middleware      Create a new middleware class
 make:migration       Create a new migration file
 make:model           Create a new Eloquent model class
 make:provider        Create a new service provider class
 make:request         Create a new form request class
migrate
 migrate:install      Create the migration repository
 migrate:refresh      Reset and re-run all migrations
 migrate:reset        Rollback all database migrations
 migrate:rollback     Rollback the last database migration
 migrate:status       Show a list of migrations up/down
queue
 queue:failed         List all of the failed queue jobs
 queue:failed-table   Create a migration for the failed queue jobs database table
 queue:flush          Flush all of the failed queue jobs
 queue:forget         Delete a failed queue job
 queue:listen         Listen to a given queue
 queue:restart        Restart queue worker daemons after their current job
 queue:retry          Retry a failed queue job
 queue:subscribe      Subscribe a URL to an Iron.io push queue
 queue:table          Create a migration for the queue jobs database table
 queue:work           Process the next job on a queue
route
 route:cache          Create a route cache file for faster route registration
 route:clear          Remove the route cache file
 route:list           List all registered routes
schedule
 schedule:run         Run the scheduled commands
session
 session:table        Create a migration for the session database table
vendor
 vendor:publish       Publish any publishable assets from vendor packages

とか出ればOK。コマンド眺めるだけでも充実してるのがわかる。 artisanはアルチザンと読むらしい。ちなみにlaravelはララベル。

ついでにサーバも立ち上げてみる。

$ php artisan serve
Laravel development server started on http://localhost:8000

昔はphp artisan serveとかあったらしいけど何故か消えたらしい。まあ別にphpコマンドで十分だから必要ないのかもしれない。

アプリ名の設定

php artisan app:name MyBlog

app以下のnamespaceが変わるだけっぽい。 ただしこのままだとcomposerの設定が残ってて動かないので、php composer.phar updateを一発叩く必要がある。

その他の設定

環境によってはstorageに書き込み権限が必要。 面倒だったらHomesteadというVagrant boxを使うと初期設定を全部スキップできる。

ルーティング

app/Http/routes.phpが最初に読まれるファイルになる。 ここに処理を直接書くこともできる。マイクロフレームワークっぽい。

// app/Http/routes.php

Route::get('/hello', function()
{
    return 'Hello World';
});

これでhttp://localhost:8000/helloにアクセスするとHello Worldと表示される。

ルーティング(コントローラー)

まあ正直これだとだるいので、コントローラーにも書ける。どっちも選べるのは良い。 routes.phpを見た時点で気がついているだろうけど、ルーティングに文字列を入れるとコントローラー指定になる。

// app/Http/routes.php
Route::get('/', 'WelcomeController@index');

でもこれでも個別で指定するの面倒なので

// app/Http/routes.php
Route::controller('article', 'ArticleController');

とかでコントローラーまとめて指定できる。 この場合、URLとメソッドの対応をどのようにすればいいのかと言うと、[リクエストメソッドの小文字][URLパラメータの頭大文字]という指定でできる。つまり/article/indexは、public function getIndex()となる。このへんリフレクションでやってるっぽいので、ちょっと遅いかもしれない。Route::get()のような指定方法のほうが高速に見える。コントローラー使わないのが不便だけど最速っぽい。 ちなみにいわゆるsnake_caseCamelCase変換ではなく、url-patternFunction_nameという変換が入る。ちょっと変な感じ。function getShow_user()みたいな変なメソッド名になる。 Controllerの書き方は他のやつをコピペするか、この後のCRUD自動生成で生成でも良い。 laravel-annotationsを入れるとアノテーションでも書ける。

CRUD自動生成

昔懐かしのscaffoldみたいなやつ。

php artisan make:controller ArticleController

で、routes.php

// app/Http/routes.php
Route::resource('article', 'ArticleController');

とすると、indexとかstoreとかshowとかってメソッドにすぐアクセスできるようになる。 昔懐かしのscaffoldと違うのは、中身は用意してくれないので、自分で書く必要がある。まあ管理画面作るくらいなら便利なんじゃないのって感じ。余計なお世話はしてくれない。

ビュー(テンプレート)

ビューはresources/views/に入れる。なんでControllerと同階層じゃなくてトップレベルなの?という疑問が残るが、それがいいと思ったのだろう。 テンプレートエンジンはBladeと呼ばれるLaravel用エンジンが使われる。{{ $hoge }}で変数を出力して、条件式などは@から始まる書式を使う。拡張子.blade.php。末尾phpである必要あるのかな?

// app/Http/Controllers/ArticleController.php

class ArticleController extends Controller {
     public function getIndex()
     {
        return view('article.index', ['hoge’=><fuga>']);
     }

とか作って

<!— resources/views/article/index.blade.php —>
@extends('app')

@section('content')
article index {{ $hoge }}
@endsection

で表示ができる。 ちなみに昔は{{ }}はエスケープせずに表示で、エスケープする場合は{{{ }}}だったらしいが、5からはどちらもエスケープするようになり、エスケープしない場合は{!! !!}という書式を使う必要がある。

モデル

モデルはデフォルトでapp/直下に置くらしい。うーん。 とりあえずここではそのままapp/直下に置くが、autoloadされるディレクトリであればどこでもいいらしいので、必要であれば変えれば良い。 app/Article.phpを作る。

<?php namespace MyBlog;
// app/Article.php

use Illuminate\Database\Eloquent\Model;

class Article extends Model {
     protected $table = 'articles';
}

スキーマはここには書かず、database/migrations/に書く。このファイルはartisanコマンドで生成する。

$ php artisan make:migration --table=articles create_articles
Created Migration: 2015_02_05_120749_create_articles

中身を書く

// database/migrations/[日付とか]_create_articles.php

     /**
      * Run the migrations.
      *
      * @return void
      */
     public function up()
     {
          Schema::create('articles', function(Blueprint $table)
          {
            $table->increments('id');
            $table->string('title');
            $table->text('body');
            $table->dateTime('created_at');
            $table->timestamp('updated_at');
          });
     }

     /**
      * Reverse the migrations.
      *
      * @return void
      */
     public function down()
     {
          Schema::drop('articles');
     }

流す前にDBの設定をしておく。config/database.phpで設定する…と思いきや、ルートにある.envファイルを書き換えれば大抵は大丈夫なようになっている。.env.exampleをコピーして.envファイルを作り、DB_xxxを変更する。なおMySQLがデフォルト。 変更したらデータベースは事前に作っておいて、以下のコマンドを流す。

$ php artisan migrate:install
Migration table created successfully.

$ php artisan migrate:refresh
Nothing to rollback.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrated: 2015_02_05_120749_create_articles

こんな感じ。

とりあえずインタラクティブシェルで試す。

$ php artisan tinker
Psy Shell v0.3.3 (PHP 5.6.4 — cli) by Justin Hileman
>>> $articles = \MyBlog\Article::all();
=> <Illuminate\Database\Eloquent\Collection #00000000104d1a7300000001133ee921> {}
>>> $article = new \MyBlog\Article();
=> <MyBlog\Article #00000000104d1a7200000001133ee921> {
       incrementing: true,
       timestamps: true,
       exists: false,
       snakeAttributes: true,
       manyMethods: [
           "belongsToMany",
           "morphToMany",
           "morphedByMany"
       ]
   }
>>> $article->title = 'test';
=> "test"
>>> $article->body = 'test test';
=> "test test"
>>> $article->save();
=> true
>>> $articles = \MyBlog\Article::all();
=> <Illuminate\Database\Eloquent\Collection #00000000104d1a7000000001133ee921> {}
>>> $articles = \MyBlog\Article::all()[0];
=> <MyBlog\Article #00000000104d1a7800000001133ee921> {
       incrementing: true,
       timestamps: true,
       exists: true,
       snakeAttributes: true,
       manyMethods: [
           "belongsToMany",
           "morphToMany",
           "morphedByMany"
       ]
   }
>>> $articles = \MyBlog\Article::all()[0]->title;
=> "test"
>>> $articles = \MyBlog\Article::all()[0]->body;
=> "test test"
>>> \MyBlog\Article::all()[0]->delete();
=> true
>>>

画面に出してみる。

// app/Http/Controllers/ArticleController.php

     /**
      * Display a listing of the resource.
      *
      * @return Response
      */
     public function getIndex()
     {
        $articles = \MyBlog\Article::all();
        return view('article.index', ['articles'=>$articles]);
     }

     /**
      * Show the form for creating a new resource.
      *
      * @return Response
      */
     public function getCreate()
     {
        $article = new \MyBlog\Article();
        $article->title = 'test';
        $article->body = 'test test';
        $article->save();

        return redirect('article/index');
     }
<!— resources/views/article/index.blade.php —>
@extends('app')

@section('content')

@foreach ($articles as $article)
<section>
    <h3>{{ $article->title }}</h3>
    <p>{{ $article->body }}</p>
</section>
@endforeach

@endsection

これで/article/createを叩くと、延々とデータが増えるのを観察することができる。

GAE/GOでデプロイする(二段階認証編)

チュートリアル通りにやったらできねーよ!的なやつ

% ~/go_appengine/goapp deploy
09:38 AM Application: helloworld; version: 1
09:38 AM Host: appengine.google.com
09:38 AM
Starting update of app: helloworld, version: 1
09:38 AM Getting current resource limits.
Email: (メールアドレス)
Password for (メールアドレス):
Use an application-specific password instead of your regular account password.
See http://www.google.com/support/accounts/bin/answer.py?answer=185833
However, now the recommended way to log in is using OAuth2. See
https://developers.google.com/appengine/docs/go/tools/uploadinganapp#Go_Password-less_login_with_OAuth2

書いてある通りに対応する

% ~/go_appengine/appcfg.py --oauth2 update .
09:38 AM Application: helloworld; version: 1
09:38 AM Host: appengine.google.com
09:38 AM
Starting update of app: helloworld, version: 1
09:38 AM Getting current resource limits.
Your browser has been opened to visit:

    https://accounts.google.com/o/oauth2/auth(略)

If your browser is on a different machine then exit and re-run this
application with the command-line parameter

  --noauth_local_webserver

Authentication successful.
2014-10-15 09:38:51,495 ERROR appcfg.py:2416 An error occurred processing file '': HTTP Error 403: Forbidden Unexpected HTTP status 403. Aborting.
Error 403: --- begin server output ---
You do not have permission to modify this app (app_id=u'helloworld').
--- end server output —

よーしこれで動くかな

% ~/go_appengine/goapp deploy
09:38 AM Application: helloworld; version: 1
09:38 AM Host: appengine.google.com
09:38 AM
Starting update of app: helloworld, version: 1
09:38 AM Getting current resource limits.
Email:

ファッ
どうやら-oauthとか付ける必要があるらしい(どっかで見た)

% ~/go_appengine/goapp deploy -oauth
09:40 AM Application: helloworld; version: 1
09:40 AM Host: appengine.google.com
09:40 AM
Starting update of app: helloworld, version: 1
09:40 AM Getting current resource limits.
2014-10-15 09:40:41,340 ERROR appcfg.py:2416 An error occurred processing file '': HTTP Error 403: Forbidden Unexpected HTTP status 403. Aborting.
Error 403: --- begin server output ---
You do not have permission to modify this app (app_id=u'helloworld').
--- end server output ---
error while running appcfg.py: exit status 1

アプリ作ってねーじゃん!
ここから作って、app.yamlのapplication:を、作ったプロジェクトのProject IDにする。

% ~/go_appengine/goapp deploy -oauth
09:49 AM Application: (Project ID); version: 1
09:49 AM Host: appengine.google.com
09:49 AM
Starting update of app: (Project ID), version: 1
09:49 AM Getting current resource limits.
09:49 AM Scanning files on local disk.
09:49 AM Cloning 2 application files.
09:49 AM Uploading 1 files and blobs.
09:49 AM Uploaded 1 files and blobs
09:49 AM Compilation starting.
09:49 AM Compilation: 1 files left.
09:49 AM Compilation completed.
09:49 AM Starting deployment.
09:49 AM Checking if deployment succeeded.
09:49 AM Deployment successful.
09:49 AM Checking if updated app version is serving.
09:49 AM Completed update of app: (Project ID), version: 1

デプロイできた
http://[Project ID].appspot.com/ で表示できる。

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