Skip to content

实体、查找器和存储库#

在 XF2 中,有多种方式与数据进行交互。在 XF1 中,这主要是在模型文件中编写原始 SQL 语句。XF2 的方法已经远离了这一点,我们添加了许多新的方式来替代它。我们首先来看执行数据库查询的首选方法 —— 查找器。

查找器#

我们引入了一个新的“查找器”系统,允许以面向对象的方式逐步构建查询,从而无需编写原始的数据库查询。查找器系统与实体系统紧密配合,我们将在下面详细讨论实体系统。传递给查找器方法的第一个参数是你想要处理的实体的短类名。让我们将上面提到的一些查询转换为使用查找器系统。例如,访问单个用户记录:

PHP
$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->fetchOne();

直接查询方法和使用查找器之间的主要区别之一是,查找器返回的数据的基本单位不是数组。在调用 fetchOne 方法(仅从数据库中返回单行)的查找器对象的情况下,将返回一个实体对象。

让我们看一个稍微不同的方法,它将返回多行:

PHP
$finder = \XF::finder('XF:User');
$users = $finder->limit(10)->fetch();

此示例将从 xf_user 表中查询 10 条记录,并将它们作为 ArrayCollection 对象返回。这是一个特殊的对象,其行为类似于数组,因为它是可遍历的(你可以循环遍历它),并且它有一些特殊的方法可以告诉你它有多少个条目,按某些值分组,或其他类似数组的操作,如过滤、合并、获取第一个或最后一个条目等。

查找器查询通常应预期从表中检索所有列,因此没有特定的等效方法来仅获取某些列的某些值。

相反,要获取单个值,你只需获取一个实体并直接从该实体中读取值:

PHP
$finder = \XF::finder('XF:User');
$username = $finder->where('user_id', 1)->fetchOne()->username;

同样,要从单个列中获取值的数组,你可以使用 pluckFrom 方法:

PHP
$finder = \XF::finder('XF:User');
$usernames = $finder->limit(10)->pluckFrom('username')->fetch();

到目前为止,我们已经看到查找器应用了一些简单的 where 和 limit 约束。因此,让我们更详细地看一下查找器,包括 where 方法本身的更多细节。

where 方法#

where 方法最多支持三个参数。第一个是条件本身,例如你正在查询的列。第二个通常是运算符。第三个是要搜索的值。如果你只提供两个参数,如上所示,那么它自动意味着运算符是 =。以下是其他有效的运算符列表:

  • =
  • <>
  • !=
  • >
  • >=
  • <
  • <=
  • LIKE
  • BETWEEN

因此,我们可以获取过去 7 天内注册的有效用户列表:

PHP
$finder = \XF::finder('XF:User');
$users = $finder->where('user_state', 'valid')->where('register_date', '>=', time() - 86400 * 7)->fetch();

如你所见,你可以多次调用 where 方法,但除此之外,你可以选择将数组作为该方法的唯一参数传递,并在一次调用中构建你的条件。数组方法支持两种类型,我们可以在上面构建的查询中使用这两种类型:

PHP
$finder = \XF::finder('XF:User');
$users = $finder->where([
    'user_state' => 'valid',
    ['register_date', '>=', time() - 86400 * 7]
])
->fetch();

通常不建议或不清楚像这样混合使用,但它确实在某种程度上展示了该方法的灵活性。现在条件在数组中,我们可以指定列名(作为数组键)和值以隐含 = 运算符,或者我们可以实际定义另一个包含列、运算符和值的数组。

whereOr 方法#

在上面的示例中,两个条件都需要满足,即每个条件都由 AND 运算符连接。然而,有时只需要满足部分条件,这可以通过使用 whereOr 方法来实现。例如,如果你想搜索无效或发布消息为零的用户,可以按如下方式构建:

PHP
$finder = \XF::finder('XF:User');
$users = $finder->whereOr(
    ['user_state', '<>', 'valid'],
    ['message_count', 0]
)->fetch();

类似于上一节中的示例,除了将最多两个条件作为单独的参数传递外,你还可以将条件数组传递给第一个参数:

PHP
$finder = \XF::finder('XF:User');
$users = $finder->whereOr([
    ['user_state', '<>', 'valid'],
    ['message_count', 0],
    ['is_banned', 1]
])->fetch();

with 方法#

with 方法本质上等同于使用 INNER|LEFT JOIN 语法,尽管它依赖于实体是否定义了“关系”。我们不会在下一页中讨论这一点,但这应该让你了解它的工作原理。让我们现在使用 Thread 查找器来检索特定线程:

PHP
$finder = \XF::finder('XF:Thread');
$thread = $finder->with('Forum', true)->where('thread_id', 123)->fetchOne();

此查询将获取 thread_id = 123 的 Thread 实体,但它还会在后台与 xf_forum 表进行连接。在控制如何进行 INNER JOIN 而不是 LEFT JOIN 方面,这就是第二个参数的用途。在这种情况下,我们将“必须存在”参数设置为 true,因此它将连接语法切换为使用 INNER 而不是默认的 LEFT

我们将在下一节中详细介绍如何访问从此连接中获取的数据。

还可以将关系数组传递给 with 方法以进行多次连接。

PHP
$finder = \XF::finder('XF:Thread');
$thread = $finder->with(['Forum', 'User'], true)->where('thread_id', 123)->fetchOne();

这将连接到 xf_user 表以获取线程作者。然而,第二个参数仍然是 true,我们可能不需要对用户连接进行 INNER 连接,因此我们可以改为链接方法:

PHP
$finder = \XF::finder('XF:Thread');
$thread = $finder->with('Forum', true)->with('User')->where('thread_id', 123)->fetchOne();

order、limit 和 limitByPage 方法#

order 方法#

此方法允许你修改查询,以便按特定顺序获取结果。它接受两个参数,第一个是列名,第二个是可选的排序方向。因此,如果你想列出消息最多的 10 个用户,可以按如下方式构建查询:

PHP
$finder = \XF::finder('XF:User');
$users = $finder->order('message_count', 'DESC')->limit(10);

注意

现在可能是提到查找器方法可以以任何顺序调用的好时机。例如:$threads = $finder->limit(10)->where('thread_id', '>', 123)->order('post_date')->with('User')->fetch(); 尽管如果你以这种顺序编写 MySQL 查询,你肯定会遇到一些语法问题,但查找器系统仍将以正确的顺序构建它,上面的代码虽然看起来很奇怪且可能不推荐,但完全有效。

与标准 MySQL 查询一样,可以按多列对结果集进行排序。为此,你可以再次调用 order 方法。还可以使用数组将多个排序子句传递给 order 方法。

PHP
$finder = \XF::finder('XF:User');
$users = $finder->order('message_count', 'DESC')->order('register_date')->limit(10);

limit 方法#

我们已经看到了如何将查询限制为返回特定数量的记录:

PHP
$finder = \XF::finder('XF:User');
$users = $finder->limit(10)->fetch();

然而,实际上有一种替代直接调用 limit 方法的方法:

PHP
$finder = \XF::finder('XF:User');
$users = $finder->fetch(10);

你可以直接将限制传递给 fetch() 方法。还值得注意的是,limit(和 fetch)方法支持两个参数。第一个显然是限制,第二个是偏移量。

PHP
$finder = \XF::finder('XF:User');
$users = $finder->limit(10, 100)->fetch();

这里的偏移量值本质上意味着前 100 个结果将被丢弃,之后的 10 个结果将被返回。这种方法对于提供分页结果很有用,尽管我们实际上还有一种更简单的方法来做到这一点...

limitByPage 方法#

此方法是一种辅助方法,它根据你当前查看的“页面”和你需要的“每页”数量来设置适当的限制和偏移量。

PHP
$finder = \XF::finder('XF:User');
$users = $finder->limitByPage(3, 20);

在这种情况下,限制将设置为 20(这是我们的每页值),偏移量将设置为 40,因为我们从第 3 页开始。

有时,我们需要获取比限制更多的数据。过度获取有助于检测在当前页面之后是否有更多数据要显示,或者如果你需要根据权限过滤初始结果集。我们可以使用第三个参数来做到这一点:

PHP
$finder = \XF::finder('XF:User');
$users = $finder->limitByPage(3, 20, 1);

这将获取最多 21 个用户(20 + 1),从第 3 页开始。

getQuery 方法#

当你第一次开始使用查找器时,尽管它很直观,但你可能偶尔会想知道你是否正确使用它,以及它是否会构建你期望的查询。我们有一个名为 getQuery 的方法,它可以告诉我们当前查找器对象将构建的查询。例如:

PHP
$finder = \XF::finder('XF:User')
    ->where('user_id', 1);

\XF::dumpSimple($finder->getQuery());

这将输出类似于以下内容:

Dump
string(67) "SELECT `xf_user`.*
FROM `xf_user`
WHERE (`xf_user`.`user_id` = 1)"

你可能不会经常需要它,但如果查找器没有返回你预期的结果,它会很有用。有关 dumpSimple 方法的更多信息,请参阅 Dump a variable 部分。

自定义查找器方法#

到目前为止,我们已经看到查找器对象使用类似于 XF:UserXF:Thread 的参数进行设置。在大多数情况下,这标识了查找器正在处理的实体类,并将解析为例如 XF\Entity\User。然而,它还可以表示一个查找器类。查找器类是可选的,但它们作为一种向特定查找器类型添加自定义查找器方法的方式。要查看此操作,让我们看一下与 XF:User 相关的查找器类,它可以在 XF\Finder\User 类中找到。

以下是该类中的一个示例查找器方法:

PHP
public function isRecentlyActive($days = 180)
{
    $this->where('last_activity', '>', time() - ($days * 86400));
    return $this;
}

这使我们能够在任何用户查找器对象上调用该方法。因此,如果我们采用前面的示例:

PHP
$finder = \XF::finder('XF:User');
$users = $finder->isRecentlyActive(20)->order('message_count', 'DESC')->limit(10);

此查询,之前只是按消息数量降序返回 10 个用户,现在将返回过去 20 天内最近活跃的 10 个用户。

即使对于许多实体类型,查找器类不存在,仍然可以以与 扩展类 部分中提到的相同方式扩展这些不存在的类。

实体系统#

如果你熟悉 XF1,你可能熟悉实体背后的一些概念,因为它们最终源自那里的 DataWriter 系统。如果你不太熟悉它们,以下部分应该会给你一些概念。

实体结构#

Structure 对象由许多属性组成,这些属性定义了实体的结构及其相关的数据库表。结构对象本身是在其相关的实体中设置的。让我们看一下用户实体中的一些常见属性:

#

PHP
$structure->table = 'xf_user';

这告诉实体在更新和插入记录时使用哪个数据库表,并告诉查找器在构建要执行的查询时从哪个表读取。此外,它还在知道查询需要连接到哪些其他表方面发挥作用。

短名称#

PHP
$structure->shortName = 'XF:User';

这只是实体本身和查找器类(如果适用)的短类名。

内容类型#

PHP
$structure->contentType = 'user';

这定义了此实体表示的内容类型。在大多数实体结构中不需要此属性。它用于连接到“内容类型”系统使用的特定内容(将在另一节中介绍)。

主键#

PHP
$structure->primaryKey = 'user_id';

定义表示数据库表中主键的列。如果表支持多个列作为主键,则可以将其定义为数组。

#

PHP
$structure->columns = [
    'user_id' => ['type' => self::UINT, 'autoIncrement' => true, 'nullable' => true, 'changeLog' => false],
    'username' => ['type' => self::STR, 'maxLength' => 50,
        'required' => 'please_enter_valid_name'
    ]
    // 以及更多列 ...
];

这是实体配置的关键部分,因为它详细说明了实体负责的每个数据库列的具体信息。这告诉我们预期的数据类型,是否需要值,它应该匹配的格式,它是否应该是唯一值,它的默认值是什么,等等。

根据 type,实体管理器知道是否以某种方式编码或解码值。这可能是一个将值转换为字符串或整数的简单过程,或者稍微复杂一些,例如在写入数据库时对数组使用 json_encode(),或在从数据库读取时对 JSON 字符串使用 json_decode(),以便将值正确返回给实体对象作为数组,而无需我们手动执行此操作。它还可以支持逗号分隔的值被适当地编码/解码。

有时需要在写入之前对值进行一些额外的验证或修改。例如,在用户实体中,查看 verifyStyleId() 方法。当在 style_id 字段上设置值时,我们会自动检查是否存在名为 verifyStyleId() 的方法,如果存在,我们首先将值通过该方法运行。

行为#

PHP
$structure->behaviors = [
    'XF:ChangeLoggable' => []
];

这是此实体应使用的行为类的数组。行为类是一种允许某些代码在多个实体类型之间通用重用的方式(仅在实体更改时,而不是在读取时)。一个很好的例子是 XF:Likeable 行为,它能够自动执行某些操作,以支持可以“点赞”的内容的实体。这包括在内容中发生可见性更改时自动重新计算计数,以及在删除内容时自动删除点赞。

获取器#

PHP
$structure->getters = [
    'is_super_admin' => true,
    'last_activity' => true
];

当调用命名字段时,会自动调用获取器方法。例如,如果我们从用户实体请求 is_super_admin,这将自动检查并使用 getIsSuperAdmin() 方法。值得注意的是,xf_user 表实际上没有名为 is_super_admin 的字段。这实际上存在于 Admin 实体上,但我们已将其添加为获取器方法,作为访问该值的简写方式。获取器方法还可以用于直接覆盖现有字段的值,这就是 last_activity 值的情况。last_activity 实际上是一个缓存值,通常在用户注销时更新。然而,我们将用户的最新活动日期存储在 xf_session_activity 表中,因此我们可以使用此 getLastActivity 方法返回该值,而不是缓存的最后活动值。如果你曾经需要完全绕过获取器方法,只需获取真正的实体值,只需在列名后加上下划线,例如 $user->last_activity\_

因为实体就像任何其他 PHP 对象一样,你可以向它们添加更多方法。一个常见的用例是添加可以在实体本身上调用的权限检查方法。

关系#

PHP
$structure->relations = [
    'Admin' => [
        'entity' => 'XF:Admin',
        'type' => self::TO_ONE,
        'conditions' => 'user_id',
        'primary' => true
    ]
];

这就是定义关系的方式。什么是关系?它们定义了实体之间的关系,可用于执行与其他表的连接查询或在实体上动态获取相关记录。如果我们记得查找器上的 with 方法,如果我们想要获取特定用户并预先获取用户的 Admin 记录(如果存在),那么我们可以执行以下操作:

PHP
$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->with('Admin')->fetchOne();

这将使用用户实体中为 Admin 关系定义的信息以及 XF:Admin 实体结构的详细信息,以了解此用户查询应在 xf_admin 表和 user_id 列上执行 LEFT JOIN。要从用户实体访问管理员最后登录日期:

PHP
$lastLogin = $user->Admin->last_login; // 返回最后管理员登录的时间戳

然而,并不总是需要在查找器中执行连接以获取实体的相关信息。例如,如果我们采用上面的示例而不调用 with 方法:

PHP
$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->fetchOne();
$lastLogin = $user->Admin->last_login; // 返回最后管理员登录的时间戳

我们仍然可以在此处获取 last_login 值。它通过执行额外的查询来动态获取 Admin 实体。

上面的示例使用了 TO_ONE 类型,因此此关系将一个实体与另一个实体相关联。我们还有一个 TO_MANY 类型。

无法获取整个 TO_MANY 关系(例如,使用连接 / 查找器上的 with 方法),但以查询为代价,可以随时动态读取它,例如在最后的 last_login 示例中。

用户实体上定义的一个这样的关系是 ConnectedAccounts 关系:

PHP
$structure->relations = [
    'ConnectedAccounts' => [
        'entity' => 'XF:UserConnectedAccount',
        'type' => self::TO_MANY,
        'conditions' => 'user_id',
        'key' => 'provider'
    ]
];

此关系能够返回与当前用户 ID 匹配的 xf_user_connected_account 表中的记录作为 FinderCollection。这类似于我们在 查找器 部分中提到的 ArrayCollection 对象。关系定义指定集合应按 provider 字段进行键控。

尽管无法在执行查找器查询时获取多条记录,但可以使用 TO_MANY 关系从该关系中获取单条记录。

关系(Finder):关系能够返回与当前用户ID匹配的xf_user_connected_account表中的记录,以FinderCollection的形式返回。这类似于我们在[查找器(Finder)]部分提到的ArrayCollection对象。关系定义指定集合应由provider字段进行键名。

虽然在查找操作中无法获取多条记录,但可以使用TO_MANY关系从该关系中获取单条记录。例如,如果想查看用户是否与特定的连接账户提供者相关联,可以在查询时获取:

PHP
$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->with('ConnectedAccounts|facebook')->fetchOne();

选项(Options)

PHP
$structure->options = [
    'custom_title_disallowed' => preg_split('/\r?\n/', $options->disallowedCustomTitles),
    'admin_edit' => false,
    'skip_email_confirm' => false
];
实体选项是在满足某些条件时修改实体行为的方式。例如,如果将admin_edit设置为true(即在管理控制台编辑用户时),则允许用户邮箱为空的检查将被跳过。

实体生命周期: Entity在数据库中管理记录的生命周期方面发挥重要作用。除了读取和写入值外,还可以使用Entity删除记录,并在这些操作发生时触发特定事件,以便执行某些任务或更新相关记录。以下是实体保存时发生的一些事件:

  • _preSave():在保存过程开始前触发,主要用作额外的预保存验证或在保存前设置附加数据。
  • _postSave():数据保存后,但在事务提交之前,此方法被调用,可以执行在实体保存后应触发的其他工作。

还有_preDelete()_postDelete(),它们的工作原理类似,但针对删除操作。

Entity还能够提供关于其当前状态的信息,如isInsert()isUpdate()方法,用于检测是否新插入记录或更新现有记录。isChanged()方法可以告诉您某个字段是否自上次保存以来已更改。

以下是User实体中这些方法实际应用的一些例子:

PHP
protected function _preSave()
{
    if ($this->isChanged('user_group_id') || $this->isChanged('secondary_group_ids'))
    {
        $groupRepo = $this->getUserGroupRepo();
        $this->display_style_group_id = $groupRepo->getDisplayGroupIdForUser($this);
    }

    // ...
}

protected function _postSave()
{
    // ...

    if ($this->isUpdate() && $this->isChanged('username') && $this->getExistingValue('username') != null)
    {
        $this->app()->jobManager()->enqueue('XF:UserRenameCleanUp', [
            'originalUserId' => $this->user_id,
            'originalUserName' => $this->getExistingValue('username'),
            'newUserName' => $this->username
        ]);
    }

    // ...
在预保存阶段,我们根据用户更改的用户组获取并缓存新的显示组ID。在后保存阶段,我们触发一个任务,用于在用户名称更改后运行。

仓库(Repositories): 仓库是XF2中的新概念,但你可能会将其与XF1中的“模型”对象相比较。XF2中没有庞大的模型对象,因为我们有更好的方式从数据库中获取和写入数据。因此,我们不再需要包含所有插件所需的查询和操作方式的大型类,而是使用查找器,它提供了更大的灵活性。

另外,值得记住的是,在XF1中,模型对象承载了太多功能,现在许多功能已经过时。例如,在XF1中,所有权限重建代码都集中在权限模型中。而在XF2中,我们有专门的服务和对象来处理这些。

那么,仓库是什么?它们与实体和查找器相对应,包含通常返回特定目的的查找器对象的方法。为什么不直接返回查找器查询的结果?如果返回查找器对象本身,它为插件提供了扩展点,可以在返回实体或集合之前修改查找器对象。

仓库也可能包含一些特定于缓存重建等操作的特定方法。