Object-Relational Mapping (ORM) 解決物件導向模型與關聯式資料庫的阻抗失配:
| 框架 | 特色 | 學習曲線 | 效能 | 適用場景 |
|---|---|---|---|---|
| Doctrine ORM | 功能最完整、DQL 查詢語言、事件系統、遷移工具、快取整合 | 陡峭 | 中等 (大量物件較慢) | 大型企業應用、長期維護專案 |
| Eloquent (Laravel) | Active Record、語法優雅、關聯定義簡潔、集合操作強大 | 平緩 | 中等 | Laravel 專案、中小型應用 |
| Propel | XML Schema 定義、代碼生成、查詢建構器、較老牌 | 中等 | 中等 | 傳統專案、偏好代碼生成 |
| RedBeanPHP | 零配置、動態建表、On-the-fly、極簡 API | 極低 | 較慢 (動態特性) | 快速原型、小專案、開發階段 |
| Cycle ORM | Data Mapper、嚴格型別、PHP 8.1+、效能導向、無反射 | 中等 | 高 (編譯期優化) | 現代高效能應用 |
| Atlas.Orm | Mapper 模式、無狀態、嚴格分離、PSR 相容 | 中等 | 高 | 領域驅動設計 (DDD) 專案 |
<?php
// 核心介面
interface EntityInterface {
public function getId();
public function setId($id);
}
// 基礎實體類別
abstract class Entity implements EntityInterface {
protected $id;
protected $data = [];
protected $originalData = [];
protected $dirty = false;
public function getId() { return $this->id; }
public function setId($id) { $this->id = $id; }
public function __get($name) { return $this->data[$name] ?? null; }
public function __set($name, $value) {
$this->data[$name] = $value;
$this->dirty = true;
}
public function __isset($name) { return isset($this->data[$name]); }
public function toArray() { return $this->data; }
public function getDirtyData() {
return array_diff_assoc($this->data, $this->originalData);
}
public function markClean() {
$this->originalData = $this->data;
$this->dirty = false;
}
}
// 映射定義 (可改用屬性/註解)
class EntityMap {
public string $table;
public string $primaryKey = 'id';
public array $columns = []; // ['property' => 'column_name']
public array $relations = []; // 關聯定義
}
// 實體管理器 (核心)
class EntityManager {
private PDO $pdo;
private array $maps = [];
private array $identityMap = []; // 單一實體實例快取
public function __construct(PDO $pdo) { $this->pdo = $pdo; }
public function map(string $class, EntityMap $map) { $this->maps[$class] = $map; }
// 查詢單一實體
public function find(string $class, $id) {
$key = $class . ':' . $id;
if (isset($this->identityMap[$key])) return $this->identityMap[$key];
$map = $this->maps[$class];
$sql = "SELECT * FROM {$map->table} WHERE {$map->primaryKey} = ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) return null;
$entity = new $class();
$entity->setId($row[$map->primaryKey]);
foreach ($map->columns as $prop => $col) {
$entity->$prop = $row[$col] ?? null;
}
$entity->markClean();
$this->identityMap[$key] = $entity;
return $entity;
}
// 條件查詢
public function findBy(string $class, array $criteria, array $orderBy = [], $limit = null, $offset = null) {
$map = $this->maps[$class];
$where = [];
$params = [];
foreach ($criteria as $prop => $value) {
$col = $map->columns[$prop] ?? $prop;
$where[] = "$col = ?";
$params[] = $value;
}
$sql = "SELECT * FROM {$map->table}";
if ($where) $sql .= " WHERE " . implode(' AND ', $where);
if ($orderBy) {
$orderSql = [];
foreach ($orderBy as $prop => $dir) {
$col = $map->columns[$prop] ?? $prop;
$orderSql[] = "$col " . strtoupper($dir);
}
$sql .= " ORDER BY " . implode(', ', $orderSql);
}
if ($limit) $sql .= " LIMIT $limit";
if ($offset) $sql .= " OFFSET $offset";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$entities = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$entity = new $class();
$entity->setId($row[$map->primaryKey]);
foreach ($map->columns as $prop => $col) {
$entity->$prop = $row[$col] ?? null;
}
$entity->markClean();
$entities[] = $entity;
}
return $entities;
}
// 持久化 (新增/更新)
public function persist(EntityInterface $entity) {
$class = get_class($entity);
$map = $this->maps[$class];
if ($entity->getId() === null) {
// INSERT
$data = $entity->toArray();
unset($data[$map->primaryKey]); // 假設自增
$cols = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$map->table} ($cols) VALUES ($placeholders)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($data);
$entity->setId($this->pdo->lastInsertId());
} else {
// UPDATE (僅髒資料)
$dirty = $entity->getDirtyData();
if (!$dirty) return; // 無變更
$sets = [];
foreach ($dirty as $prop => $value) {
$col = $map->columns[$prop] ?? $prop;
$sets[] = "$col = :$prop";
}
$sql = "UPDATE {$map->table} SET " . implode(', ', $sets) . " WHERE {$map->primaryKey} = :id";
$params = $dirty;
$params['id'] = $entity->getId();
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
}
$entity->markClean();
}
// 刪除
public function remove(EntityInterface $entity) {
$class = get_class($entity);
$map = $this->maps[$class];
$sql = "DELETE FROM {$map->table} WHERE {$map->primaryKey} = ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$entity->getId()]);
unset($this->identityMap[$class . ':' . $entity->getId()]);
}
// 事務支援
public function transaction(callable $callback) {
$this->pdo->beginTransaction();
try {
$result = $callback($this);
$this->pdo->commit();
return $result;
} catch (Exception $e) {
$this->pdo->rollBack();
throw $e;
}
}
}
// 使用範例
// 定義實體
class User extends Entity {
// 屬性自動透過 __get/__set 處理
}
// 設定映射
$em = new EntityManager($pdo);
$em->map(User::class, (new EntityMap())->setTable('users')->setColumns([
'name' => 'name',
'email' => 'email',
'password' => 'password_hash',
'createdAt' => 'created_at',
]));
// 業務代碼
$user = $em->find(User::class, 1);
$user->name = '李四';
$em->persist($user); // 自動判斷 INSERT/UPDATE
// 批次查詢
$users = $em->findBy(User::class, ['status' => 1], ['createdAt' => 'DESC'], 20, 0);
?>
$em->createQueryBuilder()->select()->where()->getResult()`