elazar/flystream

PHP stream wrapper for Flysystem v2 and v3

1.2.0 2024-08-15 19:42 UTC

This package is auto-updated.

Last update: 2024-08-27 15:14:10 UTC


README

PHP Version Support Packagist Version Software License Buy Me a Cofee

Flysystem v2/3 + PHP stream wrappers = 🔥

Flystream 允许您通过将它们注册为自定义协议来使用核心 PHP 文件系统函数与 Flysystem 文件系统进行交互。

MIT 许可证 下发布。

支持的使用场景

  • 使用 Flysystem 与另一个库交互,该库使用 PHP 文件系统函数而不是 Flysystem 与文件系统交互。
  • 在测试中拦截文件系统操作进行验证。
  • 提高测试速度,否则测试代码需要访问本地文件系统。

不支持的使用场景

已知问题

  • 如果在使用后没有明确关闭文件或目录句柄(例如,使用 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 需要两个参数

  1. 一个包含 PHP 文件系统函数所使用的自定义协议名称的字符串;以及
  2. 实现 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,以及该接口的两个实现,分别是LocalLockRegistryPermissiveLockRegistry。默认情况下,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在每次使用相关自定义协议时隐式创建流包装器类的实例,并且不允许依赖注入。

这要求使用服务定位器来访问依赖项,特别是单例,以便流包装器使用用户配置以覆盖默认依赖项实现的相同容器。流包装器类将其对服务定位器的使用限制在单个方法中,该方法从单例实例的容器中检索依赖项。它还支持注入自定义单例实例,特别是用于测试。这些措施限制了使用服务定位器模式的缺点。