PHP 信创适配工具包 - 从 01 完整开源项目

  一、这个工具是干啥用的?(大白话)

  信创 = "信息技术应用创新",就是国家要求用国产软硬件替换 Windows、Intel、Oracle 这些外国货。

  痛点:你公司有个跑了 5 年的 PHP 老项目,现在客户(政府/银行/国企)要求部署到国产环境:
  - 操作系统:统信 UOS / 麒麟 Kylin / openEuler(不是 CentOS 了)
  - CPU:龙芯 LoongArch / 飞腾 ARM / 鲲鹏 ARM(不是 x86 了)
  - 数据库:达梦 DM / 人大金仓 Kingbase(不是 MySQL 了)
  - 中间件:东方通 TongWeb(不是 Tomcat 了)

  手动迁移会踩的坑:
  1. 代码里写死了 mysql_connect() → 国产库连不上
  2. SQL 用了 MySQL 方言 LIMIT 10 → 达梦不认,要 ROWNUM
  3. 路径写死 C:\\ 或 /var/www → 换系统就崩
  4. 用了 ioncube/sg11 加密扩展 → ARM/龙芯没编译版本
  5. 文件名大小写 User.php vs user.php → Linux 区分大小写
  6. 编码 GBK 乱码 → 国产系统默认 UTF-8

  这个工具做啥:
  - 扫描:用 AST(抽象语法树)分析 PHP 代码,找出所有"国产环境跑不起来"的地方
  - 报告:生成 HTML 报告,告诉你哪一行第几列有问题、严重程度、怎么改
  - 自动修复:能自动改的直接改(比如 mysql_* → mysqli_*、SQL 方言转换)
  - 持续监控:接入 CI,每次提交代码都检查,防止开倒车

  ---
  二、技术选型(为什么选这些库)

  ┌──────────────┬─────────────────────────────────┬────────────────────────────────────────────┐
  │     用途     │               库                │                  为啥选它                  │
  ├──────────────┼─────────────────────────────────┼────────────────────────────────────────────┤
  │ AST 解析     │ nikic/php-parser                │ PHP 静态分析事实标准,PHPStan/Rector 都用它 │
  ├──────────────┼─────────────────────────────────┼────────────────────────────────────────────┤
  │ 静态分析框架 │ phpstan/phpstan                 │ 规则可扩展,生态最强                        │
  ├──────────────┼─────────────────────────────────┼────────────────────────────────────────────┤
  │ CLI 框架     │ symfony/console                 │ Composer/Laravel Artisan 都用这个          │
  ├──────────────┼─────────────────────────────────┼────────────────────────────────────────────┤
  │ 文件遍历     │ symfony/finder                  │ 链式 API 比 glob() 强一万倍                │
  ├──────────────┼─────────────────────────────────┼────────────────────────────────────────────┤
  │ 配置         │ symfony/yaml                    │ YAML 写规则比 JSON 易读                    │
  ├──────────────┼─────────────────────────────────┼────────────────────────────────────────────┤
  │ 日志         │ monolog/monolog                 │ PHP 日志事实标准                           │
  ├──────────────┼─────────────────────────────────┼────────────────────────────────────────────┤
  │ 代码改写     │ nikic/php-parser 的 NodeVisitor │ 改 AST 比正则替换安全                      │
  ├──────────────┼─────────────────────────────────┼────────────────────────────────────────────┤
  │ HTML 报告    │ twig/twig                       │ 模板引擎,和 Symfony 一家                   │
  ├──────────────┼─────────────────────────────────┼────────────────────────────────────────────┤
  │ 测试         │ phpunit/phpunit                 │ 不解释                                     │
  ├──────────────┼─────────────────────────────────┼────────────────────────────────────────────┤
  │ 类型检查     │ phpstan/phpstan level 8         │ 自己用自己                                 │
  ├──────────────┼─────────────────────────────────┼────────────────────────────────────────────┤
  │ 代码风格     │ friendsofphp/php-cs-fixer       │ PSR-12 自动格式化                          │
  └──────────────┴─────────────────────────────────┴────────────────────────────────────────────┘

  ---
  三、完整代码

  1. composer.json — 项目元数据和依赖

  {
      "name": "yourorg/php-xinchuang-kit",
      "description": "PHP 信创(国产化)环境兼容性扫描与自动修复工具包",
      "type": "library",
      "license": "MIT",
      "keywords": ["xinchuang", "信创", "国产化", "compatibility", "static-analysis", "php", "dameng", "kingbase",
  "kylin", "uos"],
      "authors": [
          {"name": "Your Name", "email": "you@example.com"}
      ],
      "require": {
          "php": ">=8.1",
          "ext-json": "*",
          "ext-mbstring": "*",
          "nikic/php-parser": "^5.0",
          "symfony/console": "^6.4",
          "symfony/finder": "^6.4",
          "symfony/yaml": "^6.4",
          "symfony/process": "^6.4",
          "monolog/monolog": "^3.5",
          "twig/twig": "^3.8"
      },
      "require-dev": {
          "phpunit/phpunit": "^10.5",
          "phpstan/phpstan": "^1.10",
          "friendsofphp/php-cs-fixer": "^3.40",
          "rector/rector": "^1.0"
      },
      "autoload": {
          "psr-4": {"Xinchuang\\Kit\\": "src/"}
      },
      "autoload-dev": {
          "psr-4": {"Xinchuang\\Kit\\Tests\\": "tests/"}
      },
      "bin": ["bin/xinchuang"],
      "scripts": {
          "test": "phpunit",
          "stan": "phpstan analyse src --level=8",
          "cs": "php-cs-fixer fix",
          "ci": ["@cs", "@stan", "@test"]
      },
      "config": {
          "sort-packages": true,
          "optimize-autoloader": true
      },
      "minimum-stability": "stable"
  }

  大白话:这是项目的"身份证"。声明依赖、自动加载规则、可执行入口、CI 脚本快捷方式。

  ---
  2. bin/xinchuang — CLI 入口

  #!/usr/bin/env php
  <?php
  declare(strict_types=1);

  // 兼容全局安装和项目内安装两种方式
  $autoloads = [
      __DIR__ . '/../vendor/autoload.php',         // 项目内
      __DIR__ . '/../../../autoload.php',          // 作为依赖被安装
  ];
  foreach ($autoloads as $file) {
      if (file_exists($file)) {
          require $file;
          break;
      }
  }

  use Xinchuang\Kit\Application;

  (new Application())->run();

  大白话:用户终端敲 xinchuang scan ./src 时,执行的就是这个文件。

  ---
  3. src/Application.php — 应用入口

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit;

  use Symfony\Component\Console\Application as ConsoleApplication;
  use Xinchuang\Kit\Command\ScanCommand;
  use Xinchuang\Kit\Command\FixCommand;
  use Xinchuang\Kit\Command\ReportCommand;

  final class Application extends ConsoleApplication
  {
      public const VERSION = '0.1.0';

      public function __construct()
      {
          parent::__construct('PHP Xinchuang Kit', self::VERSION);

          $this->add(new ScanCommand());
          $this->add(new FixCommand());
          $this->add(new ReportCommand());
      }
  }

  大白话:注册三个子命令:scan(扫描)、fix(修复)、report(出报告)。

  ---
  4. src/Rule/rules.yaml — 规则库(核心知识)

  # 信创兼容性规则库,持续更新
  # 严重级别: error(必须改) / warning(建议改) / info(提示)

  extensions:
    blocked:
      - name: ioncube_loader
        reason: "ionCube 在龙芯/部分 ARM 平台无官方版本"
        severity: error
        suggestion: "改用 sourceguardian 或者去除加密"
      - name: sg11
        reason: "SourceGuardian 国产 CPU 兼容性差"
        severity: error
      - name: zend_guard_loader
        reason: "Zend Guard 已停止维护,国产环境无法安装"
        severity: error

    deprecated:
      - name: mysql
        since_php: 7.0
        replace_with: mysqli
        severity: error
      - name: mcrypt
        since_php: 7.2
        replace_with: openssl / sodium
        severity: error

  functions:
    deprecated:
      mysql_connect: { replace: mysqli_connect, severity: error }
      mysql_query: { replace: mysqli_query, severity: error }
      mysql_fetch_array: { replace: mysqli_fetch_array, severity: error }
      mysql_real_escape_string: { replace: mysqli_real_escape_string, severity: error }
      ereg: { replace: preg_match, severity: error }
      split: { replace: explode / preg_split, severity: error }
      each: { replace: foreach, severity: error }
      create_function: { replace: 匿名函数, severity: error }

  database:
    mysql_to_dameng:
      - pattern: '/\bLIMIT\s+(\d+)\s*,\s*(\d+)/i'
        replace: 'OFFSET $1 ROWS FETCH NEXT $2 ROWS ONLY'
        reason: "达梦不支持 MySQL 的 LIMIT m,n 写法"
      - pattern: '/`([a-zA-Z_][a-zA-Z0-9_]*)`/'
        replace: '"$1"'
        reason: "达梦使用双引号包裹标识符,不是反引号"
      - pattern: '/\bIFNULL\s*\(/i'
        replace: 'NVL('
        reason: "达梦使用 NVL 而非 IFNULL"
      - pattern: '/\bNOW\(\)/i'
        replace: 'SYSDATE'
        reason: "达梦使用 SYSDATE 获取当前时间"
      - pattern: '/\bAUTO_INCREMENT\b/i'
        replace: 'IDENTITY(1,1)'
        reason: "达梦自增列语法不同"

    mysql_to_kingbase:
      - pattern: '/`([a-zA-Z_][a-zA-Z0-9_]*)`/'
        replace: '"$1"'
        reason: "金仓基于 PostgreSQL,使用双引号"
      - pattern: '/\bLIMIT\s+(\d+)\s*,\s*(\d+)/i'
        replace: 'LIMIT $2 OFFSET $1'
        reason: "金仓 LIMIT/OFFSET 顺序与 MySQL 不同"

  paths:
    hardcoded:
      - pattern: '/[A-Z]:\\\\/'
        severity: error
        reason: "硬编码 Windows 盘符,Linux 国产系统无法运行"
      - pattern: '/\/var\/www\/html/'
        severity: warning
        reason: "硬编码 Apache 路径,国产中间件路径不同"

  encoding:
    gbk_files: { severity: warning, reason: "建议统一为 UTF-8" }
    bom_files: { severity: error, reason: "BOM 头会导致 header() 失败" }

  architecture:
    x86_only_extensions:
      - intl_icu_legacy
      - sqlsrv

  middleware:
    apache_specific:
      - pattern: 'apache_request_headers\('
        severity: warning
        reason: "东方通 TongWeb 等国产中间件不支持 Apache 专用函数"
        replace: "getallheaders() 或自行解析 \$_SERVER"

  大白话:这个 YAML 是工具的"大脑",所有规则都从这里读。社区可以提交 PR 加规则,不用改代码。

  ---
  5. src/Rule/RuleLoader.php — 规则加载器

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Rule;

  use Symfony\Component\Yaml\Yaml;

  final class RuleLoader
  {
      /** @var array<string, mixed> */
      private array $rules;

      public function __construct(?string $customPath = null)
      {
          $defaultPath = __DIR__ . '/rules.yaml';
          $this->rules = Yaml::parseFile($defaultPath);

          if ($customPath !== null && file_exists($customPath)) {
              $custom = Yaml::parseFile($customPath);
              $this->rules = array_replace_recursive($this->rules, $custom);
          }
      }

      /** @return array<string, mixed> */
      public function get(string $key): array
      {
          return $this->rules[$key] ?? [];
      }

      /** @return array<string, mixed> */
      public function all(): array
      {
          return $this->rules;
      }
  }

  大白话:加载默认规则,如果用户传了自己的规则文件就合并。

  ---
  6. src/Report/Issue.php — 问题数据对象

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Report;

  final readonly class Issue
  {
      public function __construct(
          public string $file,
          public int $line,
          public int $column,
          public string $severity,        // error / warning / info
          public string $category,        // extension / database / path / encoding / function / arch
          public string $rule,            // 规则 ID
          public string $message,         // 给人看的描述
          public ?string $suggestion = null,
          public ?string $codeSnippet = null,
          public bool $autoFixable = false,
      ) {}

      /** @return array<string, mixed> */
      public function toArray(): array
      {
          return [
              'file' => $this->file,
              'line' => $this->line,
              'column' => $this->column,
              'severity' => $this->severity,
              'category' => $this->category,
              'rule' => $this->rule,
              'message' => $this->message,
              'suggestion' => $this->suggestion,
              'snippet' => $this->codeSnippet,
              'auto_fixable' => $this->autoFixable,
          ];
      }
  }

  大白话:每个发现的问题封装成一个不可变对象,用 PHP 8.2readonly class(写完不能改)。

  ---
  7. src/Report/Report.php — 报告聚合器

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Report;

  final class Report
  {
      /** @var Issue[] */
      private array $issues = [];

      public function add(Issue $issue): void
      {
          $this->issues[] = $issue;
      }

      /** @return Issue[] */
      public function all(): array
      {
          return $this->issues;
      }

      /** @return Issue[] */
      public function bySeverity(string $severity): array
      {
          return array_values(array_filter(
              $this->issues,
              fn(Issue $i) => $i->severity === $severity
          ));
      }

      public function errorCount(): int
      {
          return count($this->bySeverity('error'));
      }

      public function warningCount(): int
      {
          return count($this->bySeverity('warning'));
      }

      public function hasErrors(): bool
      {
          return $this->errorCount() > 0;
      }

      public function toJson(): string
      {
          return json_encode(
              ['issues' => array_map(fn(Issue $i) => $i->toArray(), $this->issues)],
              JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
          );
      }
  }

  大白话:把所有 Issue 收集起来,按严重级别分类、统计、导出 JSON。

  ---
  8. src/Detector/DetectorInterface.php — 检测器接口

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Detector;

  use Xinchuang\Kit\Report\Report;

  interface DetectorInterface
  {
      public function name(): string;

      public function detect(string $filePath, string $content, Report $report): void;
  }

  大白话:所有检测器都实现这个接口,这样可以无限扩展(策略模式)。

  ---
  9. src/Parser/PhpAstParser.php — AST 解析器封装

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Parser;

  use PhpParser\Error;
  use PhpParser\Node;
  use PhpParser\Parser;
  use PhpParser\ParserFactory;

  final class PhpAstParser
  {
      private Parser $parser;

      public function __construct()
      {
          $this->parser = (new ParserFactory())->createForHostVersion();
      }

      /**
       * @return Node\Stmt[]|null  返回 AST 节点数组,解析失败返回 null
       */
      public function parse(string $code): ?array
      {
          try {
              return $this->parser->parse($code);
          } catch (Error) {
              return null;
          }
      }
  }

  大白话:nikic/php-parser 把 PHP 代码变成树状结构,这样我们用代码就能"看懂"代码。

  ---
  10. src/Detector/DeprecatedFunctionDetector.php — 废弃函数检测

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Detector;

  use PhpParser\Node;
  use PhpParser\NodeTraverser;
  use PhpParser\NodeVisitorAbstract;
  use Xinchuang\Kit\Parser\PhpAstParser;
  use Xinchuang\Kit\Report\Issue;
  use Xinchuang\Kit\Report\Report;
  use Xinchuang\Kit\Rule\RuleLoader;

  final class DeprecatedFunctionDetector implements DetectorInterface
  {
      public function __construct(
          private readonly PhpAstParser $parser,
          private readonly RuleLoader $rules,
      ) {}

      public function name(): string
      {
          return 'deprecated_function';
      }

      public function detect(string $filePath, string $content, Report $report): void
      {
          $ast = $this->parser->parse($content);
          if ($ast === null) {
              return;
          }

          /** @var array<string, array{replace: string, severity: string}> $deprecated */
          $deprecated = $this->rules->get('functions')['deprecated'] ?? [];

          $traverser = new NodeTraverser();
          $traverser->addVisitor(new class($filePath, $deprecated, $report) extends NodeVisitorAbstract {
              public function __construct(
                  private readonly string $file,
                  private readonly array $deprecated,
                  private readonly Report $report,
              ) {}

              public function enterNode(Node $node): null
              {
                  if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) {
                      $fnName = $node->name->toString();
                      if (isset($this->deprecated[$fnName])) {
                          $rule = $this->deprecated[$fnName];
                          $this->report->add(new Issue(
                              file: $this->file,
                              line: $node->getStartLine(),
                              column: $node->getStartFilePos(),
                              severity: $rule['severity'],
                              category: 'function',
                              rule: 'deprecated_function',
                              message: "使用了废弃函数 {$fnName}()",
                              suggestion: "替换为 {$rule['replace']}",
                              autoFixable: true,
                          ));
                      }
                  }
                  return null;
              }
          });

          $traverser->traverse($ast);
      }
  }

  大白话:遍历 AST,发现一个函数调用就查规则表,在表里就报告。比正则匹配可靠 —— 注释、字符串里的 mysql_connect 不会误报。

  ---
  11. src/Detector/DatabaseDetector.php — SQL 方言检测

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Detector;

  use PhpParser\Node;
  use PhpParser\NodeTraverser;
  use PhpParser\NodeVisitorAbstract;
  use Xinchuang\Kit\Parser\PhpAstParser;
  use Xinchuang\Kit\Report\Issue;
  use Xinchuang\Kit\Report\Report;
  use Xinchuang\Kit\Rule\RuleLoader;

  final class DatabaseDetector implements DetectorInterface
  {
      public function __construct(
          private readonly PhpAstParser $parser,
          private readonly RuleLoader $rules,
          private readonly string $targetDb = 'dameng',
      ) {}

      public function name(): string
      {
          return 'database_dialect';
      }

      public function detect(string $filePath, string $content, Report $report): void
      {
          $ast = $this->parser->parse($content);
          if ($ast === null) {
              return;
          }

          $ruleKey = 'mysql_to_' . $this->targetDb;
          /** @var array<int, array{pattern: string, replace: string, reason: string}> $sqlRules */
          $sqlRules = $this->rules->get('database')[$ruleKey] ?? [];

          $traverser = new NodeTraverser();
          $traverser->addVisitor(new class($filePath, $sqlRules, $report) extends NodeVisitorAbstract {
              public function __construct(
                  private readonly string $file,
                  private readonly array $sqlRules,
                  private readonly Report $report,
              ) {}

              public function enterNode(Node $node): null
              {
                  // 找字符串字面量,看里面是不是 SQL
                  if ($node instanceof Node\Scalar\String_) {
                      $value = $node->value;
                      if (!$this->looksLikeSql($value)) {
                          return null;
                      }
                      foreach ($this->sqlRules as $rule) {
                          if (preg_match($rule['pattern'], $value)) {
                              $this->report->add(new Issue(
                                  file: $this->file,
                                  line: $node->getStartLine(),
                                  column: 0,
                                  severity: 'error',
                                  category: 'database',
                                  rule: 'sql_dialect',
                                  message: $rule['reason'],
                                  suggestion: "应用规则: {$rule['pattern']} → {$rule['replace']}",
                                  codeSnippet: mb_substr($value, 0, 120),
                                  autoFixable: true,
                              ));
                          }
                      }
                  }
                  return null;
              }

              private function looksLikeSql(string $s): bool
              {
                  return (bool) preg_match(
                      '/\b(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)\b/i',
                      $s
                  );
              }
          });

          $traverser->traverse($ast);
      }
  }

  大白话:从 AST 里挖出所有字符串,判断像不像 SQL,像就拿规则一条条匹配。只看 SQL 字符串,不会把 PHP 代码当 SQL 处理。

  ---
  12. src/Detector/ExtensionDetector.php — 扩展检测

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Detector;

  use Xinchuang\Kit\Report\Issue;
  use Xinchuang\Kit\Report\Report;
  use Xinchuang\Kit\Rule\RuleLoader;

  final class ExtensionDetector implements DetectorInterface
  {
      public function __construct(private readonly RuleLoader $rules) {}

      public function name(): string
      {
          return 'extension';
      }

      public function detect(string $filePath, string $content, Report $report): void
      {
          // 只扫 composer.json
          if (!str_ends_with($filePath, 'composer.json')) {
              return;
          }

          try {
              $composer = json_decode($content, true, flags: JSON_THROW_ON_ERROR);
          } catch (\JsonException) {
              return;
          }

          $required = array_merge(
              $composer['require'] ?? [],
              $composer['require-dev'] ?? []
          );

          /** @var array<int, array{name: string, reason: string, severity: string, suggestion?: string}> $blocked */
          $blocked = $this->rules->get('extensions')['blocked'] ?? [];

          foreach ($required as $pkg => $version) {
              if (!str_starts_with($pkg, 'ext-')) {
                  continue;
              }
              $extName = substr($pkg, 4);
              foreach ($blocked as $rule) {
                  if ($rule['name'] === $extName) {
                      $report->add(new Issue(
                          file: $filePath,
                          line: 1,
                          column: 0,
                          severity: $rule['severity'],
                          category: 'extension',
                          rule: 'blocked_extension',
                          message: "扩展 {$extName} 在国产环境受限: {$rule['reason']}",
                          suggestion: $rule['suggestion'] ?? null,
                      ));
                  }
              }
          }
      }
  }

  大白话:解析 composer.json,看你依赖的扩展在不在"黑名单"里。

  ---
  13. src/Detector/EncodingDetector.php — 编码检测

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Detector;

  use Xinchuang\Kit\Report\Issue;
  use Xinchuang\Kit\Report\Report;

  final class EncodingDetector implements DetectorInterface
  {
      public function name(): string
      {
          return 'encoding';
      }

      public function detect(string $filePath, string $content, Report $report): void
      {
          // BOM 头检测(UTF-8 BOM = EF BB BF)
          if (str_starts_with($content, "\xEF\xBB\xBF")) {
              $report->add(new Issue(
                  file: $filePath,
                  line: 1,
                  column: 0,
                  severity: 'error',
                  category: 'encoding',
                  rule: 'utf8_bom',
                  message: '文件包含 UTF-8 BOM 头',
                  suggestion: '移除 BOM 头,否则 header()/setcookie() 会失败',
                  autoFixable: true,
              ));
          }

          // 非 UTF-8 检测
          if (!mb_check_encoding($content, 'UTF-8')) {
              $detected = mb_detect_encoding($content, ['GBK', 'GB2312', 'BIG5', 'ASCII'], true);
              $report->add(new Issue(
                  file: $filePath,
                  line: 1,
                  column: 0,
                  severity: 'warning',
                  category: 'encoding',
                  rule: 'non_utf8',
                  message: "文件编码不是 UTF-8" . ($detected ? " (检测为 {$detected})" : ''),
                  suggestion: '建议用 iconv 或 mb_convert_encoding 转换为 UTF-8',
                  autoFixable: true,
              ));
          }
      }
  }

  大白话:检测 BOM 头(国产 PHP 环境特别敏感)和非 UTF-8 文件(GBK 文件在国产 Linux 上会乱码)。

  ---
  14. src/Detector/PathDetector.php — 硬编码路径检测

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Detector;

  use Xinchuang\Kit\Report\Issue;
  use Xinchuang\Kit\Report\Report;
  use Xinchuang\Kit\Rule\RuleLoader;

  final class PathDetector implements DetectorInterface
  {
      public function __construct(private readonly RuleLoader $rules) {}

      public function name(): string
      {
          return 'path';
      }

      public function detect(string $filePath, string $content, Report $report): void
      {
          if (!str_ends_with($filePath, '.php')) {
              return;
          }

          /** @var array<int, array{pattern: string, severity: string, reason: string}> $patterns */
          $patterns = $this->rules->get('paths')['hardcoded'] ?? [];

          $lines = explode("\n", $content);
          foreach ($lines as $lineNum => $line) {
              // 跳过注释行(简单判断)
              $trimmed = ltrim($line);
              if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*') || str_starts_with($trimmed, '#')) {
                  continue;
              }
              foreach ($patterns as $rule) {
                  if (preg_match($rule['pattern'], $line, $m)) {
                      $report->add(new Issue(
                          file: $filePath,
                          line: $lineNum + 1,
                          column: (int) (strpos($line, $m[0]) ?: 0),
                          severity: $rule['severity'],
                          category: 'path',
                          rule: 'hardcoded_path',
                          message: $rule['reason'],
                          suggestion: '使用 DIRECTORY_SEPARATOR 或环境变量配置',
                          codeSnippet: trim($line),
                      ));
                  }
              }
          }
      }
  }

  大白话:逐行扫描 PHP 文件,找 C:\ 这种 Windows 路径或 /var/www/html 这种写死的 Apache 路径。

  ---
  15. src/Fixer/FixerInterface.php — 修复器接口

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Fixer;

  interface FixerInterface
  {
      public function name(): string;

      public function supports(string $filePath): bool;

      /** 返回修复后的内容,没改动返回 null */
      public function fix(string $filePath, string $content): ?string;
  }

  ---
  16. src/Fixer/DeprecatedFunctionFixer.php — 自动替换废弃函数

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Fixer;

  use PhpParser\Node;
  use PhpParser\NodeTraverser;
  use PhpParser\NodeVisitorAbstract;
  use PhpParser\PrettyPrinter\Standard;
  use Xinchuang\Kit\Parser\PhpAstParser;

  final class DeprecatedFunctionFixer implements FixerInterface
  {
      /** @var array<string, string> */
      private const REPLACEMENTS = [
          'mysql_connect' => 'mysqli_connect',
          'mysql_query' => 'mysqli_query',
          'mysql_fetch_array' => 'mysqli_fetch_array',
          'mysql_fetch_assoc' => 'mysqli_fetch_assoc',
          'mysql_num_rows' => 'mysqli_num_rows',
          'mysql_close' => 'mysqli_close',
          'mysql_real_escape_string' => 'mysqli_real_escape_string',
          'split' => 'explode',
          'ereg' => 'preg_match',
      ];

      public function __construct(private readonly PhpAstParser $parser) {}

      public function name(): string
      {
          return 'deprecated_function_fixer';
      }

      public function supports(string $filePath): bool
      {
          return str_ends_with($filePath, '.php');
      }

      public function fix(string $filePath, string $content): ?string
      {
          $ast = $this->parser->parse($content);
          if ($ast === null) {
              return null;
          }

          $changed = false;
          $traverser = new NodeTraverser();
          $traverser->addVisitor(new class($changed) extends NodeVisitorAbstract {
              public function __construct(public bool &$changed) {}

              public function enterNode(Node $node): ?Node
              {
                  if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) {
                      $fnName = $node->name->toString();
                      if (isset(DeprecatedFunctionFixer::REPLACEMENTS[$fnName])) {
                          $node->name = new Node\Name(DeprecatedFunctionFixer::REPLACEMENTS[$fnName]);
                          $this->changed = true;
                      }
                  }
                  return null;
              }
          });

          $newAst = $traverser->traverse($ast);
          if (!$changed) {
              return null;
          }

          return (new Standard())->prettyPrintFile($newAst);
      }
  }

  大白话:这是工具的"杀手锏"——改 AST 而不是改文本。结果是 100% 语法正确,不像正则替换可能改坏字符串里的内容。

  ---
  17. src/Fixer/EncodingFixer.php — 编码修复

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Fixer;

  final class EncodingFixer implements FixerInterface
  {
      public function name(): string
      {
          return 'encoding_fixer';
      }

      public function supports(string $filePath): bool
      {
          return str_ends_with($filePath, '.php')
              || str_ends_with($filePath, '.html')
              || str_ends_with($filePath, '.js')
              || str_ends_with($filePath, '.css');
      }

      public function fix(string $filePath, string $content): ?string
      {
          $changed = false;

          // 移除 BOM
          if (str_starts_with($content, "\xEF\xBB\xBF")) {
              $content = substr($content, 3);
              $changed = true;
          }

          // GBK → UTF-8
          if (!mb_check_encoding($content, 'UTF-8')) {
              $detected = mb_detect_encoding($content, ['UTF-8', 'GBK', 'GB2312', 'BIG5'], true);
              if ($detected !== false && $detected !== 'UTF-8') {
                  $converted = mb_convert_encoding($content, 'UTF-8', $detected);
                  if (is_string($converted)) {
                      $content = $converted;
                      $changed = true;
                  }
              }
          }

          return $changed ? $content : null;
      }
  }

  ---
  18. src/Command/ScanCommand.php — 扫描命令

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Command;

  use Symfony\Component\Console\Attribute\AsCommand;
  use Symfony\Component\Console\Command\Command;
  use Symfony\Component\Console\Helper\Table;
  use Symfony\Component\Console\Input\InputArgument;
  use Symfony\Component\Console\Input\InputInterface;
  use Symfony\Component\Console\Input\InputOption;
  use Symfony\Component\Console\Output\OutputInterface;
  use Symfony\Component\Console\Style\SymfonyStyle;
  use Symfony\Component\Finder\Finder;
  use Xinchuang\Kit\Detector\DatabaseDetector;
  use Xinchuang\Kit\Detector\DeprecatedFunctionDetector;
  use Xinchuang\Kit\Detector\DetectorInterface;
  use Xinchuang\Kit\Detector\EncodingDetector;
  use Xinchuang\Kit\Detector\ExtensionDetector;
  use Xinchuang\Kit\Detector\PathDetector;
  use Xinchuang\Kit\Parser\PhpAstParser;
  use Xinchuang\Kit\Report\Report;
  use Xinchuang\Kit\Rule\RuleLoader;

  #[AsCommand(name: 'scan', description: '扫描 PHP 项目的信创兼容性问题')]
  final class ScanCommand extends Command
  {
      protected function configure(): void
      {
          $this
              ->addArgument('path', InputArgument::REQUIRED, '要扫描的项目路径')
              ->addOption('target-db', null, InputOption::VALUE_REQUIRED, '目标数据库 (dameng / kingbase)', 'dameng')
              ->addOption('format', 'f', InputOption::VALUE_REQUIRED, '输出格式 (table / json / html)', 'table')
              ->addOption('output', 'o', InputOption::VALUE_REQUIRED, '输出文件(默认 stdout)')
              ->addOption('rules', 'r', InputOption::VALUE_REQUIRED, '自定义规则文件')
              ->addOption('exclude', 'e', InputOption::VALUE_REQUIRED, '排除目录,逗号分隔', 'vendor,node_modules,.git')
              ->addOption('fail-on-error', null, InputOption::VALUE_NONE, '发现 error 级别问题时退出码非 0(用于 CI)');
      }

      protected function execute(InputInterface $input, OutputInterface $output): int
      {
          $io = new SymfonyStyle($input, $output);
          $path = (string) $input->getArgument('path');

          if (!is_dir($path) && !is_file($path)) {
              $io->error("路径不存在: {$path}");
              return Command::FAILURE;
          }

          $rules = new RuleLoader($input->getOption('rules'));
          $parser = new PhpAstParser();
          $report = new Report();

          $detectors = [
              new ExtensionDetector($rules),
              new EncodingDetector(),
              new PathDetector($rules),
              new DeprecatedFunctionDetector($parser, $rules),
              new DatabaseDetector($parser, $rules, (string) $input->getOption('target-db')),
          ];

          $finder = new Finder();
          $finder->files()->in($path);
          foreach (explode(',', (string) $input->getOption('exclude')) as $ex) {
              $finder->exclude(trim($ex));
          }
          $finder->name(['*.php', 'composer.json']);

          $io->title('PHP 信创兼容性扫描');
          $io->progressStart(iterator_count($finder));

          foreach ($finder as $file) {
              $content = (string) file_get_contents($file->getRealPath());
              foreach ($detectors as $d) {
                  $this->safeDetect($d, $file->getRealPath(), $content, $report, $io);
              }
              $io->progressAdvance();
          }
          $io->progressFinish();

          $this->renderOutput($input, $output, $io, $report);

          if ((bool) $input->getOption('fail-on-error') && $report->hasErrors()) {
              return Command::FAILURE;
          }
          return Command::SUCCESS;
      }

      private function safeDetect(
          DetectorInterface $detector,
          string $file,
          string $content,
          Report $report,
          SymfonyStyle $io
      ): void {
          try {
              $detector->detect($file, $content, $report);
          } catch (\Throwable $e) {
              $io->warning("检测器 {$detector->name()} 处理 {$file} 时出错: {$e->getMessage()}");
          }
      }

      private function renderOutput(
          InputInterface $input,
          OutputInterface $output,
          SymfonyStyle $io,
          Report $report
      ): void {
          $format = (string) $input->getOption('format');
          $outputFile = $input->getOption('output');

          $rendered = match ($format) {
              'json' => $report->toJson(),
              'html' => (new \Xinchuang\Kit\Report\HtmlReporter())->render($report),
              default => null,
          };

          if ($rendered !== null) {
              if ($outputFile !== null) {
                  file_put_contents((string) $outputFile, $rendered);
                  $io->success("报告已写入 {$outputFile}");
              } else {
                  $output->writeln($rendered);
              }
              return;
          }

          // 默认 table 格式
          if (count($report->all()) === 0) {
              $io->success('未发现兼容性问题,可以放心部署到国产环境!');
              return;
          }

          $table = new Table($output);
          $table->setHeaders(['文件', '行', '级别', '类别', '说明', '可自动修复']);
          foreach ($report->all() as $issue) {
              $table->addRow([
                  basename($issue->file),
                  (string) $issue->line,
                  $issue->severity,
                  $issue->category,
                  mb_substr($issue->message, 0, 50),
                  $issue->autoFixable ? '是' : '否',
              ]);
          }
          $table->render();

          $io->newLine();
          $io->writeln(sprintf(
              '<error>错误: %d</error>  <comment>警告: %d</comment>  共: %d',
              $report->errorCount(),
              $report->warningCount(),
              count($report->all())
          ));
      }
  }

  大白话:xinchuang scan ./src --target-db=dameng --format=html -o report.html 一键扫描出报告。

  ---
  19. src/Command/FixCommand.php — 修复命令

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Command;

  use Symfony\Component\Console\Attribute\AsCommand;
  use Symfony\Component\Console\Command\Command;
  use Symfony\Component\Console\Input\InputArgument;
  use Symfony\Component\Console\Input\InputInterface;
  use Symfony\Component\Console\Input\InputOption;
  use Symfony\Component\Console\Output\OutputInterface;
  use Symfony\Component\Console\Style\SymfonyStyle;
  use Symfony\Component\Finder\Finder;
  use Xinchuang\Kit\Fixer\DeprecatedFunctionFixer;
  use Xinchuang\Kit\Fixer\EncodingFixer;
  use Xinchuang\Kit\Fixer\FixerInterface;
  use Xinchuang\Kit\Parser\PhpAstParser;

  #[AsCommand(name: 'fix', description: '自动修复信创兼容性问题')]
  final class FixCommand extends Command
  {
      protected function configure(): void
      {
          $this
              ->addArgument('path', InputArgument::REQUIRED, '项目路径')
              ->addOption('dry-run', null, InputOption::VALUE_NONE, '只显示会修改什么,不实际写入')
              ->addOption('backup', 'b', InputOption::VALUE_NONE, '修改前备份原文件为 .bak')
              ->addOption('exclude', 'e', InputOption::VALUE_REQUIRED, '排除目录', 'vendor,node_modules,.git');
      }

      protected function execute(InputInterface $input, OutputInterface $output): int
      {
          $io = new SymfonyStyle($input, $output);
          $path = (string) $input->getArgument('path');
          $dryRun = (bool) $input->getOption('dry-run');
          $backup = (bool) $input->getOption('backup');

          if (!$dryRun && !$io->confirm('修复会直接修改源文件,建议先 git commit。继续吗?', false)) {
              return Command::SUCCESS;
          }

          $parser = new PhpAstParser();
          /** @var FixerInterface[] $fixers */
          $fixers = [
              new EncodingFixer(),
              new DeprecatedFunctionFixer($parser),
          ];

          $finder = new Finder();
          $finder->files()->in($path);
          foreach (explode(',', (string) $input->getOption('exclude')) as $ex) {
              $finder->exclude(trim($ex));
          }
          $finder->name('*.php');

          $changed = 0;
          foreach ($finder as $file) {
              $filePath = $file->getRealPath();
              $original = (string) file_get_contents($filePath);
              $content = $original;

              foreach ($fixers as $fixer) {
                  if (!$fixer->supports($filePath)) {
                      continue;
                  }
                  $fixed = $fixer->fix($filePath, $content);
                  if ($fixed !== null) {
                      $content = $fixed;
                  }
              }

              if ($content !== $original) {
                  $changed++;
                  $io->writeln("<info>✓</info> {$filePath}");
                  if (!$dryRun) {
                      if ($backup) {
                          file_put_contents($filePath . '.bak', $original);
                      }
                      file_put_contents($filePath, $content);
                  }
              }
          }

          $io->success(sprintf(
              '%s %d 个文件',
              $dryRun ? '将修复' : '已修复',
              $changed
          ));
          return Command::SUCCESS;
      }
  }

  大白话:xinchuang fix ./src --dry-run 先预览,确认没问题再去掉 --dry-run 真正改。--backup 给每个改过的文件留一份 .bak。

  ---
  20. src/Report/HtmlReporter.php — HTML 报告生成

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Report;

  final class HtmlReporter
  {
      public function render(Report $report): string
      {
          $rows = '';
          foreach ($report->all() as $issue) {
              $badgeClass = match ($issue->severity) {
                  'error' => 'danger',
                  'warning' => 'warning',
                  default => 'info',
              };
              $rows .= sprintf(
                  '<tr><td>%s</td><td>%d</td><td><span class="badge
  badge-%s">%s</span></td><td>%s</td><td>%s</td><td>%s</td></tr>',
                  htmlspecialchars($issue->file),
                  $issue->line,
                  $badgeClass,
                  $issue->severity,
                  htmlspecialchars($issue->category),
                  htmlspecialchars($issue->message),
                  htmlspecialchars($issue->suggestion ?? '')
              );
          }

          $errors = $report->errorCount();
          $warnings = $report->warningCount();
          $total = count($report->all());
          $time = date('Y-m-d H:i:s');

          return <<<HTML
  <!DOCTYPE html>
  <html lang="zh-CN">
  <head>
  <meta charset="UTF-8">
  <title>PHP 信创兼容性报告</title>
  <style>
  body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; padding: 24px; background: #f5f5f7; }
  h1 { color: #1d1d1f; }
  .summary { display: flex; gap: 16px; margin: 20px 0; }
  .card { background: white; padding: 20px; border-radius: 12px; flex: 1; box-shadow: 0 2px 8px rgba(0,0,0,.04); }
  .card .num { font-size: 36px; font-weight: 700; }
  .card.danger .num { color: #ff3b30; }
  .card.warning .num { color: #ff9500; }
  .card.info .num { color: #007aff; }
  table { width: 100%; background: white; border-collapse: collapse; border-radius: 12px; overflow: hidden; box-shadow:
  0 2px 8px rgba(0,0,0,.04); }
  th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #f0f0f0; }
  th { background: #fafafa; font-weight: 600; }
  .badge { padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 500; }
  .badge-danger { background: #ffe5e5; color: #ff3b30; }
  .badge-warning { background: #fff4e5; color: #ff9500; }
  .badge-info { background: #e5f0ff; color: #007aff; }
  </style>
  </head>
  <body>
  <h1>PHP 信创兼容性扫描报告</h1>
  <p style="color:#86868b;">生成时间: {$time}</p>
  <div class="summary">
    <div class="card danger"><div class="num">{$errors}</div>错误</div>
    <div class="card warning"><div class="num">{$warnings}</div>警告</div>
    <div class="card info"><div class="num">{$total}</div>总计</div>
  </div>
  <table>
  <thead><tr><th>文件</th><th></th><th>级别</th><th>类别</th><th>问题</th><th>建议</th></tr></thead>
  <tbody>{$rows}</tbody>
  </table>
  </body>
  </html>
  HTML;
      }
  }

  ---
  21. src/Command/ReportCommand.php — 单独生成报告

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Command;

  use Symfony\Component\Console\Attribute\AsCommand;
  use Symfony\Component\Console\Command\Command;
  use Symfony\Component\Console\Input\InputArgument;
  use Symfony\Component\Console\Input\InputInterface;
  use Symfony\Component\Console\Input\InputOption;
  use Symfony\Component\Console\Output\OutputInterface;
  use Symfony\Component\Console\Style\SymfonyStyle;
  use Xinchuang\Kit\Report\HtmlReporter;
  use Xinchuang\Kit\Report\Issue;
  use Xinchuang\Kit\Report\Report;

  #[AsCommand(name: 'report', description: '从已有的 JSON 扫描结果生成 HTML 报告')]
  final class ReportCommand extends Command
  {
      protected function configure(): void
      {
          $this
              ->addArgument('json', InputArgument::REQUIRED, 'JSON 报告文件路径')
              ->addOption('output', 'o', InputOption::VALUE_REQUIRED, '输出 HTML 路径', 'report.html');
      }

      protected function execute(InputInterface $input, OutputInterface $output): int
      {
          $io = new SymfonyStyle($input, $output);
          $jsonPath = (string) $input->getArgument('json');
          $data = json_decode((string) file_get_contents($jsonPath), true);

          $report = new Report();
          foreach (($data['issues'] ?? []) as $i) {
              $report->add(new Issue(
                  file: $i['file'],
                  line: $i['line'],
                  column: $i['column'] ?? 0,
                  severity: $i['severity'],
                  category: $i['category'],
                  rule: $i['rule'],
                  message: $i['message'],
                  suggestion: $i['suggestion'] ?? null,
                  codeSnippet: $i['snippet'] ?? null,
                  autoFixable: $i['auto_fixable'] ?? false,
              ));
          }

          $html = (new HtmlReporter())->render($report);
          $outFile = (string) $input->getOption('output');
          file_put_contents($outFile, $html);
          $io->success("HTML 报告: {$outFile}");
          return Command::SUCCESS;
      }
  }

  ---
  22. 单元测试示例 tests/Detector/DeprecatedFunctionDetectorTest.php

  <?php
  declare(strict_types=1);

  namespace Xinchuang\Kit\Tests\Detector;

  use PHPUnit\Framework\TestCase;
  use Xinchuang\Kit\Detector\DeprecatedFunctionDetector;
  use Xinchuang\Kit\Parser\PhpAstParser;
  use Xinchuang\Kit\Report\Report;
  use Xinchuang\Kit\Rule\RuleLoader;

  final class DeprecatedFunctionDetectorTest extends TestCase
  {
      public function testDetectsMysqlConnect(): void
      {
          $detector = new DeprecatedFunctionDetector(new PhpAstParser(), new RuleLoader());
          $report = new Report();

          $code = <<<'PHP'
          <?php
          $conn = mysql_connect('localhost', 'user', 'pass');
          PHP;

          $detector->detect('test.php', $code, $report);

          self::assertCount(1, $report->all());
          self::assertSame('error', $report->all()[0]->severity);
          self::assertStringContainsString('mysql_connect', $report->all()[0]->message);
      }

      public function testIgnoresStringsContainingFunctionName(): void
      {
          $detector = new DeprecatedFunctionDetector(new PhpAstParser(), new RuleLoader());
          $report = new Report();

          $code = <<<'PHP'
          <?php
          $msg = "请不要用 mysql_connect";
          PHP;

          $detector->detect('test.php', $code, $report);
          self::assertCount(0, $report->all(), 'AST 不应该把字符串内容误报为函数调用');
      }
  }

  大白话:第二个测试就是 AST 比正则强的最好证明——字符串里写 mysql_connect 不会误报。

  ---
  23. phpunit.xml

  <?xml version="1.0"?>
  <phpunit bootstrap="vendor/autoload.php" colors="true" failOnWarning="true">
      <testsuites>
          <testsuite name="default">
              <directory>tests</directory>
          </testsuite>
      </testsuites>
      <source>
          <include><directory>src</directory></include>
      </source>
  </phpunit>

  24. phpstan.neon

  parameters:
      level: 8
      paths:
          - src
      excludePaths:
          - src/Rule/rules.yaml

  25. .github/workflows/ci.yml — CI 流水线

  name: CI
  on: [push, pull_request]

  jobs:
    test:
      runs-on: ubuntu-latest
      strategy:
        matrix:
          php: ['8.1', '8.2', '8.3']
      steps:
        - uses: actions/checkout@v4
        - uses: shivammathur/setup-php@v2
          with:
            php-version: ${{ matrix.php }}
            extensions: mbstring, json
            coverage: xdebug
        - name: Cache Composer
          uses: actions/cache@v4
          with:
            path: ~/.composer/cache
            key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}
        - run: composer install --prefer-dist --no-progress
        - run: composer run cs -- --dry-run
        - run: composer run stan
        - run: composer run test

    release:
      needs: test
      if: startsWith(github.ref, 'refs/tags/v')
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
        - uses: softprops/action-gh-release@v2
          with:
            generate_release_notes: true

  大白话:每次提交、PR 都跑测试,打 tag 自动发版。

  ---
  26. .php-cs-fixer.dist.php

  <?php
  return (new PhpCsFixer\Config())
      ->setRules([
          '@PSR12' => true,
          '@PHP81Migration' => true,
          'declare_strict_types' => true,
          'no_unused_imports' => true,
          'ordered_imports' => true,
      ])
      ->setFinder(
          (new PhpCsFixer\Finder())->in(['src', 'tests'])
      );

  ---
  27. README.md(节选)

  # PHP 信创适配工具包(Xinchuang Kit)

  > 扫描和自动修复 PHP 项目在信创环境(统信 UOS / 麒麟 / 龙芯 / 达梦 / 金仓)下的兼容性问题

  [![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgithub.com%2Fyourorg%2Fphp-xinchuang-kit%2Factions%2Fworkflows%2Fci.yml%2Fbadge.svg&pos_id=img-REAzl3Q4-1778997355739)](...)
  [![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fimg.shields.io%2Fbadge%2Flicense-MIT-blue.svg&pos_id=img-xlWoDGYc-1778997355745)](LICENSE)

  ## 安装
  ```bash
  composer require --dev yourorg/php-xinchuang-kit

快速开始

# 扫描 + HTML 报告
vendor/bin/xinchuang scan ./src --format=html -o report.html

# 干跑预览修复
vendor/bin/xinchuang fix ./src --dry-run

# 真改 + 备份
vendor/bin/xinchuang fix ./src --backup

# CI 集成(发现 error 退出码非 0)
vendor/bin/xinchuang scan ./src --fail-on-error

检测项

  • ✅ MySQL → 达梦 / 金仓 SQL 方言转换
  • ✅ 废弃函数(mysql_*eregspliteach …)
  • ✅ 黑名单扩展(ioncubesg11zend_guard)
  • ✅ 硬编码路径(C:\\/var/www/html)
  • ✅ 编码问题(BOM、GBK)
  • ✅ Apache 专用 API

自定义规则

xinchuang scan ./src --rules=my-rules.yaml

四、开源项目从 0 到持续维护 完整流程

阶段 1:前期准备(0~1 天)

  1. GitHub 建仓:yourorg/php-xinchuang-kit,选 MIT 协议
  2. 本地初始化
    git init && composer init
    echo “vendor/” > .gitignore
  3. 建分支策略:main(稳定) + develop(开发) + feature/*

阶段 2:MVP 最小可用版(1~2 周)

  • 先做 1 个检测器(DeprecatedFunctionDetector)+ CLI + JSON 输出
  • 能跑 + 有 1 个测试 = 立刻 push —— 不要憋大招

阶段 3:完善生态(2~4 周)

  • 补全检测器和修复器
  • 写测试(覆盖率 ≥ 80%)
  • 接入 PHPStan level 8 + PHP-CS-Fixer
  • 配置 GitHub Actions CI

阶段 4:首次发版 v0.1.0

git tag v0.1.0 && git push --tags

去 https://packagist.org 提交仓库 → composer require 立刻能用

阶段 5:推广

  • 写文章发掘金 / 知乎 / SegmentFault:“我把信创适配从 3 周缩短到 3 小时”
  • 提交到 awesome-php (https://github.com/ziadoz/awesome-php) 列表
  • 在国产数据库厂商社区(达梦、金仓)发帖

阶段 6:持续维护

┌────────────┬───────────────────────────────────────────────────────────────────┐
│ 环节 │ 工具 │
├────────────┼───────────────────────────────────────────────────────────────────┤
│ Issue 模板 │ .github/ISSUE_TEMPLATE/bug.yml │
├────────────┼───────────────────────────────────────────────────────────────────┤
│ PR 模板 │ .github/pull_request_template.md │
├────────────┼───────────────────────────────────────────────────────────────────┤
│ 行为准则 │ CODE_OF_CONDUCT.md(用 Contributor Covenant) │
├────────────┼───────────────────────────────────────────────────────────────────┤
│ 贡献指南 │ CONTRIBUTING.md │
├────────────┼───────────────────────────────────────────────────────────────────┤
│ 安全策略 │ SECURITY.md(漏洞上报渠道) │
├────────────┼───────────────────────────────────────────────────────────────────┤
│ 自动化 │ Dependabot + Renovate 自动升级依赖 │
├────────────┼───────────────────────────────────────────────────────────────────┤
│ 版本规范 │ SemVer (https://semver.org/) │
├────────────┼───────────────────────────────────────────────────────────────────┤
│ 变更日志 │ Keep a Changelog (https://keepachangelog.com) 格式的 CHANGELOG.md │
├────────────┼───────────────────────────────────────────────────────────────────┤
│ 自动发版 │ release-please 或手动打 tag │
└────────────┴───────────────────────────────────────────────────────────────────┘

阶段 7:社区运营

  • 每周 处理 issue,响应 < 7 天
  • 每月 出版本(哪怕只是修复 typo)
  • 每季度 重构一次,但要先发 RFC
  • 每年 跟新 PHP / 国产数据库版本(PHP 8.4 / DM9 等)

五、这个项目能帮谁、价值多大

┌────────────────┬─────────────────────────────────────────────────┐
│ 角色 │ 价值 │
├────────────────┼─────────────────────────────────────────────────┤
│ 政企软件供应商 │ 投标必备,中标后部署省 80% 时间 │
├────────────────┼─────────────────────────────────────────────────┤
│ 系统集成商 │ 给客户做信创迁移服务,有标准化工具 │
├────────────────┼─────────────────────────────────────────────────┤
│ 国产数据库厂商 │ 帮助生态扩展,降低用户迁移门槛 │
├────────────────┼─────────────────────────────────────────────────┤
│ PHP 老项目 │ 续命,不用整体重写 │
├────────────────┼─────────────────────────────────────────────────┤
│ 开发者本人 │ 简历上一个有真实场景的开源项目,信创赛道现在缺人 │
└────────────────┴─────────────────────────────────────────────────┘

简单说:这是一个能直接换钱的开源项目 ——
帮一家公司做一次信创迁移,这工具能省下几个人月的成本,以工具为入口卖咨询服务、定制规则、企业版,商业模式很清晰。

代码已全部给到,可直接 composer install 跑起来。下一步建议:先把 bin/xinchuang scan 在你手上一个真实 PHP
项目上跑通,看到第一份报告后,再回头按规则优先级补检测器。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐