定制 wp-php-toolkit/zip 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

wp-php-toolkit/zip

最新稳定版本:v0.7.5

Composer 安装命令:

composer require wp-php-toolkit/zip

包简介

Zip component for WordPress.

README 文档

README

slug zip
title Zip
install wp-php-toolkit/zip
see_also
../learn/02-streaming-archives.html | Tutorial — Streaming archives | Walk through ZIP and EPUB writers from the toolkit's worked example. filesystem | Filesystem | Treat an archive like a swappable filesystem backend. bytestream | ByteStream | Feed readers and writers without whole-file buffers. httpclient | HttpClient | Stream downloaded archives into validation or extraction workflows.

Read and write ZIP archives in pure PHP — no libzip, no ZipArchive. Streams entries one at a time, so you can build EPUBs, .docx files, and multi-gigabyte plugin bundles without buffering the archive in memory.

Why this exists

Common PHP ZIP workflows rely on the ZipArchive extension or shelling out to zip. Those are awkward in hosts without libzip, WebAssembly builds, and code paths that need to stream archive data through toolkit byte streams.

The Zip component reads and writes Stored and Deflate archives in pure PHP. The decoder is pull-based, so listing the central directory of a 2 GB ZIP costs roughly the size of the directory itself. The encoder accepts any ByteWriteStream as a sink and writes one entry at a time.

Read a file out of a ZIP

ZipFilesystem implements this toolkit's Filesystem interface, so once you wrap the byte reader you can call get_contents(), ls(), and is_dir() just like the other filesystem backends.

Try this: after Run, add a second append_file() call before $enc->close() for a notes.md entry, then call print_r( $zip->ls( '/' ) ) at the end. The directory listing reflects the new entry without re-reading the file.

<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';

use WordPress\ByteStream\MemoryPipe;
use WordPress\ByteStream\ReadStream\FileReadStream;
use WordPress\ByteStream\WriteStream\FileWriteStream;
use WordPress\Zip\FileEntry;
use WordPress\Zip\ZipDecoder;
use WordPress\Zip\ZipEncoder;
use WordPress\Zip\ZipFilesystem;

$path = tempnam( sys_get_temp_dir(), 'demo' ) . '.zip';
$out  = FileWriteStream::from_path( $path, 'truncate' );
$enc  = new ZipEncoder( $out );
$enc->append_file( new FileEntry( array(
	'path'               => 'readme.txt',
	'compression_method' => ZipDecoder::COMPRESSION_NONE,
	'body_reader'        => new MemoryPipe( 'Hello from inside the zip.' ),
) ) );
$enc->close();
$out->close_writing();

$zip = ZipFilesystem::create( FileReadStream::from_path( $path ) );
echo $zip->get_contents( 'readme.txt' );
Hello from inside the zip.

Build an EPUB from scratch

An EPUB follows one strict ZIP rule: write the mimetype entry first and store it without compression. Deflate the rest of the archive normally.

Gotcha: E-readers reject EPUBs whose mimetype entry has compression. Use COMPRESSION_NONE for that single entry.

<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';

use WordPress\ByteStream\MemoryPipe;
use WordPress\ByteStream\ReadStream\FileReadStream;
use WordPress\ByteStream\WriteStream\FileWriteStream;
use WordPress\Zip\FileEntry;
use WordPress\Zip\ZipDecoder;
use WordPress\Zip\ZipEncoder;
use WordPress\Zip\ZipFilesystem;

$path = tempnam( sys_get_temp_dir(), 'book' ) . '.epub';
$out  = FileWriteStream::from_path( $path, 'truncate' );
$enc  = new ZipEncoder( $out );

// 1) The mimetype entry MUST be first and stored uncompressed.
$enc->append_file( new FileEntry( array(
	'path'               => 'mimetype',
	'compression_method' => ZipDecoder::COMPRESSION_NONE,
	'body_reader'        => new MemoryPipe( 'application/epub+zip' ),
) ) );

$container = <<<'XML'
<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles><rootfile full-path="EPUB/package.opf" media-type="application/oebps-package+xml"/></rootfiles>
</container>
XML;

foreach ( array(
	'META-INF/container.xml' => $container,
	'EPUB/package.opf'       => <<<'XML'
<package version="3.0" xmlns="http://www.idpf.org/2007/opf"><metadata/><manifest/><spine/></package>',
	'EPUB/chapter1.xhtml'    => <<<'XML'
<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Chapter 1</h1><p>It was a dark and stormy night.</p></body></html>
XML,
) as $name => $body ) {
	$enc->append_file( new FileEntry( array(
		'path'               => $name,
		'compression_method' => ZipDecoder::COMPRESSION_DEFLATE,
		'body_reader'        => new MemoryPipe( $body ),
	) ) );
}
$enc->close();
$out->close_writing();

$zip = ZipFilesystem::create( FileReadStream::from_path( $path ) );
printf( "mimetype: %s\n", $zip->get_contents( 'mimetype' ) );
printf( "size on disk: %d bytes\n", filesize( $path ) );
mimetype: application/epub+zip
size on disk: 726 bytes

Stream a large entry without buffering it

Calling get_contents() on a 500 MB CSV inside a ZIP would eat 500 MB of RAM. Use open_read_stream() instead and inflate-as-you-go.

Gotcha: Only one entry stream open at a time. Drain or finish the previous stream before opening the next.

<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';

use WordPress\ByteStream\MemoryPipe;
use WordPress\ByteStream\ReadStream\FileReadStream;
use WordPress\ByteStream\WriteStream\FileWriteStream;
use WordPress\Zip\FileEntry;
use WordPress\Zip\ZipDecoder;
use WordPress\Zip\ZipEncoder;
use WordPress\Zip\ZipFilesystem;

$path = tempnam( sys_get_temp_dir(), 'big' ) . '.zip';
$out  = FileWriteStream::from_path( $path, 'truncate' );
$enc  = new ZipEncoder( $out );
$enc->append_file( new FileEntry( array(
	'path'               => 'data.csv',
	'compression_method' => ZipDecoder::COMPRESSION_DEFLATE,
	'body_reader'        => new MemoryPipe( str_repeat( "id,value,timestamp\n1,foo,2024\n2,bar,2024\n", 5000 ) ),
) ) );
$enc->close();
$out->close_writing();

$zip    = ZipFilesystem::create( FileReadStream::from_path( $path ) );
$stream = $zip->open_read_stream( 'data.csv' );

$rows  = 0;
$bytes = 0;
$tail  = '';
while ( ! $stream->reached_end_of_data() ) {
	$n = $stream->pull( 8192 );
	if ( 0 === $n ) break;
	$chunk  = $tail . $stream->consume( $n );
	$lines  = explode( "\n", $chunk );
	$tail   = array_pop( $lines );
	$rows  += count( $lines );
	$bytes += $n;
}
printf( "Inflated %d bytes in 8 KB chunks, parsed %d rows.\n", $bytes, $rows );
Inflated 205000 bytes in 8 KB chunks, parsed 15000 rows.

Repack: modify one file, copy the rest

Updating one file in a ZIP without rewriting the others is impossible at the format level — the central directory points at byte offsets. The pragmatic answer is repack: stream the source archive into a new one, swapping the file you care about.

<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';

use WordPress\ByteStream\MemoryPipe;
use WordPress\ByteStream\ReadStream\FileReadStream;
use WordPress\ByteStream\WriteStream\FileWriteStream;
use WordPress\Zip\FileEntry;
use WordPress\Zip\ZipDecoder;
use WordPress\Zip\ZipEncoder;
use WordPress\Zip\ZipFilesystem;

$src_path = tempnam( sys_get_temp_dir(), 'orig' ) . '.zip';
$src_out  = FileWriteStream::from_path( $src_path, 'truncate' );
$src_enc  = new ZipEncoder( $src_out );
foreach ( array(
	'config.json'   => '{"debug":false,"version":"1.0"}',
	'app/index.php' => <<<'HTML'
<?php echo "hello";
XML,
	'app/style.css' => 'body{color:#333}
HTML,
) as $name => $body ) {
	$src_enc->append_file( new FileEntry( array(
		'path'               => $name,
		'compression_method' => ZipDecoder::COMPRESSION_DEFLATE,
		'body_reader'        => new MemoryPipe( $body ),
	) ) );
}
$src_enc->close();
$src_out->close_writing();

$source   = ZipFilesystem::create( FileReadStream::from_path( $src_path ) );
$dst_path = tempnam( sys_get_temp_dir(), 'repacked' ) . '.zip';
$dst_out  = FileWriteStream::from_path( $dst_path, 'truncate' );
$dst_enc  = new ZipEncoder( $dst_out );

$dirs = array( '/' );
while ( $dirs ) {
	$dir = array_shift( $dirs );
	foreach ( $source->ls( $dir ) as $name ) {
		$path = rtrim( $dir, '/' ) . '/' . $name;
		if ( $source->is_dir( $path ) ) {
			$dirs[] = $path;
			continue;
		}
		$rel  = ltrim( $path, '/' );
		$body = ( 'config.json' === $rel )
			? '{"debug":true,"version":"1.0.1"}'
			: $source->get_contents( $rel );
		$dst_enc->append_file( new FileEntry( array(
			'path'               => $rel,
			'compression_method' => ZipDecoder::COMPRESSION_DEFLATE,
			'body_reader'        => new MemoryPipe( $body ),
		) ) );
	}
}
$dst_enc->close();
$dst_out->close_writing();

$repacked = ZipFilesystem::create( FileReadStream::from_path( $dst_path ) );
echo "new config.json: " . $repacked->get_contents( 'config.json' ) . "\n";
echo "untouched: " . $repacked->get_contents( 'app/index.php' ) . "\n";
new config.json: {"debug":true,"version":"1.0.1"}
untouched: <?php echo "hello";
XML,
	'app/style.css' => 'body{color:#333}

Defend against zip-slip

A malicious archive can name an entry ../../etc/passwd and trick a naive extractor into clobbering files outside the destination. ZipDecoder::sanitize_path() strips leading ../ segments and collapses internal /../ sequences before exposing the path.

<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';

use WordPress\Zip\ZipDecoder;

$evil_inputs = array(
	'../../etc/passwd',
	'./safe/path.txt',
	'a/../../b/secret',
	'a//b///c.txt',
	'../../../../root/.ssh/authorized_keys',
);
foreach ( $evil_inputs as $name ) {
	printf( "%-45s => %s\n", $name, ZipDecoder::sanitize_path( $name ) );
}
../../etc/passwd                              => etc/passwd
./safe/path.txt                               => ./safe/path.txt
a/../../b/secret                              => a/../b/secret
a//b///c.txt                                  => a/b/c.txt
../../../../root/.ssh/authorized_keys         => root/.ssh/authorized_keys

Pipe ZIP entries into an InMemoryFilesystem

Real-world recipe: take an uploaded plugin ZIP, expand it into an InMemoryFilesystem so you can validate, edit, or scan it before it ever touches disk. Three components compose into something you couldn't build with ZipArchive alone.

<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';

use WordPress\ByteStream\MemoryPipe;
use WordPress\ByteStream\ReadStream\FileReadStream;
use WordPress\ByteStream\WriteStream\FileWriteStream;
use WordPress\Filesystem\InMemoryFilesystem;
use WordPress\Zip\FileEntry;
use WordPress\Zip\ZipDecoder;
use WordPress\Zip\ZipEncoder;
use WordPress\Zip\ZipFilesystem;
use function WordPress\Filesystem\copy_between_filesystems;

$path = tempnam( sys_get_temp_dir(), 'app' ) . '.zip';
$out  = FileWriteStream::from_path( $path, 'truncate' );
$enc  = new ZipEncoder( $out );
foreach ( array(
	'app/index.php'        => <<<'HTML'
<?php echo "ok";',
	'app/lib/util.php'     => '<?php // util
HTML,
	'app/assets/style.css' => 'body{margin:0}',
	'app/README.md'        => '# App',
) as $name => $body ) {
	$enc->append_file( new FileEntry( array(
		'path'               => $name,
		'compression_method' => ZipDecoder::COMPRESSION_DEFLATE,
		'body_reader'        => new MemoryPipe( $body ),
	) ) );
}
$enc->close();
$out->close_writing();

$zip = ZipFilesystem::create( FileReadStream::from_path( $path ) );
$mem = InMemoryFilesystem::create();
copy_between_filesystems( array(
	'source_filesystem' => $zip,
	'source_path'       => '/',
	'target_filesystem' => $mem,
	'target_path'       => '/',
) );

$mem->put_contents( '/app/VERSION', '1.0.0' );
echo "files now in memory:\n";
$dirs = array( '/' );
$files = array();
while ( $dirs ) {
	$dir = array_shift( $dirs );
	foreach ( $mem->ls( $dir ) as $name ) {
		$p = rtrim( $dir, '/' ) . '/' . $name;
		if ( $mem->is_dir( $p ) ) {
			$dirs[] = $p;
			continue;
		}
		$files[] = $p;
	}
}
sort( $files );
foreach ( $files as $path ) {
	echo "  " . $path . "\n";
}
files now in memory:
  /app/README.md
  /app/VERSION
  /app/assets/style.css
  /app/index.php

When to use which type

UseFor
ZipFilesystem::create()Reading. You want get_contents(), ls(), is_dir() over a ZIP. The most common case.
ZipEncoderWriting. Stream entries into any ByteWriteStream sink. Required when format rules matter (EPUB, .docx).
ZipDecoderLow-level read access to the central directory and individual entry headers. Most code reaches for ZipFilesystem instead.
open_read_stream() on a ZipFilesystemInflating a single large entry without buffering it whole in memory.
copy_between_filesystems()Moving entries from a ZIP into another filesystem (memory, local, SQLite).

Footgun: Updating an entry in place is impossible. The central directory points at byte offsets — change one entry's compressed size and every later offset shifts. Repack into a new archive instead.

Footgun: Never extract entry paths verbatim. Always run paths through ZipDecoder::sanitize_path(). Without it, a hostile archive can write outside the destination directory.

Footgun: Encrypted archives aren't supported. If you need to read AES-encrypted ZIPs, this isn't the component. The file format technically allows encryption, but the toolkit deliberately excludes it because the implementation surface is large and the use case is rare in WordPress contexts.

统计信息

  • 总下载量: 53
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 1
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: GPL-2.0-or-later
  • 更新时间: 2025-09-06

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固