通用概念

以下部分将详细介绍开发 XenForo 附加组件时将会遇到的一些通用系统和概念。 如果您熟悉 XenForo 1.x 的开发,那麽这些概念中的很多内容您都会覚得很熟悉,不过值得回顾一下,因为有一些优秀的新工具和功能可以帮助您开发附加组件。

Vendor 组件

XF2 并不像 XF1 那样由一个特定的框架来驱动,然而,我们使用了某些流行的、经过良好测试的、开源的软件包来帮助完成特定的任务。 例如,我们使用一个名为 SwiftMailer 的项目来发送电子邮件,以及一个名为 Guzzle 的项目作为 HTTP 客户端。 所有的第三方项目都是从 src/vendor 目录下加载的。

当前,附加组件开发者还不能将自己的依赖关系添加到这个位置。

整合开发环境 (IDE)

在开始 XF2 开发工作之前,你可能需要花一些时间来评估你将实际创建和编辑 PHP 文件的应用程序。 这通常被称为 IDE。 有许多可用的选项,从基本的记事本到像 Sublime Text 这样可以通过附加组件扩展以获得更好的 PHP 支持的东西,直到一个合适的 IDE,如 PhpStorm。 在内部,我们使用 PhpStorm 作为我们的首选 IDE。 这是一个高级的商业产品,但也有可能是免费的。 无论哪种方式,没有人能告诉你最符合您须求的最佳应用进程是哪个,你应该花些时间多使用一些产品(甚至是免费的),并使用这些经验来找出你的喜好。

自动加载器

XF2 使用一个由 Composer 自动生成的自动加载器。 这允许所有的 XF 代码、第三方供应商代码,以及任何附加组件的开发者代码在整个项目中自动包含进去,而不需要手动 include/require 你的 class。

所有 XF 附加组件的自动加载根目录是 src/addons 目录。 这意味着你所有的 class 名都会相对于这个基础位置。 值得注意的是,XF2 采用了严格的 "每个文件一个类" 的命名模式。 每个文件应该只包含一个类,而且这个类的名字应该确定该类的 PHP 文件在文件系统中的确切位置。

例如,如果你想在一个名为 src/addons/Demo/Setup.php 的文件中创建一个新的 class(其中 Demo 是你的 附加组件 ID),那麽这个 class 将被命名为 Demo\Setup。 相反,如果你有一个名为 Demo\Entity\Thing 的 class,那麽你会知道这个 class 的文件位于路径 src/addons/Demo/Entity/Thing.php 中。

命名空间

在整个 XF 中,我们使用了 命名空间,这样我们可以更简洁地参考同一命名空间中的类。建议所有的附加组件也使用命名空间。 在上面的例子中,我们谈到了一个名为 Demo\Setup 的类。 使用命名空间,这个类实际上会被简単地命名为 Setup,但命名空间会被设置为 Demo。 作为一个更具体的例子,我们在上面也谈到了一个名为 Demo\Entity\Thing 的类。 让我们来看看这个类的 PHP 代码是什麽样子的:

<?php

namespace Demo\Entity;

class Thing
{

}

如果在 src/addons/Demo/Entity 目录下有一个名为 AnotherThing 的 class,我们可以在 Thing class 中简単地将这个 class 引用为 AnotherThing,因为这个 class 在同一个 Demo\Entity 命名空间中。

短类名

偶尔,XF 中参考的 class 会被缩短。 例如,如果您希望调用 User 实体(更多关于实体的内容见下文),那麽您可能会看到被引用的 class 名只是 XF:User。 短类名的使用和它们所解析的全类名,完全是依照 context sensitive 的。 因此,在调用实体的 context 中,短类名将解析为以下全类名 XF/Entity/UserXF 部分表示文件路径 (基于附加的 ID ),Entity 部分是通过调用实体暗示的,User 部分表示特定的实体。 同样当你开始创建自己的 class 时,你也会使用简短的 class 名来引用自己的 class。 例如,如果你需要为你的 Demo 附加组件创建一个新的 Thing 实体,那麽你会写出以下内容。

\XF::em()->create('Demo:Thing');

这将解析为 Demo\Entity\Thing class。 同样,如果你想访问一个 Thing 保存库,你可以这样写。

\XF::repository('Demo:Thing');

请注意这些短类名是如何相同的。 保存库的调用实际上会解析为 Demo\Repository\Thing

继承类

XF2 中大多数的 class 都是可继承的,这使得开发人员可以继承和复盖核心代码,而无需直接编辑它。 如果你熟悉 XF1 的开发,你会对下面的过程有些熟悉:

  1. 创建一个监听器 PHP 文件
  2. 创建一个 class,该 class 最终将继承原有的 class
  3. 编写一个 function,该 function 与预期的 load_class 事件的 callback 签章相符合,并添加继承 class 的名称
  4. 在 Admin CP 中添加一个 "代码事件监听器",指定上述 function 的监听器 class 和 method 名,并可选地提示继承的是哪个 class

在 XF2 中,我们去掉了这些事件,而采用了一个名为 "Class extensions" 的特定系统。 这个过程如下。

  1. 创建一个 class,该 class 最终将继承原有的 class
  2. 在 Admin CP 中添加一个 "Class extension",指定你要继承的 class 的名称和正在继承的 class 的名称。

这显然减少了一些继承 class 所需的模板,也提供了一个专门的 UI 来查看和管理这些继承。 让我们通过继承 public Member controller 来看看这个过程,并添加一个新的动作来显示一个简単的消息。

首先要做的是创建一个附加组件。 我们之前已经介绍过如何使用 xf-addon:create 命令 在这里。 在这个例子中,我们将假设你创建了一个 ID 和标题为 "Demo" 的附加组件。

现在,您将在以下位置中出现此附加组件的 addon.json 文件 src/addons/Demo/addon.json

Note

虽然严格来说,你可以把继承 class 放在附加组件目录下任何你喜欢的地方,但建议把继承 class 放在一个容易识别的目录下
a) class 所属的附加组件
b) 被继承 的 class 类型
c) 被继承的 class 名称
在下面的例子中,我们要继承 public 的 XF Member Controller,所以我们将我们的继承 class 放在下面的路径中: src/addons/Demo/XF/Pub/Controller/Member.php

在我们将 Class extension 添加到 Admin CP 之前,继承的 class 需要存在。 所以,请按照下面的说明进行操作:

  1. src/addons/Demo 内新建一个名为 XF 的目录。
  2. src/addons/Demo/XF 内新建一个名为 Pub 的目录。
  3. src/addons/Demo/XF/Pub 内新建一个名为 Controller 的目录。
  4. src/addons/Demo/XF/Pub/Controller 内新建一个名为 Member.php 的文件。

你的PHP文件初始内容,应该如下所示:

<?php

namespace Demo\XF\Pub\Controller;

class Member extends XFCP_Member
{

}

如果你对一般的 PHP class 继承比较熟悉,但对 XF 不熟悉,上面的例子最初可能会让你感到困惑。 原因是你可能期望直接继承 XF\Pub\Controller\Member 类目录,而不是 XFCP_Member。 在 XF 中,我们使用 "XenForo Class Proxy" 系统(简称 XFCP )来构建一个 "inheritance chain",最终允许一个 class 被多个附加组件继承。 惯例是参考一个虚拟的继承 class,也就是当前的 class 名 Member,并以 XFCP_ 为前缀。

现在 class 已经创建好了,我们可以在 Admin CP > 开发 > Class extensions > 添加 Class extensions 页面上创建 Class extension。

你需要做的就是在第一个字段中输入基本 class 名(XF\Pub\Controller\Member),在第二个字段中输入继承 class 名(你刚刚创建的)(Demo\XF\Pub\Controller\Member),然后点击 "保存" 按钮。

你的 Class extension 现在应该是被激活的,但当前没有做任何事情。 要做一些事情,我们需要通过创建一个与现有 method 同名的 method 来 override 这个 class 中的现有 method,或者创建一个全新的 method。 让我们来做后者,创建全新的 method。

<?php

namespace Demo\XF\Pub\Controller;

class Member extends XFCP_Member
{
    public function actionHelloWorld()
    {
        return $this->message('Hello world!');
    }
}

我们在 Controller 基础知识 的页面中会更多的讲到 Controller 、 Action 和 Reply,所以现在不用特别担心了解这些。

现在我们已经添加了一些代码到我们的继承 Controller 中,让我们来看看它的运作。 简単地输入以下网址 (相对于你的论坛URL): index.php?members/hello-world。 现在你应该会看到一个 "Hello world!" 的消息!

如前所述,在一个 class 中也可以复盖现有的方法。 例如,如果我们将 actionHelloWorld() 改为 actionIndex(),那麽你将不再有 "Notable members" 列表,而是显示 "Hello world!" 消息! 事实上,这并不是继承现有 Controller 动作(或任何 class 方法)的正确方式,但我们会在 修改 Controller Action Reply(properly) 一节中详细介绍。

类型提示

XF 中的很多物件都是通过工厂方法实体化的。 例如,如果我们想实体化一个特定的保存库,我们会写以下内容:

$repo = \XF::repository('Demo:Thing');

这是一种非常方便和一致的实体化物件的方式。 我们只要看一眼,就知道会实体化什麽物件。 该方法中生成的代码知道如何为我们请求的东西返回正确的物件。

然而,不幸的是,你的 IDE 可能不知道(至少默认情况下是这样)。 就 IDE 而言,这个方法将返回一个 XF\Mvc\Entity\Repository 的物件实体。 这在一定程度上是有用的,但是在特定的 Demo\Repository\Thing 物件中可能有很多方法是你的 IDE 不知道的。 这最终意味着,当你试图在代码中使用你的 $repo 物件时,你的 IDE 将无法提出建议或自动完成方法名和它所需要的参数。

这就是类型提示的用处,而且大多数 IDE 和一些能够 "识别 PHP" 的文本编辑器都应该标准地支援这种语法。 我们只需将保存库的调用修改如下:

/** @var \Demo\Repository\Thing $repo */
$repo = \XF::repository('Demo:Thing');

保存库调用上面的类型提示现在告诉 IDE,$repo 涉及到一个由 Demo\Repository\Thing class 表示的物件,而不是原来自动推断的物件。

类型提示在继承 class 的时候也特别有用。 我们的 Class extension 方法有一个潜在的问题,那就是基本上你的 class 并没有继承你要继承的原 class,而是通过一个实际上并不存在的 class 来代理这个 class,比如 上面例子 中的 XFCP_Member

为了解决这个问题,我们会自动生成一个名为 extension_hint.php 的文件,并将其保存在你的 _output 目录下。

这就增加了一个 IDE 可以读取但 PHP 不能读取的参考,所以 IDE 现在可以理解,当我们在这个继承 class 的任何方法中使用 $this 时,它可以建议并自动完成在 Member Controller 或其父类中可用的方法和属性。