モデルと DAO
モデル
BaseModel を継承すると、配列や JSON への変換、暗号化・復号、自動タイムスタンプが利用できます。プロパティは private で宣言し、ゲッターとセッターを用意してください。マッピングはメソッド名にもとづいて行われます。
<?php
namespace ExampleUser;
use _Flexagon\Base\BaseModel;
class UserModel extends BaseModel {
private ?int $id = null;
private string $name;
private ?string $address = null;
public function getId(): ?int { return $this->id; }
public function setId(?int $id): void { $this->id = $id; }
public function getName(): string { return $this->name; }
public function setName(string $name): void { $this->name = $name; }
public function getAddress(): ?string { return $this->address; }
public function setAddress(?string $a): void { $this->address = $a; }
}
BaseModel が提供するメソッドは次のとおりです。
| メソッド | 説明 |
|---|---|
getArray(bool $containsObject = true, bool $forceAll = false) |
ゲッターの値を連想配列に変換します |
setByArray(array $data) |
セッターを通じて値を設定します。snake_case と camelCase のどちらのキーも受け付けます |
getJson(int $flags = JSON_UNESCAPED_UNICODE) |
JSON にシリアライズします(ネストしたオブジェクトは配列に展開されます) |
encrypt() / decrypt() |
@encrypt アノテーションが付いたプロパティを暗号化・復号します |
getCreatedAt() / getUpdatedAt() |
Unix タイムスタンプ |
getCreatedAtDatetimeString() |
Y-m-d H:i:s 形式の文字列 |
BaseModel は JsonSerializable を実装しているため、json_encode($model) をそのまま使えます。
setByArray() はリフレクションを使って、よくある型の不一致を調整します。nullable なプロパティでは空文字列が null になり、backed enum のプロパティではスカラー値が対応するケースに変換されます。そのため、フォームの入力値をそのままモデルに渡すことができます。
DAO
BaseMySqlDAO を継承して $tableName を指定すると、基本的な CRUD 操作が使えるようになります。
フレームワークは $tableName をバッククォート(MySQL)または角括弧(SQL Server)で囲むため、'order' のような予約語もそのまま使えます。すでにクォートされた名前が再度囲まれることはありません。'audit.users'、'[audit].[users]'、'`app`.`users`' はいずれも同じ文を生成します。
<?php
namespace ExampleUser;
use _Flexagon\Base\BaseMySqlDAO;
class UserDAO extends BaseMySqlDAO {
protected string $tableName = 'users';
public function insert(UserModel $user): int|false { return $this->_insert($user); }
public function update(UserModel $user): bool { return $this->_update($user); }
public function delete(UserModel $user): bool { return $this->_delete($user); }
public function findById(int $id): ?UserModel {
return $this->_select(['id' => $id], new UserModel());
}
/** @return UserModel[] */
public function findByCity(string $city, int $page = 1, int $perPage = 20): array {
return $this->_selectList(
new UserModel(), $page, $perPage,
'`address` = :CITY', ['CITY' => $city],
'`created_at` DESC'
);
}
public function countByCity(string $city): int {
return $this->_selectTotalCount('`address` = :CITY', ['CITY' => $city]);
}
}
CRUD メソッドは意図的に protected になっています。各 DAO は公開する操作を明示的に選ぶことで、公開インターフェースを最小限に保ち、ドメインに沿ったものにします。
| メソッド | 説明 |
|---|---|
_insert($modelOrArray) |
INSERT。自動採番された値を返します(失敗時は false) |
_update($modelOrArray) |
主キーによる UPDATE |
_updateByQuery($modelOrArray, $whereSql, $params) |
任意の条件による UPDATE |
_select($modelOrArray, ?object $vo) |
1 行を取得します。$vo を渡した場合はオブジェクト、それ以外は配列を返します |
_selectByQuery($whereSql, $params, ?object $vo) |
任意の条件で 1 行を取得します |
_selectList(?object $vo, $page, $perPage, $whereSql, $params, $orderSql) |
ページングされた一覧を取得します |
_selectTotalCount($whereSql, $params) |
条件に一致する行数を数えます |
_delete($modelOrArray) |
主キーによる DELETE |
_deleteByQuery($whereSql, $params) |
任意の条件による DELETE |
_insertAll($modelList, $chunkSize = 500) |
チャンクごとに 1 つの文を使う一括 INSERT。挿入した行数を返します |
_truncate() |
TRUNCATE |
PostgreSQL では BasePgSqlDAO、SQLite では BaseSqliteDAO、SQL Server では BaseMsSqlDAO を継承します。いずれも同じメソッド群を提供します。すべての DAO 実装は BaseSqlDAO を継承しており、CRUD 操作、スキーマのキャッシュ、トランザクション処理はそこで共通化されています。サブクラスにはデータベース方言に固有の処理だけが含まれます。具体的には、識別子のクォート(`name` / "name" / [name])、スキーマの取得(DESCRIBE / information_schema / PRAGMA table_info)、ページング(LIMIT / LIMIT·OFFSET / OFFSET·FETCH)です。
PostgreSQL:ダブルクォートで囲まれた識別子は大文字と小文字が区別され、フレームワークは
$tableNameをクォートします。テーブル名は保存されているとおり、通常は小文字で記述してください('Users'ではなく'users')。クォートなしの名前はsearch_pathに従って解決されます。別のスキーマを使う場合は'audit.users'と記述します。
SQLite:データソースはファイルで、
DataSourceModel::file()で作成します。_Global::$DATA_SOURCES['default'] = DataSourceModel::file(PROJECT_ROOT . '/var/app.sqlite');ディレクトリはあらかじめ存在している必要があります。パスが誤っている場合、空のデータベースが作成されるのではなく接続に失敗します。
INTEGER PRIMARY KEYが自動採番カラムになり、_truncate()はDELETEを実行し、外部キー制約は有効になります。SQLite は書き込み時にファイル全体をロックするため、同時に書き込みが発生するサイトでは WAL を一度有効にしてください(PRAGMA journal_mode = WAL)。
SQL を直接実行する必要がある場合は、$this->db(Mysql インスタンス)を使います。
public function findTopSpenders(int $limit): array {
$this->db->executeQuery(
'SELECT u.* FROM users u JOIN orders o ON o.user_id = u.id
GROUP BY u.id ORDER BY SUM(o.amount) DESC LIMIT :LIMIT',
['LIMIT' => $limit]
);
return $this->db->getAllResultAsObject(new UserModel());
}
カラムとプロパティのマッピング
データベースの snake_case のカラムは、PHP の camelCase のプロパティに自動的にマッピングされます。
| DB カラム | ゲッター / セッター | プロパティ |
|---|---|---|
id |
getId() / setId() |
$id |
user_name |
getUserName() / setUserName() |
$userName |
created_at |
getCreatedAt() / setCreatedAt() |
$createdAt |
パラメータのバインド
クエリは名前付きプレースホルダーを使った PDO のプリペアドステートメントで実行されます。フレームワークが生成するクエリでは、大文字のカラム名がプレースホルダーとして使われます(user_name → :USER_NAME)。
同じプレースホルダーがクエリ内に複数回現れる場合、SqlUtil::expandQueryParams() が自動的に別々のパラメータへ展開するため、PDO の重複バインドの制約を回避できます。
$this->_selectList(new UserModel(), 1, 20,
'`name` LIKE :KEYWORD OR `address` LIKE :KEYWORD', // 複数回使用できます
['KEYWORD' => "%{$keyword}%"]
);
デバッグ
echo $userDAO->debugQuery(); // パラメータを埋め込んだ実際の SQL
echo $userDAO->getError(); // 最後のエラーメッセージ
$userDAO->debugDumpParams(); // PDOStatement::debugDumpParams()
_Global::$DEBUG_QUERY_BREAKPOINT を有効にすると、すべてのクエリの実行後に Mysql::debugQueryAfter() が呼び出されます。このメソッドは意図的に空になっているため、実行されたクエリを確認するための IDE のブレークポイントとして便利に使えます。