PHP Censor 2.1.6 Ubuntu 22.04 安装指南

在 Ubuntu 22.04.5 LTS 上从零安装 PHP Censor 2.1.6:Composer 在线与源码包离线两条路径、Nginx+PHP 8.1 站点配置、systemd 托管 Worker、首次构建验证与排障实录。

PHP CensorPHPCI持续集成DevOps2026/10/28

PHP Censor 2.1.6 Ubuntu 22.04 安装指南

目标:在一台 Ubuntu 22.04.5 LTS 服务器上,从空的系统开始,完成 PHP Censor 2.1.6 的依赖安装、数据库初始化、Web 站点接入、Worker 常驻托管与首次构建验证,全程命令与回显均为实测。文中同时给出「Composer 在线安装」与「官方源码包离线安装」两条路径,读者可按机房网络条件二选一。

一、环境信息

本次安装在一台 Ubuntu 22.04.5 LTS 服务器上完成,配置与版本如下。所有路径、端口均为该服务器的真实取值,可直接对照复现。

项目取值说明
操作系统Ubuntu 22.04.5 LTS (Jammy Jellyfish)PRETTY_NAME 实测
主机名php-censor/etc/hostname
访问域名php-censor.local写入 Nginx server_name 与 app/config.yml 的 url
PHP8.1.2-1ubuntu2.28(cli / fpm)Ubuntu 22.04 默认 PHP 版本
PHP-FPM socket/run/php/php8.1-fpm.sock属主 www-data:www-data
Nginxnginx/1.18.0 (Ubuntu)监听 80
数据库MariaDB 10.6.23监听 127.0.0.1:3306
队列Beanstalkd 1.12监听 127.0.0.1:11300
Composer2.2.6Ubuntu 源内版本
PHP Censor2.1.6VERSION.md 与 ./bin/console --version 双重确认
应用目录/var/www/php-censor站点根目录为 public
站点配置/etc/nginx/sites-available/php-censor软链到 sites-enabled
运行账号php-censor(uid 999)Web 与 Worker 共用
安装方式方案 A:Composer create-project;方案 B:官方源码包 + composer install两方案均实测

说明:本文所有 systemctl 命令均针对单个单元执行,输出为实测回显;输出过长处标注「输出节选(实测首尾)」。

二、前置条件

2.1 官方系统要求核对

官方 README 的 System requirements 一节列了六条硬要求,安装前应逐条对齐,否则后续步骤会在意料之外的地方失败:

官方要求本文取值是否满足
Unix-like OS(明确不支持 Windows)Ubuntu 22.04.5 LTS满足
PHP 7.4+,需 OpenSSL,且启用 exec()、shell_exec()、proc_open()PHP 8.1.2满足
Web 服务器(Nginx 或 Apache2)Nginx 1.18.0满足
数据库(MySQL/MariaDB 或 PostgreSQL)MariaDB 10.6.23满足
Beanstalkd 队列Beanstalkd 1.12满足
可写的应用目录与 runtime 目录/var/www/php-censor见 7.1 节

exec()、shell_exec()、proc_open() 这三个函数经常被安全基线通过 disable_functions 关掉,而 PHP Censor 的插件全靠它们调用外部命令。安装前先确认:

php -r 'echo ini_get("disable_functions"), PHP_EOL;'

实测输出为空行(disable_functions 未设置):

⚠️ 注意:若该行输出里出现 exec,shell_exec,proc_open,必须在 /etc/php/8.1/fpm/php.ini 与 /etc/php/8.1/cli/php.ini 中删掉对应项,否则构建立刻失败,且报错只显示「命令执行失败」,排查成本很高。

2.2 网络与软件源

本次安装全部走 Ubuntu 官方源与 Packagist 官方源,未发生下载降级。为确认官方源可用,安装前实测:

curl -sI --max-time 25 https://repo.packagist.org/packages.json | head -1
HTTP/2 200

说明:若机房访问 Packagist 缓慢,可临时切换国内 Composer 源;但 PHP Censor 的 composer.json 里有 extra.platform.php: 7.4.* 的平台固定,不要在同一台机器上混用不同源的锁文件,否则依赖树可能不一致。

2.3 端口规划

端口用途监听地址是否对外
80Nginx / PHP Censor Web0.0.0.0是
3306MariaDB127.0.0.1否
11300Beanstalkd127.0.0.1否

数据库与队列都只监听回环地址,是本次安装的安全基线之一;这一点在第九节配置说明中会再次强调。

三、版本选型说明

3.1 为什么选 2.1.6

在 GitHub Releases 页按「最新且非预发布」筛选,2026-10-28 查得的最新稳定版为 2.1.6(2025-03-15 发布)。选择它有三个理由:

  1. 它是官方 README 标注的「Current stable version」所在分支(release-2.1)的最新版;
  2. 它包含两项安全修复(remember me key 问题、CVE-2024-50345),低于该版本的 2.1.x 均有已知缺陷;
  3. 相比 master 上的 2.2 WIP 与 3.0 预发布,2.1.6 有完整的发布说明与可回退的标签。

3.2 与其他版本的差异

版本状态最低 PHP是否用于生产
2.1.6当前稳定版>=7.4是(本文采用)
2.0.14上一稳定线的最新修复版>=7.4仅存量系统
2.2WIP,master 分支>=7.4否
3.0预发布(Packagist 有 dev-pre-release-3.0.0)>=8.1否
1.3.7 及更早官方标注 UNSUPPORTED>=5.6,<8.0否

关键差异:2.x 与 3.0 的 PHP 基线不同(7.4 对 8.1),且 3.0 仍在预发布阶段,接口与配置可能变动。生产环境固定使用 2.1.6,待 3.0 正式发布后再评估迁移。

3.3 PHP 8.1 的兼容性判断

composer.json 声明 php: >=7.4.0,同时 extra.platform.php 固定为 7.4.*。后者意味着 Composer 会按 PHP 7.4 的兼容矩阵解析依赖——它不会因为本机是 8.1 就放开依赖版本。这样装出来的依赖在 8.1 上运行是安全的(Symfony 5.4 支持 8.1),但也意味着未来升级 PHP 8.2/8.3 时要重新验证依赖树。

四、安装方案 A:Composer 在线安装

4.1 安装系统依赖

先补齐 PHP 扩展与支撑服务。PHP Censor 需要 openssl、PDO、json、dom、libxml、SimpleXML、curl、bcmath 八个扩展,Ubuntu 下分别对应 php8.1-* 包:

apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
    nginx mariadb-server beanstalkd git unzip curl \
    php8.1-cli php8.1-fpm php8.1-mysql php8.1-xml \
    php8.1-mbstring php8.1-curl php8.1-bcmath php8.1-zip \
    php8.1-gd composer

安装完成后逐项核对版本(输出为实测回显):

php -v
composer --version
nginx -v
mariadb --version
beanstalkd -v
PHP 8.1.2-1ubuntu2.28 (cli) (built: Oct 19 2026 18:54:45) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.2, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.2-1ubuntu2.28, Copyright (c), by Zend Technologies
Composer 2.2.6 2022-02-04 17:00:38
nginx version: nginx/1.18.0 (Ubuntu)
mariadb  Ver 15.1 Distrib 10.6.23-MariaDB, for debian-linux-gnu (x86_64) using  EditLine wrapper
beanstalkd 1.12

确认 PHP 扩展齐全(输出为单行,此处按空格拆行展示):

[PHP Modules] bcmath calendar Core ctype curl date dom exif FFI fileinfo filter ftp gd
gettext hash iconv intl json libxml mbstring mysqli mysqlnd openssl pcntl pcre PDO
pdo_mysql Phar posix readline Reflection session shmop SimpleXML sockets sodium SPL
standard sysvmsg sysvsem sysvshm tokenizer xml xmlreader xmlwriter xsl Zend OPcache zip zlib
[Zend Modules] Zend OPcache

4.2 启动四个支撑服务

systemctl enable --now mariadb
systemctl enable --now nginx
systemctl enable --now php8.1-fpm
systemctl enable --now beanstalkd

逐个确认状态(每行只对一个单元,回显为实测):

systemctl is-active mariadb
systemctl is-active nginx
systemctl is-active php8.1-fpm
systemctl is-active beanstalkd
active
active
active
active

同时确认端口已监听:

ss -ltnp | grep -E ':(80|3306|11300) '
LISTEN 0      80         127.0.0.1:3306       0.0.0.0:*    users:(("mariadbd",pid=15088,fd=21))
LISTEN 0      511          0.0.0.0:80         0.0.0.0:*    users:(("nginx",pid=15049,fd=6))
LISTEN 0      1024       127.0.0.1:11300      0.0.0.0:*    users:(("beanstalkd",pid=15044,fd=3))

说明:LISTEN 0 511 中的 511 是 socket backlog,80 是监听端口。三个端口分别绑定在回环(3306、11300)与全地址(80),与 2.3 节的规划一致。

4.3 创建数据库与账号

PHP Censor 不会自动建库,安装命令只负责建表。这里先建库并授权,库名 php-censor、账号 php-censor、口令 q1w2e3r4(演示口令,生产请替换):

mariadb -uroot -e "
CREATE DATABASE \`php-censor\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'php-censor'@'localhost' IDENTIFIED BY 'q1w2e3r4';
GRANT ALL PRIVILEGES ON \`php-censor\`.* TO 'php-censor'@'localhost';
FLUSH PRIVILEGES;"

验证库已建立并可用该账号登录:

mariadb -uphp-censor -pq1w2e3r4 php-censor -e "SELECT DATABASE(), VERSION();"
DATABASE()	VERSION()
php-censor	10.6.23-MariaDB-0ubuntu0.22.04.1

⚠️ 注意:库名与账号名都带连字符,在 SQL 中必须用反引号包裹(`php-censor`),否则会被解析成「库名 php 减 表名 censor」。这是本节最容易踩的坑。

4.4 用 Composer 创建项目

官方推荐用 create-project 拉取项目,它会把代码与依赖一次性装好:

mkdir -p /var/www
cd /var/www
composer create-project php-censor/php-censor php-censor --keep-vcs --no-interaction

安装过程会解析并下载 130 个包,输出很长,此处只保留末尾节选(实测):

  - Installing voku/anti-xss (4.1.42): Extracting archive
   0/130 [>---------------------------]   0%
  38/130 [========>-------------------]  29%
  60/130 [============>---------------]  46%
  83/130 [=================>----------]  63%
 105/130 [======================>-----]  80%
 126/130 [===========================>]  96%
 130/130 [============================] 100%
Package swiftmailer/swiftmailer is abandoned, you should avoid using it. Use symfony/mailer instead.
Package php-cs-fixer/diff is abandoned, you should avoid using it. No replacement was suggested.
Package sebastian/phpcpd is abandoned, you should avoid using it. No replacement was suggested.
Package webmozart/path-util is abandoned, you should avoid using it. Use symfony/filesystem instead.
Generating autoload files
89 packages you are using are looking for funding.
Use the `composer fund` command to get more info!
infection/extension-installer: No extensions found

create-project 默认连 require-dev 一起装,因此是 130 个包、目录约 151MB。生产环境若不需要开发工具链,可在项目目录内补执行一次 composer install --no-dev --optimize-autoloader 收敛体积(方案 B 即为该形态,实测 62 个包、约 71MB)。

确认代码版本与目录:

cd /var/www/php-censor
git describe --tags
cat VERSION.md
./bin/console --version
2.1.6
2.1.6
PHP Censor v2.1.6

4.5 执行安装命令

php-censor:install 负责检查环境、建表、创建管理员。参数含义见下表(可用 ./bin/console help php-censor:install 查看):

参数本次取值说明
--urlhttp://php-censor.local写入 app/config.yml 的 url,影响页面链接与 Webhook 回调地址
--db-typemysql可选 mysql 或 pgsql
--db-hostlocalhost数据库地址
--db-portdefault特殊值:MySQL 取 3306,PostgreSQL 取 5432
--db-name / --db-user / --db-passwordphp-censor / php-censor / q1w2e3r44.3 节创建的库与账号
--admin-name / --admin-password / --admin-emailadmin / q1w2e3r4 / admin@php-censor.local初始管理员
--queue-host / --queue-port / --queue-namelocalhost / 11300 / php-censorBeanstalkd 连接信息
cd /var/www/php-censor
./bin/console php-censor:install \
    --url='http://php-censor.local' \
    --db-type=mysql \
    --db-host=localhost \
    --db-port=default \
    --db-name=php-censor \
    --db-user=php-censor \
    --db-password='q1w2e3r4' \
    --admin-name=admin \
    --admin-password='q1w2e3r4' \
    --admin-email='admin@php-censor.local' \
    --queue-host=localhost \
    --queue-port=11300 \
    --queue-name=php-censor

实测输出(完整照录):


***************************************
*  Welcome to PHP Censor installation *
***************************************

Checking requirements...

OK

Please answer the following questions:
--------------------------------------


Setting up your database...
using migration paths
 - /var/www/php-censor/src/Migrations
warning no environment specified, defaulting to: php-censor
using adapter mysql
using database php-censor
ordering by creation time

 == 20200505070903 InitialMigrationV2: migrating
 == 20200505070903 InitialMigrationV2: migrated 0.0586s

 == 20220223061912 WebhookRequestsNewTable: migrating
 == 20220223061912 WebhookRequestsNewTable: migrated 0.0079s

 == 20220429095102 DashboardCoverage: migrating
 == 20220429095102 DashboardCoverage: migrated 0.0029s

 == 20220604191600 SecretsNewTable: migrating
 == 20220604191600 SecretsNewTable: migrated 0.0080s

All Done. Took 0.0843s
OK
User account created!
Default project group created!

四张迁移全部 up,User account created! 与 Default project group created! 两行确认管理员与默认项目组已建好。

验证建表结果:

mariadb -uphp-censor -pq1w2e3r4 php-censor -e "SHOW TABLES;"
Tables_in_php-censor
build_errors
build_metas
builds
environments
migrations
project_groups
projects
secrets
users
webhook_requests

五、安装方案 B:官方源码包 + composer install

5.1 适用场景

机房不能直连 Packagist、或需要把代码版本钉死在某个 tag 上时,用官方源码包更合适:先在一台可联网的机器上把包与 vendor 备好,再整体搬到内网服务器。本节的下载与依赖安装均在目标服务器上实测。

5.2 下载与校验

官方源码包地址遵循 archive/refs/tags/<版本>.tar.gz 规则:

cd /tmp
curl -sSL --max-time 120 -o php-censor-2.1.6.tar.gz \
  https://github.com/php-censor/php-censor/archive/refs/tags/2.1.6.tar.gz
ls -lh php-censor-2.1.6.tar.gz
sha256sum php-censor-2.1.6.tar.gz

实测:

-rw-r--r-- 1 root root 13M Oct 27 17:29 php-censor-2.1.6.tar.gz
2f441a108d1668d38185f1623edd7c0e62fdb30a7f78e01c13d25f62ca360e99  php-censor-2.1.6.tar.gz

说明:sha256 随手记录,便于内网搬运后比对,防止传输损坏。该值对应 2026-10-28 从 GitHub 拉取的 2.1.6 标签包。

5.3 解压与安装依赖

cd /opt
tar -xzf /tmp/php-censor-2.1.6.tar.gz
mv php-censor-2.1.6 php-censor
cd php-censor
composer install --no-dev --optimize-autoloader --no-interaction

实测输出末尾节选:

 62/62 [============================] 100%
Package swiftmailer/swiftmailer is abandoned, you should avoid using it. Use symfony/mailer instead.
Package php-cs-fixer/diff is abandoned, you should avoid using it. No replacement was suggested.
Package sebastian/phpcpd is abandoned, you should avoid using it. No replacement was suggested.
Package webmozart/path-util is abandoned, you should avoid using it. Use symfony/filesystem instead.
Generating optimized autoload files
42 packages you are using are looking for funding.
Use the `composer fund` command to get more info!

--no-dev 只装 62 个运行时包,--optimize-autoloader 生成类映射以加快加载。验证可执行:

cd /opt/php-censor
./bin/console --version
PHP Censor v2.1.6

两条路径的体积对比(du -sh 实测):方案 A 为 151M,方案 B 为 71M。差异全部来自开发依赖(PHPUnit、Psalm、Infection、PHP-CS-Fixer 等)。

5.4 后续初始化

方案 B 的初始化命令与方案 A 完全一致,只是工作目录换成 /opt/php-censor:

cd /opt/php-censor
./bin/console php-censor:install --url='http://php-censor.local' --db-type=mysql \
    --db-host=localhost --db-port=default --db-name=php-censor \
    --db-user=php-censor --db-password='q1w2e3r4' \
    --admin-name=admin --admin-password='q1w2e3r4' \
    --admin-email='admin@php-censor.local' \
    --queue-host=localhost --queue-port=11300 --queue-name=php-censor

⚠️ 注意:同一台服务器上不要同时安装两套并指向同一个库。php-censor:install 会重复执行迁移,第二次执行会因迁移记录已存在而报错跳过;若确需并存,必须使用不同的 --db-name。

六、Web 服务器与 PHP-FPM 配置

6.1 站点配置文件

站点根目录必须指向项目下的 public(而不是项目根),并把所有不存在的路径重写到 index.php:

server {
    listen 80;
    listen [::]:80;

    server_name php-censor.local;
    root /var/www/php-censor/public;

    index index.php index.html;

    charset utf-8;
    client_max_body_size 32m;

    access_log /var/log/nginx/php-censor.access.log;
    error_log  /var/log/nginx/php-censor.error.log;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_read_timeout 300s;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

逐项说明关键指令:

指令取值作用
root/var/www/php-censor/public只暴露 public,app/config.yml 与数据库口令不会落到 Web 根下
try_files ... /index.php?$query_string固定写法PHP Censor 的 URL 全部由 index.php 派发,缺这行会出现 404
fastcgi_passunix:/run/php/php8.1-fpm.sock与 PHP 版本强绑定,升 PHP 版本时必须同步改
client_max_body_size32m上传构建产物(覆盖率 HTML 等)时的上限
fastcgi_read_timeout300s构建详情页渲染长日志时避免 504
location ~ /\.deny all禁止访问 .git、.env 等隐藏文件

写入并启用:

ln -sf /etc/nginx/sites-available/php-censor /etc/nginx/sites-enabled/php-censor
rm -f /etc/nginx/sites-enabled/default
nginx -t

实测:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

6.2 重载并验证 HTTP 响应

systemctl reload nginx
curl -sI -H "Host: php-censor.local" http://127.0.0.1/ | head -8

实测:

HTTP/1.1 302 Found
Server: nginx/1.18.0 (Ubuntu)
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Cache-Control: max-age=0, private, must-revalidate
Set-Cookie: PHPSESSID=r31590l81ooe0588j9mqtvm3t7; path=/
Cache-Control: no-cache, private
Date: Tue, 27 Oct 2026 17:23:04 GMT

302 是预期结果:未登录访问根路径会被重定向到 /session/login。若返回 502,说明 PHP-FPM 未启动或 socket 路径不匹配;若返回 404,说明 try_files 未生效。

6.3 PHP-FPM socket 确认

ls -l /run/php/
total 4
lrwxrwxrwx 1 root     root     30 Oct 27 17:21 php-fpm.sock -> /etc/alternatives/php-fpm.sock
-rw-r--r-- 1 root     root      5 Oct 27 17:21 php8.1-fpm.pid
srw-rw---- 1 www-data www-data  0 Oct 27 17:21 php8.1-fpm.sock

php8.1-fpm.sock 的属主是 www-data:www-data,权限 srw-rw----,Nginx 主进程通常以 www-data 运行,具备读写权限。若改用自定义 FPM 用户,需把 Nginx 用户加入同一组。

七、systemd 托管与 Worker

7.1 创建运行账号与目录权限

Web 与 Worker 应使用同一个低权限账号,避免构建产物因属主不一致而写入失败:

useradd -r -m -d /home/php-censor -s /bin/bash php-censor
id php-censor
chown -R php-censor:php-censor /var/www/php-censor
chmod -R g+rwX /var/www/php-censor/app /var/www/php-censor/public/artifacts
usermod -aG php-censor www-data

实测:

uid=999(php-censor) gid=999(php-censor) groups=999(php-censor)
drwxr-xr-x 12 php-censor php-censor 4096 Oct 27 17:22 /var/www/php-censor
drwxrwxr-x  2 php-censor php-censor 4096 Oct 27 17:22 /var/www/php-censor/app

app/ 需要写权限(配置、缓存),public/artifacts 需要写权限(覆盖率报告),因此单独放宽到组可写,其余目录保持 755。

7.2 Worker 的 systemd 模板单元

官方 Worker 文档给出的是模板单元,支持用 @N 起多个并发 Worker。本次的单机配置如下:

[Unit]
Description=PHP Censor Worker %I
After=network.target mariadb.service beanstalkd.service

[Service]
Type=simple
User=php-censor
Group=php-censor
WorkingDirectory=/var/www/php-censor
ExecStart=/usr/bin/php /var/www/php-censor/bin/console php-censor:worker
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

关键项说明:

配置项取值为什么
After含 mariadb.service beanstalkd.serviceWorker 启动即连库与队列,先起依赖可减少启动期重试
User / Groupphp-censor不用 root,构建脚本无法越权
WorkingDirectory/var/www/php-censor插件按相对路径找 vendor/bin,必须固定
ExecStart绝对路径的 php + 绝对路径 consolesystemd 不读登录 shell 的 PATH
Restart=always常驻Worker 异常退出后自动拉起,替代 nohup

部署与启动:

systemctl daemon-reload
systemctl enable --now php-censor@1
systemctl is-active php-censor@1
systemctl status php-censor@1 --no-pager -n 5

实测:

Created symlink /etc/systemd/system/multi-user.target.wants/php-censor@1.service → /etc/systemd/system/php-censor@.service.
active
● php-censor@1.service - PHP Censor Worker 1
     Loaded: loaded (/etc/systemd/system/php-censor@.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2026-10-27 17:27:10 UTC; 2s ago
   Main PID: 16199 (php)
      Tasks: 1 (limit: 2932)
     Memory: 15.0M
        CPU: 47ms
             └─16199 /usr/bin/php /var/www/php-censor/bin/console php-censor:worker

Oct 27 17:27:10 php-censor systemd[1]: Started PHP Censor Worker 1.

说明:以上为 systemctl status 输出的节选(省略了 CGroup 行);Memory: 15.0M 为 Worker 空载时的常驻内存。

7.3 Worker 常驻确认

ps -o pid,user,rss,etime,args -C php | grep -E "worker|PID"
    PID USER       RSS     ELAPSED COMMAND
  16199 php-cen+ 38580       01:13 /usr/bin/php /var/www/php-censor/bin/console php-censor:worker

RSS 约 37MB,进程属主为 php-cen+(即 php-censor 截断显示)。

7.4 登录验证

登录表单带一次性 CSRF 令牌(字段名 login_form),用 curl 走完整流程:先取令牌与 Cookie,再提交账号口令:

cd /tmp
TOKEN=$(curl -s -c c.txt -H "Host: php-censor.local" \
  http://127.0.0.1/session/login | grep -oP 'name="login_form" value="\K[^"]+')
curl -s -b c.txt -c c.txt -o /dev/null \
  -w "login_status=%{http_code} redirect=%{redirect_url}\n" \
  -H "Host: php-censor.local" \
  -d "login_form=$TOKEN" -d "email=admin@php-censor.local" -d "password=q1w2e3r4" \
  http://127.0.0.1/session/login
curl -s -b c.txt -H "Host: php-censor.local" http://127.0.0.1/ | grep -oE "<title>[^<]*</title>"

实测:

login_status=302 redirect=http://php-censor.local/
<title>Dashboard</title>

302 跳回首页 + 页面标题为 Dashboard,说明登录成功且会话有效。

7.5 首次构建验证

为验证「Web 侧建项目 → 队列 → Worker → 插件执行」整条链路,建一个最小示例项目 /var/www/demo-app:

mkdir -p /var/www/demo-app/src
cat > /var/www/demo-app/composer.json <<'JSON'
{
    "name": "demo/calculator",
    "require": { "php": ">=7.4" },
    "autoload": { "psr-4": { "Demo\\": "src/" } }
}
JSON
cat > /var/www/demo-app/src/Calculator.php <<'PHP'
<?php
namespace Demo;
class Calculator
{
    public function add(int $a, int $b): int
    {
        return $a + $b;
    }
}
PHP
cat > /var/www/demo-app/.php-censor.yml <<'YAML'
build_settings:
  ignore:
    - "vendor"

setup:
  composer_step:
    plugin: composer
    action: "install"

test:
  lint_step:
    plugin: shell
    commands:
      - "php -l src/Calculator.php"
      - "echo demo build ok"
YAML

在 Web 界面 project/add 页选择仓库类型 local、引用填 /var/www/demo-app、项目组选默认组,保存后项目落库。随后用命令行触发构建:

cd /var/www/php-censor
./bin/console php-censor:create-build 1 --branch=master --commit=a1b2c3d4 \
    --email=admin@php-censor.local --message="demo build"
Build Created

等待 Worker 取任务执行后,查询构建状态与日志:

mariadb -uphp-censor -pq1w2e3r4 php-censor -e \
  "SELECT id,project_id,commit_id,branch,status,errors_total FROM builds ORDER BY id DESC LIMIT 1;"
id	project_id	commit_id	branch	status	errors_total
1	1	a1b2c3d4	master	2	0

status=2 表示构建成功,errors_total=0 表示无错误。构建日志(实测原文,含终端的颜色转义序列):

Shell command: mkdir -p "<PHP_CENSOR_PATH>/runtime/builds/1/1_7b48ec00" && cp -Rf /var/www/demo-app/* "<BUILD_PATH>/"
Working copy created: <BUILD_PATH>/

RUNNING PLUGIN: Composer (Step: composer_step) (Stage: Setup)
Shell command: /usr/bin/composer --no-ansi --no-interaction  --working-dir="<BUILD_PATH>/" install
No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https:/getcomposer.org/install for more information.
Loading composer repositories with package information
Updating dependencies
Nothing to modify in lock file
Writing lock file
Installing dependencies from lock file (including require-dev)
Nothing to install, update or remove
Generating autoload files
PLUGIN: SUCCESS

RUNNING PLUGIN: Shell (Step: lint_step) (Stage: Test)
Shell command: php -l src/Calculator.php
No syntax errors detected in src/Calculator.php
Shell command: echo demo build ok
demo build ok
PLUGIN: SUCCESS

BUILD SUCCESS!

REMOVING BUILD.

这条日志证明四件事:Worker 正常工作、Composer 插件在 setup 阶段执行、Shell 插件在 test 阶段执行、构建成功后自动清理工作副本(REMOVING BUILD.)。

八、方案对比与选择建议

维度方案 A:Composer create-project方案 B:官方源码包 + composer install
是否需联网需要 Packagist打包时需要;目标机只需 composer install 或直接搬运 vendor
版本锁定跟 Packagist 解析结果,需显式指定版本才钉死由 tag 直接钉死,可记录 sha256
依赖数量130 个(含 dev)62 个(--no-dev)
目录体积约 151MB约 71MB
是否带 Git 元数据带(--keep-vcs)带(tar 包内含 .git 目录结构)
适用场景可联网、需要开发调试内网、离线、生产瘦身

选择建议:能联外网且需要跑测试/改代码的开发机用方案 A;生产服务器与内网环境用方案 B,并在打包机上保留 composer.lock 与 vendor 目录,目标机只需解压即可启动。两条路径初始化命令完全相同,切换成本只在「代码怎么来」这一步。

九、配置说明

安装命令已在 app/config.yml 生成完整配置。关键段与本次取值:

php-censor:
    language: en
    per_page: 10
    url: 'http://php-censor.local'
    queue:
        port: '11300'
        lifetime: 600
        host: localhost
        name: php-censor
    realtime_ui: true
    webhook:
        log_requests: false
    log:
        rotate: false
        max_files: 0
    email_settings:
        from_address: 'PHP Censor <no-reply@php-censor.local>'
        smtp_address: null
        smtp_port: null
        smtp_username: null
        smtp_password: null
        smtp_encryption: false
    ssh:
        strength: 2048
        comment: admin@php-censor
    build:
        remove_builds: true
        writer_buffer_size: 500
        allow_public_artifacts: false
        keep_builds: 100
    security:
        disable_auth: false
        default_user_id: 1
        auth_providers:
            internal: { type: internal }
    database:
        servers:
            read: [{ host: localhost, port: 3306 }]
            write: [{ host: localhost, port: 3306 }]
        type: mysql
        name: php-censor
        username: php-censor
        password: q1w2e3r4

需要立即关注的四项:

  1. url:必须与浏览器访问地址一致,否则邮件通知里的链接、Webhook 回跳地址全部错误。
  2. queue.name:Worker 与 Web 必须一致,改了一边就要同步另一边,否则任务永远不被消费。
  3. build.keep_builds: 100:每个项目保留最近 100 次构建,超出部分由 php-censor:remove-old-builds 清理。
  4. security.disable_auth: false:保持鉴权开启;若关闭,务必配合反向代理的访问控制。

配置文件权限:

chown php-censor:php-censor /var/www/php-censor/app/config.yml
chmod 640 /var/www/php-censor/app/config.yml

⚠️ 注意:app/config.yml 里有数据库明文口令。站点根目录指向 public、且 Nginx 对 /. 前缀做了 deny all,两道措施叠加才能保证该文件不可通过 HTTP 读取。改动 Nginx 配置时不要删掉这两条约束。

十、升级与卸载

10.1 升级

2.1.x 内的小版本升级步骤(以 2.1.6 为基线):

systemctl stop php-censor@1
cd /var/www/php-censor
cp -a app/config.yml /var/backups/config.yml.$(date +%F)
git fetch --tags
git checkout 2.1.6
composer install --no-dev --optimize-autoloader --no-interaction
./bin/console php-censor-migrations:migrate
systemctl start php-censor@1
systemctl is-active php-censor@1

迁移命令会输出每条迁移的 up 记录;迁移前务必先备份数据库(见配套的命令与运维篇)。跨大版本(2.1 → 3.0)不适用本流程,需按官方升级文档评估配置差异。

10.2 卸载

systemctl disable --now php-censor@1
rm -f /etc/systemd/system/php-censor@.service
systemctl daemon-reload
rm -f /etc/nginx/sites-enabled/php-censor
rm -f /etc/nginx/sites-available/php-censor
systemctl reload nginx
rm -rf /var/www/php-censor
mariadb -uroot -e "DROP DATABASE \`php-censor\`; DROP USER 'php-censor'@'localhost';"

如需彻底清理,再删除 php-censor 系统账号与 /home/php-censor。Beanstalkd 与 MariaDB 若仅供本应用使用,可一并 systemctl disable --now,但删除数据目录前务必确认无其他业务共用。

十一、常见问题

11.1 安装时提示 requirements 检查失败

php-censor:install 开头的 Checking requirements... 会检查 PHP 版本、扩展与目录可写性。实测中最常见的两类原因:

  • ext-* 缺失:对照 4.1 节的扩展清单,缺哪个装对应的 php8.1-* 包,装完重启 PHP-FPM;
  • 目录不可写:app/ 或 runtime/ 属主不是运行账号时直接失败,按 7.1 节 chown -R 修正。

11.2 页面返回 502 Bad Gateway

按顺序排查:

systemctl is-active php8.1-fpm
ls -l /run/php/php8.1-fpm.sock
tail -5 /var/log/nginx/php-censor.error.log

先确认 FPM 活跃、socket 存在且属主为 www-data,再确认 fastcgi_pass 的路径与 ls 结果一字不差。实测中升级 PHP 版本后旧 socket 名会失效,配置文件忘记改就会出现 502。

11.3 创建项目时返回 500

本次实测遇到两种 500,均记录如下:

现象一:POST /project/add 返回 500,Nginx 错误日志有

PHP message: PHP Warning:  Undefined array key "ssh_public_key" in /var/www/php-censor/src/Controller/ProjectController.php on line 414

定位:表单提交缺少 ssh_public_key 字段(用脚本直接 POST 时容易漏),控制器读取该键时未做空值保护。

解决:提交表单时把 ssh_public_key 与 ssh_private_key 一并带上(值可为空)。浏览器正常操作不受影响,因为表单会自动带上隐藏字段。

现象二:同一请求返回 200 但页面停留在表单,help-block 里显示

Project group is required.

定位:group_id 未提交。PHP Censor 要求项目必须归属某个项目组。

解决:提交时带上 group_id=1(安装时自动创建的默认组)。

11.4 Worker 启动了但构建一直「等待」

按链路顺序排查:

systemctl is-active beanstalkd
mariadb -uphp-censor -pq1w2e3r4 php-censor -e "SELECT id,status FROM builds ORDER BY id DESC LIMIT 3;"
journalctl -u php-censor@1 --no-pager -n 20

三个检查点依次是:队列服务在跑、构建记录已入库(status 为等待态)、Worker 日志无异常退出。实测中若 queue.name 与 Web 侧配置不一致,Worker 会连上队列但永远取不到任务,日志里没有任何报错——这是最难发现的一种配置失误。

11.5 Composer 安装依赖时提示 abandoned 包

实测输出的几行提示(swiftmailer/swiftmailer、php-cs-fixer/diff、sebastian/phpcpd、webmozart/path-util)是上游包被标记废弃,不影响安装与运行。PHP Censor 2.1.6 仍依赖它们,属于「知道即可」的提示,不建议手工替换依赖版本。

11.6 构建日志显示 <BUILD_PATH> 之类的占位符

构建日志中的 <BUILD_PATH>、<PHP_CENSOR_PATH> 是插件在执行前的变量插值标记,实际执行的命令已被替换为真实路径。这是正常输出,不是错误;排查时应以数据库 builds.log 中 Shell command: 那一行的后半段为准。

十二、参考链接