elazar / flystream
PHP stream wrapper for Flysystem v2 and v3
Requires
- php: ^8.1
- league/flysystem: ^2.1 || ^3.0
- psr/container: ^2.0
- psr/log: ^2.0 || ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- league/flysystem-memory: ^2.0 || ^3.0
- monolog/monolog: ^3.0
- pestphp/pest: ^2.0
README
Flysystem v2/3 + PHP stream wrappers = 🔥
Flystream 允许您通过将它们注册为自定义协议来使用核心 PHP 文件系统函数与 Flysystem 文件系统进行交互。
在 MIT 许可证 下发布。
支持的使用场景
- 使用 Flysystem 与另一个库交互,该库使用 PHP 文件系统函数而不是 Flysystem 与文件系统交互。
- 在测试中拦截文件系统操作进行验证。
- 提高测试速度,否则测试代码需要访问本地文件系统。
不支持的使用场景
- Flystream 不支持 Flysystem v1。如果您想要一个适用于 v1 的类似库,请参阅 twistor/flysystem-stream-wrapper。
已知问题
- 如果在使用后没有明确关闭文件或目录句柄(例如,使用
fclose()
或closedir()
),PHP 将在 关闭 期间隐式尝试关闭它。这种情况可能在某些环境中触发段错误。此问题已在 此处解决,并在 PHP 7.4.23、8.0.10 和 8.1.0 中可用。在旧版本中,最简单的解决方案是确保显式关闭文件和目录句柄。
要求
- PHP 8.1+
- Flysystem 2 或 3
安装
使用 Composer。
composer require elazar/flystream
注意:这将自动安装适用于您环境的最新版本的 Flysystem 核心库。但是,您必须自己处理适配器的安装。有关官方适配器的列表,请参阅 Flysystem 文档。
使用方法
如果您想运行下面的示例,您需要安装 league/flysystem-memory
。
composer require league/flysystem-memory
下面的示例不是全面的,但应该能提供对 Flystream 功能的基本理解。
<?php /** * 1. Configure your Flysystem filesystem to use the Flystream path * normalizer; see the "Path Normalization" section of this README for * more details. */ use Elazar\Flystream\ServiceLocator; use League\Flysystem\Filesystem; use League\Flysystem\InMemory\InMemoryFilesystemAdapter; use League\Flysystem\PathNormalizer; $adapter = new InMemoryFilesystemAdapter; $config = [ /* ... */ ]; $pathNormalizer = ServiceLocator::get(PathNormalizer::class); $filesystem = new Filesystem($adapter, $config, $pathNormalizer); /** * 2. Register the filesystem with Flystream and associate it with a * custom protocol (e.g. 'mem'). */ use Elazar\Flystream\FilesystemRegistry; $registry = ServiceLocator::get(FilesystemRegistry::class); $registry->register('mem', $filesystem); /** * 3. Interact with the filesystem using the custom protocol. */ mkdir('mem://foo'); $file = fopen('mem://foo/bar', 'w'); fwrite($file, 'baz'); fclose($file); file_put_contents('mem://foo/bar', 'bay'); $contents = file_get_contents('mem://foo/bar'); // or $contents = stream_get_contents(fopen('mem://foo/bar', 'r')); if (file_exists('mem://foo/bar')) { rename('mem://foo/bar', 'mem://foo/baz'); touch('mem://foo/bar'); } $file = fopen('mem://foo/baz', 'r'); fseek($file, 2); $position = ftell($file); ftruncate($file, 0); fclose($file); $dir = opendir('mem://foo'); while (($entry = readdir($dir)) !== false) { echo $entry, PHP_EOL; } closedir($dir); unlink('mem://foo/bar'); unlink('mem://foo/baz'); rmdir('mem://foo'); // These won't have any effect because Flysystem doesn't support them. chmod('mem://foo', 0755); chown('mem://foo', 'root'); chgrp('mem://foo', 'root'); /** * 4. Optionally, unregister the filesystem with Flystream. */ $registry->unregister('mem');
配置
对于最基本的使用,Flystream 需要两个参数
- 一个包含 PHP 文件系统函数所使用的自定义协议名称的字符串;以及
- 实现 Flysystem
FilesystemOperator
接口的对象(例如,Filesystem
类的实例)。
路径规范化
Flysystem Filesystem
类支持在传递给底层适配器之前对提供的路径进行规范化。Flysystem PathNormalizer
接口表示此规范化过程。
Flysystem 默认使用的此接口的实现是 WhitespacePathNormalizer
,它处理规范化目录分隔符(即将 \
转换为 /
),删除异常空白字符,并解析相对路径。
如果您使用第三方适配器,可能需要路径标准化,包括移除用于将Flysystem文件系统注册到Flystream的自定义协议。因此,默认情况下,Flystream注册了一个它定义的自定义路径标准化器StripProtocolPathNormalizer
。您可以通过以下方式配置您的Filesystem
实例以使用此标准化器。
<?php use Elazar\Flystream\ServiceLocator; use League\Flysystem\Filesystem; use League\Flysystem\PathNormalizer; // $adapter = ... // $config = ... $normalizer = ServiceLocator::get(PathNormalizer::class); $filesystem = new Filesystem($adapter, $config, $normalizer);
如果您希望将StripProtocolPathNormalizer
移除的协议限制在指定列表中,您可以通过指定一个自定义实例并为其第一个参数设置值来实现。
<?php use Elazar\Flystream\ServiceLocator; use Elazar\Flystream\StripProtocolPathNormalizer; // To remove a single protocol, specify it as a string $pathNormalizer = new StripProtocolPathNormalizer('foo'); // To remove more than one protocol, specify them as an array of strings $pathNormalizer = new StripProtocolPathNormalizer(['foo', 'bar']); ServiceLocator::set(PathNormalizer::class, $pathNormalizer);
StripProtocolPathNormalizer
还支持在执行自己的标准化后应用第二个路径标准化器。默认情况下,它使用Flysystem的WhitespacePathNormalizer
作为这个二级标准化器。如果您希望StripProtocolPathNormalizer
不使用二级标准化器,您可以像这样覆盖此行为。
<?php use Elazar\Flystream\PassThruPathNormalizer; use League\Flysystem\PathNormalizer; use Elazar\Flystream\ServiceLocator; use Elazar\Flystream\StripProtocolPathNormalizer; ServiceLocator::set(PathNormalizer::class, new StripProtocolPathNormalizer( // This is the default and results in the removal of all protocols null, // This normalizer returns the given path unchanged new PassThruPathNormalizer ));
如果您不想应用任何路径标准化,您可以使用Flystream提供的PassThruPathNormalizer
标准化器类来实现。
<?php use Elazar\Flystream\PassThruPathNormalizer; use League\Flysystem\PathNormalizer; use Elazar\Flystream\ServiceLocator; ServiceLocator::set(PathNormalizer::class, new PassThruPathNormalizer);
缓冲
Flysystem不支持追加操作,部分原因是其一些驱动程序不支持(例如AWS S3)。
PHP流写入缓冲区的默认大小在PHP 7.4和8.0之间有所不同,如果写入的数据大小超过缓冲区大小,可能会导致多个写入操作。
由于这些情况,Flystream将写入的数据缓冲,然后将其写入或“刷新”到目的地。
Flystream提供了对这些缓冲策略的原生支持
- 内存:严格在内存中缓冲。这具有最佳性能,但内存使用率也最高。
- 文件:严格在临时文件中缓冲。这具有最差性能,但内存使用率最低。
- 溢出:在内存中缓冲到可配置的限制,然后切换到使用临时文件。其性能和内存使用率通常介于上述两种策略之间。
默认情况下,Flystream使用内存策略以获得最佳性能。以下是覆盖此设置以使用不同策略的示例。
<?php use Elazar\Flystream\BufferInterface; use Elazar\Flystream\ServiceLocator; // To use the File strategy: use Elazar\Flystream\FileBuffer; ServiceLocator::set(BufferInterface::class, FileBuffer::class); // To use the Overflow configuration with a default memory cap of 2 MB: use Elazar\Flystream\OverflowBuffer; ServiceLocator::set(BufferInterface::class, OverflowBuffer::class); // To use the Overflow configuration with a custom memory cap: // @var int Memory limit in bytes (2 MB in this example) $maxMemory = 2 * 1024**2; $buffer = new OverflowBuffer; $buffer->setMaxMemory($maxMemory); ServiceLocator::set(BufferInterface::class, $buffer);
您可能需要检查您的memory_limit
PHP INI设置,并使用分析器或类似于memory_get_usage()
和memory_get_peak_usage()
的功能来了解哪种策略最适合您的用例。
另一种选择是使用您自己的缓冲策略实现,通过创建一个实现BufferInterface
的类,然后按照上述示例配置Flystream以使用它。
可见性
Flysystem实现了可见性的抽象层和用于处理Unix样式可见性的实现。
默认情况下,Flystream使用默认配置的Unix样式可见性实现。如果您想覆盖其设置,您可以使用配置的实例覆盖它。
<?php use Elazar\Flystream\ServiceLocator; use League\Flysystem\UnixVisibility\PortableVisibilityConverter; use League\Flysystem\UnixVisibility\VisibilityConverter; ServiceLocator::set(VisibilityConverter::class, new PortableVisibilityConverter( // ... ));
您还可以配置Flystream以使用自定义的可见性实现。
<?php use Elazar\Flystream\ServiceLocator; use League\Flysystem\UnixVisibility\VisibilityConverter; use My\CustomVisibilityConverter; // If your implementation doesn't require constructor parameters: ServiceLocator::set(VisibilityConverter::class, CustomVisibilityConverter::class); // If your implementation requires constructor parameters: ServiceLocator::set(VisibilityConverter::class, new CustomVisibilityConverter( // ... ));
锁定
默认情况下,Flysystem的Local适配器在写入和更新过程中使用文件锁定,但允许覆盖此行为。
Flystream也遵循类似的模式。它定义了一个接口LockRegistryInterface
,以及该接口的两个实现,分别是LocalLockRegistry
和PermissiveLockRegistry
。默认情况下,Flystream使用前者,这是一个简单的实现,可以防止当前PHP进程读取已经打开用于写入的文件或写入已经打开用于读取的文件。
如果您想完全禁用锁定,可以将Flystream配置为使用后者,该实现会接受所有请求的锁定和释放。
<?php use Elazar\Flystream\LockRegistryInterface; use Elazar\Flystream\PermissiveLockRegistry; use Elazar\Flystream\ServiceLocator; ServiceLocator::set( LockRegistryInterface::class, PermissiveLockRegistry::class );
另一个选项是创建自己的锁定注册表实现,例如使用库如php-lock/lock
(https://github.com/php-lock/lock)在PHP进程间处理分布式锁定的实现。
<?php namespace My; use Elazar\Flystream\Lock; use Elazar\Flystream\LockRegistryInterface; class CustomLockRegistry implements LockRegistryInterface { public function acquire(Lock $lock): bool { // ... } public function release(Lock $lock): bool { // ... } }
然后,配置Flystream使用它。
<?php use Elazar\Flystream\LockRegistryInterface; use Elazar\Flystream\ServiceLocator; use My\CustomLockRegistry; // If your implementation doesn't require constructor parameters: ServiceLocator::set( LockRegistryInterface::class, CustomLockRegistry::class ); // If your implementation requires constructor parameters: ServiceLocator::set( LockRegistryInterface::class, new CustomLockRegistry( // ... ) );
日志记录
Flystream支持任何PSR-3日志记录器,并记录对其流包装器方法的所有调用。
默认情况下,它使用随psr/log
(https://packagist.org.cn/packages/psr/log)一起提供的NullLogger
实现,该实现会丢弃日志条目。您可以通过使用不同的记录器来覆盖此设置,例如Monolog。
<?php use Elazar\Flystream\ServiceLocator; use Monolog\Logger; use Psr\Log\LoggerInterface; $logger = new Logger; // configure $logger here ServiceLocator::set(LoggerInterface::class, $logger);
核心缓冲区实现不实现日志记录。但是,从Flystream 0.4.0版本开始,可以将缓冲区实例包装在LoggingCompositeBuffer
类的实例中,以记录对其实例方法的调用。下面是使用默认的MemoryBuffer
缓冲区实现进行此操作的示例。
<?php use Elazar\Flystream\BufferInterface; use Elazar\Flystream\LoggingCompositeBuffer; use Elazar\Flystream\MemoryBuffer; use Elazar\Flystream\ServiceLocator; use Monolog\Logger; use Psr\Log\LoggerInterface; $logger = new Logger; // configure $logger here $buffer = new LoggingCompositeBuffer(new MemoryBuffer, $logger); ServiceLocator::set(BufferInterface::class, $buffer);
设计
服务定位器
Flystream使用单例服务定位器而不是更常见的依赖注入配置,因为PHP使用其流包装器类的方式。具体来说,PHP在每次使用相关自定义协议时隐式创建流包装器类的实例,并且不允许依赖注入。
这要求使用服务定位器来访问依赖项,特别是单例,以便流包装器使用用户配置以覆盖默认依赖项实现的相同容器。流包装器类将其对服务定位器的使用限制在单个方法中,该方法从单例实例的容器中检索依赖项。它还支持注入自定义单例实例,特别是用于测试。这些措施限制了使用服务定位器模式的缺点。