/ PHP  Composer  Packagist  PHP包开发  单元测试  PHPUnit  开源  PSR-4 

用 Composer 从零构建并发布 PHP 包:完整实战教程


文章封面

Composer 是 PHP 生态中不可或缺的依赖管理工具,但很多开发者只停留在"使用别人的包"这一阶段。如果你有一段复用性极强的代码——无论是工具函数库、中间件,还是 SDK——将它封装成标准 Composer 包并发布到 Packagist,能让整个团队甚至全球开发者受益。本文将手把手带你完成从零创建包到成功发布的每一步,包括目录规范、自动加载配置、单元测试集成、版本语义化,以及发布时的常见踩坑点。

一、为什么要发布 Composer 包?

很多团队的代码库里都存在大量"内部 utils":重复造轮子、跨项目 copy-paste、版本不同步……这些问题的根源是缺乏统一的包管理机制。发布 Composer 包能带来:

  • 版本可控:通过 composer.json 的 version 字段或 Git tag 精确锁定版本

  • 依赖隔离:每个包有独立的依赖树,不污染主项目

  • CI 友好:可为独立包单独跑测试,质量更容易保证

  • 开源沉淀:把内部积累的好代码贡献给社区,建立技术影响力

接下来我们以一个真实场景为例:封装一个 PHP 数组工具库 yourname/arr-helper,提供常用的多维数组操作方法。

二、标准包目录结构与 composer.json 配置

首先创建项目目录并初始化:

mkdir arr-helper && cd arr-helper
composer init

composer init 会交互式引导你填写包名、描述、作者信息等。完成后,手动调整 composer.json,使其符合 Packagist 规范:

{
  "name": "yourname/arr-helper",
  "description": "A collection of useful PHP array utilities",
  "type": "library",
  "license": "MIT",
  "authors": [
    {
      "name": "Your Name",
      "email": "you@example.com"
    }
  ],
  "require": {
    "php": "^8.1"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0"
  },
  "autoload": {
    "psr-4": {
      "YourName\\ArrHelper\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "YourName\\ArrHelper\\Tests\\": "tests/"
    }
  }
}

推荐的目录结构如下:

arr-helper/
├── src/
│   └── Arr.php          # 核心类
├── tests/
│   └── ArrTest.php      # 单元测试
├── composer.json
├── .gitignore
├── README.md
└── LICENSE

⚠️ 踩坑记录autoload.psr-4 的命名空间末尾必须有反斜杠 \\,路径末尾必须有斜杠 /,否则自动加载会静默失败,调试起来极其痛苦。

三、编写核心类与实际功能

以下是 src/Arr.php 的实现,包含若干常用方法:

<?php

declare(strict_types=1);

namespace YourName\ArrHelper;

class Arr
{
    /**
     * 用"点号"路径访问多维数组
     * 例: Arr::get($data, 'user.profile.name', 'default')
     */
    public static function get(array $array, string $key, mixed $default = null): mixed
    {
        if (array_key_exists($key, $array)) {
            return $array[$key];
        }

        foreach (explode('.', $key) as $segment) {
            if (!is_array($array) || !array_key_exists($segment, $array)) {
                return $default;
            }
            $array = $array[$segment];
        }

        return $array;
    }

    /**
     * 将多维数组"扁平化"为一维,保留点号路径作为 key
     */
    public static function flatten(array $array, string $prefix = ''): array
    {
        $result = [];
        foreach ($array as $key => $value) {
            $newKey = $prefix !== '' ? $prefix . '.' . $key : (string)$key;
            if (is_array($value) && !empty($value)) {
                $result = array_merge($result, self::flatten($value, $newKey));
            } else {
                $result[$newKey] = $value;
            }
        }
        return $result;
    }

    /**
     * 从数组中按条件过滤,支持回调
     */
    public static function where(array $array, callable $callback): array
    {
        return array_values(array_filter($array, $callback));
    }

    /**
     * 将数组按指定字段分组
     */
    public static function groupBy(array $array, string $key): array
    {
        $result = [];
        foreach ($array as $item) {
            $groupKey = is_array($item) ? ($item[$key] ?? 'null') : 'null';
            $result[$groupKey][] = $item;
        }
        return $result;
    }
}

以上方法都是真实项目中高频使用的数组操作,尤其是 Arr::get()(灵感来自 Laravel 的 Arr 门面),在处理接口返回的嵌套 JSON 时能大幅减少 isset() 判断。

四、编写 PHPUnit 单元测试

发布到 Packagist 的包没有测试是不负责任的。先安装 PHPUnit:

composer install

创建 tests/ArrTest.php

<?php

declare(strict_types=1);

namespace YourName\ArrHelper\Tests;

use PHPUnit\Framework\TestCase;
use YourName\ArrHelper\Arr;

class ArrTest extends TestCase
{
    public function testGetWithDotNotation(): void
    {
        $data = ['user' => ['name' => 'Alice', 'age' => 30]];
        $this->assertSame('Alice', Arr::get($data, 'user.name'));
        $this->assertSame('unknown', Arr::get($data, 'user.email', 'unknown'));
    }

    public function testFlatten(): void
    {
        $data = ['a' => ['b' => ['c' => 1]], 'd' => 2];
        $flat = Arr::flatten($data);
        $this->assertSame(['a.b.c' => 1, 'd' => 2], $flat);
    }

    public function testGroupBy(): void
    {
        $data = [
            ['type' => 'fruit', 'name' => 'apple'],
            ['type' => 'veggie', 'name' => 'carrot'],
            ['type' => 'fruit', 'name' => 'banana'],
        ];
        $grouped = Arr::groupBy($data, 'type');
        $this->assertCount(2, $grouped['fruit']);
        $this->assertCount(1, $grouped['veggie']);
    }
}

运行测试:

./vendor/bin/phpunit tests/

如果看到 OK (3 tests, 4 assertions),说明代码质量过关,可以继续发布流程。

⚠️ 踩坑记录:PHPUnit 11.x 要求 PHP 8.2+,如果你的 PHP 版本是 8.1,需要指定 "phpunit/phpunit": "^10.0",否则 Composer 会报依赖冲突。

五、推送到 GitHub 并发布到 Packagist

在 GitHub 上创建新仓库(比如 github.com/yourname/arr-helper),然后推送代码:

git init
git add .
git commit -m "feat: initial release v1.0.0"
git remote add origin git@github.com:yourname/arr-helper.git
git push -u origin main

打上语义化版本 tag:

git tag v1.0.0
git push origin v1.0.0

然后前往 packagist.org 注册账号,点击 Submit,输入你的 GitHub 仓库地址。Packagist 会自动解析 composer.json 并注册包。

建议同时在 GitHub 仓库 Settings → Webhooks 中添加 Packagist 的 Webhook,这样每次 git push 都会自动同步:

Payload URL: https://packagist.org/api/github
Content type: application/json
Secret: (your Packagist API token)

发布成功后,任何人都可以通过以下命令安装你的包:

composer require yourname/arr-helper

六、版本语义化与维护建议

Packagist 生态遵循 SemVer(语义化版本)规范,即 主版本.次版本.补丁版本

  • 补丁版本(1.0.x):向下兼容的 Bug 修复

  • 次版本(1.x.0):新增功能,但不破坏现有 API

  • 主版本(x.0.0):破坏性变更,升级需用户手动迁移

实际维护中还需注意:

  • CHANGELOG.md 中记录每个版本的变更,方便用户决策是否升级

  • composer.json 中添加 "abandoned" 字段,当包不再维护时及时告知用户

  • 为包添加 GitHub Actions 自动测试,确保每个 PR 都经过 CI 验证

  • 避免在 1.x 版本中随意删除 public 方法,这属于破坏性变更

一个典型的 GitHub Actions 配置(.github/workflows/tests.yml):

name: Tests
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 }}
      - run: composer install
      - run: ./vendor/bin/phpunit

遵循这套流程,你的 Composer 包就具备了专业开源项目的基本素养。从内部工具到社区贡献,只需规范化这一步。

发布评论

热门评论区: