pkp项目后端开发引导
PKP下OPS、OJS、OMP入门教程(以OPS为例)
上手PKP下的多个出版社系统例如OJS、OPS、OMP等后端系统之前请最好阅读一遍本文档,本文档默认读者已经具备部分基础的开发技能如面向对象编程、接口、继承等。并且在阅读本文档之前请先阅读一遍官方的在线文档(虽然官方的文档永远不咋更新也不够详细罢了:PKP 开发文档)
项目概览
PKP下有多个出版商系统,这里主要指的是采用php编写的后端,而他们的前端均在ui-library项目中,三个后端也还依赖于lib/pkp这个子模块,因此在每个项目的主路径下均存在lib目录,这个目录下存在两个字模块目录(需要安装),分别是pkp和ui-library,也就是说三个系统的后端都存在lib/pkp和lib/ui-library,这三个系统的大部分功能都被抽象出来在这两个字模块里了,其中lib/pkp是php的抽象公共业务后端,三个系统都存在提交文件、用户管理、审核等操作所以进行了复用,而lib/ui-library则是采用了vue编写的纯前端,但这里要特别说明的是:
采用Vue不代表前后端分离,在pkp中使用vue只是为了减少代码复用,在pkp的架构下vue是用来编写组件的,真正的页面是由后端代码中的模版引擎调用vue组件然后再进行条件渲染后返回给前端的
参与开发PKP的同学应该大多都有一些后端基础,特别是学习Java的人数众多,所以以下是以OPS为例对比Java那边技术栈的表格,PKP开发的后端是和大多数Java Web项目类似采用了单体MVC架构(而不是DDD或者微服务架构):
技术栈对照表
| 技术领域 | OKW-OPS (PHP) | Java (Spring Boot) 对照 | 说明 |
|---|---|---|---|
| 语言 | PHP 8.2+ | Java 17+ | 弱类型,但核心库大量使用了强类型声明。 |
| 入口 | index.php |
main() 方法 |
所有请求(Web/API)都通过此文件进入。 |
| 依赖注入 | Illuminate\Container |
Spring IoC Container | 使用了 Laravel 的容器组件。 |
| 路由 | Dispatcher / Slim (API) |
@RequestMapping |
自研的 Dispatcher 用于页面,Slim 框架用于 API。 |
| ORM/DAO | DAO / Repository |
JPA / Hibernate / MyBatis | PKP中混合使用了传统的 DAO 模式和现代的 Repository 模式,猜测是因为原来老代码的功能进行了保留没有重构为Repository的Collector(构建器模式) |
| 模板引擎 | Smarty (.tpl) |
Thymeleaf / JSP | 服务端渲染 HTML。 |
| 前端框架 | Vue.js + jQuery | React / Angular | 新功能使用 Vue,旧功能保留 jQuery。 |
| 构建工具 | Composer + NPM | Maven / Gradle | Composer 管理 PHP 依赖,NPM 管理前端依赖。 |
开始开发和安装OPS
以下命令均在mac上进行的安装,如果你是windows平台请先安装WSL的ubuntu 20以及以上版本然后再切换到WSL中进行安装和开发(注意ubuntu中没有brew,这是mac的包管理工具),而且安装的过程中必须开启梯子否则例如npm时候的某些包会掉,另外其他出版商系统例如OJS和OMP也是完全类似的操作,只需要注意替换下方命令的地址即可,理论上你看到我这篇文章的时候我们已经建立好了OKW-OPS或者OKW-OMP了,所以直接从BCPress组织的仓库里克隆即可,不需要从pkp官方的ops或者omp仓库克隆代码,注意要先加入我们的BCPress组织否则没有权限。
(仓库管理者要注意分支,例如OPS不要使用老版本例如3.5.0-1而要用尽量新一些的例如3.5.0-3,因为安装依赖的时候pkp官方会用到一些在线的东西例如全球所有学术机构名单,这个名单是在线的而返回的json数据格式一变就会导致安装失败例如3.5.0-1就有这个问题所以我升级到了3.5.0-3),
正常情况下从我们的项目中开发是不需要修改config.inc.php,但如果你想自己修改请注意里面的一下几个内容:base_url是你启动后的访问路径一般在本地开发环境用127.0.0.1:8000,installed = On 表示已经安装过了,改成Off启动项目会去数据库中创建表等操作,[database]下面的host username等是配置数据库的相关内容,files_dir是用来存放上传文件的路径
先去github上从dev分支创建出属于你自己的分支 假设我这里叫szy分支
从仓库克隆后端代码,
1
2git clone git@github.com:BenchCouncil-Press/OKW-OPS.git
cd OKW-OPS切换到自己的分支去做开发
1 | git checkout szy |
- 安装lib/pkp和lib/ui-library依赖
1 | git submodule sync --recursive |
- 安装node和php以及composer(提前安装过可以跳过,注意下面的命令不要在项目主目录执行而是应该到你的~目录下执行或者你直接用命令
cd ~,如果是mac直接打开你的终端执行即可)
OPS3.5.0-3依赖的是node 20及以上的版本,后端的php依赖的是php8.2及以上的版本
假如你没有brew的话:
1 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" |
如果有了brew就直接安装PHP8.2
1 | brew install php@8.2 |
(可选)把PHP8.2切换为默认PHP
1 | brew link --overwrite --force php@8.2 |
验证版本
1 | php -v |
理论上会显示:
1 | PHP 8.2.x |
安装Composer
1 | brew install composer |
验证:
1 | composer -V |
安装Node(这里强烈推荐使用nvm,因为开发环境可能有多个node版本需要用nvm来进行管理)
1 | brew install nvm |
添加到 ~/.zshrc
1 | export NVM_DIR="$HOME/.nvm" |
1 | nvm install 20 |
1 | node -v |
理论上会输出:
1 | v20.x.x |
- 安装php、node依赖
切换回项目主路径(假设我的项目在/Users/sammie/phpproject/OKW-OPS)
1 | cd ~/phpproject/OKW-OPS |
用composer安装php依赖
1 | composer --working-dir=lib/pkp install |
如果是OJS或者OMP的话需要用到支付要多这一步(OPS不需要安装这个)
1 | composer --working-dir=plugins/paymethod/paypal install |
用node自带的npm安装依赖(假如这一步因为网络下载报错了直接删除项目主路径下的node_modules,重新执行命令即可)
1 | npm install |
修改config.inc.php中的文件上传路径,我这里用的是我的本地路径,因为pkp官方的默认上传行为就是上传到后端安装的服务器上,后续我们可能会改成上传到AWS上进行管理
1 | files_dir = /Users/sammie/pkp-files/ops # 这是我的路径,你的电脑上没有/Users/sammie 这是mac的默认用户路径 |
然后直接启动项目(注意这里的127.0.0.1:8000是config.inc.php中的base_url):
1 | php -S 127.0.0.1:8000 |
项目架构图(以OPS为例)
进行实际功能开发前先看看这个图,这是我自己理解的整个PKP后端的架构图。可以把这个图下载下来然后对照着下面我博客中解释的内容来看。
我们从前端->后端->数据库这个逻辑来解释整个PKP的架构,需要特别声明的一点是整个PKP的架构非常复杂,既有独立架构又有耦合架构,特别是关于前端页面的部分,所以需要点耐心。
前端渲染
先说最难搞的就是关于前后端是否分离的这个问题:
在PKP中,前后端既不是完全分离也不是完全耦合,而是部分前后端分离
在PKP中的Vue(也就是ui-library)和PHP引入的PKP自研的模版引擎(Smarty)共同配合完成的页面构建和返回,之所以没有像一般项目那样直接是Vue+Java或者Vue+PHP这样完全前后端分离是因为这种出版商系统要在前端考虑SEO,也就是必须给用户返回完整的html页面而不是一个Vue的空架子然后再渲染数据那样对SEO很差,当然后来出现了SSR/SSG这种直接在服务端渲染好html再给用户的框架(例如Nuxt就实现了这种服务端渲染的功能,我们的OKW就是基于Nuxt做的),不过这都是后话了。
还要强调的一点是PHP的代码里实际上有两套逻辑,一套适用于api一套适用于页面page,因为pkp官方希望除了页面这一套流程以外后端也可以分离出去提供api对外访问,就像我们的OKW-Portal+OJS+OPS+OMP一样,所以api走的Controller和page的Controller是不一样的,由路由来决定这两者的请求分别路由到哪一个Handler。
在PKP内的前端【页面】流程是这样配合的:
核心概念:Smarty 返回的是“HTML 骨架 + Vue 挂载点 + JS/CSS 资源引用 +(少量)初始化数据”;然后浏览器加载 build.js 后,Vue 才开始“接管某些区域”。
PKP 这边的工程思路也写得很直白:他们倾向继续走“经典 SSR”,Vue 主要负责更交互、对 SEO 不敏感的区域;并且 Vue 不会再像以前那样整页实例化,而是通过特定标记(例如 data-vue-root)来精确控制“哪些 DOM 区域归 Vue 管”。
所以典型渲染链路长这样:
浏览器请求某个 OPS 页面(例如后台工作流页面)
PHP 控制器处理权限/数据准备
Smarty 模板输出 HTML
输出页面结构(header/sidebar/容器)
在某个位置留一个挂载点,例如:
1
<div data-vue-root="SubmissionWizard"></div>
引入静态资源:
styles/build.cssjs/build.js
浏览器执行
js/build.js- build.js 扫描页面中的挂载点(带
data-vue-root的元素) createApp(...).mount(...)把对应 Vue 组件挂上去
- build.js 扫描页面中的挂载点(带
Vue 组件再去请求 API 拿动态数据(或接收 Smarty 注入的少量初始 props/config)
只所以用这么奇怪的方式是因为PKP官方想要用Vue逐步的替代Smarty,将来社区也希望可以完全替代Smarty,但暂时目前还是这种SSR+局部Vue的模式,但是前端的部分不用管,我们有OKW-Portal(完全的Vue前后端分离模式),我们只需要在后端实现api/v1下面的功能向外暴露api即可。
后端业务逻辑
后端的业务逻辑稍微复杂一些,但总体还是MVC架构。
一个请求过来以后首先路由会判断是api请求还是page请求(由请求路径区分),然后路由到对应的Handler上去,如果是Page则路由到pages(OPS特有的)或者/lib/pkp/pages(三个系统公共的)下的Handler,如果是API的则请求到api/v1(OPS特有的)或者lib/pkp/api/v1/(三个系统公共的)下的Handler。
例如这是一个api请求
1 | https://example.org/publicknowledge/api/v1/submission/1 |
其中https://example.org的部分是baseUrl,publicknowledge表示context,而api/v1/submission这部分都是router路由的路径,最后的1表示的是参数,这个路径会被请求到**/api/v1/submissions/index.php**然后需要说明的是在submissions/1这部分是被定义为了submission的endpoint,什么是endpoint将来会解释,简单理解成挂在到submissions下的对外暴露点即可。
这是一个page请求
1 | https://example.org/publicknowledge/$$$call$$$/grid/issues/back-issue-grid/edit-issue?issudID=1 |
路由器将查找并加载Handler位于/controllers/grid/issues/BackIssueGridHandler.php.
控制器 URL 由类名派生而来Handler。Handler后缀被去掉,剩余的名称从 . 转换PascalCase为kebab-case.
进入到Handler后会先进行权限认证,注意每个请求都要在HTTP head中带有Authorization字段(这个值是登陆后由后端生成的加密认证token,后端解密后可以拿到用户的username、role等信息),从这里面拿到role进行权限管理,然后每个请求需要实现Role的策略类,也就是RoleBasedHandlerOperationPolicy方法。
权限认证后就进入真正的业务逻辑处理,注意有些基本的类(Service Container,这里面一般是DAO)是被Laravel进行了依赖管理的。大部分都走Repository Facades层(现代化的数据访问接口)、Service Layer(业务服务,特别是OPS自己的特有业务),然后PKP为了扩展系统功能还有一些插件,plugin可以根据系统的Hook机制来增加和插入任何功能。
要特别强调的是,为了项目将来一次性部署方便,每次新增或者修改了数据库的表结构要使用 Laravel 的 Schema Builder 语法修改表结构,以及修改dbscripts/xml/install.xml,这样别人才能在部署的时候得到一个完整的表结构。
项目目录解释
OKW-OPS 的文件结构分为 应用层 (OPS) 和 **核心层 (Lib/PKP)**。
根目录
index.php: 绝对入口。初始化环境,加载配置,启动应用。config.inc.php: 全局配置文件(数据库连接、Base URL、调试开关等)。**类似于 SpringBoot的application.properties**。api/: REST API 定义目录。classes/: OPS 特有的后端逻辑类。这些是扩展和继承自lib/pkp中的类pages/: OPS 特有的前端页面控制器(Page Handlers)。plugins/: 插件目录。public/: 存放用户上传的公开文件(如站点 Logo、样式表)。dbscripts/: 数据库脚本(安装、升级)。locale/: 多语言翻译文件 (.po)。lib/pkp/: 核心框架库(这是最重要的目录,80% 的逻辑在这里)。
classes/ (OPS 业务逻辑)
这里存放 OPS 特有的业务实体和逻辑,通常继承自 lib/pkp/classes/ 中的基类。
core/:Application.php: 应用单例类,继承自PKPApplication。Request.php: 处理 HTTP 请求。
submission/:Submission.php: 核心实体,代表一个投稿。继承自PKPSubmission。DAO.php: 投稿的数据库访问对象。
publication/:Publication.php: 代表投稿的一个版本(Version),每个publication会关联一个submission,而且在submission表中会有个current_publication字段来管理这个submission的当前publication是哪一个。
search/: 搜索引擎相关逻辑。server/:Server.php: 代表一个预印本服务器(类似于 OJS 的 Journal)。
lib/pkp/classes/ (核心框架逻辑)
这是整个系统的核心逻辑,OJS、OPS、OMP的主要业务逻辑都抽象到了这里。
core/: 核心组件 (Dispatcher,Registry,PKPApplication)。db/: 数据库操作基类 (DAO,SchemaDAO)。security/:Role.php: 角色定义(管理员、作者、编辑等)。authorization/: 权限控制策略(非常重要,类似于 Spring Security)。
submission/: 投稿工作流的通用逻辑。user/: 用户管理 (User,UserDAO)。plugins/: 插件系统的基类 (Plugin,Hook)。services/: 业务服务层,提供更高级的 API (PKPContextService等)。file/: 文件管理 (FileManager,PublicFileManager)。
pages/ & lib/pkp/pages/ (Web 控制器)
采用传统的 Page Controller 模式。URL 结构通常为 index.php/{context}/{page}/{op}。
pages/index/: 首页。pages/dashboard/: 用户仪表盘。pages/workflow/: 投稿处理工作流。pages/admin/: 站点管理(lib/pkp/pages/admin)。- 每个目录下通常有
index.php(路由分发) 和Handler.php(控制器逻辑)。
api/ (REST API)
v1/: 版本 1 的 API。- 每个子目录(如
dois,users)包含index.php,指向对应的 Controller 类。 - API 控制器位于
lib/pkp/api/v1/或classes/api/v1/(较少见,大多在 PKP 库中)。
核心架构与请求生命周期 (Architecture & Lifecycle)
请求处理流程 (Request Flow)
- Entry (
index.php):- 加载
lib/pkp/includes/bootstrap.php。 - 创建
Application实例。 - 调用
Application->execute()。
- 加载
- Dispatch (
Dispatcher.php):- 检查 URL 模式。
- API 请求: 路由到
PKP\API\v1\...控制器。 - 页面请求: 路由到
pages/.../Handler.php。
- Middleware / Policy (权限检查):
- 在 Handler 执行
op方法前,会先执行authorize()。 - 加载定义在
lib/pkp/classes/security/authorization/下的策略 (Policies)。
- 在 Handler 执行
- Execution (业务逻辑):
- Controller/Handler 调用
Repository或DAO获取数据。 - 数据可能来自 MySQL 数据库。
- Controller/Handler 调用
- Response (响应):
- 页面: 使用
TemplateManager(Smarty) 渲染.tpl模板,返回 HTML。 - API: 返回 JSON 数据。
- 页面: 使用
依赖注入与服务容器 (Service Container)
项目使用 Laravel 的 Container。
注册服务: 在
PKPApplication::registerBaseBindings()或 Service Providers 中。获取服务:
1
2
3$request = Application::get()->getRequest();
// 或者通过 Facade
$submission = Repo::submission()->get($id);
数据库访问 (Repository Pattern & DAO)
目前处于混合状态:
- Repository (推荐/新代码):
- 位于
lib/pkp/classes/*/Repository.php。 - 使用
Collector(构建器模式) 进行查询。 - 示例:
Repo::submission()->getCollector()->filterByContextId($id)->getMany()。
- 位于
- DAO (遗留代码):
- 位于
lib/pkp/classes/db/DAO.php的子类。 - 直接编写 SQL 语句。
- 示例:
DAORegistry::getDAO('UserDAO')->getById($id)。
- 位于
示例:如何进行开发与修改
场景 A: 添加一个新的 API 接口
假设你要添加一个接口获取“热门预印本”。
定义路由: 在
api/v1/下创建目录trending/和index.php。1
return new \PKP\handler\APIHandler(new \APP\API\v1\trending\TrendingController());
创建控制器: 在
classes/API/v1/trending/TrendingController.php。- 继承
PKPBaseController。 - 实现
getMany方法。
- 继承
配置权限: 在构造函数中定义
RoleBasedHandlerOperationPolicy。实现逻辑: 使用
Repo::submission()查询数据并返回。
场景 B: 修改现有的业务逻辑
不要直接修改 lib/pkp 下的核心文件! 这会导致无法升级。
- 方法 1: 使用 Hook。查找是否有 Hook 可以拦截该逻辑。
- 方法 2: 编写 Plugin。通用插件 (
GenericPlugin) 可以注入几乎任何逻辑。
场景 C: 修改前端页面
- 找到对应的 Handler: 根据 URL 找到
pages/下的 Handler。 - 找到对应的 Template: Handler 中会调用
$templateMgr->display('path/to/template.tpl')。 - 修改 Template: 模板文件位于
lib/pkp/templates/或templates/。- 推荐通过 Theme Plugin 覆盖模板,而不是直接修改文件。
场景 D: 数据库变更
- 修改
dbscripts/xml/install.xml(仅影响新安装)。 - 创建 Migration 类 (
classes/migration/),使用 Laravel 的 Schema Builder 语法修改表结构。




