djinorm / djin
Djin ORM 库
Requires
- php: >=7.2.0
- ext-json: *
- ext-mbstring: *
- psr/container: ^1.0
Requires (Dev)
- php-di/php-di: ^5.4
- phpunit/phpunit: ^7.1
- sebastian/comparator: ^2.0
Suggests
- php-di/php-di: Dependency injection container
- dev-master
- v8.x-dev
- v7.x-dev
- 7.5.3
- 7.5.1
- 7.5.0
- 7.4.3
- 7.4.2
- 7.4.1
- 7.4.0
- 7.3.1
- 7.3.0
- 7.2.0
- 7.1.1
- 7.1.0
- 7.0.0
- v6.x-dev
- 6.2.3
- 6.2.2
- 6.2.1
- 6.2.0
- 6.1.0
- 6.0.3
- 6.0.2
- 6.0.1
- 6.0.0
- 5.1.1
- 5.1.0
- 5.0.0
- 4.3.0
- 4.2.4
- 4.2.3
- 4.2.2
- 4.2.0
- 4.1.2
- 4.1.1
- 4.1.0
- 4.0.1
- 4.0.0
- 3.2.0
- 3.1.0
- 3.0.1
- 3.0.0
- 2.8.0
- 2.7.0
- 2.6.2
- 2.6.1
- 2.6.0
- 2.5.0
- 2.4.0
- 2.3.3
- 2.3.2
- 2.3.1
- 2.3.0
- 2.2.0
- 2.1.0
- 2.0.1
- 2.0.0
- 1.2.0
- 1.1.0
- 1.0.0
- 0.1.0
- dev-readme
This package is auto-updated.
Last update: 2024-09-16 01:09:57 UTC
README
轻量级 ORM,适用于与任何类型数据库交互,无论是关系型、文档型、列存储或键值存储。您可以完全控制数据的保存和检索方式。为此,只需创建模型的存储库即可。
安装
composer require djinorm/djin
前言
DjinORM 由一系列组件和接口组成,它们之间的交互可以封装其工作逻辑。
模型
模型是我们希望保存在数据库中的对象,是一个独立、完整的实体,可以包含任何嵌套对象和数组。任何实现 \DjinORM\Djin\Model\ModelInterface 接口的自定义类都可以作为模型。每个模型都必须有自己的唯一 Id,通过该 Id 将模型相互关联。
Id
Id 是包含模型唯一标识符的对象。每个模型都应该返回对象 \DjinORM\Djin\Id\Id,并且正是通过它将模型相互关联。在 PHP 中,所有对象都通过引用传递,因此我们可以为某个模型分配一个常量 Id,该 Id 将自动应用于所有其关联关系。示例
假设我们有两个简单的模型:User 和 Comment。用户可以发表评论,其中每个评论都应与用户关联。
用户模型
<?php use DjinORM\Djin\Id\Id; use DjinORM\Djin\Model\ModelInterface; class User implements ModelInterface { /** @var Id */ private $id; /** @var string */ private $name; public function __construct(string $name) { //Обратите внимание, мы создаем новый, пустой Id. Конкретное значение ему мы присвоим позже $this->id = new Id(); $this->name = $name; } /** * Реализацию данного метода требует ModelInterface * @return Id */ public function getId(): Id { return $this->id; } public function getName(): string { return $this->name; } /** * Реализацию данного метода требует ModelInterface * @return string */ public static function getModelName() : string { return 'user'; } }
评论模型
<?php use DjinORM\Djin\Id\Id; use DjinORM\Djin\Model\ModelInterface; class Comment implements ModelInterface { /** @var Id */ private $id; /** @var Id */ private $userId; /** @var string */ private $text; public function __construct(User $user, string $text) { $this->id = new Id(); $this->userId = $user->getId(); $this->text = $text; } /** * Реализацию данного метода требует ModelInterface * @return Id */ public function getId(): Id { return $this->id; } public function getUserId(): Id { return $this->userId; } public function getText(): string { return $this->text; } /** * Реализацию данного метода требует ModelInterface * @return string */ public static function getModelName() : string { return 'comment'; } }
现在让我们看看如何关联用户和评论
<?php $user = new User('Timur'); $comment = new Comment($user, 'Hello world!');
由于 PHP 中的对象是通过引用传递的,我们可以为我们的用户设置一个恒定的 Id,该 Id 将直接传递到评论中
<?php // Проставляем Id = 10 $user->getId()->setPermanentId(10); echo $user->getId()->toScalar(); //Выведет 10 echo $comment->getUserId()->toScalar(); //Также выведет 10
剩下的问题是谁以及如何设置 Id。关于这一点,我们稍后再谈。
IdGenerator
在 DjinORM 中,通过一个实现接口 \DjinORM\Djin\Id\IdGeneratorInterface 的单独组件设置永久 Id,该接口在将模型写入数据库之前直接分配 Id。
这里需要对那些只使用过 MySQL 并习惯了数据库自动分配 Id 的人来说说。在 DjinORM 中,这种方法是不可行的,但这并没有什么不好。相反,您可以使用任何计数器,例如 Redis、PostgreSQL 中的 sequences、UUID 字符串,或者甚至使用类似于 SELECT MAX(id) FROM table
的查询来为每个生成分配(尽管这样做并不好,因为它会导致锁定)。
ORM 包含 3 个现成的 Id-生成器(尽管您也可以创建自己的)
- \DjinORM\Djin\Id\UuidGenerator - 不需要任何外部解决方案来生成 Id,返回一个随机的 128 位字符串,例如
550e8400-e29b-41d4-a716-446655440000
。请谨慎使用,因为使用字符串作为 Id 会极大地增加数据库的大小。 - \DjinORM\Djin\Id\RedisIdGenerator - 我们正是使用它。它需要 ext-redis 以及在 redis 失去数据时,使用类似于
SELECT MAX(id) FROM table
的机制来恢复计数器当前状态的机制。 - \DjinORM\Djin\Id\MemoryIdGenerator - 非常适合用于测试。从指定的数字开始连续生成数字,将状态存储在内存中的变量中
- \DjinORM\Djin\Id\IdGeneratorInterface - 生成器的接口。只有一个方法
IdGeneratorInterface::getNextId(ModelInterface $model)
,它接收一个模型作为输入,返回一个表示 Id 的标量值,同时不设置模型本身的 Id。通过这种方法,您可以根据模型的类和状态生成 Id
仓库
仓库中包含所有用于在数据库中查找、提取和保存模型的“脏活”。每个仓库都必须实现接口 \DjinORM\Djin\Repository\RepositoryInterface。正是仓库具有搜索、保存和删除模型的方法。它知道您正在与哪个数据库工作,以及如何将您的模型转换为用于保存到数据库的数据数组,以及如何将此数组转换回您的模型。正是仓库负责设置永久 Id,因此 IdGenerator 应通过构造函数作为依赖项传递。
假设为此目的使用了 反射,您可以使用它来访问类的私有属性,获取和修改它们的值,创建对象而不调用其构造函数,等等。为了简化 DjinORM 中通过反射处理模型的工作,存在一个特殊的辅助器 \DjinORM\Djin\Helpers\RepoHelper。
使用它,我们可以轻松地将我们的模型 Comment 转换为普通数组
<?php use \DjinORM\Djin\Helpers\RepoHelper; $user = new User('Timur'); $comment = new Comment($user, 'Hello world!'); $data = [ 'id' => RepoHelper::getProperty($comment, 'id')->toScalar(), 'userId' => RepoHelper::getProperty($comment, 'userId')->toScalar(), 'text' => RepoHelper::getProperty($comment, 'text'), ];
以及将从数据库中提取的数组转换回模型
<?php use \DjinORM\Djin\Helpers\RepoHelper; use \DjinORM\Djin\Id\Id; $data = [ 'id' => 1, 'userID' => 10, 'text' => 'Hello world!', ]; $comment = RepoHelper::newWithoutConstructor(Comment::class); RepoHelper::setProperty($comment, 'id', new Id($data['id'])); RepoHelper::setProperty($comment, 'userId', new Id($data['userId'])); RepoHelper::setProperty($comment, 'text', $data['text']);
这种方法的优点是您可以完全控制数据的存储:您可以提取任何级别的嵌套对象,如何转换数据以记录到数据库,以及如何对数据进行操作。在某些情况下,这种方法是合理的,但需要大量的代码,尤其是在您需要转换复杂对象并检查某些值的存在时。此外,通常情况下,一些复杂的对象会作为嵌套在不同模型中。
关于如何解决这个问题,请参阅下一节。
Hydrator & Mapper
首先,我们应该考虑映射器。假设我们决定让上面的示例中的评论可以由匿名用户创建。也就是说,评论没有作者。在这种情况下,评论的 userId 字段可以是 Id,也可以是 null。在这种情况下,我们每次都必须检查 userId
是否为 null
,并据此创建/提取 Id 的值或保持 null
。现在想象一下,除了评论,我们还有像 Post、Role 以及许多其他模型,它们也使用 Id。因此,将转换 Id 的逻辑放到某个地方是合理的。这正是映射器出现的原因。
映射器
映射器可以将特定类型的数据转换为标量表示或数组,以及将标量数据转换为所需类型,在内部执行所有转换和 null 检查。每个映射器都必须实现接口 \DjinORM\Djin\Hydrator\Mappers\MapperInterface - 查看此接口的代码,这将使您了解很多
Djin 提供了一组典型的映射器,可以转换大多数常见的数据类型
- \DjinORM\Djin\Hydrator\Mappers\IdMapper - 将 Id 的标量表示转换为 Id 对象,以及反向转换
- \DjinORM\Djin\Hydrator\Mappers\ArrayMapper - 数组映射器
- \DjinORM\Djin\Hydrator\Mappers\BoolMapper - 布尔值映射器
- \DjinORM\Djin\Hydrator\Mappers\DatetimeMapper - DateTime和DateTimeImmutable值映射器
- \DjinORM\Djin\Hydrator\Mappers\DeepIdentityMapper - 复杂组合结构映射器,允许保存和从数据库中提取任何值集。实际上,这是serialize()的类似物,只是除了在设置映射器时保存类名外,您还指定了类名别名。这种方法允许您在不担心先前存储库无法重新创建模型或对象的情况下安全地重命名类和更改命名空间
- \DjinORM\Djin\Hydrator\Mappers\FloatMapper - 实数映射器
- \DjinORM\Djin\Hydrator\Mappers\IntMapper - 整数映射器
- \DjinORM\Djin\Hydrator\Mappers\NestedMapper - 组合映射器,可以向其中传递任何其他映射器集以转换复杂组合对象
- \DjinORM\Djin\Hydrator\Mappers\NestedArrayMapper - 组合映射器,可以向其中传递任何其他映射器集以转换复杂的组合数组
- \DjinORM\Djin\Hydrator\Mappers\RelationMapper - 关系映射器(以下将介绍)
- \DjinORM\Djin\Hydrator\Mappers\StringMapper - 字符串值映射器
- \DjinORM\Djin\Hydrator\Mappers\ValueObjectMapper - 值对象映射器,用于某些对象仅包含一个值的情况,并用作简单的OO包装
实体解析器
因此,我们有映射器可以转换特定类型的数据,但它们本身是无用的。它们需要一个管理它们的组件。为此,有\DjinORM\Djin\Hydrator\Hydrator,它负责将复杂对象转换为简单数组以及相反。此外,实体解析器包含映射器的点符号方案,并可以根据其符号返回映射器的实例。这在需要在存储库中进一步处理数据时很有用。
例如,下面是针对User和Comment模型的实体解析器
<?php use DjinORM\Djin\Hydrator\Hydrator; use DjinORM\Djin\Hydrator\Mappers\IdMapper; use DjinORM\Djin\Hydrator\Mappers\StringMapper; $userHydrator = new Hydrator(User::class, [ new IdMapper('id'), new StringMapper('name'), ]); $commentHydrator = new Hydrator(Comment::class, [ new IdMapper('id'), new IdMapper('userId'), new StringMapper('text'), ]);
因此,我们的存储库现在可能看起来像这样
<?php use DjinORM\Djin\Hydrator\Hydrator; use DjinORM\Djin\Hydrator\Mappers\IdMapper; use DjinORM\Djin\Hydrator\Mappers\StringMapper; class UserRepo implements \DjinORM\Djin\Repository\RepositoryInterface { const TABLE_NAME = 'users'; private $hydrator; public function __construct() { $this->hydrator = new Hydrator(User::class, [ new IdMapper('id'), new StringMapper('name'), ]); } public function findById($id) : ?ModelInterface { $sql = "SELECT * FROM {$this::TABLE_NAME} WHERE id = {$id}"; //Это лишь пример. Всегда используйте биндинги! ... $data = some_function_that_fetch_data($sql); //Здесь простой массив из базы будет превращен в объект User $model = $this->hydrator->hydrate($data); return $model; } ... public function insert(ModelInterface $model) { //Здесь модель будет превращена в обычный массив $data = $this->hydrator->extract($model); // some_function_make_insert_sql($data); } ... } class CommentRepo implements \DjinORM\Djin\Repository\RepositoryInterface { const TABLE_NAME = 'comments'; private $hydrator; public function __construct() { $this->hydrator = new Hydrator(Comment::class, [ new IdMapper('id'), new IdMapper('userId'), new StringMapper('text'), ]); } public function findById($id) : ?ModelInterface { $sql = "SELECT * FROM {$this::TABLE_NAME} WHERE id = {$id}"; //Это лишь пример. Всегда используйте биндинги! ... $data = some_function_that_fetch_data($sql); //Здесь простой массив из базы будет превращен в объект Comment $model = $this->hydrator->hydrate($data); return $model; } ... public function insert(ModelInterface $model) { //Здесь модель будет превращена в обычный массив $data = $this->hydrator->extract($model); // some_function_make_insert_sql($data); } ... }
当然,在真实的项目中,您不仅可以使用SQL数据库,还可以使用任何其他数据库。您可以将通用逻辑放入抽象存储库中,并对代码进行优化。例如,您可以查看SQL存储库djin-repo-sql
在存储库中我们可以做任何事情:例如,在关系数据库的情况下,我们可以将嵌套对象保存到其他表,将它们转换为JSON或将它们展开为点符号表示的单独字段。您自己控制数据如何存储。
模型管理器
组件 \DjinORM\Djin\Manager\ModelManager 的任务是整合所有功能。正是它最大限度地简化了与模型的实际工作,将模型、仓库、协调数据保存等功能联系起来。
ModelManager 构造函数可以传递 4 个参数
- PSR 兼容的容器(必需),它可以根据仓库类名称返回对象。例如,PHP-DI
- callable onBeforeCommit(ModelManager $manager, array $modelsToSave, array $modelsToDelete),在其中可以开始您的数据库事务
- callable onAfterCommit(ModelManager $manager, array $modelsToSave, array $modelsToDelete),在其中可以提交您的数据库事务
- callable onCommitException(ModelManager $manager, array $modelsToSave, array $modelsToDelete),在其中可以回滚您的数据库事务
此外,我们需要配置 ModelManager,使其知道它将与之合作的仓库和模型。为此,我们传递给它仓库和模型或模型数组,仓库可以与之一起工作。当仓库可以保存多个不同模型,或继承自某个通用模型时,这很有用
<?php $manager = new \DjinORM\Djin\Manager\ModelManager($container); $manager->setModelRepository(UserRepo::class, User::class); $manager->setModelRepository(CommentRepo::class, [Comment::class]);
如果我们的仓库只保存一个模型,则可以省略模型类指示。模型将根据 \DjinORM\Djin\Repository\RepositoryInterface::getModelClass
接口定义
<?php $manager = new \DjinORM\Djin\Manager\ModelManager($container); $manager->setModelRepository(UserRepo::class); $manager->setModelRepository(CommentRepo::class);
现在我们可以以类似以下的方式处理模型
<?php $manager = new \DjinORM\Djin\Manager\ModelManager($container); ... $user = new User('Timur'); $comment = new Comment($user, 'Hello world'); //Подготовит модели для сохранения в БД, но не запишет их в БД $manager->persists([$user, $comment]); //Если вдруг мы передумали сохранять модель, то можно вызвать метод delete(), //который отменит сохранение новой, только что созданной модели, либо удалит //уже существующую в базе модель $manager->delete($comment); //Достанет подготовленные для сохранения модели, вызовет методы репозиториев, //проставляющие перманентные Id, выполнит onBeforeCommit, запишет модели //в БД, и вызовет onAfterCommit или onCommitException в зависимости от результата $manager->commit(); //Если же мы хотим найти какую-то модель, то мы можем сначала достать ее репозиторий // несколькими способами. Напримр так $userRepo = $manager->getModelRepository(User::class); //или так $userRepo = $manager->getModelRepository($user); //или так $userRepo = $manager->getRepositoryByModelName(User::getModelName()); //Находим пользователя. Это будет объект User $user = $userRepo->findById(1); //Изменяем пользователя $user->setName('Anonim'); //Сохраняем изменения $manager->persists($user); $manager->commit(); //Удаляем пользователя. Вызов delete(), как и persists() не осуществляет запись в БД. //Для фактического удаления модели из БД нужно вызвать commit(); $manager->delete($user); //Реально удаляем запись из БД $manager->commit();
关系
使用 Id 对象进行关联操作快速且高效。但 Id 对象仅在创建记录时按引用传递。如果 User::$id 和 Comment::$userId 在创建时引用同一个 Id 对象,则在后续会话中从数据库提取它们时,它们将引用具有相同永久值的不同 Id 对象。通常,这不会造成问题,因为 Id 的值无法更改。但 Id 本身与模型没有任何关联。例如,在作者可能是 User 或 Bot 的情况下,我们如何确定评论的作者是谁呢?
正是为了解决这类问题,存在 \DjinORM\Djin\Model\Relation。让我们重写 Comment,使其可以作为作者接受任何人
<?php use DjinORM\Djin\Id\Id; use DjinORM\Djin\Model\ModelInterface; use DjinORM\Djin\Model\Relation; class Comment implements ModelInterface { /** @var Id */ private $id; /** @var Relation */ private $author; /** @var string */ private $text; public function __construct(ModelInterface $author, string $text) { $this->id = new Id(); $this->author = Relation::link($author); $this->text = $text; } /** * Реализацию данного метода требует ModelInterface * @return Id */ public function getId(): Id { return $this->id; } public function getAuthor(): Relation { return $this->author; } public function getText(): string { return $this->text; } /** * Реализацию данного метода требует ModelInterface * @return string */ public static function getModelName() : string { return 'comment'; } }
关系包含模型 ModelInterface::getId()
的值和 ModelInterface::getModelName()
中的名称。对于关系,也有专门的映射器。
现在,通过关系,我们可以轻松地找到评论的作者,无论他是何种模型
<?php $manager = new \DjinORM\Djin\Manager\ModelManager($container); $commentRepo = $manager->getModelRepository(Comment::class); $comment = $commentRepo->findById(10); $author = $manager->findRelation($comment->getAuthor());