引言
Composer 是 PHP 的事实标准依赖管理器,驱动着 Laravel、Symfony 等几乎所有现代 PHP 项目。但对大多数开发者而言,Composer 只停留在「composer install 拉依赖」这一步:composer.json 里那些字段到底怎么配?require、require-dev、autoload、repositories 各司何职?PSR-4 自动加载是怎么做到「写个 use 就能自动找到类」的?
更进一步:如果你想把一段可复用的逻辑做成一个包发布给团队甚至全球 PHP 社区,就需要理解语义化版本、版本约束、包分发与CI 验证的全链路。本文从 Composer 的核心机制讲起,一路走到发布维护一个生产级 PHP 包。
关联阅读:https://plumephp.com/php-laravel-internals/ 展示了服务容器如何消费这些包;PSR 编码风格规范可参考 https://plumephp.com/php8-modern-features/ 中有关类型声明的实践。
目录
- 1. composer.json 全景:核心字段解析
- 2. require / require-dev / repositories
- 3. PSR-4 自动加载:从 use 到文件路径的映射
- 4. PSR 标准体系:PSR-0/4/12 与最佳实践
- 5. 语义化版本与版本约束详解
- 6. 打造一个包:目录结构、测试与 CI
- 7. 发布到 Packagist:首次发布流程
- 8. 维护与升级:Lockfile、Composer.lock 与 VCS 仓库
- 9. 常见坑与调试技巧
- 延伸阅读
1. composer.json 全景:核心字段解析
1.1 一个最小可用的 composer.json
{
"name": "acme/awesome-logger",
"description": "A simple structured logger for PHP 8.1+",
"type": "library",
"license": "MIT",
"require": {
"php": ">=8.1",
"psr/log": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
},
"autoload": {
"psr-4": {
"Acme\\Logger\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Acme\\Logger\\Tests\\": "tests/"
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
1.2 关键字段速查
| 字段 | 含义 | 必填 |
|---|---|---|
name | 包名,格式 厂商/包名 | 是 |
description | 一句话描述,Packagist 展示 | 建议 |
type | library/project/composer-plugin 等 | 建议 |
license | 开源协议 | 建议 |
require | 运行时依赖 | 是 |
require-dev | 开发期依赖(测试、静态分析) | 建议 |
autoload | 包自身类自动加载规则 | 是 |
autoload-dev | 测试代码的自动加载 | 建议 |
minimum-stability | 允许的最低版本稳定性 | 可选 |
prefer-stable | 有稳定版本时优先选稳定 | 可选 |
2. require / require-dev / repositories
2.1 require 与 require-dev 的边界
# 运行时依赖进 require
composer require monolog/monolog
# 开发期工具进 require-dev
composer require --dev phpunit/phpunit
composer require --dev friendsofphp/php-cs-fixer
安装时 composer install 会装全量;生产部署时用 composer install --no-dev 跳过开发依赖,体积与攻击面都更小。
2.2 repositories:自定义仓库
默认从 Packagist 拉取,但可以配置额外源:
{
"repositories": [
{ "type": "vcs", "url": "https://github.com/your-company/private-pkg.git" },
{ "type": "path", "url": "../local-packages/*" },
{ "type": "composer", "url": "https://repo.packagist.example" }
]
}
path 仓库非常适合本地联调未发布的包:改动即时生效,无需每次 composer update。
2.3 composer install 与 update 的区别
| 命令 | 行为 | 使用时机 |
|---|---|---|
composer install | 严格按 composer.lock 装 | 部署、团队同步 |
composer update | 重新解析约束,更新 lock | 升级依赖 |
composer require xxx | 添加依赖并 update | 加新包 |
3. PSR-4 自动加载:从 use 到文件路径的映射
3.1 原理:命名空间 → 目录
PSR-4 规则:命名空间前缀对应一个目录,类名末尾追加 .php。
{
"autoload": {
"psr-4": {
"Acme\\Logger\\": "src/"
}
}
}
use 语句中的类 | 推导出的文件 |
|---|---|
Acme\Logger\StructuredLogger | src/StructuredLogger.php |
Acme\Logger\Handler\FileHandler | src/Handler/FileHandler.php |
Acme\Logger\Exception\LogException | src/Exception/LogException.php |
规则的精髓是「前缀最长的优先匹配」,同一个 autoload 里可以有多个前缀,甚至嵌套:
{
"psr-4": {
"Acme\\Logger\\": "src/",
"Acme\\Logger\\Handler\\": "src/Handlers/"
}
}
Acme\Logger\Handler\X 会优先匹配第二个更长的前缀。
3.2 自动加载的注册
包被安装后,Composer 生成的 vendor/autoload.php 注册了全部依赖的自动加载器:
require __DIR__.'/vendor/autoload.php'; // 一次引入,全部可用
$logger = new Acme\Logger\StructuredLogger();
3.3 与 classmap 与 files 的区别
| 加载方式 | 场景 | 说明 |
|---|---|---|
psr-4 | 标准命名空间类 | 推荐,按需加载 |
classmap | 无法用 PSR-4 表达的类 | 编译成类名→路径映射表 |
files | 函数、常量定义文件 | 立即加载(如全局 helper) |
{
"autoload": {
"files": ["src/functions.php"],
"classmap": ["src/legacy/"]
}
}
4. PSR 标准体系:PSR-0/4/12 与最佳实践
4.1 PSR 家族简表
| PSR | 内容 | 状态 |
|---|---|---|
| PSR-0 | 旧的自动加载规范 | 已废弃,被 PSR-4 取代 |
| PSR-1 | 基础编码规范 | 已被 PSR-12 整合 |
| PSR-4 | 自动加载规范 | 现行 |
| PSR-12 | 扩展编码风格规范 | 现行 |
| PSR-3 | 日志接口 | 现行 |
4.2 PSR-4 与 PSR-0 的区别
PSR-0 用下划线分隔命名空间与类名,PSR-4 不再把下划线映射为目录分隔符,且类文件内命名空间与声明必须一致。现代包一律用 PSR-4。
4.3 PSR-12 编码风格要点
// PSR-12 要点示例
declare(strict_types=1);
namespace Acme\Logger;
use Psr\Log\LoggerInterface;
final class StructuredLogger implements LoggerInterface
{
public function __construct(
private string $channel = 'app', // 构造器属性提升
) {}
public function log($level, string|\Stringable $message, array $context = []): void
{
// ...
}
}
- 大括号
{放行尾(方法/类),控制结构同行后接{ - 方法与属性按
public/protected/private顺序 - 类型声明优先用
string|int联合类型
5. 语义化版本与版本约束详解
5.1 语义化版本(SemVer)
MAJOR.MINOR.PATCH:
- MAJOR:不兼容的 API 变更
- MINOR:向后兼容的新功能
- PATCH:向后兼容的缺陷修复
5.2 版本约束语法
| 写法 | 含义 | 示例 |
|---|---|---|
^1.2.3 | >=1.2.3 且 <2.0.0(兼容 1.x 新版) | 常用推荐 |
~1.2.3 | >=1.2.3 且 <1.3.0 | 只允许 patch |
>=1.0 <2.0 | 显式区间 | 精确控制 |
1.2.* | 1.2.x 系列最新 | 简洁 |
1.2.3 | 精确锁定 | 少用 |
5.3 为什么生产要 lock
composer.lock 记录了最终选定的精确版本哈希。部署时 composer install 按 lock 安装,保证线上与本地完全一致——这是可复现部署的基石。包发布者则不提交 lock(让消费者自己解析)。
6. 打造一个包:目录结构、测试与 CI
6.1 推荐的包目录结构
awesome-logger/
├── src/ # 生产代码 (PSR-4: Acme\Logger\)
│ ├── StructuredLogger.php
│ ├── Handler/
│ └── Exception/
├── tests/ # 测试代码 (autoload-dev)
│ └── StructuredLoggerTest.php
├── docs/
├── composer.json
├── phpunit.xml
├── phpstan.neon
├── .github/workflows/ci.yml
├── .gitignore
└── LICENSE
6.2 测试:PHPUnit 基础
<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="unit">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory>src</directory>
</include>
</coverage>
</phpunit>
use PHPUnit\Framework\TestCase;
final class StructuredLoggerTest extends TestCase
{
public function test_emits_json_line(): void
{
$logger = new StructuredLogger('test');
$out = tmpfile();
$logger->toStream($out)->info('hello', ['user' => 42]);
rewind($out);
$json = json_decode(stream_get_contents($out), true);
self::assertSame('info', $json['level']);
self::assertSame(42, $json['context']['user']);
}
}
6.3 CI:GitHub Actions
name: CI
on:
push:
pull_request:
jobs:
test:
strategy:
matrix:
php: ['8.1', '8.2', '8.3']
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
- run: composer install --prefer-dist --no-progress
- run: composer test
- run: composer phpstan
{
"scripts": {
"test": "phpunit",
"phpstan": "phpstan analyse --level=max src tests",
"cs": "php-cs-fixer fix --dry-run --diff"
}
}
6.4 静态分析:PHPStan 门槛
level=max 是强类型包的高标准。PHPStan 能捕获未定义属性、类型收窄错误、可空性错误等运行时才暴露的问题。
7. 发布到 Packagist:首次发布流程
7.1 前置条件
- 代码托管在 GitHub(或 GitLab/Bitbucket)。
composer.json元数据齐全。- 有稳定 tag(
1.0.0)或至少能解析出版本。 - 包名未被占用。
7.2 提交并打 tag
git init
git add .
git commit -m "feat: initial release"
git remote add origin git@github.com:acme/awesome-logger.git
git push -u origin main
# 打语义化版本 tag(Packagist 靠 tag 识别版本)
git tag 1.0.0
git push --tags
7.3 在 Packagist 注册
- 打开 packagist.org,用 GitHub 登录。
- 「Submit」页面粘贴仓库 URL。
- Packagist 通过 GitHub Webhook 自动同步新 tag。
7.4 验证包可用
# 在新项目里直接引用
composer require acme/awesome-logger
Packagist 会运行包的 CI 校验,若 composer.json 语法或依赖有问题会给出警告。
7.5 建议的 release 流程
# 打补丁 / 小功能 / 破坏性版本分别对应 PATCH / MINOR / MAJOR
git tag 1.1.0
git push --tags
composer update # 消费方升级
8. 维护与升级:Lockfile、Composer.lock 与 VCS 仓库
8.1 消费者升级策略
# 平滑升级:按 lock 更新到允许的最新
composer update acme/awesome-logger
# 谨慎场景:精确到 tag
composer require acme/awesome-logger:1.1.0
8.2 包内依赖的锁定
包自身不应提交 lock,但可在 CI 里用 composer.lock 保证测试可复现。消费者会用自己的 lock 解析。
8.3 安全更新与审计
composer audit # 扫描已知漏洞
composer audit --locked # 基于 lock 审计
composer update --dry-run # 预览变更
维护者应关注依赖的 CVE,及时发 patch 版。
9. 常见坑与调试技巧
9.1 常见错误与解法
| 现象 | 原因 | 解法 |
|---|---|---|
Class not found | PSR-4 前缀与目录不匹配 | 检查 autoload 映射、composer dump-autoload |
| 版本解析冲突 | 约束太紧或太松 | 用 composer why-not 排查 |
| lock 与约束不一致 | 手动改过 composer.json | composer update --lock |
| 本地改了包不生效 | 忘了 dump-autoload | composer dump-autoload |
9.2 调试三板斧
# 1. 看当前自动加载出的文件路径
php -r "require 'vendor/autoload.php';
\$r = new ReflectionClass('Acme\\Logger\\StructuredLogger');
echo \$r->getFileName().PHP_EOL;"
# 2. 查看包为何不被允许
composer why-not acme/awesome-logger 2.0.0
# 3. 深度调试解析
composer update --dry-run -vvv
9.3 包开发的心法
- 面向接口与向后兼容:MAJOR 版本之前珍惜语义化版本承诺。
- 测试先行:一个 0 依赖的纯函数包最容易做到高覆盖。
- 文档即门面:README 里写清安装、用法、FAQ。
延伸阅读
- https://plumephp.com/php-laravel-internals/ — 服务容器如何把 Composer 装进来的包组装起来
- https://plumephp.com/php-testing-practice/ — PHPUnit 生态深入(Mock 与集成测试)
- https://plumephp.com/php-performance-tuning/ — 自动加载与 Opcache 对包加载性能的影响
- PSR-4 规范 与 Composer 官方文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。