チュートリアル

MVCフレームワーク

このフレームワークは処理をMVC(Model-View-Controller)に分割して管理します。

Model(モデル)

モデルはデータを扱う部品です。データの検索、変換、検証、登録などを行います。

View(ビュー)

ビューはデータの表示を担当します。主にHTMLのテンプレートを扱います。

Controller(コントローラ)

コントローラはユーザからのリクエストを扱います。モデルとビューの助けを借りてユーザに結果を返します。

ディレクトリ構成

public_html / config.php ... 設定ファイル
     |        index.php ... 起動ファイル
     |
     +-- app / ... アプリケーション格納ディレクトリ
     |    |
     |    +-- controllers / ... コントローラ格納ディレクトリ
     |    |
     |    +-- models / ... モデル格納ディレクトリ
     |    |
     |    +-- views / ... ビュー格納ディレクトリ
     |
     +-- libs /
          |
          +-- cores / ... コアライブラリ格納ディレクトリ
          |
          +-- plugins / ... プラグイン格納ディレクトリ

app/ 内に作成したいアプリケーションのファイルを格納します。モデルは app/models/ 内に、ビューは app/views/ 内に、コントローラは app/controllers/ 内にそれぞれ格納します。

libs/cores/ 内にフレームワークの動作に必須の命令(コアライブラリ)が格納されています。この内容は編集する必要はありません。

libs/plugins/ 内にフレームワーク内部からは直接呼び出されていない命令(プラグイン)が格納されています。自分で作成した共通関数のファイルもここに格納できます。

処理の流れ

大まかな処理の流れは以下のとおりです。(具体例はブログチュートリアルを参考にしてください。)

  • 例えば http://www.example.com/index.php/test1/test2 へリクエストが送られたとします。
  • 自動的にコントローラ app/controllers/test1/test2.php が読み込まれます。
  • 必要に応じて、コントローラ内でモデルを呼び出します。
  • 必要に応じて、コントローラ内で連想配列 $_view に値を割り当てます。
  • 自動的にビュー app/views/test1/test2.php が読み込まれます。
  • 必要に応じて、ビュー内で連想配列 $_view の内容を表示します。

URLリライティング

フレームワークを呼び出すURLは通常 http://www.example.com/index.php/test1/test2 のような形式ですが、mod_rewrite を使用すれば http://www.example.com/test1/test2 のような形式にすることができます。

mod_rewrite を使用する場合、index.php と同じディレクトリ内に .htaccess ファイルを作成し、以下の内容を記載します。

DirectoryIndex index.php

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule (.*) index.php/$1
</IfModule>

さらに、config.php を修正します。

define('MAIN_FILE', $_SERVER['SCRIPT_NAME']);

この部分に、設置ディレクトリのパスを指定します。例えば http://www.example.com/test/sample/ に設置する場合、以下のように修正します。(最後の / は不要です。)

define('MAIN_FILE', '/test/sample');

mod_rewrite を使用する場合で公開ディレクトリ直下に設置する場合、以下のように修正します。

define('MAIN_FILE', '');

規約

コントローラの規約

コントローラは app/controllers/ 内に格納します。

URLで指定された2つの値に従ってコントローラが読み込まれます。(3つ目以降の値を指定しても、コントローラの呼び出しに影響しません。)値が省略されると homeindex が省略されたものとみなされます。

  • http://www.example.com/index.php/test1/test2 へリクエストを送ると、app/controllers/test1/test2.php が読み込まれます。
  • http://www.example.com/index.php/test1 へリクエストを送ると http://www.example.com/index.php/test1/index へのリクエストとみなされ、app/controllers/test1/index.php が読み込まれます。
  • http://www.example.com/ へリクエストを送ると http://www.example.com/index.php/home/index へのリクエストとみなされ、app/controllers/home/index.php が読み込まれます。

app/controllers/before.php があるとコントローラの実行前に、app/controllers/after.php があるとコントローラの実行後にそれぞれ自動で読み込まれます。ここに、すべてのコントローラで共通して実行したい処理を書いておくことができます。

また、/test1/test1/test2 へのリクエストの場合、app/controllers/before_test1.php があると before.php の実行直後に、app/controllers/after_test1.php があると after.php の実行直前にそれぞれ自動で読み込まれます。

コントローラ内で連想配列 $_view に値を格納すると、ビューから参照することができます。

モデルの規約

モデルは app/models/ 内に格納します。

データベースのテーブル名とモデルのファイル名は一致させ、原則として1テーブルにつき1モデルを作成します。テーブル名(モデル名)は複数形を推奨します。

例えば members テーブルのモデルは app/models/members.php となります。ファイルを作成することにより、select_members()insert_members()update_members()delete_members() などの命令が使えるようになります。これらはモデル内に以下のような関数を定義することにより、各機能を上書きすることができます。

// データを取得する命令
function select_members($queries)
{
    $queries = db_placeholder($queries);
    $queries['from'] = DATABASE_PREFIX . 'members';
    $results = db_select($queries);
    return $results;
}

// データを登録する命令
function insert_members($queries)
{
    $queries = db_placeholder($queries);
    $queries['insert_into'] = DATABASE_PREFIX . 'members';
    $resource = db_insert($queries);
    return $resource;
}

// データを編集する命令
function update_members($queries)
{
    $queries = db_placeholder($queries);
    $queries['update'] = DATABASE_PREFIX . 'members';
    $resource = db_update($queries);
    return $resource;
}

// データを削除する命令
function delete_members($queries)
{
    $queries = db_placeholder($queries);
    $queries['delete_from'] = DATABASE_PREFIX . 'members';
    $resource = db_delete($queries);
    return $resource;
}

// データを整理する命令
function normalize_members($queries)
{
    return $queries;
}

// データを検証する命令
function validate_members($queries)
{
    return array();
}

テーブルに対応する命令(テーブルのデータの初期値を定義する命令など)を追加する場合、モデル内に追加します。その際、命令の名前にはモデル名を含めることを推奨します。具体的には default_members()select_members_by_id() のような名前にします。

単体テストなどに支障をきたすため、モデル内でトランザクションは呼び出さず、コントローラ側で制御することを推奨します。

ビューの規約

ビューは app/views/ 内に格納します。

URLで指定された2つの値に従ってコントローラが読み込まれます。値が省略されると homeindex が省略されたものとみなされます。

  • http://www.example.com/index.php/test1/test2 へリクエストを送ると、app/views/test1/test2.php が読み込まれます。
  • http://www.example.com/index.php/test1 へリクエストを送ると http://www.example.com/index.php/test1/index へのリクエストとみなされ、app/views/test1/index.php が読み込まれます。
  • http://www.example.com/ へリクエストを送ると http://www.example.com/index.php/home/index へのリクエストとみなされ、app/views/home/index.php が読み込まれます。

コントローラ内で view('test1/test2.php'); のように書くと、上のルールを無視して任意のビューを読み込めます。またその際、$contents = view('test1/test2.php', true); のように書くと結果を直接出力せずに取得できます。

コントローラ内で連想配列 $_view に値を格納すると、ビューから参照することができます。

ページが見つからない場合は404エラー画面が表示されますが、app/views/404.php があると404エラー画面のビューとして使われます。

コアライブラリ

フレームワークの動作に必須の命令ですが、自分で書くプログラムに流用することもできます。

コアライブラリによって提供される命令をフレームワークから提供される命令で紹介しています。

プラグイン

フレームワーク内部からは直接呼び出されていない命令です。プラグインは libs/plugins/ 内に格納されています。

はじめから用意されているプラグインによって提供される命令をフレームワークから提供されるプラグインで紹介しています。

プラグインは自分で追加することもできます。プラグインは関数をまとめただけのファイルなので特別な規約はありません。ですが、ファイル名はその機能を端的にあらわす名前にし、関数名はファイル名をプレフィックスとすることを推奨します。(file.phpfile_info()file_mimetype() を実装するなど。)

サービス

app/ 内に services ディレクトリを作成すると、サービス用のディレクトリと認識されます。このディレクトリ内に置いたPHPファイルは自動で読み込まれるようになります。

levisにおけるサービスとは、「MVCに記述していた共通処理を何らかの観点でまとめたもので、他案件での使い回しが難しいもの」としています。(他案件でも使いまわせるものは、プラグインにすることを推奨します。)
処理をまとめることにより単体テストを容易にする、という側面もあります。

サービスは関数をまとめただけのファイルなので特別な規約はありません。ですが、ファイル名はその機能を端的にあらわす名前にし、関数名は service_ とファイル名をプレフィックスとすることを推奨します。(news.phpservice_news_recent_articles()service_news_recent_count() を実装するなど。)

単体テストなどに支障をきたすため、サービス内でトランザクションは呼び出さず、コントローラ側で制御することを推奨します。

モデル内の命令として実装するかサービスとして実装するか…の基準ですが、モデルは

  • データベースのテーブルに対応する、基本的なデータのやりとりを集約させる。
  • 自身の select_xxx()insert_xxx() を使わない範囲の、基本的なデータ操作を行う。(無限ループ回避のためにも、自身の select_xxx() などはモデル内で呼ばない。)
  • 他モデルの情報を主として扱うデータ操作は行わない。(アソシエーションのように主とならないものなら、他モデルの情報を扱うこともある。)

という思想で作っており、サービスは上の範疇を超えるものという考えで作成しています。

外部ライブラリ

外部のライブラリを使用したり独自に作成した汎用クラスを使用する場合、libs/vendors/ を作成してその中に格納することを推奨します。制限は無いので、フレームワークとまったく関係のないディレクトリに置いても何も問題はありません。

libs/vendors/ 内のファイルを呼び出す場合、require_once()import() で読み込む必要があります。自動で読み込まれたりはしません。

外部のライブラリを使用する場合、プラグインやサービスとしてラッパー関数を作ると使い方を統一することができます。これも制限は無いので、ライブラリを直接呼び出しても何も問題はありません。

情報表示

http://www.example.com/index.php/?_mode=info_levis へアクセスすると、フレームワークの情報が表示されます。表示されない場合、config.php の以下の部分を 12 に設定してください。(本番環境では 0 にしておくことを推奨します。)

define('DEBUG_LEVEL', 0);

データベース

config.php にある、以下の部分でデータベースの接続設定を行います。データベースを使用しない場合、設定の必要はありません。

define('DATABASE_TYPE', '');
define('DATABASE_HOST', '');
define('DATABASE_PORT', '');
define('DATABASE_USERNAME', '');
define('DATABASE_PASSWORD', '');
define('DATABASE_NAME', '');
define('DATABASE_PREFIX', '');

上から「接続方法」「ホスト」「ポート番号」「ユーザー名」「パスワード」「データベース名」「テーブル名のプレフィックス」です。

SQLiteを使用する場合、「データベース名」はデータベースファイルへのパスを設定します。例えば DATABASE_NAMEdb/levis.db と設定した場合、index.php と階層に db ディレクトリを作成してパーミッションを 707 に設定し、さらにその中に levis.db を作成してパーミッションを 606 に設定します。

PDOでMySQLへ接続する場合、一例ですが以下のように設定します。

define('DATABASE_TYPE', 'pdo_mysql');
define('DATABASE_HOST', 'localhost');
define('DATABASE_PORT', '');
define('DATABASE_USERNAME', 'root');
define('DATABASE_PASSWORD', '1234');
define('DATABASE_NAME', 'levis');
define('DATABASE_PREFIX', '');

PDOでPostgreSQLへ接続する場合、一例ですが以下のように設定します。

define('DATABASE_TYPE', 'pdo_pgsql');
define('DATABASE_HOST', 'localhost');
define('DATABASE_PORT', '');
define('DATABASE_USERNAME', 'root');
define('DATABASE_PASSWORD', '1234');
define('DATABASE_NAME', 'levis');
define('DATABASE_PREFIX', '');

PDOでSQLite3へ接続する場合、一例ですが以下のように設定します。

define('DATABASE_TYPE', 'pdo_sqlite');
define('DATABASE_HOST', '');
define('DATABASE_PORT', '');
define('DATABASE_USERNAME', '');
define('DATABASE_PASSWORD', '');
define('DATABASE_NAME', 'db/levis.db');
define('DATABASE_PREFIX', '');

PDOでSQLite2へ接続する場合、一例ですが以下のように設定します。

define('DATABASE_TYPE', 'pdo_sqlite2');
define('DATABASE_HOST', '');
define('DATABASE_PORT', '');
define('DATABASE_USERNAME', '');
define('DATABASE_PASSWORD', '');
define('DATABASE_NAME', 'db/levis.db');
define('DATABASE_PREFIX', '');

mysql関数で接続する場合、一例ですが以下のように設定します。

define('DATABASE_TYPE', 'mysql');
define('DATABASE_HOST', 'localhost');
define('DATABASE_PORT', '');
define('DATABASE_USERNAME', 'root');
define('DATABASE_PASSWORD', '1234');
define('DATABASE_NAME', 'levis');
define('DATABASE_PREFIX', '');

pg関数で接続する場合、一例ですが以下のように設定します。

define('DATABASE_TYPE', 'pgsql');
define('DATABASE_HOST', 'localhost');
define('DATABASE_PORT', '');
define('DATABASE_USERNAME', 'root');
define('DATABASE_PASSWORD', '1234');
define('DATABASE_NAME', 'levis');
define('DATABASE_PREFIX', '');

sqlite関数で接続する場合、一例ですが以下のように設定します。

define('DATABASE_TYPE', 'sqlite');
define('DATABASE_HOST', '');
define('DATABASE_PORT', '');
define('DATABASE_USERNAME', '');
define('DATABASE_PASSWORD', '');
define('DATABASE_NAME', 'db/levis.db');
define('DATABASE_PREFIX', '');

文字コード

config.php にある、以下の部分でデータベースの文字コード設定を行います。

MySQL使用時に DATABASE_CHARSET を指定すると、「'SET NAMES ' . DATABASE_CHARSET」でデータベースの文字コードが設定されます。

データベースへ入力する際、DATABASE_CHARSET_INPUT_FROM から DATABASE_CHARSET_INPUT_TO に文字コードが変換されます。同じ値が指定されている場合、何も行われません。

データベースから出力する際、DATABASE_CHARSET_OUTPUT_FROM から DATABASE_CHARSET_OUTPUT_TO に文字コードが変換されます。同じ値が指定されている場合、何も行われません。

define('DATABASE_CHARSET', 'UTF8');
define('DATABASE_CHARSET_INPUT_FROM', 'UTF-8');
define('DATABASE_CHARSET_INPUT_TO', 'UTF-8');
define('DATABASE_CHARSET_OUTPUT_FROM', 'UTF-8');
define('DATABASE_CHARSET_OUTPUT_TO', 'UTF-8');

データベースの文字コードが SJIS で表示の際に EUC-JP を使う場合、一例ですが以下のように指定します。

define('DATABASE_CHARSET', 'SJIS');
define('DATABASE_CHARSET_INPUT_FROM', 'auto');
define('DATABASE_CHARSET_INPUT_TO', 'SJIS');
define('DATABASE_CHARSET_OUTPUT_FROM', 'auto');
define('DATABASE_CHARSET_OUTPUT_TO', 'EUC-JP');

エラー

PDOを使用する場合、PDO::ATTR_ERRMODEPDO::ERRMODE_SILENT で実行されます。(つまり、例外を投げません。)
エラーの内容を取得したい場合、db_error() 関数を使います。

管理画面

http://www.example.com/index.php/?_mode=db_admin へアクセスすると、データベースの管理画面が表示されます(データベース使用時のみ)。表示されない場合、config.php の以下の部分を 12 に設定してください。(本番環境では 0 にしておくことを推奨します。)

define('DEBUG_LEVEL', 0);

リクエスト

GETリクエストとPOSTリクエストは、通常のPHPプログラムと同様に $_GET$_POST で取得します。$_REQUEST はフレームワーク側で扱う特別なリクエスト以外は受け付けないようになっているため、基本的には使えません。(このフレームワークに限らず、意図しない値を受け取らないためにも $_REQUEST ではなく $_GET$_POST の利用が推奨されます。)

_ から始まる名前はフレームワーク内部で使用しているので、使わないことを推奨します。

index.php/test1/test2/test3 のようなリクエストの場合、「test1」「test2」「test3」の値はコントローラ内では $_params[0]$_params[1]$_params[2] でそれぞれ取得できます。

グローバル変数

グローバル変数を扱いたい場合、PHPの文法通り $GLOBALS を利用できます。ただし _ から始まる名前はフレームワーク内部で使用しているので、使わないことを推奨します。

セッション

セッションは常に開始されているので、通常のPHPプログラムと同様に $_SESSION でセッションを読み書きします。ただし _ から始まる名前はフレームワーク内部で使用しているので、使わないことを推奨します。

セッションの開始を自分で制御したい場合、config.php で以下の設定を false にしてください。

define('SESSION_AUTOSTART', true);

この場合、セッションは以下のコードで開始できます。

session()

デバッグ

config.php の以下の部分を 1 に設定すると、エラー発生時に詳細が表示されます。また、データベース管理ツールなども利用できるようになります。2 に設定するとさらに error() を呼び出したときのスタックトレースも表示されるようになり、実行したSQLもその都度画面に表示されるようになります。

define('DEBUG_LEVEL', 0);

DEBUG_LEVEL0 に設定されている場合でも、以下の部分にパスワードを指定しておくとデータベース管理ツールなどへのアクセスが許可されます。

define('DEBUG_PASSWORD', '');

例えば以下のように設定すると、データベース管理ツールなどへアクセスする際にパスワードが求められます。

define('DEBUG_PASSWORD', '1234');

また、以下の部分にIPアドレスを指定しておくとデータベース管理ツールなどへのアクセスが許可されます。

define('DEBUG_ADDR', '');

例えば以下のように設定すると、IPアドレス 127.0.0.1 からアクセスした場合のみ、データベース管理ツールなどにアクセスできます。

define('DEBUG_ADDR', '127.0.0.1');

IPアドレスは , で区切って複数指定できます。

define('DEBUG_ADDR', '127.0.0.1,127.0.0.2,127.0.0.3');

ロギング

特定のタイミングでログを自動記録するように設定できます。ログの記録場所は config.php の以下の部分で設定します。

define('LOGGING_PATH', 'log/');

以下の部分を true に設定すると、システムエラー(必要なファイルを読み込めなかった、データベースにアクセスできなかった、など)がログに記録されます。記録する場合、LOGGING_PATH で指定したディレクトリ内に message ディレクトリを作成し、書き込み権限を与えておきます。

define('LOGGING_MESSAGE', false);

以下の部分を true に設定すると、GETアクセスがログに記録されます。記録する場合、LOGGING_PATH で指定したディレクトリ内に get ディレクトリを作成し、書き込み権限を与えておきます。

define('LOGGING_GET', false);

以下の部分を true に設定すると、POSTアクセスがログに記録されます。記録する場合、LOGGING_PATH で指定したディレクトリ内に post ディレクトリを作成し、書き込み権限を与えておきます。

define('LOGGING_POST', false);

以下の部分を true に設定すると、ファイルアップロードがログに記録されます。記録する場合、LOGGING_PATH で指定したディレクトリ内に files ディレクトリを作成し、書き込み権限を与えておきます。

define('LOGGING_FILES', false);

ブログチュートリアル

簡易な記事管理システムを例に、プログラムの作成手順を紹介します。ここでは、PHP5+MySQLで作成するものとします。また、作成するファイルの文字コードはすべてUTF-8Nとします。

インストール

任意のディレクトリにフレームワークを配置します。

データベースの設定

config.php を編集します。設定内容は環境に合わせます。

define('DATABASE_TYPE', 'pdo_mysql');
define('DATABASE_HOST', 'localhost');
define('DATABASE_PORT', '');
define('DATABASE_USERNAME', 'root');
define('DATABASE_PASSWORD', '1234');
define('DATABASE_NAME', 'levis');
define('DATABASE_PREFIX', '');

ダミーデータの登録

任意のデータベース管理ツールもしくはlevisのデータベース管理ツールから、テーブルを作成します。

CREATE TABLE articles(
    id       INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '代理キー',
    created  DATETIME     NOT NULL                COMMENT '作成日時',
    modified DATETIME     NOT NULL                COMMENT '更新日時',
    title    VARCHAR(255)                         COMMENT 'タイトル',
    body     TEXT                                 COMMENT '本文',
    PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '記事';

データを登録します。

INSERT INTO articles VALUES(NULL, NOW(), NOW(), 'テスト1', 'これはテスト1です。');
INSERT INTO articles VALUES(NULL, NOW(), NOW(), 'テスト2', 'これはテスト2です。');
INSERT INTO articles VALUES(NULL, NOW(), NOW(), 'テスト3', 'これはテスト3です。');

これで準備は完了です。引き続き、プログラムを作成していきます。

モデルの作成

app/models/articles.php を作成します。ひとまずファイルの内容はカラで大丈夫です。これにより、以下の命令が使えるようになります。

select_articles()
データを取得する命令です。
insert_articles()
データを登録する命令です。
update_articles()
データを編集する命令です。
delete_articles()
データを削除する命令です。
normalize_articles()
データを整理する命令です。
validate_articles()
データを検証する命令です。

コントローラの作成

app/controllers/blog/list.php を作成し、以下の内容を入力します。select_articles()app/models/articles.php を作成することにより使えるようになった関数です。

<?php

$_view['articles'] = select_articles(array(
    'order_by' => 'id DESC',
));

ビューの作成

app/views/blog/list.php を作成し、以下の内容を入力します。

<html>
    <head>
        <meta charset="<?php t(MAIN_CHARSET) ?>" />
        <title>ブログ</title>
    </head>
    <body>
        <h1>ブログ</h1>
        <table>
            <tr>
                <th>ID</th>
                <th>登録日時</th>
                <th>修正日時</th>
                <th>タイトル</th>
            </tr>
            <?php foreach ($_view['articles'] as $article) : ?>
            <tr>
                <td><?php h($article['id']) ?></td>
                <td><?php h(localdate('Y/m/d H:i', $article['created'])) ?></td>
                <td><?php h(localdate('Y/m/d H:i', $article['modified'])) ?></td>
                <td><?php h($article['title']) ?></td>
            </tr>
            <?php endforeach ?>
        </table>
    </body>
</html>

動作確認

index.php/blog/list にアクセスし、記事一覧が表示されれば成功です。

引き続き機能を実装していきます。

記事の個別表示

app/controllers/blog/view.php を作成し、以下の内容を入力します。

<?php

$articles = select_articles(array(
    'where' => 'id = ' . db_escape($_GET['id']),
));
if (empty($articles)) {
    warning('記事が見つかりません。');
} else {
    $_view['article'] = $articles[0];
}

app/views/blog/view.php を作成し、以下の内容を入力します。

<html>
    <head>
        <meta charset="<?php t(MAIN_CHARSET) ?>" />
        <title>ブログ</title>
    </head>
    <body>
        <h1>ブログ</h1>
        <table>
            <tr>
                <th>ID</th>
                <td><?php h($_view['article']['id']) ?></td>
            </tr>
            <tr>
                <th>登録日時</th>
                <td><?php h(localdate('Y/m/d H:i', $_view['article']['created'])) ?></td>
            </tr>
            <tr>
                <th>修正日時</th>
                <td><?php h(localdate('Y/m/d H:i', $_view['article']['modified'])) ?></td>
            </tr>
            <tr>
                <th>タイトル</th>
                <td><?php h($_view['article']['title']) ?></td>
            </tr>
            <tr>
                <th>本文</th>
                <td><?php h($_view['article']['body']) ?></td>
            </tr>
        </table>
    </body>
</html>

app/views/blog/list.php にある

<td><?php h($article['title']) ?></td>

この部分を以下のように修正します。

<td><a href="<?php t(MAIN_FILE) ?>/blog/view?id=<?php t($article['id']) ?>"><?php h($article['title']) ?></a></td>

記事一覧の各タイトルから、個別表示ページヘリンクされます。

記事の追加

app/controllers/blog/add.php を作成し、以下の内容を入力します。

<?php

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $resource = insert_articles(array(
        'values' => '(NULL, ' . db_escape(localdate('Y-m-d H:i:s')) . ', ' . db_escape(localdate('Y-m-d H:i:s')) . ', ' . db_escape($_POST['title']) . ', ' . db_escape($_POST['body']) . ')',
    ));
    if (!$resource) {
        error('データを登録できません。');
    }

    redirect('/blog/list');
}

app/views/blog/add.php を作成し、以下の内容を入力します。

<html>
    <head>
        <meta charset="<?php t(MAIN_CHARSET) ?>" />
        <title>ブログ</title>
    </head>
    <body>
        <h1>ブログ</h1>
        <form action="<?php t(MAIN_FILE) ?>/blog/add" method="post">
            <fieldset>
                <legend>登録フォーム</legend>
                <dl>
                    <dt>タイトル</dt>
                        <dd><input type="text" name="title" size="30" value="" /></dd>
                    <dt>本文</dt>
                        <dd><textarea name="body" rows="10" cols="50"></textarea></dd>
                </dl>
                <p><input type="submit" value="登録する" /></p>
            </fieldset>
        </form>
    </body>
</html>

app/views/blog/list.php に以下のコードを追加します。

<p><a href="<?php t(MAIN_FILE) ?>/blog/add">新規登録</a></p>

index.php/blog/list にアクセスすると「新規登録」リンクが表示されるので、そこから記事を投稿できます。

データのバリデーション

app/models/articles.php に以下の内容を入力します。

<?php

function validate_articles($queries)
{
    $messages = array();

    // タイトル
    if (isset($queries['title'])) {
        if ($queries['title'] === '') {
            $messages[] = 'タイトルが入力されていません。';
        } elseif (mb_strlen($queries['title'], MAIN_INTERNAL_ENCODING) > 20) {
            $messages[] = 'タイトルは20文字以内で入力してください。';
        }
    }

    // 本文
    if (isset($queries['body'])) {
        if (mb_strlen($queries['body'], MAIN_INTERNAL_ENCODING) > 1000) {
            $messages[] = '本文は1000文字以内で入力してください。';
        }
    }

    return $messages;
}

app/controllers/blog/add.php

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

この直後に以下の内容を追加します。

$warnings = validate_articles(array(
    'title' => isset($_POST['title']) ? $_POST['title'] : '',
    'body'  => isset($_POST['body'])  ? $_POST['body']  : '',
));
if (!empty($warnings)) {
    warning($warnings);
}

例えばタイトルを空欄のままで記事を投稿しようとすると、警告画面が表示されるようになります。

記事の編集

app/controllers/blog/edit.php を作成し、以下の内容を入力します。

<?php

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $warnings = validate_articles(array(
        'title' => isset($_POST['title']) ? $_POST['title'] : '',
        'body'  => isset($_POST['body'])  ? $_POST['body']  : '',
    ));
    if (!empty($warnings)) {
        warning($warnings);
    }

    $resource = update_articles(array(
        'set'   => 'modified = ' . db_escape(date('Y-m-d H:i:s')) . ', title = ' . db_escape($_POST['title']) . ', body = ' . db_escape($_POST['body']),
        'where' => 'id = ' . db_escape($_POST['id']),
    ));
    if (!$resource) {
        error('データを編集できません。');
    }

    redirect('/blog/list');
} else {
    $articles = select_articles(array(
        'where' => 'id = ' . db_escape($_GET['id']),
    ));
    if (empty($articles)) {
        warning('記事が見つかりません。');
    } else {
        $_view['article'] = $articles[0];
    }
}

app/views/blog/edit.php を作成し、以下の内容を入力します。

<html>
    <head>
        <meta charset="<?php t(MAIN_CHARSET) ?>" />
        <title>ブログ</title>
    </head>
    <body>
        <h1>ブログ</h1>
        <form action="<?php t(MAIN_FILE) ?>/blog/edit" method="post">
            <fieldset>
                <legend>編集フォーム</legend>
                <input type="hidden" name="id" value="<?php t($_view['article']['id']) ?>" />
                <dl>
                    <dt>タイトル</dt>
                        <dd><input type="text" name="title" size="30" value="<?php t($_view['article']['title']) ?>" /></dd>
                    <dt>本文</dt>
                        <dd><textarea name="body" rows="10" cols="50"><?php t($_view['article']['body']) ?></textarea></dd>
                </dl>
                <p><input type="submit" value="編集する" /></p>
            </fieldset>
        </form>
    </body>
</html>

app/views/blog/list.php にある

<td><a href="<?php t(MAIN_FILE) ?>/blog/view?id=<?php t($article['id']) ?>"><?php h($article['title']) ?></a></td>

この部分を以下のように修正します。

<td>
    <a href="<?php t(MAIN_FILE) ?>/blog/view?id=<?php t($article['id']) ?>"><?php h($article['title']) ?></a>
    <a href="<?php t(MAIN_FILE) ?>/blog/edit?id=<?php t($article['id']) ?>">編集</a>
</td>

記事一覧の「編集」リンクから、記事を編集できるようになります。

記事の削除

app/controllers/blog/delete.php を作成し、以下の内容を入力します。

<?php

$resource = delete_articles(array(
    'where' => 'id = ' . db_escape($_GET['id']),
));
if (!$resource) {
    error('データを削除できません。');
}

redirect('/blog/list');

app/views/blog/list.php にある

<td>
    <a href="<?php t(MAIN_FILE) ?>/blog/view?id=<?php t($article['id']) ?>"><?php h($article['title']) ?></a>
    <a href="<?php t(MAIN_FILE) ?>/blog/edit?id=<?php t($article['id']) ?>">編集</a>
</td>

この部分を以下のように修正します。

<td>
    <a href="<?php t(MAIN_FILE) ?>/blog/view?id=<?php t($article['id']) ?>"><?php h($article['title']) ?></a>
    <a href="<?php t(MAIN_FILE) ?>/blog/edit?id=<?php t($article['id']) ?>">編集</a>
    <a href="<?php t(MAIN_FILE) ?>/blog/delete?id=<?php t($article['id']) ?>">削除</a>
</td>

記事一覧の「削除」リンクから、記事を削除できるようになります。これで単純な登録編集削除の仕組みが実装できました。

なお簡単なものなら、雛形作成(Scaffold)の機能を使えばフレームワークが自動でコードを作成します。