@blog.justoneplanet.info

日々勉強

androidのOpenGL ESでテクスチャを貼る

■画像

以下のようにして画像をテクスチャとして貼ることができる。

public class MainActivity extends Activity {
    private GLSurfaceView gLSurfaceView;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        gLSurfaceView = new GLSurfaceView(this);
        gLSurfaceView.setRenderer(new Renderer());
        setContentView(gLSurfaceView);
    }
    
    @Override
    public void onPause()
    {
        super.onPause();
        gLSurfaceView.onPause();
    }
    
    @Override
    public void onResume()
    {
        super.onResume();
        gLSurfaceView.onResume();
    }
    
    private class Renderer implements GLSurfaceView.Renderer
    {
        @Override
        public void onDrawFrame(GL10 gl) {
            gl.glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
            
            float left   = -0.9f;
            float top    = 0.9f;
            float right  = 0.9f;
            float bottom = -0.9f;
            
            // テクスチャの描画設定=>次の描画オブジェクトに適用される
            {
                float[] uv = {
                        left, top,
                        left, bottom,
                        right, top,
                        right, bottom,
                };

                // Java => OpenGL にあたっての変換
                ByteBuffer bb = ByteBuffer.allocateDirect(uv.length * 4);
                bb.order(ByteOrder.nativeOrder());
                FloatBuffer fb = bb.asFloatBuffer();
                fb.put(uv);
                fb.position(0);
                
                gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
                gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, fb);// UV配列をOpen GLに紐付け
            }
            
            // ポリゴンの描画
            {
                top    = -top;
                bottom = -bottom;
                float[] vertexes = {
                        left,  top,    0.0f,
                        left,  bottom, 0.0f,
                        right, top,    0.0f,
                        right, bottom, 0.0f
                };
                
                // Java => OpenGL にあたっての変換
                ByteBuffer bb = ByteBuffer.allocateDirect(vertexes.length * 4);
                bb.order(ByteOrder.nativeOrder());
                FloatBuffer fb = bb.asFloatBuffer();
                fb.put(vertexes);
                fb.position(0);
                
                gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);// 頂点バッファの有効化
                gl.glVertexPointer(3, GL10.GL_FLOAT, 0, fb);// 頂点バッファをOpen GLに紐付け
                gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);// 描画する
            }
        }

        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            gl.glViewport(0, 0, width, height);// 描画領域を指定
            gl.glColor4f(1.0f, 0.9f, 0.9f, 1.0f);// 短形の描画色を指定
            gl.glEnable(GL10.GL_TEXTURE_2D);// テクスチャを有効にする
            
            // テクスチャ用メモリの確保
            int[] buffers = new int[1];// テクスチャ管理IDの配列
            gl.glGenTextures(1, buffers, 0);// 1枚分のメモリを確保
            gl.glBindTexture(GL10.GL_TEXTURE_2D, buffers[0]);// 1枚目を使う指定

            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.chrome);
            GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
            
            gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);// 縮小時のフィルタ
            gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);// 拡大時のフィルタ
            
            bitmap.recycle();// bitmapを破棄
        }

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        }
   }
}

■文字列

以下のようにcanvasを利用してして文字列を貼り付けることができる。

public class MainActivity extends Activity {
    private GLSurfaceView gLSurfaceView;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        gLSurfaceView = new GLSurfaceView(this);
        gLSurfaceView.setRenderer(new Renderer());
        setContentView(gLSurfaceView);
    }
    
    @Override
    public void onPause()
    {
        super.onPause();
        gLSurfaceView.onPause();
    }
    
    @Override
    public void onResume()
    {
        super.onResume();
        gLSurfaceView.onResume();
    }
    
    private class Renderer implements GLSurfaceView.Renderer
    {
        private int textureName;

        @Override
        public void onDrawFrame(GL10 gl) {
            gl.glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
            
            float left   = -0.9f;
            float top    = 0.9f;
            float right  = 0.9f;
            float bottom = -0.9f;
            
            // テクスチャの描画設定=>次の描画オブジェクトに適用される
            {
                float[] uv = {
                        left, top,
                        left, bottom,
                        right, top,
                        right, bottom,
                };

                ByteBuffer bb = ByteBuffer.allocateDirect(uv.length * 4);
                bb.order(ByteOrder.nativeOrder());
                FloatBuffer fb = bb.asFloatBuffer();
                fb.put(uv);
                fb.position(0);
                
                gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
                gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, fb);
            }
            
            // ポリゴンの描画
            {
                top    = -top;
                bottom = -bottom;
                float[] vertexes = {
                        left,  top,    0.0f,
                        left,  bottom, 0.0f,
                        right, top,    0.0f,
                        right, bottom, 0.0f
                };
                
                ByteBuffer bb = ByteBuffer.allocateDirect(vertexes.length * 4);
                bb.order(ByteOrder.nativeOrder());
                FloatBuffer fb = bb.asFloatBuffer();
                fb.put(vertexes);
                fb.position(0);
                
                gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
                gl.glVertexPointer(3, GL10.GL_FLOAT, 0, fb);
                gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
            }
        }

        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            gl.glViewport(0, 0, width, height);
            gl.glColor4f(1.0f, 0.9f, 0.9f, 1.0f);
            
            Bitmap bitmap = Bitmap.createBitmap(256, 256, Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            Paint paint = new Paint();
            paint.setColor(Color.WHITE);
            paint.setStyle(Style.FILL);
            canvas.drawColor(0);
            canvas.drawText("hogehogehogehogehogehogehogehogehogehoge", 0, 15, paint);
            
            gl.glEnable(GL10.GL_TEXTURE_2D);// テクスチャを有効にする
            int[] buffers = new int[1];
            gl.glGenTextures(1, buffers, 0);
            textureName = buffers[0];
            
            gl.glBindTexture(GL10.GL_TEXTURE_2D, textureName);
            GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
            
            gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
            gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
            
            bitmap.recycle();
        }

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        }
   }
}

■座標

右上が(x, y) = (1.0, 1.0)で、左下が(x, y) = (-1.0, -1.0)となっていると非常に扱いにくい場合もある。以下のようにすることでピクセルに変換できる。

@Override
public void onDrawFrame(GL10 gl) {
    gl.glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
    
    int x = 0;
    int y = 0;
    int width = 512;
    int height = 512;
    {
        float left   = ((float) x / (float) textureWidth);
        float top    = ((float) y / (float) textureHeight);
        float right  = left + ((float) width  / (float) textureWidth);
        float bottom = top  + ((float) height / (float) textureHeight);
        
        float[] uv = {
                left, top,
                left, bottom,
                right, top,
                right, bottom,
        };
        ByteBuffer bb = ByteBuffer.allocateDirect(uv.length * 4);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer fb = bb.asFloatBuffer();
        fb.put(uv);
        fb.position(0);
        
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
        gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, fb);
    }
    {
        float left   = ((float) x / (float) screenWidth)  * 2.0f - 1.0f;
        float top    = ((float) y / (float) screenHeight) * 2.0f - 1.0f;
        float right  = left + ((float) width  / (float) screenWidth)  * 2.0f;
        float bottom = top  + ((float) height / (float) screenHeight) * 2.0f;
        
        top    = -top;
        bottom = -bottom;
        
        float[] positions = {
                left,  top,    0.0f,
                left,  bottom, 0.0f,
                right, top,    0.0f,
                right, bottom, 0.0f
        };
        
        ByteBuffer bb = ByteBuffer.allocateDirect(positions.length * 4);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer fb = bb.asFloatBuffer();
        fb.put(positions);
        fb.position(0);
        
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, fb);
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
        
    }
    counter++;
}

■まとめ

JavaScriptでCanvasを操作してアニメーションする感じに似ている。もっと言うとPhotoshopで作業をする感じにも似ている。

androidで外部アプリがインストールされているか調べる

以下のようにして調べるだけでなく、インストールされていない場合はマーケットを起動させる。

public class Main extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        PackageManager pm = getPackageManager();
        try {
            pm.getApplicationInfo("info.justoneplanet.android.kaomoji", 0);
            Intent intent = new Intent("info.justoneplanet.android.kaomoji");
            startActivityForResult(intent, 1);
        }
        catch (NameNotFoundException e) {
            confirmMarket();
        }
        return true;
    }

    /**
     * confirmMarket
     * ダイアログを出してマーケットからアプリをダウンロードさせる
     */
    private void confirmMarket() {
        AlertDialog.Builder alertDialoBuilder = new AlertDialog.Builder(this);
        alertDialoBuilder.setTitle(R.string.client_not_installed);
        alertDialoBuilder.setMessage(R.string.confirm_goto_market);
        alertDialoBuilder.setPositiveButton(
                R.string.yes,
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Uri uri = Uri.parse("market://search?q=pname:info.justoneplanet.android.kaomoji");
                        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                        startActivity(intent);
                    }
                }
        );
        alertDialoBuilder.setNegativeButton(R.string.no, null);
        AlertDialog alertDialog = alertDialoBuilder.create();
        alertDialog.show();
    }
}

外部アプリを起動するだけで良い場合は以下のようにする。

try {
    Intent intent = new Intent(Intent.ACTION_MAIN);
    intent.setClassName(Constants.MESSENGERE_PACKAGE_NAME, Constants.MESSENGERE_ACTIVITY_NAME);
    startActivity(intent);
}
catch (ActivityNotFoundException e) {
    Uri uri = Uri.parse("market://search?q=pname:" + Constants.MESSENGERE_PACKAGE_NAME);
    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
    startActivity(intent);
}

C言語のプリプロセッサ

プリプロセッサによりある種の言語仕様が与えられる。

■ファイルのインクルード

以下のようにすると、自身のファイルの場所から探索が始まる。

#include "filename"

以下のようにすると、コンパイラの規則に従った探索が行われる。stdio.hなどは以下の方法でインクルードする。

#include <filename>

■マクロ

#define NAME VALUE

NAMEとVALUEは任意であり以下のようにすることもできる。

#define forever for(;;)

以下のようにすることで使用できる。

int main (int argc, const char * argv[])
{
    forever
    printf("hogehoge\n");
}

永久にhogehogeが表示される。また、以下のように関数を定義することもできる

#define max(a, b) (a > b)? (a) : (b)

int main (int argc, const char * argv[])
{
    int d = max(1, 2);
    printf("こんにちは!%d円よこしてください\n", d);
}

内部では以下のようにインライン展開されている。

x = ((p + q) > (r + s))? (p + q) : (r + s);

以下のような場合は注意する必要がある。

#define sq(x) x * x

int main (int argc, const char * argv[])
{
    int z = 3;
    sq(z + 1);// 7 = z + 1 * z + 1 = 3 + 1 * 3 + 1
    sq((z + 1));// 16
}

以下のようにすることで正しい結果が期待できる。

#define sq(x) (x) * (x)

■未定義

以下のようにして未定義にすることができる。

#undef hello

void hello() {}

■条件付き取り込み

以下のようにすることで条件に応じた処理をすることができる。

#if !defined(INCLUDED_MAIN)
define INCLUDED_MAIN

int hoge();

#endif

インクルードガードとかでも使う。

ポインタを使ってみる

以下のように書いてみる。

int x = 1;
int *ip;
ip = &x;
printf("%p", ip);// 0xbfffef28

ipにはメモリのアドレスが入ってることが分かる。アドレスが指している値を読むには以下のようにする。

int x = 1;
int *ip;
ip = &x;
printf("%d", *ip);// 1

*ipを書き換えたらxの値はどうなるのか?

int x = 1;
int *ip;
ip = &x;
printf("%d", *ip);// 1
(*ip)++;
printf("%d", x);// 2

まぁ、当然書き換わる。

アドレスの操作

*が付いている場合はアドレスの操作になる。

int x = 1;
int *a = &x;

値の操作

以下のようにすると値を操作できる。

int x = 1;
int *a = &x;
a = 2;

■ヌルポインタ

アドレスを代入していないことを示すためにヌルポインタが存在する。

int main(int argc, char *argv[])
{
    int *a = NULL;
    int x = 2;
    a = &x;
    printf("%d", *a);// 2
}

以下のようにも記述できる。

int main(int argc, char *argv[])
{
    int *a = NULL;
    int x = 2;
    a = &x;
    printf("%d", *a);// 2
}

当然だけど全く関係ないアドレスにアクセスすればバグになる。

■関数の引数としてのポインタ

引数として与えられた変数の値を入れ替えるには以下のようにする。

swap(&x, &y);

cの関数において引数は全て値渡しとなるためアドレスを渡す必要があり、以下のようにすることで入れ替えすることができる。

void swap(int *x, int *y)
{
    int temp = *x;
    *x = *y;
    *y = temp;
}

■ポインタと配列

以下のように記述してみる。

int x[] = {1, 2, 3, 4, 5};
int *px;
px = &x[0];
printf("%d", *px);// 1
printf("%d", *(px + 1));// 2
printf("%d", *(px + 2));// 3
printf("%d", *(px + 3));// 4
printf("%d", *(px + 4));// 5

pxは配列xの0番目を参照し、+1ごとに配列の右を参照する。低レイヤーな言語を触るとメモリが見えるのが面白いよね。

ちなみに(やってはいけないが)-1すると以下のようになる。

printf("%p", (px - 1));//0xbfffef14
printf("%d", *(px - 1));//-1073746152

■文字ポインタと関数

以下のような文字定数は文字の配列でもある。

"This is a pen."

記憶領域においては末端文字\0が配列の最期にあるため1文字大きい。

char *str = "This is a pen.";

Cでは文字列全体を一つの単位として処理するような演算子は提供されていない。

ちなみ以下の記述において前者は文字列を変更することができるがアドレスが変わることはない。後者はアドレスを変える事ができるが文字列を変えることはできない。

char strA[] = "This is a pen.";
char *strB = "This is a pen.";

ちなみに以下のようにして上述の文字列を出力することができる。

char strA[] = "This is a pen.";
for(int i = 0; i < sizeof(strA) / sizeof(strA[0]); i++){
    printf("%c", strA[i]);
}
char *strB = "This is a pen.";
while(*strB != '\0'){
    printf("%c", *strB);
    strB++;
}

strcpy

配列
void strcpy(char *s, char *t)
{
    int i = 0;
    while((s[i] = t[i]) != '\0'){
        i++;
    }
}
ポインタ
void strcpy(char *s, char *t)
{
    while((*s = *t) != '\0'){
        s++;
        t++;
    }
}

ふむふむ。なるほど。

strcmp

配列
int strcmp(char *s, char *t)
{
    for(int i = 0; s[i] == t[i]; i++){
        if(s[i] == '\0'){
            return 0;
        }
    }
    return s[i] - t[i];
}
ポインタ
int strcmp(char *s, char *t)
{
    for(; *s == *t; s++, t++){
        if(*s == '\0'){
            return 0;
        }
    }
    return *s - *t;
}

ふむふむ。なるほど。

構造体を使ってみる

構造体は操作しやすくするために一つの名前でまとめられた事なった型の変数の集まりである。

■定義

struct point {
    int x;
    int y;
};

■宣言

以下のようにして使用する。

int main(int argc, char *argv[])
{
    struct point a;
    a.x = 10;
}

以下のように値を設定することもできる。

int main(int argc, char *argv[])
{
    struct point a = {10, 20};
}

同時に定義と宣言をする

以下のようにして定義と宣言を同時に行うことができる。

int main(int argc, char *argv[])
{
    struct {
        int x;
        int y;
    }a, b, c;
    
    a.x = 10;
    a.y = 10;
    b.x = 20;
    b.y = 20;
    c.x = 30;
    c.y = 30;
}

関数の戻り値としての構造体

以下のような構造体を定義する。

struct point {
    int x;
    int y;
};

関数の戻り値として上述の構造体を返すには以下のようにする。

struct point getPoint(int x, int y)
{
    struct point p = {x, y};
    return p;
}

以下のようにして関数getPointを使用する。

int main(int argc, char *argv[])
{
    getPoint(10, 20);
}

structは型のようなもんだし、クラスのような感じでさえある。

■応用

少し応用して以下のようにしてみる。

struct point {
    int x;
    int y;
};
struct triangle {
    struct point p1;
    struct point p2;
    struct point p3;
};

下のように各頂点を定義する。

int main(int argc, char *argv[])
{
    struct triangle a;
    a.p1.x = 5;
    a.p1.y = 1;
    a.p2.x = 7;
    a.p2.y = 6;
    a.p3.x = 9;
    a.p3.y = 1;
}

■ポインタ

以下のように変数を宣言することでstruct point型へのポインタを表現できる。

struct point **pp;

以下のようにしてみる。

struct point a, *pp;
a.x = 2;
a.y = 4;
pp = &a;
printf("%d, %d", (*pp).x, (*pp).y);// 2, 4

これはよく用いられる一方で演算子の優先度のためにカッコで括る必要がある。手間なので同義の以下の表現が使用できる。

printf("%d, %d", pp->x, pp->y);

CakePHPでモデルを作る

前回に引き続き今更ではあるがメモっておく。

■モデルのファイル生成

bakeする

bakeの前に解説用に以下のテーブルを作る。

CREATE TABLE `users` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `email` varchar(255) NOT NULL,
  `created` datetime NOT NULL,
  `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `is_public` tinyint(1) NOT NULL DEFAULT '1',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

以下のコマンドを実行する。

./cake/console/cake bake

以下のように表示される。

Welcome to CakePHP v1.3.10 Console
---------------------------------------------------------------
App : app
Path: /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app
---------------------------------------------------------------
Interactive Bake Shell
---------------------------------------------------------------
[D]atabase Configuration
[M]odel
[V]iew
[C]ontroller
[P]roject
[F]ixture
[T]est case
[Q]uit
What would you like to Bake? (D/M/V/C/P/F/T/Q) 
> M

Mを選択すると以下の選択肢が表示されるのでdefaultを選択する。

---------------------------------------------------------------
Bake Model
Path: /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/models/
---------------------------------------------------------------
Use Database Config: (default/production) 
[default] > default

この時点でDBにテーブルが存在していないと以下のように表示される。

Your database does not have any tables.

当然だけどデータストアの定義がされていないのにコードを書き始めてはいけない。

テーブルが存在している場合は以下のように表示される。

Possible Models based on your current database:
1. User
Enter a number from the list above,
type in the name of another model, or 'q' to exit 

1番を選択すると以下のように質問される。

Would you like to supply validation criteria 
for the fields in your model? (y/n) 

yを選択すると以下のように表示される。

Field: id
Type: integer
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
1 - alphanumeric
2 - between
3 - blank
4 - boolean
5 - cc
6 - comparison
7 - custom
8 - date
9 - decimal
10 - email
11 - equalto
12 - extension
13 - inlist
14 - ip
15 - maxlength
16 - minlength
17 - money
18 - multiple
19 - notempty
20 - numeric
21 - phone
22 - postal
23 - range
24 - ssn
25 - time
26 - url
27 - userdefined
28 - uuid
29 - Do not do any validation on this field.

どんな入力値チェックをするかという事ですな。ここで設定した値を元にソースを書きだしてくれる。但し、idはautoincrementで特にinsertするカラムではないので29を選択する。

Field: name
Type: string
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
1 - alphanumeric
2 - between
3 - blank
4 - boolean
5 - cc
6 - comparison
7 - custom
8 - date
9 - decimal
10 - email
11 - equalto
12 - extension
13 - inlist
14 - ip
15 - maxlength
16 - minlength
17 - money
18 - multiple
19 - notempty
20 - numeric
21 - phone
22 - postal
23 - range
24 - ssn
25 - time
26 - url
27 - userdefined
28 - uuid
29 - Do not do any validation on this field.
... or enter in a valid regex validation string.

次に表示されるのはnameカラムに対するバリデーションだ。必須項目としたいので19を入力すると以下のように表示される。

Would you like to add another validation rule? (y/n) 

1つのカラムに複数のバリデーションを適用できるので必要な場合は番号を入力する。次はパスワードだ。

Field: password
Type: string
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
1 - alphanumeric
2 - between
3 - blank
4 - boolean
5 - cc
6 - comparison
7 - custom
8 - date
9 - decimal
10 - email
11 - equalto
12 - extension
13 - inlist
14 - ip
15 - maxlength
16 - minlength
17 - money
18 - multiple
19 - notempty
20 - numeric
21 - phone
22 - postal
23 - range
24 - ssn
25 - time
26 - url
27 - userdefined
28 - uuid
29 - Do not do any validation on this field.
... or enter in a valid regex validation string.
  
[19] > 

1と19あたりを設定すると思う。次はemailだ。

Field: email
Type: string
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
1 - alphanumeric
2 - between
3 - blank
4 - boolean
5 - cc
6 - comparison
7 - custom
8 - date
9 - decimal
10 - email
11 - equalto
12 - extension
13 - inlist
14 - ip
15 - maxlength
16 - minlength
17 - money
18 - multiple
19 - notempty
20 - numeric
21 - phone
22 - postal
23 - range
24 - ssn
25 - time
26 - url
27 - userdefined
28 - uuid
29 - Do not do any validation on this field.
... or enter in a valid regex validation string.
  
[10] > 

なんとカラム名から判断して10番を提示してくれるではないか。Cakeは気がきくのだがこれは時としてお節介にもなる。そんなこんなでバリデーションを設定していくと次に以下のような質問をされる。

Would you like to define model associations
(hasMany, hasOne, belongsTo, etc.)? (y/n) 

関連するテーブルを聞かれているのだが、まだ他のテーブル存在していないのでnを入力する。

---------------------------------------------------------------
The following Model will be created:
---------------------------------------------------------------
Name:       User
DB Table:   `users`
Validation: Array
(
    [name] => Array
        (
            [notempty] => notempty
        )

    [password] => Array
        (
            [notempty] => notempty
        )

    [email] => Array
        (
            [email] => email
        )

    [is_public] => Array
        (
            [boolean] => boolean
        )

)

-------------------------

上述のように生成されるファイルの確認をされるのでyを押す。

ファイルが存在している場合は上書きの確認メッセージが表示される。

Creating file /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/models/user.php
File `/Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/models/user.php` exists, overwrite? (y/n/q) 
[n] >

次に単体テスト用のファイルをbakeするか聞かれる。

SimpleTest is not installed. Do you want to bake unit test files anyway? (y/n)

やっぱちゃんとたりたいのでyを選択する。ちなみにSimpleTestのインストールは後からでも全然構わない。ファイルは「./app/tests/cases/models/user.test.php」に自動的に生成されている。

■生成したファイル

./app/models/user.php

<?php
class User extends AppModel {
    var $name = 'User';
    var $displayField = 'name';
    var $validate = array(
        'name' => array(
            'notempty' => array(
                'rule' => array('notempty'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'password' => array(
            'notempty' => array(
                'rule' => array('notempty'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'email' => array(
            'email' => array(
                'rule' => array('email'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'is_public' => array(
            'boolean' => array(
                'rule' => array('boolean'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
    );
}

コメントアウトされている部分については一番上を参考にする。

./app/tests/cases/models/user.test.php

/* User Test cases generated on: 2011-07-20 02:35:04 : 1311096904*/
App::import('Model', 'User');

class UserTestCase extends CakeTestCase {
	var $fixtures = array('app.user');

	function startTest() {
		$this->User =& ClassRegistry::init('User');
	}

	function endTest() {
		unset($this->User);
		ClassRegistry::flush();
	}

}

しかし、PHP5の文法で書きだして欲しいかつインデントはスペースを使って欲しいものだ。

■SimpleTest

debugモードで動作しているサイトのtest.phpにアクセスするとSimpleTestがダウンロードされていない場合、リンクが表示されるのでそこからダウンロードする。。。のもいいんだが分かりにくいのでコマンドラインから操作する。

cd vendors
wget http://sourceforge.net/projects/simpletest/files/simpletest/simpletest_1.0.1/simpletest_1.0.1.tar.gz/download
tar xvzf simpletest_1.0.1.tar.gz
rm simpletest_1.0.1.tar.gz

上述の操作でSimpleTestが使用できるようになった。テストケースについてはまた別の機会に解説する。

eclipseプラグイン

eclipseプラグインもあるのだがデバッグの構成とかいまいち設定方法が分からん。。。

■メソッドの記述

モデルクラスができたので試しにidからユーザを取得するメソッドgetByIdを書いてみる。

class User extends AppModel {
    public $name = 'User';
    public $displayField = 'name';
    public $validate = array(
        'name' => array(
            'notempty' => array(
                'rule' => array('notempty'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'password' => array(
            'notempty' => array(
                'rule' => array('notempty'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'email' => array(
            'email' => array(
                'rule' => array('email'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
        'is_public' => array(
            'boolean' => array(
                'rule' => array('boolean'),
                //'message' => 'Your custom message here',
                //'allowEmpty' => false,
                //'required' => false,
                //'last' => false, // Stop validation after this rule
                //'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
        ),
    );
    
    /**
     * getById
     * 
     * @param numeric $id
     */
    public function getById($id)
    {
        $id = (int) $id;
        $result = $this->find(
            'first',
            array(
                'conditions' => array(
                    'id' => $id
                )
            )
        );
        return $result;
    }
}

SQLは見当たらない。生のSQLを書かないのが流儀である。まぁ、サブクエリとか書くときは普通に生で書いちゃうんだけど・・・少なくとも通常のCRUDはORMを使うべきだと思う。

理由

Symfonyの記事を参考に読んだ。以下は抜粋である。

データベースはリレーショナルです。一方でPHP 5とsymfonyはオブジェクト指向です。オブジェクト指向のコンテキストでもっとも効果的にデータベースにアクセスするには、オブジェクトをリレーショナルなロジックに変換するインターフェイスが求められます。

リレーショナルから取り出すものは単なるデータの集合に過ぎないが、オブジェクトを取り出せるならコードとの相性もいいよね。

$data = "{'name' : 'pochi' , 'age' : 28}";
$data = new Dog('pochi', 28);
$data->cry();// こんな感じ

抽象化レイヤーの主な利点は、移植性です。これによって、プロジェクトの真っ最中でも、別のデータベースに切り替えることができます。

そう!DBに依存した部分の変換(泥臭い仕事)はフレームワークがこなす。しかし、プロジェクトの途中でDBが変わるなんてあるのか・・・?

詳しくはドキュメントを参照するのが良いんだが、まぁとりあえず書いてみる。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByAge
     * 年齢でユーザを検索する
     * @param numeric $age
     */
    public function getByAge($age)
    {
        $age = (int) $age;
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'age' => $age
                )
            )
        );
        return $result;
    }
}

前述と違うのは結果が複数あるのでallを使用した。条件が複数ある場合は以下のようにする。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByAgeAndGender
     * 年齢と性別でユーザを検索する
     * @param numeric $age
     * @param numeric $gender
     */
    public function getByAgeAndGender($age , $gender)
    {
        $age    = (int) $age;
        $gender = (int) $gender;
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'age'    => $age,
                    'gender' => $gender,
                )
            )
        );
        return $result;
    }
}

上述のように配列の要素を増やすだけで良い。

範囲

日付や数値などで等号ではなく大なり小なりを指定して特定の範囲に含まれるレコードが欲しい場合がある。以下のようにすると実現できる。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByAgeRangeAndGender
     * 年齢の範囲と性別でユーザを検索する
     * @param numeric $minAge
     * @param numeric $maxAge
     * @param numeric $gender
     */
    public function getByAgeRangeAndGender($minAge, $maxAge , $gender)
    {
        $minAge = (int) $minAge;
        $maxAge = (int) $maxAge;
        $gender = (int) $gender;
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'gender' => $gender,
                    'age <' => $maxAge,
                    'age >' => $minAge,
                )
            )
        );
        return $result;
    }
}

若干気持ち悪いが連想配列なのでまぁしょうがない。

OR

conditionsに追加すると全てANDで結合した条件となる。ORにする場合は以下のように記述する。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByAgeRangeAndGender
     * 年齢の範囲と性別でユーザを検索する
     * @param numeric $minAge
     * @param numeric $maxAge
     * @param numeric $gender
     */
    public function getByAgeAndGender($minAge, $maxAge , $gender)
    {
        $minAge = (int) $minAge;
        $maxAge = (int) $maxAge;
        $gender = (int) $gender;
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'gender' => $gender,
                    'or' => array(
                        'age >' => $maxAge,
                        'age <' => $minAge,
                    )
                )
            )
        );
        return $result;
    }
}
NOT

NOTにする場合は以下のように記述する。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getTarget
     * 血液型が入力されているユーザをサービスのターゲットとして検索する
     * @param string $type
     */
    public function getTarget($type)
    {
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'not' => array(
                        'blood_type' => null
                    )
                )
            )
        );
        return $result;
    }
}
順序

以下のようにすることでORDER BYと同じになる。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByAgeAndGender
     * 年齢と性別でユーザを検索する
     * @param numeric $age
     * @param numeric $gender
     */
    public function getByAgeAndGender($age , $gender)
    {
        $age    = (int) $age;
        $gender = (int) $gender;
        $result = $this->find(
            'all',
            array(
                'conditions' => array(
                    'age'    => $age,
                    'gender' => $gender,
                ),
                'order' => array(
                    'age desc',
                    'gender asc',
                )
            )
        );
        return $result;
    }
}
結合

例えば職業テーブルが存在しユーザが職業IDを持つ場合に結合する必要もあるはずだ。以下のようにして結合する。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * getByOccupation
     * @param string $occupation
     */
    public function getByOccupation($occupation)
    {
        $alias = 'Occupation';
        $result = $this->find(
            'all',
            array(
                'fields' => array(
                    "`{$this->name}`.`id`",
                    "`{$this->name}`.`name`",
                    "`{$alias}`.`name`",
                ),
                'conditions' => array(
                    "`{$this->name}`.`gender`" => $gender,
                ),
                'joins' => array(
                    array(
                        'type'       => 'LEFT',
                        'table'      => 'occupations',
                        'alias'      => $alias,
                        'conditions' => "`{$this->name}`.`occupations_id` = `{$alias}`.`id`",
                    )
                ),
            )
        );
        return $result;
    }
}

fieldsを使用しない場合は結合したテーブルのカラムが取り出せない。結合しないクエリにおいて、fieldsを使用しない場合に発行されるクエリは以下のとおりとなる。

SELECT * FROM `user` WHERE `id` = ?;

fieldsを使用しないとアスタリスクとなり全てのカラムがfetchされるので注意が必要だ。

■保存

CakePHPでは明確にInsertとUpdateが分離されていない。プライマリキーがセットされている場合はUpdateの扱いとなる。

Insert

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * register
     * 登録してIDを返す
     * @param string $name
     * @param numeric $age
     */
    public function register($name, $age)
    {
        $age    = (int) $age;
        $result = $this->save(
            array(
                'name' => $name,
                'age'  => $age,
            )
        );
        return ($result)? $this->getLastInsertID() : $result;
    }
}

Update

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * register
     * 登録してIDを返す
     * @param string $name
     * @param numeric $age
     * @param numeric $id
     */
    public function register($name, $age, $id)
    {
        $id    = (int) $id;
        $result = $this->save(
            array(
                'id'   => $id,
                'name' => $name,
                'age'  => $age,
            )
        );
        return $result;
    }
}

■削除

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * del
     * @param numeric $id
     */
    public function del($id)
    {
        $this->delete((int) $id);
    }
}

第二引数でcascadeも指定できるけど、MySQLで直接定義してるよね・・・。以下のように条件で削除することもできる。

class User extends AppModel {
    public $name = 'User';
    // validationは省略
        
    /**
     * del
     * @param string $name
     */
    public function del($name)
    {
        $this->deleteAll(array(
            'name' => $name
        ));
    }
}

大体モデルはコンナ感じでいいかな。

Gaucheをインストールする

yum install gcc make
wget http://prdownloads.sourceforge.net/gauche/Gauche-0.9.1.tgz
tar xvzf Gauche-0.9.1.tgz
cd Gauche-0.9.1
./configure
make
make install

CakePHPを使う準備をする

今更な感があるがメモとして残しておく。さらに面倒なのでローカルのXAMPPで説明する。

■ダウンロード

以下のようにダウンロードして解凍する。

mkdir cake.sample.justoneplanet.info
cd cake.sample.justoneplanet.info/
wget https://github.com/cakephp/cakephp/tarball/1.3.10
tar xvzf cakephp-cakephp-1.3.10-0-g8671aa3.tar.gz
rm cakephp-cakephp-1.3.10-0-g8671aa3.tar.gz
rm -fr cakephp-cakephp-b0abad1/

以下のような状態になっている。

  • cake.sample.justoneplanet.info/app
  • cake.sample.justoneplanet.info/cake
  • cake.sample.justoneplanet.info/plugins
  • cake.sample.justoneplanet.info/vendors
  • cake.sample.justoneplanet.info/index.php
  • cake.sample.justoneplanet.info/README

■Apacheの設定

まぁ、バーチャルホストの設定を変更する。

vi /Applications/XAMPP/etc/extra/httpd-vhosts.conf
<VirtualHost *:80>
    DocumentRoot "/Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/webroot"
    ServerName cake.sample.justoneplanet.info
    ServerAlias www.cake.sample.justoneplanet.info
#    SetEnv CAKE_ENV default
</VirtualHost>

hostsファイルを変更しApacheを再起動する。

■パーミッションの設定

起動すると大量のエラー文が表示されるので、以下のコマンドを実行してパーミッションを変更する。

chmod -R 0777 app/tmp/

また、以下のコマンドを実行して「Security.salt」と「Security.cipherSeed」を変更する。

vi app/config/core.php

■データベースの設定

ファイルを直接編集しても良いんだけど、せっかくなのでbakeを使う。

./cake/console/cake bake

以下のように表示されるので順に設定する。

Welcome to CakePHP v1.3.10 Console
---------------------------------------------------------------
App : app
Path: /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app
---------------------------------------------------------------
Your database configuration was not found. Take a moment to create one.
---------------------------------------------------------------
Database Configuration:
---------------------------------------------------------------
Name:
[default] > default
Driver: (db2/firebird/mssql/mysql/mysqli/odbc/oracle/postgres/sqlite/sybase) 
[mysql] > mysql
Persistent Connection? (y/n) 
[n] > n
Database Host:
[localhost] > localhost
Port?
[n] > n
User:
[root] > hogehoge
Password:
> **********
Database Name:
[cake] > sample
Table Prefix?
[n] > n
Table encoding?
[n] > utf8

設定が終わると確認メッセージが表示され他のデータベース設定をするか聞かれる。

Look okay? (y/n) 
[y] > y
Do you wish to add another database configuration?  
[n] > y

今回は以下のようにしてもう一つ設定する。

Welcome to CakePHP v1.3.10 Console
---------------------------------------------------------------
App : app
Path: /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app
---------------------------------------------------------------
Your database configuration was not found. Take a moment to create one.
---------------------------------------------------------------
Database Configuration:
---------------------------------------------------------------
Name:
[default] > production
Driver: (db2/firebird/mssql/mysql/mysqli/odbc/oracle/postgres/sqlite/sybase) 
[mysql] > mysql
Persistent Connection? (y/n) 
[n] > n
Database Host:
[localhost] > localhost
Port?
[n] > n
User:
[root] > hogehoge
Password:
> **********
Database Name:
[cake] > sample
Table Prefix?
[n] > n
Table encoding?
[n] > utf8

開発用と本番用で設定を用意した。開発上は関係ないと思われるが、以前に使用していたdevelopmentという名前だと配置した段階でdefaultという設定名が必要になるようなので、開発環境はdefaultという名前を用いることにした。

app/app_model.php

以下のように記述して本番環境用の設定も読み込めるようにする。

class AppModel extends Model {
    /**
     * __construct
     * @param mixed $id
     * @param mixed $table
     * @param mixed $ds
     */
    public function __construct($id = false, $table = null, $ds = null)
    {
        $this->useDbConfig = Configure::read('Config.environment');
        parent::__construct($id, $table, $ds);
    }
}

■開発環境と本番環境の設定の分離

以下のコマンドを実行する。

vi app/config/bootstrap.php
mkdir app/config/environment

以下の記述を最終行に付加する。

Configure::write('Config.environment', isset($_SERVER['CAKE_ENV']) ? $_SERVER['CAKE_ENV'] : "default");
require_once("environment/" . basename(Configure::read('Config.environment') . ".php"));

新しくできたディレクトリに以下の2ファイルを追加する。

default.php

開発用設定ファイル。

Configure::write('debug', 2);
Configure::write('Cache.disable', true);

エラーメッセージを表示するのと、キャッシュを無効にしておく。

production.php

本番用設定ファイル。

Configure::write('debug', 0);
Configure::write('Cache.disable', false);

エラーメッセージは見せないようにする。

■おまけ

PHP5.3以上を使っていると思うので、core.php以下の部分をコメントアウトし引数を変更する。

date_default_timezone_set('Asia/Tokyo');

macにLAMP環境を構築する

自分用のメモ。

■Apache

vim /private/etc/apache2/httpd.conf

以下の部分を変更して

#LoadModule php5_module libexec/apache2/libphp5.so

以下のようにする。

LoadModule php5_module libexec/apache2/libphp5.so

以下のコマンドを実行し、vhosts用のファイルを作る。

cp /private/etc/apache2/extra/httpd-vhosts.conf /private/etc/apache2/other/httpd-vhosts.conf
vim /private/etc/apache2/other/httpd-vhosts.conf

ログファイルの場所などを間違えるとエラーが出て上手くいかないので注意する。

エラー

以下のコマンドでログを確認すれば大体原因が分かる。

vim /var/log/apache2/error_log

■PHP

以下のコマンドでpearをインストールする。

sudo php /usr/lib/php/install-pear-nozlib.phar

■MySQL

以下のコマンドでインストールする。

brew install mysql
unset TMPDIR
mysql_install_db --verbose --user=`whoami` --basedir="$(brew --prefix mysql)" --datadir=/usr/local/var/mysql --tmpdir=/tmp
mysql.server start
/usr/local/Cellar/mysql/5.5.20/bin/mysql_secure_installation

Homebrewのインストール

/usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/gist/323731)"

my.cnf

ls $(brew --prefix mysql)/support-files/my-*
sudo cp $(brew --prefix mysql)/support-files/my-small.cnf /etc/my.cnf
vim /etc/my.cnf

参考