Skip to content

Packagist 发布

Packagist 是 PHP 生态系统的官方包仓库,类似于 Node.js 的 npm registry 或 Python 的 PyPI。将你的 PHP 包发布到 Packagist 后,其他开发者就可以通过 composer require vendor/package 来安装使用。本节将完整介绍从创建可发布的包到在 Packagist 上发布的全流程。

前置知识

阅读本节前,建议先了解:

基础概念

Packagist 的角色

你的 GitHub/GitLab 仓库

Packagist(包索引/元数据)

Composer 客户端(搜索、解析、安装)

你的项目(使用 composer require 安装)

Packagist 本身不存储代码,它是一个元数据索引服务。代码仍然存储在 Git 仓库中(GitHub、GitLab、Bitbucket 等)。

包名规范

vendor/package
  │       │
  │       └── 包名(小写字母、数字、连字符)
  └── 命名空间(对应 GitHub/GitLab 用户名或组织名)

示例:

  • monolog/monolog
  • symfony/console
  • guzzlehttp/guzzle
  • spatie/laravel-permission
  • myorg/http-client

包名规范

  • 全部小写
  • 只包含字母、数字和连字符 -
  • 不能以连字符开头或结尾
  • vendor 部分建议与 GitHub/GitLab 用户名一致

详细说明

第一步:创建符合 Composer 规范的库

my-http-client/
├── composer.json        # 包的核心配置
├── src/
│   └── HttpClient.php   # 库的源代码
├── tests/
│   └── HttpClientTest.php
├── .gitignore
├── LICENSE
└── README.md

composer.json 配置

json
{
    "name": "myorg/http-client",
    "description": "A lightweight HTTP client wrapper for PHP 8.1+",
    "version": "1.0.0",
    "type": "library",
    "keywords": ["http", "client", "guzzle", "wrapper", "php"],
    "license": "MIT",
    "authors": [
        {
            "name": "Zhang San",
            "email": "zhangsan@example.com",
            "homepage": "https://example.com",
            "role": "Developer"
        }
    ],
    "support": {
        "email": "support@example.com",
        "issues": "https://github.com/myorg/http-client/issues",
        "forum": "https://github.com/myorg/http-client/discussions",
        "wiki": "https://github.com/myorg/http-client/wiki",
        "source": "https://github.com/myorg/http-client",
        "docs": "https://github.com/myorg/http-client/blob/main/README.md"
    },
    "require": {
        "php": "^8.1",
        "guzzlehttp/guzzle": "^7.5",
        "psr/http-message": "^1.0 || ^2.0",
        "psr/http-client": "^1.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^10.0",
        "phpstan/phpstan": "^1.10",
        "nyholm/psr7": "^1.5"
    },
    "suggest": {
        "ext-curl": "For better HTTP performance"
    },
    "autoload": {
        "psr-4": {
            "MyOrg\\HttpClient\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "MyOrg\\HttpClient\\Tests\\": "tests/"
        }
    },
    "minimum-stability": "stable",
    "prefer-stable": true,
    "config": {
        "sort-packages": true,
        "allow-plugins": {}
    },
    "extra": {
        "branch-alias": {
            "dev-main": "1.x-dev"
        }
    }
}

包的核心字段说明

字段是否必须说明
name必须包名,格式 vendor/package
description推荐包的简短描述
version可选通常由 Git tag 自动推断
type推荐library(包)或 project(项目)
keywords可选搜索关键词,有助于被发现
license推荐许可证标识
authors推荐作者信息
support可选获取支持的渠道
require必须依赖声明
autoload必须自动加载配置

第二步:编写源代码

php
<?php
declare(strict_types=1);

namespace MyOrg\HttpClient;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;

class HttpClient
{
    private Client $client;

    public function __construct(
        ?string $baseUri = null,
        float $timeout = 30.0,
        array $headers = []
    ) {
        $config = array_filter([
            'base_uri' => $baseUri,
            'timeout' => $timeout,
            'headers' => $headers,
        ], fn($v) => $v !== null);

        $this->client = new Client($config);
    }

    /**
     * 发送 GET 请求
     */
    public function get(string $uri, array $query = [], array $headers = []): ResponseInterface
    {
        return $this->request('GET', $uri, [
            'query' => $query,
            'headers' => $headers,
        ]);
    }

    /**
     * 发送 POST 请求
     */
    public function post(string $uri, array $data = [], array $headers = []): ResponseInterface
    {
        return $this->request('POST', $uri, [
            'json' => $data,
            'headers' => $headers,
        ]);
    }

    /**
     * 发送 PUT 请求
     */
    public function put(string $uri, array $data = [], array $headers = []): ResponseInterface
    {
        return $this->request('PUT', $uri, [
            'json' => $data,
            'headers' => $headers,
        ]);
    }

    /**
     * 发送 DELETE 请求
     */
    public function delete(string $uri, array $query = [], array $headers = []): ResponseInterface
    {
        return $this->request('DELETE', $uri, [
            'query' => $query,
            'headers' => $headers,
        ]);
    }

    /**
     * 发送通用请求
     */
    public function request(string $method, string $uri, array $options = []): ResponseInterface
    {
        try {
            return $this->client->request($method, $uri, $options);
        } catch (GuzzleException $e) {
            throw new HttpException(
                sprintf('HTTP 请求失败: %s', $e->getMessage()),
                (int) $e->getCode(),
                $e
            );
        }
    }

    /**
     * 获取原始 Guzzle 客户端(高级用途)
     */
    public function getGuzzleClient(): Client
    {
        return $this->client;
    }
}
php
<?php
declare(strict_types=1);

namespace MyOrg\HttpClient;

use RuntimeException;

class HttpException extends RuntimeException
{
}

第三步:编写测试

php
<?php
declare(strict_types=1);

namespace MyOrg\HttpClient\Tests;

use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use MyOrg\HttpClient\HttpClient;
use PHPUnit\Framework\TestCase;

class HttpClientTest extends TestCase
{
    private HttpClient $client;

    protected function setUp(): void
    {
        $mock = new MockHandler([
            new Response(200, [], '{"status":"ok"}'),
            new Response(200, [], '{"created":true}'),
        ]);

        $handler = HandlerStack::create($mock);
        $this->client = new HttpClient(null, 30.0, [
            'handler' => $handler,
        ]);
    }

    public function testGetRequest(): void
    {
        $response = $this->client->get('/api/test');
        $this->assertSame(200, $response->getStatusCode());
        $this->assertSame('{"status":"ok"}', (string) $response->getBody());
    }

    public function testPostRequest(): void
    {
        $response = $this->client->post('/api/users', ['name' => 'Test']);
        $this->assertSame(200, $response->getStatusCode());
    }
}

第四步:创建 Git 仓库和标签

bash
# 初始化 Git 仓库
cd my-http-client
git init

# 创建 .gitignore
cat > .gitignore << 'EOF'
/vendor/
.phpunit.result.cache
.php-cs-fixer.cache
composer.lock
*.cache
EOF

# 添加远程仓库
git remote add origin https://github.com/myorg/http-client.git

# 提交代码
git add .
git commit -m "Initial release v1.0.0"

# 推送到 GitHub
git push -u origin main

# 创建版本标签
git tag -a v1.0.0 -m "Release 1.0.0"
git push origin v1.0.0

版本号与 Git 标签

Packagist 从 Git 标签读取版本号。标签格式必须符合语义化版本规范:

  • v1.0.0(推荐,带 v 前缀)
  • 1.0.0(也可以,不带前缀)
  • 1.0.0-beta.1(预发布版本)
  • 2.0.0-RC.1(发布候选)

第五步:在 Packagist 上注册

方式一:通过网站注册

  1. 访问 https://packagist.org/
  2. 点击 "Submit" 按钮
  3. 登录(使用 GitHub 账号)
  4. 输入仓库 URL:https://github.com/myorg/http-client
  5. 点击 "Check" 然后点击 "Submit"

方式二:使用 GitHub 集成(推荐)

  1. 登录 Packagist
  2. 进入 Settings → GitHub
  3. 授权 Packagist 访问你的 GitHub 仓库
  4. 之后每次推送 tag 时自动更新
bash
# 启用 GitHub 自动更新后
# 推送新版本标签时 Packagist 会自动更新
git tag -a v1.1.0 -m "Release 1.1.0"
git push origin v1.1.0
# Packagist 自动检测并更新包信息

第六步:验证发布

bash
# 搜索你的包
composer search myorg/http-client

# 查看包信息
composer show myorg/http-client

# 安装测试
composer require myorg/http-client:^1.0

# 验证自动加载
php -r "
require 'vendor/autoload.php';
\$client = new MyOrg\HttpClient\HttpClient();
echo get_class(\$client) . PHP_EOL;
"

实战示例

场景一:发布新版本

bash
#!/bin/bash
# release.sh — 发布新版本脚本

VERSION=$1

if [ -z "$VERSION" ]; then
    echo "用法: ./release.sh <version>"
    echo "示例: ./release.sh 1.2.0"
    exit 1
fi

# 1. 运行测试
echo "=== 运行测试 ==="
composer test
if [ $? -ne 0 ]; then
    echo "错误: 测试失败,中止发布"
    exit 1
fi

# 2. 运行代码检查
echo "=== 代码检查 ==="
composer check
if [ $? -ne 0 ]; then
    echo "错误: 代码检查未通过,中止发布"
    exit 1
fi

# 3. 更新版本号
echo "=== 更新版本号 ==="
# 更新 composer.json 中的 version 字段
composer config version $VERSION
git add composer.json
git commit -m "chore: bump version to $VERSION"

# 4. 创建标签
echo "=== 创建标签 ==="
git tag -a "v$VERSION" -m "Release $VERSION"

# 5. 推送
echo "=== 推送到远程仓库 ==="
git push origin main
git push origin "v$VERSION"

echo "=== 版本 $VERSION 发布完成 ==="
echo "Packagist 将自动更新(如已启用 GitHub 集成)"

场景二:维护 CHANGELOG

markdown
# Changelog

## [1.2.0] - 2024-06-15

### Added
- Added retry mechanism for failed requests
- Added support for custom middleware
- Added `HttpClient::patch()` method

### Changed
- Improved error messages for connection timeouts

### Fixed
- Fixed memory leak when handling large responses
- Fixed header case sensitivity issue

### Deprecated
- Deprecated `HttpClient::rawRequest()` in favor of `HttpClient::request()`

## [1.1.0] - 2024-05-01

### Added
- Added `HttpClient` constructor options
- Added PSR-17 factory support

## [1.0.0] - 2024-04-01

### Added
- Initial release
- GET, POST, PUT, DELETE methods
- Guzzle integration

场景三:多许可证选择

json
{
    "license": "MIT"
}

常用许可证:

许可证类型特点
MIT宽松最自由,几乎无限制
Apache-2.0宽松含专利授权
GPL-3.0传染性衍生作品必须开源
LGPL-3.0弱传染性库文件可闭源使用
BSD-2-Clause宽松类似 MIT
json
{
    "license": [
        "MIT",
        "Apache-2.0"
    ]
}

注意事项

1. 不要在 composer.json 中硬编码版本号

json
{
    "name": "myorg/http-client",
    // 不要写 version 字段,让 Packagist 从 Git 标签推断
    // "version": "1.0.0"  ← 可选,通常省略
}

版本号推断

Packagist 通过 Git 标签推断版本号。如果 composer.json 中有 version 字段,优先使用该值。通常推荐省略 version 字段,让 Git 标签作为唯一版本来源。

2. composer.lock 不应纳入库的版本控制

gitignore
# 库项目的 .gitignore
/vendor/
composer.lock
.phpunit.result.cache

库 vs 项目

  • 库(library)composer.lock 不纳入版本控制。使用者会根据 composer.json 的约束自行解析版本。
  • 项目(project)composer.lock 必须纳入版本控制。确保所有环境使用相同版本。

3. Packagist 更新延迟

bash
# 如果 Packagist 没有自动更新
# 手动触发更新(通过 API 或网站)
curl -X POST https://packagist.org/api/update-package?username=XXX&apiToken=XXX \
    -F 'repository[repository_url]=https://github.com/myorg/http-client'

# 或者在 Packagist 网站上点击 "Force Update"

4. 包的弃用

如果不再维护某个包,可以在 Packagist 上标记为弃用:

  1. 登录 Packagist
  2. 进入包设置页面
  3. 点击 "Abandon package"
  4. 可选:设置替代包("replace": {"old/package": "self.version"}

最佳实践

1. 完整的 composer.json 模板

json
{
    "name": "myorg/my-package",
    "description": "A short description of the package",
    "type": "library",
    "keywords": ["keyword1", "keyword2"],
    "license": "MIT",
    "authors": [
        {
            "name": "Your Name",
            "email": "your@email.com",
            "homepage": "https://yourwebsite.com",
            "role": "Developer"
        }
    ],
    "support": {
        "issues": "https://github.com/myorg/my-package/issues",
        "source": "https://github.com/myorg/my-package"
    },
    "require": {
        "php": "^8.1"
    },
    "require-dev": {
        "phpunit/phpunit": "^10.0"
    },
    "autoload": {
        "psr-4": {
            "MyOrg\\MyPackage\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "MyOrg\\MyPackage\\Tests\\": "tests/"
        }
    },
    "minimum-stability": "stable",
    "prefer-stable": true,
    "config": {
        "sort-packages": true,
        "allow-plugins": {}
    }
}

2. 持续集成(GitHub Actions)

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php-version: ['8.1', '8.2', '8.3']

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP ${{ matrix.php-version }}
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php-version }}
          extensions: mbstring, curl
          coverage: xdebug

      - name: Install dependencies
        run: composer install --prefer-dist --no-progress

      - name: Run tests
        run: composer test

      - name: Run PHPStan
        run: composer phpstan

3. 发布检查清单

下一节

继续学习:PSR-1 编码标准 — 了解 PHP 编码标准基础规范。

参考链接