モデルと 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 のブレークポイントとして便利に使えます。