innmind / doctrine
消除不必要的状态的声明式方法
Requires
- php: ~8.2
- doctrine/orm: ^2.7
- innmind/immutable: ~4.0|~5.0
- innmind/specification: ~4.0
- ramsey/uuid: ^4.0
Requires (Dev)
- innmind/black-box: ^5.5.2
- innmind/coding-standard: ~2.0
- phpunit/phpunit: ~10.2
- scienta/doctrine-json-functions: ^4.3
- symfony/cache: ^5.0
- symfony/console: <7.0
- vimeo/psalm: ~5.6
Suggests
- scienta/doctrine-json-functions: To be able to search within a json field
README
重要
本项目是 Formal ORM 的一个跳板。它不再积极维护,将在某个时候存档。
这个库是对 Doctrine 的抽象,目的是消除所有隐式状态。
当代码库增长时,管理应用程序的状态可能会变得很困难,而状态(尤其是隐式状态)是应用程序中错误的一个来源。
Doctrine 通常在与数据库交互时是默认选择(并非事实),因为它捆绑了 Symfony。它的接口涵盖了众多用例,但暴露了许多隐式状态,例如事务或集合中的游标。
这里通过使用函数式编程范式的原则来解决这个问题。
安装
composer require innmind/doctrine
设计选择
点击展开
Sequence
与 Set
Set
已经被舍弃,因为无法保证从返回的集合中实体的唯一性。它还会阻止使用 map
函数,因为许多实体可能映射到单个新值,这可能导致新手的意外行为。这正是为什么选择 Sequence
的主要原因。
如果您真的想使用集合,您可以使用 innmind/immutable
。
强制使用 Id
Doctrine 允许您在实体持久化时为您生成一个 id。这是一个隐式状态改变。
为了避免这种隐式状态,您需要在持久化实体之前指定 id。这阻止了您依赖于数据库自动生成的 id,因为您无法避免冲突。
唯一的解决方案(据我所知)是使用 UUID
。这个库提供的 Id
使用它们,因此您不必再考虑这个问题。
所有实体的单个 Id
类
这不再是问题,因为它提供了由 vimeo/psalm
理解的模板。
在 Manager
上没有 flush
方法
可以在需要时自由调用 persist
和 flush
方法,这为您的代码库中的隐式状态打开了大门。您可能最终会无意中清除不需要持久化的实体(在错误发生之前的 persist
调用)或忘记清除持久化的实体(导致丢失状态更改)。
在这里,通过强制在给定上下文中执行所有突变(通过 Manager::mutate()
和 Manager::transaction()
)来避免这种情况。所以总是要么全部要么什么都没有。
用法
以下所有用例都使用了在 example
文件夹 中声明的代码。
所有用例的先决条件
use Innmind\Doctrine\{ Manager, Sort, }; use Example\Innmind\Doctrine\User; $manager = Manager::of($entityManager);
从数据库中获取所有实体
$manager ->repository(User::class) ->all() ->sort('username', Sort::asc) ->fetch() ->foreach(function(User $user): void { echo $user->username()."\n"; });
注意:查询被推迟到最后一刻,以便尽可能利用数据库。
分页
$numberOfElementPerPage = 10; $manager ->repository(User::class) ->all() ->sort('username', Sort::asc) ->drop($page * $numberOfElementPerPage) ->take($numberOfElementPerPage) ->fetch() ->foreach(function(User $user): void { echo $user->username()."\n"; });
过滤
它使用 Specification
模式(在库 innmind/specification
中标准化)。
use Example\Innmind\Doctrine\Username; $manager ->repository(User::class) ->matching( Username::of('alice')->or( Username::of('jane'), ), ) ->sort('username', Sort::asc) ->drop(20) ->take(10) ->fetch() ->foreach(function(User $user): void { echo $user->username()."\n"; });
本示例等同于以下SQL语句:SELECT * FROM user WHERE username = 'alice' OR username = 'jane' ORDER BY username OFFSET 20 LIMIT 10
。
注意:这一系列方法调用再次导致单个数据库调用。
添加新实体
use Innmind\Doctrine\Id; use Innmind\Immutable\Either; $user = $manager->mutate(function($manager): Either { $user = new User( Id::new(), 'someone', ); $manager ->repository(User::class) ->add($user); return Either::right($user); });
如果在函数外部尝试调用Repository::add()
或Repository::remove()
,将会抛出异常。
注意:如果函数抛出异常或返回Either::left
,则不会将任何内容写入数据库。
事务
$manager->transaction(function($manager, $flush): Either { $progress = 0; $repository = $manager->repository(User::class); foreach ($someSource as $args) { $repository->add(new User(...$args)); ++$progress; if ($progress % 20 === 0) { // flush entities to the database every 20 additions $flush(); } } return Either::right(null); });
注意:仅在导入的上下文中调用$flush
函数,因为它将所有实体从实体管理器中分离出来,这意味着如果保留了实体的引用,它们将不再被doctrine理解。
访问序列内的值
有时你可能想操作一个数组,使其可以与PHP函数如json_encode
一起使用。
use Symfony\Component\HttpFoundation\JsonResponse; /** @var list<array{username: string, registerIndex: int}> */ $data = $manager ->repository(User::class) ->all() ->sort('registerIndex', Sort::asc) ->fetch() ->map(static fn(User $user): array => [ 'username' => $user->username(), 'registerIndex' => $user->registerIndex(), ]) ->toList(); new JsonResponse($data);
根据关系进行过滤
您可以在规范属性字段中指定实体关系的属性。
use Example\Innmind\Doctrine\Child; $users = $manager ->repository(User::class) ->matching( Child::of('alice')->or( Child::of('jane'), ), );
Child
规范使用属性children.username
,从而指定用户的子用户的用户名。
注意:目前规范属性中只允许使用一层关系。
延迟加载集合
在某些情况下,要检索的实体数量可能不适合内存。为了仍然处理此类场景,您需要使用延迟的Sequence
并定期清除。
$_ = $manager ->repository(User::class) ->all() ->lazy() // instruct to load one entity at a time ->fetch() ->foreach(function($user) use ($manager) { doStuff($user); // this clear is important to make doctrine forget about the loaded // entities and will consequently free memory $manager->clear(); });
注意:但是,如果您在实体中使用双向关系,则此功能可能不起作用,因为不知道为什么,doctrine不会释放内存。
注意 2:您应该只在读取数据时使用此功能。在写入上下文中使用此功能可能会有意外的副作用!