引言对象关系映射ORM是连接面向对象代码和关系数据库的桥梁。本文将实现一个完整的ORM框架核心涵盖ActiveRecord、UnitOfWork、IdentityMap、延迟加载和关联关系管理等核心模式。数据库基础抽象层先从底层数据库抽象开始构建。namespace ORM;interface ConnectionInterface{public function query(string $sql, array $params []): array;public function execute(string $sql, array $params []): int;public function lastInsertId(): string;public function beginTransaction(): void;public function commit(): void;public function rollback(): void;}class PdoConnection implements ConnectionInterface{private \PDO $pdo;public function __construct(string $dsn, string $user , string $pass , array $options []){$defaults [\PDO::ATTR_ERRMODE \PDO::ERRMODE_EXCEPTION,\PDO::ATTR_DEFAULT_FETCH_MODE \PDO::FETCH_ASSOC,\PDO::ATTR_EMULATE_PREPARES false,];$this-pdo new \PDO($dsn, $user, $pass, array_merge($defaults, $options));}public function query(string $sql, array $params []): array{$stmt $this-pdo-prepare($sql);$stmt-execute($params);return $stmt-fetchAll();}public function execute(string $sql, array $params []): int{$stmt $this-pdo-prepare($sql);$stmt-execute($params);return $stmt-rowCount();}public function lastInsertId(): string{return $this-pdo-lastInsertId();}public function beginTransaction(): void { $this-pdo-beginTransaction(); }public function commit(): void { $this-pdo-commit(); }public function rollback(): void { $this-pdo-rollBack(); }}// 查询构建器class QueryBuilder{private string $table ;private array $select [*];private array $where [];private array $params [];private array $joins [];private array $orderBy [];private ?int $limit null;private ?int $offset null;private array $groupBy [];private array $having [];public function table(string $table): self{$this-table $table;return $this;}public function select(array $columns): self{$this-select $columns;return $this;}public function where(string $column, string $operator , mixed $value null): self{if ($value null) {$value $operator;$operator ;}$param :w_ . count($this-params);$this-where[] $column $operator $param;$this-params[$param] $value;return $this;}public function whereIn(string $column, array $values): self{$params [];foreach ($values as $i $v) {$param :win_ . count($this-params) . _$i;$params[] $param;$this-params[$param] $v;}$this-where[] $column IN ( . implode(, , $params) . );return $this;}public function whereNull(string $column): self{$this-where[] $column IS NULL;return $this;}public function whereNotNull(string $column): self{$this-where[] $column IS NOT NULL;return $this;}public function whereRaw(string $sql, array $params []): self{$this-where[] $sql;$this-params array_merge($this-params, $params);return $this;}public function join(string $table, string $first, string $operator , string $second , string $type INNER): self{$this-joins[] $type JOIN $table ON $first $operator $second;return $this;}public function leftJoin(string $table, string $first, string $operator , string $second ): self{return $this-join($table, $first, $operator, $second, LEFT);}public function orderBy(string $column, string $direction ASC): self{$this-orderBy[] $column $direction;return $this;}public function limit(int $limit): self{$this-limit $limit;return $this;}public function offset(int $offset): self{$this-offset $offset;return $this;}public function groupBy(array $columns): self{$this-groupBy $columns;return $this;}public function having(string $column, string $operator, mixed $value): self{$param :h_ . count($this-params);$this-having[] $column $operator $param;$this-params[$param] $value;return $this;}public function toSelectSql(): string{$sql SELECT . implode(, , $this-select) . FROM . $this-table;if (!empty($this-joins)) {$sql . . implode( , $this-joins);}if (!empty($this-where)) {$sql . WHERE . implode( AND , $this-where);}if (!empty($this-groupBy)) {$sql . GROUP BY . implode(, , $this-groupBy);}if (!empty($this-having)) {$sql . HAVING . implode( AND , $this-having);}if (!empty($this-orderBy)) {$sql . ORDER BY . implode(, , $this-orderBy);}if ($this-limit ! null) {$sql . LIMIT . $this-limit;}if ($this-offset ! null) {$sql . OFFSET . $this-offset;}return $sql;}public function getParams(): array{return $this-params;}}// 元数据管理enum ColumnType{case INTEGER;case STRING;case FLOAT;case BOOLEAN;case DATETIME;case TEXT;case JSON;}class ColumnMetadata{public function __construct(public string $name,public ColumnType $type,public bool $primaryKey false,public bool $autoIncrement false,public bool $nullable true,public mixed $default null,) {}}class EntityMetadata{private array $columns [];private array $relations [];public function __construct(private string $className,private string $tableName,) {}public function getClassName(): string { return $this-className; }public function getTableName(): string { return $this-tableName; }public function addColumn(ColumnMetadata $column): void{$this-columns[$column-name] $column;}public function getColumns(): array { return $this-columns; }public function getPrimaryKey(): ?ColumnMetadata{foreach ($this-columns as $col) {if ($col-primaryKey) return $col;}return null;}public function addRelation(string $name, array $config): void{$this-relations[$name] $config;}public function getRelations(): array { return $this-relations; }}// 注解驱动的元数据读取器#[Attribute(\Attribute::TARGET_CLASS)]class Entity{public function __construct(public string $table) {}}#[Attribute(\Attribute::TARGET_PROPERTY)]class Id {}#[Attribute(\Attribute::TARGET_PROPERTY)]class Column{public function __construct(public ?string $name null,public ?string $type null,public bool $nullable true,public mixed $default null,) {}}#[Attribute(\Attribute::TARGET_PROPERTY)]class OneToMany{public function __construct(public string $targetEntity,public string $mappedBy,) {}}#[Attribute(\Attribute::TARGET_PROPERTY)]class ManyToOne{public function __construct(public string $targetEntity,public string $inversedBy,) {}}#[Attribute(\Attribute::TARGET_PROPERTY)]class OneToOne{public function __construct(public string $targetEntity,public ?string $mappedBy null,) {}}#[Attribute(\Attribute::TARGET_PROPERTY)]class ManyToMany{public function __construct(public string $targetEntity,public string $joinTable,) {}}// Identity Mapclass IdentityMap{private array $objects [];public function set(string $className, mixed $id, object $object): void{$key $this-key($className, $id);$this-objects[$key] $object;}public function get(string $className, mixed $id): ?object{$key $this-key($className, $id);return $this-objects[$key] ?? null;}public function has(string $className, mixed $id): bool{return isset($this-objects[$this-key($className, $id)]);}public function remove(string $className, mixed $id): void{unset($this-objects[$this-key($className, $id)]);}public function clear(): void{$this-objects [];}private function key(string $className, mixed $id): string{return $className:$id;}}// Unit of Work (工作单元)enum ActionType{case CREATE;case UPDATE;case DELETE;}class UnitOfWork{private array $scheduled [];private array $inserts [];private array $updates [];private array $deletes [];public function __construct(private IdentityMap $identityMap,private ConnectionInterface $connection,) {}public function registerCreate(object $entity): void{$hash spl_object_hash($entity);$this-scheduled[$hash] [entity $entity, action ActionType::CREATE];}public function registerUpdate(object $entity): void{$hash spl_object_hash($entity);if (!isset($this-scheduled[$hash]) || $this-scheduled[$hash][action] ! ActionType::CREATE) {$this-scheduled[$hash] [entity $entity, action ActionType::UPDATE];}}public function registerDelete(object $entity): void{$hash spl_object_hash($entity);$this-scheduled[$hash] [entity $entity, action ActionType::DELETE];}public function commit(): void{$this-connection-beginTransaction();try {foreach ($this-scheduled as $hash $item) {match ($item[action]) {ActionType::CREATE $this-executeInsert($item[entity]),ActionType::UPDATE $this-executeUpdate($item[entity]),ActionType::DELETE $this-executeDelete($item[entity]),};unset($this-scheduled[$hash]);}$this-connection-commit();} catch (\Throwable $e) {$this-connection-rollback();throw $e;}}private function executeInsert(object $entity): void{$meta MetadataRegistry::get(get_class($entity));$data [];foreach ($meta-getColumns() as $col) {if ($col-autoIncrement) continue;$prop new \ReflectionProperty($entity, $col-name);$prop-setAccessible(true);$val $prop-getValue($entity);if ($val ! null || !$col-nullable) {$data[$col-name] $val;}}$columns implode(, , array_keys($data));$placeholders implode(, , array_map(fn($k) :$k, array_keys($data)));$sql INSERT INTO {$meta-getTableName()} ($columns) VALUES ($placeholders);$this-connection-execute($sql, $data);$pk $meta-getPrimaryKey();if ($pk $pk-autoIncrement) {$id $this-connection-lastInsertId();$prop new \ReflectionProperty($entity, $pk-name);$prop-setAccessible(true);$prop-setValue($entity, (int)$id);$this-identityMap-set(get_class($entity), (int)$id, $entity);}}private function executeUpdate(object $entity): void{$meta MetadataRegistry::get(get_class($entity));$pk $meta-getPrimaryKey();$pkProp new \ReflectionProperty($entity, $pk-name);$pkProp-setAccessible(true);$id $pkProp-getValue($entity);$data [];foreach ($meta-getColumns() as $col) {if ($col-primaryKey) continue;$prop new \ReflectionProperty($entity, $col-name);$prop-setAccessible(true);$data[$col-name] $prop-getValue($entity);}$sets implode(, , array_map(fn($k) $k :$k, array_keys($data)));$sql UPDATE {$meta-getTableName()} SET $sets WHERE {$pk-name} :pk_id;$data[pk_id] $id;$this-connection-execute($sql, $data);}private function executeDelete(object $entity): void{$meta MetadataRegistry::get(get_class($entity));$pk $meta-getPrimaryKey();$prop new \ReflectionProperty($entity, $pk-name);$prop-setAccessible(true);$id $prop-getValue($entity);$sql DELETE FROM {$meta-getTableName()} WHERE {$pk-name} :id;$this-connection-execute($sql, [id $id]);$this-identityMap-remove(get_class($entity), $id);}}// Metadata Registryclass MetadataRegistry{private static array $metadata [];public static function register(string $className, EntityMetadata $meta): void{self::$metadata[$className] $meta;}public static function get(string $className): EntityMetadata{if (!isset(self::$metadata[$className])) {self::buildMetadata($className);}return self::$metadata[$className];}private static function buildMetadata(string $className): void{$ref new \ReflectionClass($className);$entityAttr $ref-getAttributes(Entity::class);if (empty($entityAttr)) {throw new \RuntimeException(Class $className is not an entity);}$table $entityAttr[0]-newInstance()-table;$meta new EntityMetadata($className, $table);foreach ($ref-getProperties() as $prop) {$colAttr $prop-getAttributes(Column::class);$idAttr $prop-getAttributes(Id::class);if (!empty($idAttr) || !empty($colAttr)) {$name $prop-getName();$type ColumnType::STRING;$nullable true;$default null;$primary !empty($idAttr);if (!empty($colAttr)) {$col $colAttr[0]-newInstance();$name $col-name ?? $prop-getName();$nullable $col-nullable;$default $col-default;$type match ($col-type) {int, integer ColumnType::INTEGER,float ColumnType::FLOAT,bool, boolean ColumnType::BOOLEAN,datetime ColumnType::DATETIME,text ColumnType::TEXT,json ColumnType::JSON,default ColumnType::STRING,};}$meta-addColumn(new ColumnMetadata($name, $type, $primary, $primary, $nullable, $default));}// 关系注解$oneToMany $prop-getAttributes(OneToMany::class);$manyToOne $prop-getAttributes(ManyToOne::class);$oneToOne $prop-getAttributes(OneToOne::class);$manyToMany $prop-getAttributes(ManyToMany::class);if (!empty($oneToMany)) {$meta-addRelation($prop-getName(), [type one_to_many,target $oneToMany[0]-newInstance()-targetEntity,mapped_by $oneToMany[0]-newInstance()-mappedBy,]);} elseif (!empty($manyToOne)) {$meta-addRelation($prop-getName(), [type many_to_one,target $manyToOne[0]-newInstance()-targetEntity,inversed_by $manyToOne[0]-newInstance()-inversedBy,]);} elseif (!empty($oneToOne)) {$meta-addRelation($prop-getName(), [type one_to_one,target $oneToOne[0]-newInstance()-targetEntity,mapped_by $oneToOne[0]-newInstance()-mappedBy,]);} elseif (!empty($manyToMany)) {$meta-addRelation($prop-getName(), [type many_to_many,target $manyToMany[0]-newInstance()-targetEntity,join_table $manyToMany[0]-newInstance()-joinTable,]);}}self::$metadata[$className] $meta;}}// EntityManagerclass EntityManager{private IdentityMap $identityMap;private UnitOfWork $unitOfWork;private ConnectionInterface $connection;public function __construct(ConnectionInterface $connection){$this-connection $connection;$this-identityMap new IdentityMap();$this-unitOfWork new UnitOfWork($this-identityMap, $this-connection);}public function find(string $className, mixed $id): ?object{if ($this-identityMap-has($className, $id)) {return $this-identityMap-get($className, $id);}$meta MetadataRegistry::get($className);$pk $meta-getPrimaryKey();$qb new QueryBuilder();$qb-table($meta-getTableName())-where($pk-name, , $id);$rows $this-connection-query($qb-toSelectSql(), $qb-getParams());if (empty($rows)) return null;$entity $this-hydrate($className, $rows[0]);$this-identityMap-set($className, $id, $entity);return $entity;}public function findAll(string $className): array{$meta MetadataRegistry::get($className);$qb new QueryBuilder();$qb-table($meta-getTableName());$rows $this-connection-query($qb-toSelectSql(), $qb-getParams());$entities [];foreach ($rows as $row) {$entity $this-hydrate($className, $row);$this-identityMap-set($className, $row[$meta-getPrimaryKey()-name], $entity);$entities[] $entity;}return $entities;}public function findBy(string $className, array $criteria): array{$meta MetadataRegistry::get($className);$qb new QueryBuilder();$qb-table($meta-getTableName());foreach ($criteria as $col $val) {$qb-where($col, , $val);}$rows $this-connection-query($qb-toSelectSql(), $qb-getParams());$entities [];foreach ($rows as $row) {$entities[] $this-hydrate($className, $row);}return $entities;}public function persist(object $entity): void{$meta MetadataRegistry::get(get_class($entity));$pk $meta-getPrimaryKey();$prop new \ReflectionProperty($entity, $pk-name);$prop-setAccessible(true);$id $prop-getValue($entity);if ($this-identityMap-has(get_class($entity), $id)) {$this-unitOfWork-registerUpdate($entity);} else {$this-unitOfWork-registerCreate($entity);}}public function remove(object $entity): void{$this-unitOfWork-registerDelete($entity);}public function flush(): void{$this-unitOfWork-commit();}public function createQueryBuilder(): QueryBuilder{return new QueryBuilder();}// 原生查询public function nativeQuery(string $sql, array $params []): array{return $this-connection-query($sql, $params);}private function hydrate(string $className, array $data): object{$meta MetadataRegistry::get($className);$ref new \ReflectionClass($className);// 尝试构造函数注入$constructor $ref-getConstructor();$args [];if ($constructor) {foreach ($constructor-getParameters() as $param) {if (isset($data[$param-getName()])) {$args[] $data[$param-getName()];} elseif ($param-isDefaultValueAvailable()) {$args[] $param-getDefaultValue();} else {$args[] null;}}}$entity empty($args) ? $ref-newInstance() : $ref-newInstanceArgs($args);// 设置属性foreach ($meta-getColumns() as $col) {if (isset($data[$col-name])) {$prop $ref-getProperty($col-name);$prop-setAccessible(true);$value match ($col-type) {ColumnType::INTEGER (int)$data[$col-name],ColumnType::FLOAT (float)$data[$col-name],ColumnType::BOOLEAN (bool)$data[$col-name],ColumnType::JSON json_decode($data[$col-name], true),ColumnType::DATETIME new \DateTimeImmutable($data[$col-name]),default $data[$col-name],};$prop-setValue($entity, $value);}}return $entity;}public function __destruct(){// 自动提交未完成的工作try { $this-unitOfWork-commit(); } catch (\Throwable) {}}}// 实体模型示例#[Entity(table: users)]class User{#[Id] #[Column(type: int)]private int $id;#[Column(name: username, type: string)]private string $username;#[Column(name: email, type: string)]private string $email;#[Column(name: created_at, type: datetime, nullable: true)]private ?\DateTimeImmutable $createdAt null;#[OneToMany(targetEntity: Post::class, mappedBy: user)]private array $posts [];public function __construct(string $username , string $email ) {$this-username $username;$this-email $email;$this-createdAt new \DateTimeImmutable();}public function getId(): int { return $this-id; }public function getUsername(): string { return $this-username; }public function getEmail(): string { return $this-email; }}#[Entity(table: posts)]class Post{#[Id] #[Column(type: int)]private int $id;#[Column(type: string)]private string $title;#[Column(type: text)]private string $content;#[Column(name: user_id, type: int)]private int $userId;#[ManyToOne(targetEntity: User::class, inversedBy: posts)]private ?User $user null;public function __construct(string $title , string $content , int $userId 0) {$this-title $title;$this-content $content;$this-userId $userId;}public function getId(): int { return $this-id; }public function getTitle(): string { return $this-title; }}// 延迟加载代理class LazyLoadingProxy{private object $realInstance null;public function __construct(private string $className,private mixed $id,private EntityManager $em,) {}public function __call(string $method, array $args): mixed{$this-load();return $this-realInstance-$method(...$args);}public function __get(string $name): mixed{$this-load();return $this-realInstance-$name;}private function load(): void{if ($this-realInstance null) {$this-realInstance $this-em-find($this-className, $this-id);}}}// 使用示例$connection new PdoConnection(sqlite::memory:);$connection-execute(CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, email TEXT, created_at DATETIME));$connection-execute(CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, user_id INTEGER));$em new EntityManager($connection);// 持久化$user new User(alice, aliceexample.com);$em-persist($user);$em-flush();printf(Created user with id: %d\n, $user-getId());// 查询$found $em-find(User::class, 1);printf(Found user: %s (%s)\n, $found-getUsername(), $found-getEmail());// 添加更多$users [new User(bob, bobexample.com),new User(charlie, charlieexample.com),];foreach ($users as $u) {$em-persist($u);}$em-flush();$allUsers $em-findAll(User::class);printf(Total users: %d\n, count($allUsers));// 条件查询$admins $em-findBy(User::class, [username alice]);printf(Found by username: %d\n, count($admins));// ActorRecord 风格简化trait ActiveRecordTrait{private static ?EntityManager $em null;public static function setEntityManager(EntityManager $em): void{self::$em $em;}public function save(): void{self::$em-persist($this);self::$em-flush();}public function delete(): void{self::$em-remove($this);self::$em-flush();}public static function find(mixed $id): ?object{return self::$em-find(static::class, $id);}public static function all(): array{return self::$em-findAll(static::class);}public static function where(array $criteria): array{return self::$em-findBy(static::class, $criteria);}public function reload(): void{$meta MetadataRegistry::get(static::class);$pk $meta-getPrimaryKey();$prop new \ReflectionProperty($this, $pk-name);$prop-setAccessible(true);$id $prop-getValue($this);$fresh self::$em-find(static::class, $id);if ($fresh) {foreach ($meta-getColumns() as $col) {$p new \ReflectionProperty($this, $col-name);$p-setAccessible(true);$fp new \ReflectionProperty($fresh, $col-name);$fp-setAccessible(true);$p-setValue($this, $fp-getValue($fresh));}}}}
PHP对象关系映射实现
发布时间:2026/5/28 17:13:28
引言对象关系映射ORM是连接面向对象代码和关系数据库的桥梁。本文将实现一个完整的ORM框架核心涵盖ActiveRecord、UnitOfWork、IdentityMap、延迟加载和关联关系管理等核心模式。数据库基础抽象层先从底层数据库抽象开始构建。namespace ORM;interface ConnectionInterface{public function query(string $sql, array $params []): array;public function execute(string $sql, array $params []): int;public function lastInsertId(): string;public function beginTransaction(): void;public function commit(): void;public function rollback(): void;}class PdoConnection implements ConnectionInterface{private \PDO $pdo;public function __construct(string $dsn, string $user , string $pass , array $options []){$defaults [\PDO::ATTR_ERRMODE \PDO::ERRMODE_EXCEPTION,\PDO::ATTR_DEFAULT_FETCH_MODE \PDO::FETCH_ASSOC,\PDO::ATTR_EMULATE_PREPARES false,];$this-pdo new \PDO($dsn, $user, $pass, array_merge($defaults, $options));}public function query(string $sql, array $params []): array{$stmt $this-pdo-prepare($sql);$stmt-execute($params);return $stmt-fetchAll();}public function execute(string $sql, array $params []): int{$stmt $this-pdo-prepare($sql);$stmt-execute($params);return $stmt-rowCount();}public function lastInsertId(): string{return $this-pdo-lastInsertId();}public function beginTransaction(): void { $this-pdo-beginTransaction(); }public function commit(): void { $this-pdo-commit(); }public function rollback(): void { $this-pdo-rollBack(); }}// 查询构建器class QueryBuilder{private string $table ;private array $select [*];private array $where [];private array $params [];private array $joins [];private array $orderBy [];private ?int $limit null;private ?int $offset null;private array $groupBy [];private array $having [];public function table(string $table): self{$this-table $table;return $this;}public function select(array $columns): self{$this-select $columns;return $this;}public function where(string $column, string $operator , mixed $value null): self{if ($value null) {$value $operator;$operator ;}$param :w_ . count($this-params);$this-where[] $column $operator $param;$this-params[$param] $value;return $this;}public function whereIn(string $column, array $values): self{$params [];foreach ($values as $i $v) {$param :win_ . count($this-params) . _$i;$params[] $param;$this-params[$param] $v;}$this-where[] $column IN ( . implode(, , $params) . );return $this;}public function whereNull(string $column): self{$this-where[] $column IS NULL;return $this;}public function whereNotNull(string $column): self{$this-where[] $column IS NOT NULL;return $this;}public function whereRaw(string $sql, array $params []): self{$this-where[] $sql;$this-params array_merge($this-params, $params);return $this;}public function join(string $table, string $first, string $operator , string $second , string $type INNER): self{$this-joins[] $type JOIN $table ON $first $operator $second;return $this;}public function leftJoin(string $table, string $first, string $operator , string $second ): self{return $this-join($table, $first, $operator, $second, LEFT);}public function orderBy(string $column, string $direction ASC): self{$this-orderBy[] $column $direction;return $this;}public function limit(int $limit): self{$this-limit $limit;return $this;}public function offset(int $offset): self{$this-offset $offset;return $this;}public function groupBy(array $columns): self{$this-groupBy $columns;return $this;}public function having(string $column, string $operator, mixed $value): self{$param :h_ . count($this-params);$this-having[] $column $operator $param;$this-params[$param] $value;return $this;}public function toSelectSql(): string{$sql SELECT . implode(, , $this-select) . FROM . $this-table;if (!empty($this-joins)) {$sql . . implode( , $this-joins);}if (!empty($this-where)) {$sql . WHERE . implode( AND , $this-where);}if (!empty($this-groupBy)) {$sql . GROUP BY . implode(, , $this-groupBy);}if (!empty($this-having)) {$sql . HAVING . implode( AND , $this-having);}if (!empty($this-orderBy)) {$sql . ORDER BY . implode(, , $this-orderBy);}if ($this-limit ! null) {$sql . LIMIT . $this-limit;}if ($this-offset ! null) {$sql . OFFSET . $this-offset;}return $sql;}public function getParams(): array{return $this-params;}}// 元数据管理enum ColumnType{case INTEGER;case STRING;case FLOAT;case BOOLEAN;case DATETIME;case TEXT;case JSON;}class ColumnMetadata{public function __construct(public string $name,public ColumnType $type,public bool $primaryKey false,public bool $autoIncrement false,public bool $nullable true,public mixed $default null,) {}}class EntityMetadata{private array $columns [];private array $relations [];public function __construct(private string $className,private string $tableName,) {}public function getClassName(): string { return $this-className; }public function getTableName(): string { return $this-tableName; }public function addColumn(ColumnMetadata $column): void{$this-columns[$column-name] $column;}public function getColumns(): array { return $this-columns; }public function getPrimaryKey(): ?ColumnMetadata{foreach ($this-columns as $col) {if ($col-primaryKey) return $col;}return null;}public function addRelation(string $name, array $config): void{$this-relations[$name] $config;}public function getRelations(): array { return $this-relations; }}// 注解驱动的元数据读取器#[Attribute(\Attribute::TARGET_CLASS)]class Entity{public function __construct(public string $table) {}}#[Attribute(\Attribute::TARGET_PROPERTY)]class Id {}#[Attribute(\Attribute::TARGET_PROPERTY)]class Column{public function __construct(public ?string $name null,public ?string $type null,public bool $nullable true,public mixed $default null,) {}}#[Attribute(\Attribute::TARGET_PROPERTY)]class OneToMany{public function __construct(public string $targetEntity,public string $mappedBy,) {}}#[Attribute(\Attribute::TARGET_PROPERTY)]class ManyToOne{public function __construct(public string $targetEntity,public string $inversedBy,) {}}#[Attribute(\Attribute::TARGET_PROPERTY)]class OneToOne{public function __construct(public string $targetEntity,public ?string $mappedBy null,) {}}#[Attribute(\Attribute::TARGET_PROPERTY)]class ManyToMany{public function __construct(public string $targetEntity,public string $joinTable,) {}}// Identity Mapclass IdentityMap{private array $objects [];public function set(string $className, mixed $id, object $object): void{$key $this-key($className, $id);$this-objects[$key] $object;}public function get(string $className, mixed $id): ?object{$key $this-key($className, $id);return $this-objects[$key] ?? null;}public function has(string $className, mixed $id): bool{return isset($this-objects[$this-key($className, $id)]);}public function remove(string $className, mixed $id): void{unset($this-objects[$this-key($className, $id)]);}public function clear(): void{$this-objects [];}private function key(string $className, mixed $id): string{return $className:$id;}}// Unit of Work (工作单元)enum ActionType{case CREATE;case UPDATE;case DELETE;}class UnitOfWork{private array $scheduled [];private array $inserts [];private array $updates [];private array $deletes [];public function __construct(private IdentityMap $identityMap,private ConnectionInterface $connection,) {}public function registerCreate(object $entity): void{$hash spl_object_hash($entity);$this-scheduled[$hash] [entity $entity, action ActionType::CREATE];}public function registerUpdate(object $entity): void{$hash spl_object_hash($entity);if (!isset($this-scheduled[$hash]) || $this-scheduled[$hash][action] ! ActionType::CREATE) {$this-scheduled[$hash] [entity $entity, action ActionType::UPDATE];}}public function registerDelete(object $entity): void{$hash spl_object_hash($entity);$this-scheduled[$hash] [entity $entity, action ActionType::DELETE];}public function commit(): void{$this-connection-beginTransaction();try {foreach ($this-scheduled as $hash $item) {match ($item[action]) {ActionType::CREATE $this-executeInsert($item[entity]),ActionType::UPDATE $this-executeUpdate($item[entity]),ActionType::DELETE $this-executeDelete($item[entity]),};unset($this-scheduled[$hash]);}$this-connection-commit();} catch (\Throwable $e) {$this-connection-rollback();throw $e;}}private function executeInsert(object $entity): void{$meta MetadataRegistry::get(get_class($entity));$data [];foreach ($meta-getColumns() as $col) {if ($col-autoIncrement) continue;$prop new \ReflectionProperty($entity, $col-name);$prop-setAccessible(true);$val $prop-getValue($entity);if ($val ! null || !$col-nullable) {$data[$col-name] $val;}}$columns implode(, , array_keys($data));$placeholders implode(, , array_map(fn($k) :$k, array_keys($data)));$sql INSERT INTO {$meta-getTableName()} ($columns) VALUES ($placeholders);$this-connection-execute($sql, $data);$pk $meta-getPrimaryKey();if ($pk $pk-autoIncrement) {$id $this-connection-lastInsertId();$prop new \ReflectionProperty($entity, $pk-name);$prop-setAccessible(true);$prop-setValue($entity, (int)$id);$this-identityMap-set(get_class($entity), (int)$id, $entity);}}private function executeUpdate(object $entity): void{$meta MetadataRegistry::get(get_class($entity));$pk $meta-getPrimaryKey();$pkProp new \ReflectionProperty($entity, $pk-name);$pkProp-setAccessible(true);$id $pkProp-getValue($entity);$data [];foreach ($meta-getColumns() as $col) {if ($col-primaryKey) continue;$prop new \ReflectionProperty($entity, $col-name);$prop-setAccessible(true);$data[$col-name] $prop-getValue($entity);}$sets implode(, , array_map(fn($k) $k :$k, array_keys($data)));$sql UPDATE {$meta-getTableName()} SET $sets WHERE {$pk-name} :pk_id;$data[pk_id] $id;$this-connection-execute($sql, $data);}private function executeDelete(object $entity): void{$meta MetadataRegistry::get(get_class($entity));$pk $meta-getPrimaryKey();$prop new \ReflectionProperty($entity, $pk-name);$prop-setAccessible(true);$id $prop-getValue($entity);$sql DELETE FROM {$meta-getTableName()} WHERE {$pk-name} :id;$this-connection-execute($sql, [id $id]);$this-identityMap-remove(get_class($entity), $id);}}// Metadata Registryclass MetadataRegistry{private static array $metadata [];public static function register(string $className, EntityMetadata $meta): void{self::$metadata[$className] $meta;}public static function get(string $className): EntityMetadata{if (!isset(self::$metadata[$className])) {self::buildMetadata($className);}return self::$metadata[$className];}private static function buildMetadata(string $className): void{$ref new \ReflectionClass($className);$entityAttr $ref-getAttributes(Entity::class);if (empty($entityAttr)) {throw new \RuntimeException(Class $className is not an entity);}$table $entityAttr[0]-newInstance()-table;$meta new EntityMetadata($className, $table);foreach ($ref-getProperties() as $prop) {$colAttr $prop-getAttributes(Column::class);$idAttr $prop-getAttributes(Id::class);if (!empty($idAttr) || !empty($colAttr)) {$name $prop-getName();$type ColumnType::STRING;$nullable true;$default null;$primary !empty($idAttr);if (!empty($colAttr)) {$col $colAttr[0]-newInstance();$name $col-name ?? $prop-getName();$nullable $col-nullable;$default $col-default;$type match ($col-type) {int, integer ColumnType::INTEGER,float ColumnType::FLOAT,bool, boolean ColumnType::BOOLEAN,datetime ColumnType::DATETIME,text ColumnType::TEXT,json ColumnType::JSON,default ColumnType::STRING,};}$meta-addColumn(new ColumnMetadata($name, $type, $primary, $primary, $nullable, $default));}// 关系注解$oneToMany $prop-getAttributes(OneToMany::class);$manyToOne $prop-getAttributes(ManyToOne::class);$oneToOne $prop-getAttributes(OneToOne::class);$manyToMany $prop-getAttributes(ManyToMany::class);if (!empty($oneToMany)) {$meta-addRelation($prop-getName(), [type one_to_many,target $oneToMany[0]-newInstance()-targetEntity,mapped_by $oneToMany[0]-newInstance()-mappedBy,]);} elseif (!empty($manyToOne)) {$meta-addRelation($prop-getName(), [type many_to_one,target $manyToOne[0]-newInstance()-targetEntity,inversed_by $manyToOne[0]-newInstance()-inversedBy,]);} elseif (!empty($oneToOne)) {$meta-addRelation($prop-getName(), [type one_to_one,target $oneToOne[0]-newInstance()-targetEntity,mapped_by $oneToOne[0]-newInstance()-mappedBy,]);} elseif (!empty($manyToMany)) {$meta-addRelation($prop-getName(), [type many_to_many,target $manyToMany[0]-newInstance()-targetEntity,join_table $manyToMany[0]-newInstance()-joinTable,]);}}self::$metadata[$className] $meta;}}// EntityManagerclass EntityManager{private IdentityMap $identityMap;private UnitOfWork $unitOfWork;private ConnectionInterface $connection;public function __construct(ConnectionInterface $connection){$this-connection $connection;$this-identityMap new IdentityMap();$this-unitOfWork new UnitOfWork($this-identityMap, $this-connection);}public function find(string $className, mixed $id): ?object{if ($this-identityMap-has($className, $id)) {return $this-identityMap-get($className, $id);}$meta MetadataRegistry::get($className);$pk $meta-getPrimaryKey();$qb new QueryBuilder();$qb-table($meta-getTableName())-where($pk-name, , $id);$rows $this-connection-query($qb-toSelectSql(), $qb-getParams());if (empty($rows)) return null;$entity $this-hydrate($className, $rows[0]);$this-identityMap-set($className, $id, $entity);return $entity;}public function findAll(string $className): array{$meta MetadataRegistry::get($className);$qb new QueryBuilder();$qb-table($meta-getTableName());$rows $this-connection-query($qb-toSelectSql(), $qb-getParams());$entities [];foreach ($rows as $row) {$entity $this-hydrate($className, $row);$this-identityMap-set($className, $row[$meta-getPrimaryKey()-name], $entity);$entities[] $entity;}return $entities;}public function findBy(string $className, array $criteria): array{$meta MetadataRegistry::get($className);$qb new QueryBuilder();$qb-table($meta-getTableName());foreach ($criteria as $col $val) {$qb-where($col, , $val);}$rows $this-connection-query($qb-toSelectSql(), $qb-getParams());$entities [];foreach ($rows as $row) {$entities[] $this-hydrate($className, $row);}return $entities;}public function persist(object $entity): void{$meta MetadataRegistry::get(get_class($entity));$pk $meta-getPrimaryKey();$prop new \ReflectionProperty($entity, $pk-name);$prop-setAccessible(true);$id $prop-getValue($entity);if ($this-identityMap-has(get_class($entity), $id)) {$this-unitOfWork-registerUpdate($entity);} else {$this-unitOfWork-registerCreate($entity);}}public function remove(object $entity): void{$this-unitOfWork-registerDelete($entity);}public function flush(): void{$this-unitOfWork-commit();}public function createQueryBuilder(): QueryBuilder{return new QueryBuilder();}// 原生查询public function nativeQuery(string $sql, array $params []): array{return $this-connection-query($sql, $params);}private function hydrate(string $className, array $data): object{$meta MetadataRegistry::get($className);$ref new \ReflectionClass($className);// 尝试构造函数注入$constructor $ref-getConstructor();$args [];if ($constructor) {foreach ($constructor-getParameters() as $param) {if (isset($data[$param-getName()])) {$args[] $data[$param-getName()];} elseif ($param-isDefaultValueAvailable()) {$args[] $param-getDefaultValue();} else {$args[] null;}}}$entity empty($args) ? $ref-newInstance() : $ref-newInstanceArgs($args);// 设置属性foreach ($meta-getColumns() as $col) {if (isset($data[$col-name])) {$prop $ref-getProperty($col-name);$prop-setAccessible(true);$value match ($col-type) {ColumnType::INTEGER (int)$data[$col-name],ColumnType::FLOAT (float)$data[$col-name],ColumnType::BOOLEAN (bool)$data[$col-name],ColumnType::JSON json_decode($data[$col-name], true),ColumnType::DATETIME new \DateTimeImmutable($data[$col-name]),default $data[$col-name],};$prop-setValue($entity, $value);}}return $entity;}public function __destruct(){// 自动提交未完成的工作try { $this-unitOfWork-commit(); } catch (\Throwable) {}}}// 实体模型示例#[Entity(table: users)]class User{#[Id] #[Column(type: int)]private int $id;#[Column(name: username, type: string)]private string $username;#[Column(name: email, type: string)]private string $email;#[Column(name: created_at, type: datetime, nullable: true)]private ?\DateTimeImmutable $createdAt null;#[OneToMany(targetEntity: Post::class, mappedBy: user)]private array $posts [];public function __construct(string $username , string $email ) {$this-username $username;$this-email $email;$this-createdAt new \DateTimeImmutable();}public function getId(): int { return $this-id; }public function getUsername(): string { return $this-username; }public function getEmail(): string { return $this-email; }}#[Entity(table: posts)]class Post{#[Id] #[Column(type: int)]private int $id;#[Column(type: string)]private string $title;#[Column(type: text)]private string $content;#[Column(name: user_id, type: int)]private int $userId;#[ManyToOne(targetEntity: User::class, inversedBy: posts)]private ?User $user null;public function __construct(string $title , string $content , int $userId 0) {$this-title $title;$this-content $content;$this-userId $userId;}public function getId(): int { return $this-id; }public function getTitle(): string { return $this-title; }}// 延迟加载代理class LazyLoadingProxy{private object $realInstance null;public function __construct(private string $className,private mixed $id,private EntityManager $em,) {}public function __call(string $method, array $args): mixed{$this-load();return $this-realInstance-$method(...$args);}public function __get(string $name): mixed{$this-load();return $this-realInstance-$name;}private function load(): void{if ($this-realInstance null) {$this-realInstance $this-em-find($this-className, $this-id);}}}// 使用示例$connection new PdoConnection(sqlite::memory:);$connection-execute(CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, email TEXT, created_at DATETIME));$connection-execute(CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, user_id INTEGER));$em new EntityManager($connection);// 持久化$user new User(alice, aliceexample.com);$em-persist($user);$em-flush();printf(Created user with id: %d\n, $user-getId());// 查询$found $em-find(User::class, 1);printf(Found user: %s (%s)\n, $found-getUsername(), $found-getEmail());// 添加更多$users [new User(bob, bobexample.com),new User(charlie, charlieexample.com),];foreach ($users as $u) {$em-persist($u);}$em-flush();$allUsers $em-findAll(User::class);printf(Total users: %d\n, count($allUsers));// 条件查询$admins $em-findBy(User::class, [username alice]);printf(Found by username: %d\n, count($admins));// ActorRecord 风格简化trait ActiveRecordTrait{private static ?EntityManager $em null;public static function setEntityManager(EntityManager $em): void{self::$em $em;}public function save(): void{self::$em-persist($this);self::$em-flush();}public function delete(): void{self::$em-remove($this);self::$em-flush();}public static function find(mixed $id): ?object{return self::$em-find(static::class, $id);}public static function all(): array{return self::$em-findAll(static::class);}public static function where(array $criteria): array{return self::$em-findBy(static::class, $criteria);}public function reload(): void{$meta MetadataRegistry::get(static::class);$pk $meta-getPrimaryKey();$prop new \ReflectionProperty($this, $pk-name);$prop-setAccessible(true);$id $prop-getValue($this);$fresh self::$em-find(static::class, $id);if ($fresh) {foreach ($meta-getColumns() as $col) {$p new \ReflectionProperty($this, $col-name);$p-setAccessible(true);$fp new \ReflectionProperty($fresh, $col-name);$fp-setAccessible(true);$p-setValue($this, $fp-getValue($fresh));}}}}