特殊包装器
概述
PHP 除了常见的 file://、http:// 等包装器外,还提供了几种特殊用途的包装器:data://、glob://、phar:// 和 zip://。它们适用于数据 URI 处理、文件模式匹配、PHP 归档访问和 ZIP 文件操作等场景。
包装器一览
| 包装器 | 用途 | 示例 |
|---|---|---|
data:// | 内嵌数据(RFC 2397) | data://text/plain;base64,SGVsbG8= |
glob:// | 文件模式匹配 | glob:///tmp/*.txt |
phar:// | PHP 归档访问 | phar://app.phar/config.ini |
zip:// | ZIP 文件访问 | zip://archive.zip#file.txt |
基础概念
包装器启用检查
php
<?php
// 检查已注册的流包装器
$wrappers = stream_get_wrappers();
echo "已注册包装器: " . implode(', ', $wrappers) . "\n";
// 常见输出: php, file, http, ftp, compress.zlib, compress.bzip2, data, glob, phar, zip
// 检查特定包装器是否可用
if (in_array('phar', stream_get_wrappers())) {
echo "phar 包装器可用\n";
}语法与代码
data:// — 内嵌数据
data:// 包装器遵循 RFC 2397 标准,允许在 URI 中直接嵌入数据。
php
<?php
declare(strict_types=1);
// data://text/plain — 纯文本
$text = file_get_contents('data://text/plain,Hello%20World');
echo $text . "\n"; // "Hello World"
// data://text/plain;base64 — Base64 编码
$encoded = base64_encode('你好,世界!');
$data = file_get_contents("data://text/plain;base64,{$encoded}");
echo $data . "\n"; // "你好,世界!"
// data://text/html — HTML 内容
$html = file_get_contents('data://text/html,<h1>Hello</h1>');
echo $html . "\n"; // "<h1>Hello</h1>"
// data://application/json — JSON 数据
$json = file_get_contents('data://application/json,{"name":"test"}');
$obj = json_decode($json, true);
echo $obj['name'] . "\n"; // "test"
// data://image/png;base64 — 图片数据(从 Base64 创建图片)
$base64Image = file_get_contents('/path/to/image.base64');
$imageData = file_get_contents("data://image/png;base64,{$base64Image}");
file_put_contents('/path/to/image.png', $imageData);
// data:// 参数 — 指定编码和字符集
// data://text/plain;charset=utf-8;base64,5L2g5aW9
$data = file_get_contents('data://text/plain;charset=utf-8;base64,5L2g5aW9');
echo $data . "\n"; // "你好"Data URI 生成器
php
<?php
declare(strict_types=1);
class DataUriGenerator
{
/**
* 从文件生成 Data URI
*/
public static function fromFile(string $filePath, ?string $mimeType = null): string
{
if (!file_exists($filePath)) {
throw new RuntimeException("文件不存在: {$filePath}");
}
if ($mimeType === null) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
}
$data = file_get_contents($filePath);
$base64 = base64_encode($data);
return "data:{$mimeType};base64,{$base64}";
}
/**
* 从字符串生成 Data URI
*/
public static function fromString(string $content, string $mimeType = 'text/plain'): string
{
$base64 = base64_encode($content);
return "data:{$mimeType};base64,{$base64}";
}
/**
* 生成 HTML 中的 img Data URI
*/
public static function imageTag(string $imagePath, string $alt = ''): string
{
$dataUri = self::fromFile($imagePath);
return sprintf('<img src="%s" alt="%s">', $dataUri, htmlspecialchars($alt));
}
}
// 使用示例
$csvUri = DataUriGenerator::fromFile('/data/report.csv', 'text/csv');
$csvContent = file_get_contents($csvUri);
$imgTag = DataUriGenerator::imageTag('/images/logo.png', 'Logo');
echo $imgTag . "\n";
// <img src="data:image/png;base64,iVBOR..." alt="Logo">Data URI 限制
Data URI 没有 MIME 类型验证,不适合用于不受信任的数据。浏览器对 Data URI 大小有限制(通常约 2MB)。不要将用户上传的文件转换为 Data URI。
glob:// — 文件模式匹配
php
<?php
declare(strict_types=1);
// glob:// 包装器 — 通过流方式使用 glob 模式匹配
$pattern = 'glob://' . __DIR__ . '/*.php';
// 读取所有匹配文件的内容(连接为一个字符串)
$content = file_get_contents($pattern);
// 注意: 这会将所有匹配文件的内容拼接在一起
// 逐行读取所有匹配文件
$handle = fopen($pattern, 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
echo $line;
}
fclose($handle);
}
// glob:// vs glob() 函数对比
// glob() — 返回文件名数组
$files = glob(__DIR__ . '/*.php');
foreach ($files as $file) {
echo $file . "\n";
}
// glob:// — 可以像文件一样通过流操作读取所有文件内容
$allContent = file_get_contents('glob://' . __DIR__ . '/*.md');
// 递归匹配
$allPhpFiles = file_get_contents('glob://' . __DIR__ . '/**/*.php');phar:// — PHP 归档访问
php
<?php
declare(strict_types=1);
// phar:// 允许访问 .phar 归档文件中的内容
// .phar 是 PHP 的归档格式,类似 JAR
// 读取 phar 中的文件
$config = file_get_contents('phar://app.phar/config/settings.ini');
$className = file_get_contents('phar://app.phar/src/Controller.php');
// 遍历 phar 中的文件
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('phar://app.phar/src')
);
foreach ($it as $file) {
echo $file->getPathname() . "\n";
}
// 使用 phar:// 包含文件
require 'phar://app.phar/vendor/autoload.php';
// 判断 phar 中文件是否存在
if (file_exists('phar://app.phar/README.md')) {
echo "README 存在\n";
}创建 Phar 归档
php
<?php
declare(strict_types=1);
// 创建 Phar 归档(需要 phar.readonly = 0)
$pharPath = __DIR__ . '/app.phar';
if (file_exists($pharPath)) {
Phar::unlinkArchive($pharPath);
}
$phar = new Phar($pharPath);
$phar->setStub($phar->createDefaultStub('bin/console.php'));
$phar->setMetadata([
'version' => '1.0.0',
'author' => 'Developer',
]);
// 添加文件到归档
$phar->buildFromDirectory(__DIR__ . '/src', '/src');
$phar->addFile('config/settings.ini');
// 压缩整个归档
$phar->compressFiles(Phar::GZ);
echo "Phar 创建成功: {$pharPath}\n";
echo "Phar 大小: " . filesize($pharPath) . " 字节\n";
// 设置为只读(生产环境)
// ini_set('phar.readonly', '1');Phar 安全
Phar 文件可以包含 PHP 代码。在 phar.readonly = 1(默认)时,不能创建或修改 Phar 文件。生产环境建议保持只读。
zip:// — ZIP 文件访问
php
<?php
declare(strict_types=1);
// zip:// 语法: zip://archive.zip#path/to/file
// 注意使用 # 而非 / 分隔
// 读取 ZIP 中的文件
$content = file_get_contents('zip:///path/to/archive.zip#config.json');
$config = json_decode($content, true);
// 读取 ZIP 中的文本文件
$readme = file_get_contents('zip:///path/to/archive.zip#README.md');
echo $readme . "\n";
// 使用相对路径
$readme = file_get_contents('zip://archive.zip#README.md');
// 列出 ZIP 文件内容
$zip = new ZipArchive();
if ($zip->open('archive.zip') === true) {
for ($i = 0; $i < $zip->numFiles; $i++) {
echo $zip->getNameIndex($i) . "\n";
}
$zip->close();
}ZIP 操作工具类
php
<?php
declare(strict_types=1);
class ZipHelper
{
/**
* 从 ZIP 中提取文件
*/
public static function extractFile(string $zipPath, string $entryPath, string $outputPath): void
{
$zip = new ZipArchive();
if ($zip->open($zipPath) !== true) {
throw new RuntimeException("无法打开 ZIP: {$zipPath}");
}
$content = $zip->getFromName($entryPath);
if ($content === false) {
$zip->close();
throw new RuntimeException("ZIP 中不存在: {$entryPath}");
}
file_put_contents($outputPath, $content);
$zip->close();
}
/**
* 将文件添加到 ZIP
*/
public static function addToZip(string $zipPath, array $files): void
{
$zip = new ZipArchive();
$mode = file_exists($zipPath) ? ZipArchive::OVERWRITE : ZipArchive::CREATE;
if ($zip->open($zipPath, $mode) !== true) {
throw new RuntimeException("无法创建 ZIP: {$zipPath}");
}
foreach ($files as $localName => $filePath) {
if (is_int($localName)) {
$localName = basename($filePath);
}
$zip->addFile($filePath, $localName);
}
$zip->close();
}
/**
* 创建 ZIP 备份
*/
public static function backup(string $directory, string $outputZip): void
{
$zip = new ZipArchive();
if ($zip->open($outputZip, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
throw new RuntimeException("无法创建 ZIP: {$outputZip}");
}
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($files as $file) {
$filePath = $file->getRealPath();
$relativePath = substr($filePath, strlen($directory) + 1);
if (is_file($filePath)) {
$zip->addFile($filePath, $relativePath);
}
}
$zip->close();
echo "备份完成: {$outputZip}\n";
}
}
// 使用示例
ZipHelper::backup('/data/app/uploads', '/backup/uploads_' . date('Ymd') . '.zip');
ZipHelper::addToZip('/data/reports.zip', [
'report.csv' => '/tmp/daily_report.csv',
'summary.pdf' => '/tmp/summary.pdf',
]);注意事项
安全考虑
php
<?php
// 1. data:// 注入风险
// 不要将不受信任的数据作为 data:// URI 使用
// 可能导致代码注入(如果接收端解析为 PHP)
$dangerous = $_GET['data'] ?? '';
// file_get_contents("data://text/plain,{$dangerous}") // 危险!
// 2. phar:// 反序列化漏洞
// 某些函数(如 file_exists, is_file)会触发 phar 反序列化
// 危险函数: file_exists, is_file, is_dir, is_link, stat, fopen
// 生产环境设置 phar.readonly = 1 禁用创建/修改 Phar
// 3. zip:// 路径遍历
// 确保从 ZIP 提取的文件路径不包含 .. 遍历
$entry = $zip->getNameIndex($i);
if (str_contains($entry, '..')) {
continue; // 跳过危险路径
}Phar 反序列化攻击
Phar 文件包含序列化的元数据。当通过 phar:// 或某些文件函数访问 Phar 时,会触发反序列化。攻击者可以上传恶意 Phar 文件并利用此漏洞执行代码。防御方法: 设置 phar.readonly = 1 和禁用 unserialize。
最佳实践
1. 使用场景选择
php
<?php
// data:// — 适合: 小图标、内联 CSS、API 响应中的小文件
// 不适合: 大文件、用户上传的文件
// glob:// — 适合: 批量读取同类型文件、日志合并
// 不适合: 需要精确控制每个文件的场景
// phar:// — 适合: PHP 应用分发、CLI 工具打包
// 不适合: 存储用户数据、频繁修改的内容
// zip:// — 适合: 归档提取、批量导入
// 不适合: 频繁随机访问 ZIP 内文件2. 性能建议
bash
# 启用 phar.readonly
# php.ini
phar.readonly = 1
# 检查可用的包装器
php -r "print_r(stream_get_wrappers());"