innmind/doctrine

消除不必要的状态的声明式方法

3.0.0 2024-07-07 12:42 UTC

This package is auto-updated.

Last update: 2024-09-07 13:01:19 UTC


README

Build Status codecov Type Coverage

重要

本项目是 Formal ORM 的一个跳板。它不再积极维护,将在某个时候存档。

这个库是对 Doctrine 的抽象,目的是消除所有隐式状态。

当代码库增长时,管理应用程序的状态可能会变得很困难,而状态(尤其是隐式状态)是应用程序中错误的一个来源。

Doctrine 通常在与数据库交互时是默认选择(并非事实),因为它捆绑了 Symfony。它的接口涵盖了众多用例,但暴露了许多隐式状态,例如事务或集合中的游标。

这里通过使用函数式编程范式的原则来解决这个问题。

安装

composer require innmind/doctrine

设计选择

点击展开

SequenceSet

Set 已经被舍弃,因为无法保证从返回的集合中实体的唯一性。它还会阻止使用 map 函数,因为许多实体可能映射到单个新值,这可能导致新手的意外行为。这正是为什么选择 Sequence 的主要原因。

如果您真的想使用集合,您可以使用 innmind/immutable

强制使用 Id

Doctrine 允许您在实体持久化时为您生成一个 id。这是一个隐式状态改变。

为了避免这种隐式状态,您需要在持久化实体之前指定 id。这阻止了您依赖于数据库自动生成的 id,因为您无法避免冲突。

唯一的解决方案(据我所知)是使用 UUID。这个库提供的 Id 使用它们,因此您不必再考虑这个问题。

所有实体的单个 Id

这不再是问题,因为它提供了由 vimeo/psalm 理解的模板。

Manager 上没有 flush 方法

可以在需要时自由调用 persistflush 方法,这为您的代码库中的隐式状态打开了大门。您可能最终会无意中清除不需要持久化的实体(在错误发生之前的 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:您应该只在读取数据时使用此功能。在写入上下文中使用此功能可能会有意外的副作用!