实体、查找器和存储库#
在 XF2 中,有多种方式与数据进行交互。在 XF1 中,这主要是在模型文件中编写原始 SQL 语句。XF2 的方法已经远离了这一点,我们添加了许多新的方式来替代它。我们首先来看执行数据库查询的首选方法 —— 查找器。
查找器#
我们引入了一个新的“查找器”系统,允许以面向对象的方式逐步构建查询,从而无需编写原始的数据库查询。查找器系统与实体系统紧密配合,我们将在下面详细讨论实体系统。传递给查找器方法的第一个参数是你想要处理的实体的短类名。让我们将上面提到的一些查询转换为使用查找器系统。例如,访问单个用户记录:
直接查询方法和使用查找器之间的主要区别之一是,查找器返回的数据的基本单位不是数组。在调用 fetchOne
方法(仅从数据库中返回单行)的查找器对象的情况下,将返回一个实体对象。
让我们看一个稍微不同的方法,它将返回多行:
此示例将从 xf_user
表中查询 10 条记录,并将它们作为 ArrayCollection
对象返回。这是一个特殊的对象,其行为类似于数组,因为它是可遍历的(你可以循环遍历它),并且它有一些特殊的方法可以告诉你它有多少个条目,按某些值分组,或其他类似数组的操作,如过滤、合并、获取第一个或最后一个条目等。
查找器查询通常应预期从表中检索所有列,因此没有特定的等效方法来仅获取某些列的某些值。
相反,要获取单个值,你只需获取一个实体并直接从该实体中读取值:
$finder = \XF::finder('XF:User');
$username = $finder->where('user_id', 1)->fetchOne()->username;
同样,要从单个列中获取值的数组,你可以使用 pluckFrom
方法:
$finder = \XF::finder('XF:User');
$usernames = $finder->limit(10)->pluckFrom('username')->fetch();
到目前为止,我们已经看到查找器应用了一些简单的 where 和 limit 约束。因此,让我们更详细地看一下查找器,包括 where
方法本身的更多细节。
where 方法#
where
方法最多支持三个参数。第一个是条件本身,例如你正在查询的列。第二个通常是运算符。第三个是要搜索的值。如果你只提供两个参数,如上所示,那么它自动意味着运算符是 =
。以下是其他有效的运算符列表:
=
<>
!=
>
>=
<
<=
LIKE
BETWEEN
因此,我们可以获取过去 7 天内注册的有效用户列表:
$finder = \XF::finder('XF:User');
$users = $finder->where('user_state', 'valid')->where('register_date', '>=', time() - 86400 * 7)->fetch();
如你所见,你可以多次调用 where
方法,但除此之外,你可以选择将数组作为该方法的唯一参数传递,并在一次调用中构建你的条件。数组方法支持两种类型,我们可以在上面构建的查询中使用这两种类型:
$finder = \XF::finder('XF:User');
$users = $finder->where([
'user_state' => 'valid',
['register_date', '>=', time() - 86400 * 7]
])
->fetch();
通常不建议或不清楚像这样混合使用,但它确实在某种程度上展示了该方法的灵活性。现在条件在数组中,我们可以指定列名(作为数组键)和值以隐含 =
运算符,或者我们可以实际定义另一个包含列、运算符和值的数组。
whereOr 方法#
在上面的示例中,两个条件都需要满足,即每个条件都由 AND
运算符连接。然而,有时只需要满足部分条件,这可以通过使用 whereOr
方法来实现。例如,如果你想搜索无效或发布消息为零的用户,可以按如下方式构建:
$finder = \XF::finder('XF:User');
$users = $finder->whereOr(
['user_state', '<>', 'valid'],
['message_count', 0]
)->fetch();
类似于上一节中的示例,除了将最多两个条件作为单独的参数传递外,你还可以将条件数组传递给第一个参数:
$finder = \XF::finder('XF:User');
$users = $finder->whereOr([
['user_state', '<>', 'valid'],
['message_count', 0],
['is_banned', 1]
])->fetch();
with 方法#
with
方法本质上等同于使用 INNER|LEFT JOIN
语法,尽管它依赖于实体是否定义了“关系”。我们不会在下一页中讨论这一点,但这应该让你了解它的工作原理。让我们现在使用 Thread 查找器来检索特定线程:
$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
方法以进行多次连接。
$finder = \XF::finder('XF:Thread');
$thread = $finder->with(['Forum', 'User'], true)->where('thread_id', 123)->fetchOne();
这将连接到 xf_user
表以获取线程作者。然而,第二个参数仍然是 true
,我们可能不需要对用户连接进行 INNER
连接,因此我们可以改为链接方法:
$finder = \XF::finder('XF:Thread');
$thread = $finder->with('Forum', true)->with('User')->where('thread_id', 123)->fetchOne();
order、limit 和 limitByPage 方法#
order 方法#
此方法允许你修改查询,以便按特定顺序获取结果。它接受两个参数,第一个是列名,第二个是可选的排序方向。因此,如果你想列出消息最多的 10 个用户,可以按如下方式构建查询:
注意
现在可能是提到查找器方法可以以任何顺序调用的好时机。例如:$threads = $finder->limit(10)->where('thread_id', '>', 123)->order('post_date')->with('User')->fetch();
尽管如果你以这种顺序编写 MySQL 查询,你肯定会遇到一些语法问题,但查找器系统仍将以正确的顺序构建它,上面的代码虽然看起来很奇怪且可能不推荐,但完全有效。
与标准 MySQL 查询一样,可以按多列对结果集进行排序。为此,你可以再次调用 order 方法。还可以使用数组将多个排序子句传递给 order 方法。
$finder = \XF::finder('XF:User');
$users = $finder->order('message_count', 'DESC')->order('register_date')->limit(10);
limit 方法#
我们已经看到了如何将查询限制为返回特定数量的记录:
然而,实际上有一种替代直接调用 limit 方法的方法:
你可以直接将限制传递给 fetch()
方法。还值得注意的是,limit
(和 fetch
)方法支持两个参数。第一个显然是限制,第二个是偏移量。
这里的偏移量值本质上意味着前 100 个结果将被丢弃,之后的 10 个结果将被返回。这种方法对于提供分页结果很有用,尽管我们实际上还有一种更简单的方法来做到这一点...
limitByPage 方法#
此方法是一种辅助方法,它根据你当前查看的“页面”和你需要的“每页”数量来设置适当的限制和偏移量。
在这种情况下,限制将设置为 20(这是我们的每页值),偏移量将设置为 40,因为我们从第 3 页开始。
有时,我们需要获取比限制更多的数据。过度获取有助于检测在当前页面之后是否有更多数据要显示,或者如果你需要根据权限过滤初始结果集。我们可以使用第三个参数来做到这一点:
这将获取最多 21 个用户(20 + 1),从第 3 页开始。
getQuery 方法#
当你第一次开始使用查找器时,尽管它很直观,但你可能偶尔会想知道你是否正确使用它,以及它是否会构建你期望的查询。我们有一个名为 getQuery
的方法,它可以告诉我们当前查找器对象将构建的查询。例如:
这将输出类似于以下内容:
你可能不会经常需要它,但如果查找器没有返回你预期的结果,它会很有用。有关 dumpSimple
方法的更多信息,请参阅 Dump a variable 部分。
自定义查找器方法#
到目前为止,我们已经看到查找器对象使用类似于 XF:User
和 XF:Thread
的参数进行设置。在大多数情况下,这标识了查找器正在处理的实体类,并将解析为例如 XF\Entity\User
。然而,它还可以表示一个查找器类。查找器类是可选的,但它们作为一种向特定查找器类型添加自定义查找器方法的方式。要查看此操作,让我们看一下与 XF:User
相关的查找器类,它可以在 XF\Finder\User
类中找到。
以下是该类中的一个示例查找器方法:
public function isRecentlyActive($days = 180)
{
$this->where('last_activity', '>', time() - ($days * 86400));
return $this;
}
这使我们能够在任何用户查找器对象上调用该方法。因此,如果我们采用前面的示例:
$finder = \XF::finder('XF:User');
$users = $finder->isRecentlyActive(20)->order('message_count', 'DESC')->limit(10);
此查询,之前只是按消息数量降序返回 10 个用户,现在将返回过去 20 天内最近活跃的 10 个用户。
即使对于许多实体类型,查找器类不存在,仍然可以以与 扩展类 部分中提到的相同方式扩展这些不存在的类。
实体系统#
如果你熟悉 XF1,你可能熟悉实体背后的一些概念,因为它们最终源自那里的 DataWriter 系统。如果你不太熟悉它们,以下部分应该会给你一些概念。
实体结构#
Structure
对象由许多属性组成,这些属性定义了实体的结构及其相关的数据库表。结构对象本身是在其相关的实体中设置的。让我们看一下用户实体中的一些常见属性:
表#
这告诉实体在更新和插入记录时使用哪个数据库表,并告诉查找器在构建要执行的查询时从哪个表读取。此外,它还在知道查询需要连接到哪些其他表方面发挥作用。
短名称#
这只是实体本身和查找器类(如果适用)的短类名。
内容类型#
这定义了此实体表示的内容类型。在大多数实体结构中不需要此属性。它用于连接到“内容类型”系统使用的特定内容(将在另一节中介绍)。
主键#
定义表示数据库表中主键的列。如果表支持多个列作为主键,则可以将其定义为数组。
列#
$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()
的方法,如果存在,我们首先将值通过该方法运行。
行为#
这是此实体应使用的行为类的数组。行为类是一种允许某些代码在多个实体类型之间通用重用的方式(仅在实体更改时,而不是在读取时)。一个很好的例子是 XF:Likeable
行为,它能够自动执行某些操作,以支持可以“点赞”的内容的实体。这包括在内容中发生可见性更改时自动重新计算计数,以及在删除内容时自动删除点赞。
获取器#
当调用命名字段时,会自动调用获取器方法。例如,如果我们从用户实体请求 is_super_admin
,这将自动检查并使用 getIsSuperAdmin()
方法。值得注意的是,xf_user
表实际上没有名为 is_super_admin
的字段。这实际上存在于 Admin 实体上,但我们已将其添加为获取器方法,作为访问该值的简写方式。获取器方法还可以用于直接覆盖现有字段的值,这就是 last_activity
值的情况。last_activity
实际上是一个缓存值,通常在用户注销时更新。然而,我们将用户的最新活动日期存储在 xf_session_activity
表中,因此我们可以使用此 getLastActivity
方法返回该值,而不是缓存的最后活动值。如果你曾经需要完全绕过获取器方法,只需获取真正的实体值,只需在列名后加上下划线,例如 $user->last_activity\_
。
因为实体就像任何其他 PHP 对象一样,你可以向它们添加更多方法。一个常见的用例是添加可以在实体本身上调用的权限检查方法。
关系#
$structure->relations = [
'Admin' => [
'entity' => 'XF:Admin',
'type' => self::TO_ONE,
'conditions' => 'user_id',
'primary' => true
]
];
这就是定义关系的方式。什么是关系?它们定义了实体之间的关系,可用于执行与其他表的连接查询或在实体上动态获取相关记录。如果我们记得查找器上的 with
方法,如果我们想要获取特定用户并预先获取用户的 Admin 记录(如果存在),那么我们可以执行以下操作:
$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->with('Admin')->fetchOne();
这将使用用户实体中为 Admin
关系定义的信息以及 XF:Admin
实体结构的详细信息,以了解此用户查询应在 xf_admin
表和 user_id
列上执行 LEFT JOIN
。要从用户实体访问管理员最后登录日期:
然而,并不总是需要在查找器中执行连接以获取实体的相关信息。例如,如果我们采用上面的示例而不调用 with
方法:
$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
关系:
$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
关系从该关系中获取单条记录。例如,如果想查看用户是否与特定的连接账户提供者相关联,可以在查询时获取:
$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->with('ConnectedAccounts|facebook')->fetchOne();
选项(Options):
$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实体中这些方法实际应用的一些例子:
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
]);
}
// ...
仓库(Repositories): 仓库是XF2中的新概念,但你可能会将其与XF1中的“模型”对象相比较。XF2中没有庞大的模型对象,因为我们有更好的方式从数据库中获取和写入数据。因此,我们不再需要包含所有插件所需的查询和操作方式的大型类,而是使用查找器,它提供了更大的灵活性。
另外,值得记住的是,在XF1中,模型对象承载了太多功能,现在许多功能已经过时。例如,在XF1中,所有权限重建代码都集中在权限模型中。而在XF2中,我们有专门的服务和对象来处理这些。
那么,仓库是什么?它们与实体和查找器相对应,包含通常返回特定目的的查找器对象的方法。为什么不直接返回查找器查询的结果?如果返回查找器对象本身,它为插件提供了扩展点,可以在返回实体或集合之前修改查找器对象。
仓库也可能包含一些特定于缓存重建等操作的特定方法。