interitty/utils

通过一些特定于Interitty项目使用的功能扩展了标准Nette/Utils。

v1.0.20 2024-08-30 11:51 UTC

README

通过一些特定于Interitty项目使用的功能扩展了标准的Nette/Utils

需求

安装

使用Composer安装 interitty/utils 是最佳方式

composer require interitty/utils

配置

该包使用PHPStan扩展以更好地预测魔法方法的返回类型。如果已安装phpstan/extension-installer服务,则无需进一步配置;否则,需要将以下配置添加到项目根目录中的phpstan.neon文件中。

includes:
    - ./vendor/interitty/utils/src/PHPStan/extension.neon

功能

除了使用标准的Nette\Utils\*类外,还有新的Interitty\Utils\*类提供了一些其他功能。

数组列辅助函数

默认的PHP函数array_column只能处理数组,并且仅适用于数据的第一层。然而,使用各种迭代器和生成器(例如,提供数据作为DTO对象)可能更实用。同时,使用更深层次的数据可能也很方便。

为此,创建了一个辅助函数Interitty\Utils\Arrays::arrayColumn,其行为几乎相同,但没有这些限制。

示例:数组列辅助函数的使用

$array = [
    1 => ['data' => ['key' => 'key1', 'value' => 'value1']],
    2 => ['data' => ['key' => 'key2', 'value' => 'value2']],
];
$values = Arrays::arrayColumn($array, ['data', 'value'], ['data', 'key']);

数组到生成器辅助函数

特别适用于测试目的,但在其他时候也可能有用,能够以生成器的形式传递提供的字段。

示例:数组到生成器辅助函数的使用

$array = [0 => 1, 'a' => 'b'];
$generator = Interitty\Utils\Arrays::arrayToGenerator($array);

按嵌套值排序数组

默认的PHP函数sort可以按值对一维数组进行排序。然而,根据某些嵌套键对多维字段进行排序可能更实用。例如,按创建日期或标题等嵌套属性对网页字段进行排序。

示例:按嵌套值升序排序数组

$pages = [
    0 => ['id' => 0, 'meta' => ['title' => 'a']],
    1 => ['id' => 1, 'meta' => ['title' => 'b']],
    2 => ['id' => 2, 'meta' => ['title' => 'c']],
    3 => ['id' => 3, 'meta' => ['title' => 'd']],
    4 => ['id' => 4],
];
$result = Interitty\Utils\ArraySorter::sort($pages, $['meta', 'title']);

从示例中可以看出,可能存在所需嵌套偏移键可能不一定总是可用的情况。为此,有一个可选的$fallback参数可以用来指定是否从数组中删除此类元素(ArraySorter:: FALLBACK_REMOVE)、将其留在原处(ArraySorter::FALLBACK_KEEP)、将其移动到开头(ArraySorter::FALLBACK_TOP)或将其移动到结尾(ArraySorter::FALLBACK_BOTTOM),后者是默认行为。

示例:按嵌套值降序排序数组

在许多情况下,降序排序可能很有用,例如按最新到最旧的顺序对文章列表进行排序。为此,有一个相当于rsort函数的等效函数。

$pages = [
    0 => ['id' => 0, 'meta' => ['title' => 'a']],
    1 => ['id' => 1, 'meta' => ['title' => 'b']],
    2 => ['id' => 2, 'meta' => ['title' => 'c']],
    3 => ['id' => 3, 'meta' => ['title' => 'd']],
    4 => ['id' => 4],
];
$result = Interitty\Utils\ArraySorter::rsort($pages, $['meta', 'title']);

数组拓扑排序

在某些情况下,需要根据由不完整的条件集确定的相互依赖关系对字段进行排序,即哪个元素应该在哪个元素之前或之后。为此,提供了Interitty\Utils\ArraySorter::tsort函数。

示例:使用数组拓扑排序

$simpsons = [
    'Bart' => (object)['name' => 'Bart', 'surname' => 'Simpson'],
    'Homer' => (object)['name' => 'Homer', 'surname' => 'Simpson'],
    'Lisa' => (object)['name' => 'Lisa', 'surname' => 'Simpson'],
    'Maggie' => (object)['name' => 'Maggie', 'surname' => 'Simpson'],
    'Marge' => (object)['name' => 'Marge', 'surname' => 'Simpson'],
];

// Couch order
$before['Marge'] = ['Lisa', 'Maggie'];
$after ['Bart'] = ['Homer', 'Lisa'];

$couchOrdered = Interitty\Utils\ArraySorter::tsort($simpsons, $before, $after);

FilterVariable

一旦数据来自不可靠的来源,就需要对其进行验证。为此,创建了一个辅助函数 Interitty\Utils\Validators::filterVariable,它封装了原生PHP函数 filter_var,并增加了对标志的便捷处理和对默认值的支持。它通过验证或清理数据来过滤数据。当数据源包含未知(或外来)数据时,这特别有用。验证用于验证或检查数据是否满足某些条件,但不会改变数据本身。

清理会清理数据,因此它可能通过删除不需要的字符来更改数据。它实际上并不验证数据!大多数过滤器可以通过标志进行可选的调整。

有关更多信息,请参阅PHP的filter_var()函数手册页面和验证过滤器清理过滤器

示例:FilterVariable的使用

$email = \Interitty\Utils\Validators::filterVariable($_GET['email'], FILTER_VALIDATE_EMAIL, flags: FILTER_NULL_ON_FAILURE);
$ip = \Interitty\Utils\Validators::filterVariable($_POST['ip'], FILTER_VALIDATE_IP, flags: [FILTER_FLAG_NO_PRIV_RANGE, FILTER_NULL_ON_FAILURE]);
$enabled = \Interitty\Utils\Validators::filterVariable($apiResonse, FILTER_VALIDATE_BOOL, false); // false is default value
$worldMeaning = \Interitty\Utils\Validators::filterVariable($apiResonse, FILTER_VALIDATE_REGEXP, ['default' => 42, 'regexp' => '~42~']);

数组包装功能

当需要将 array 结构临时更像 object 时。多亏了PHPStan的作者的帮助,包装器还配备了一个完全静态的类型检查器,允许您使用phpdoc声明预期的内部属性及其类型,并相应地验证魔术方法的 existence。当然,还有对深度工作的完整支持,允许您轻松访问任何内部元素,无论是对象还是数组。

示例:ArrayWrapper的使用

$array = ['key' => 'value'];
/** @var ArrayWrapper<array{key: string}> $arrayWrapper */
$arrayWrapper = ArrayWrapper::create($array);

// Check array key exists
isset($arrayWrapper['key']);
isset($arrayWrapper->key);
$arrayWrapper->isKey();
$arrayWrapper->hasKey();
$arrayWrapper->offsetExists('key');

// Get array value by key
echo $arrayWrapper['key'];
echo $arrayWrapper->key;
echo $arrayWrapper->getKey();
echo $arrayWrapper->offsetGet('key');

// Set array value by key
$arrayWrapper['key'] = 'value';
$arrayWrapper->key = 'value';
$arrayWrapper->setKey('value');
$arrayWrapper->offsetSet('key', 'value');

// Unset array value by key
unset($arrayWrapper['key']);
unset($arrayWrapper->key);
$arrayWrapper->unsetKey();
$arrayWrapper->offsetUnset('key');

// Deep key work with ArrayWrapper
$offset = ['deep', 'key'];
/** @var ArrayWrapper<array{deep: array{key: string}}> $arrayWrapper */
$arrayWrapper->offsetExists($offset);
$arrayWrapper->offsetSet($offset, 'value');
echo $arrayWrapper->offsetGet($offset);
$arrayWrapper->offsetUnset($offset);

可断言检查

标准Nette\Utils\Validators方法assert()assertField会抛出包含有关错误的详细信息的\Nette\Utils\AssertionException。对于开发者来说,在没有精确类型提示(例如在getter和setter中)的情况下,这些“断言”非常有用,以便更容易、更快地检测问题来源。然而,即使代码100%完美,这些额外的检查也会消耗一些时间、CPU和内存。幸运的是,在原生的assert函数中有一个原生解决方案。这个函数带有配置支持,可以在生产环境中完全从生成的字节码中删除这些“断言”。但是,它需要断言返回bool结果。

此扩展Interitty\Utils\Validators简单地返回true而不是void,因此它可以在assert函数中使用。

有关可能验证器语法的更多示例,请参阅官方文档

示例:可断言检查的使用

/**
 * Foo setter
 *
 * @param Foo $foo
 * @return static Provides fluent interface
 */
protected function setFoo(Foo $foo)
{
    assert(Validators::check($this->foo, 'null', 'foo before set'));
    $this->foo = $foo;
    return $this;
}

扩展和可翻译的异常

在处理边缘情况和验证条件时,但在应用程序的正常生命周期中,也可能发生需要将信息值传递给用户的信息。如果应用程序是多语言的,则需要使用参数化文本与翻译器对象一起使用,然后在实际值中替换这些参数。

为此,创建了一个名为Interitty\Exceptions\ExtendedExceptionTrait的特质,它允许所有上述功能,并另外提供了一种流畅的接口来定义单个异常参数,并使用Interitty\Exceptions\ExtendedExceptionInterface接口来记录这一点。

为了避免为了使用提到的特质而创建大量包含应用中所有异常的类,创建了一个辅助函数 Interitty\Exceptions\Exceptions::extend,该函数可以在运行时扩展传入的异常。

如果 Interitty\Exceptions\Exceptions 类使用静态设置器设置了翻译器,则此翻译器随后通过 extend 辅助函数传递给所有创建的异常,并在将它们转换为字符串时使用。

示例:扩展辅助函数在异常对象中的使用

throw Exceptions::extend(new RuntimeException('Message with :placeholder'))->addData('placeholder', 'value');

示例:扩展辅助函数在异常流畅接口中的使用

throw Exceptions::extend(RuntimeException::class)
        ->setMessage('Message with :placeholder')
        ->setCode(42)
        ->addData('placeholder', 'value');

示例:在基于 Nette 的应用程序中的初始设置

services:
    application.application:
        setup:
            - Interitty\Exceptions\Exceptions::setTempDir(%tempDir%)
            - Interitty\Exceptions\Exceptions::setTranslator(@Nette\Localization\Translator)

可读性/可写性/可执行性验证器

标准的 Nette\Utils\Validators 不支持 is_readableis_writableis_executable。此扩展 (Interitty\Utils\Validators) 简单地添加了它们。

示例:可读性/可写性的使用

/**
 * Content getter
 *
 * @return string
 */
protected function getContent(): string
{
    $path = $this->getPath();
    assert(Validators::check($path, 'readable', 'path'));
    $content = file_get_contents($path);
    return $content;
}

/**
 * Path setter
 *
 * @param string $path
 * @return static Provides fluent interface
 */
protected function setPath(string $path)
{
    assert(Validators::check($path, 'writable', 'path'));
    $this->path = $path;
    return $this;
}

集合

interitty/utils 中包含了一个简单的 Collection 实现,具有内置的类型检查,这可以防止添加错误的数据类型。

$collection = new Interitty\Iterators\Collection('string');
$collection->add('foo');

集合中的 $type 通过 Nette\Utils\Validators 验证,所以它可以比仅仅数据类型更具体。例如,它可以定义为应包含最多 42 个字符的字符串集合。

$collection = new Interitty\Iterators\Collection('string:..42');
$collection->add('foo');

集合还允许使用为每个添加的项目定义的精确整数或字符串键。

$collection = new Interitty\Iterators\Collection('integer');
$collection->add(42, 'foo');
$foo = $collection->get('foo');
$collection->delete('foo');

集合还允许通知已添加项的数量。

$collection = new Interitty\Iterators\Collection('integer');
$count = $collection->count();

还可以通过添加包含所需数据的 array 或任何 Traversable 对象来一次性添加多个项目。

$data = [
    'foo' => 42,
    'bar' => 0,
];
$collection = new Interitty\Iterators\Collection('integer');
$collection->addCollection($data);

反射

默认的 ReflectionObject 实现无法轻松访问不是对象直接变量的 private 变量,即如果它是在祖先中声明的。为此,添加了 Interitty\Utils\ReflectionObject 扩展,该扩展为 getProperty 函数添加了一个可选的第二个参数 bool $deep,可以用来启用对它的访问支持。

这还由辅助函数 getNonPublicPropertyValuesetNonPublicPropertyValue 使用,这些函数使访问此功能变得简单。这仍然是一个具有重大性能影响的 OOP 的工作区。因此,不应在生产环境中使用它。然而,它仍然可以用于测试创建,例如。

class TestObject
{
    private $property;
}

class TestNestedObject extends TestObject
{
}

$object = new TestNestedObject();
\Interitty\Utils\ReflectionObject::setNonPublicPropertyValue($object, 'property', 'value');
echo \Interitty\Utils\ReflectionObject::getNonPublicPropertyValue($object, 'property');

反射方法属性

要访问由 attribute 标记的对象的方法及其参数,有一个辅助函数 getMethodsAttributes。如果属性基于通用的 Interitty\Utils\BaseAttribute,则有机会访问标记的方法。

#[Attribute(Attribute::TARGET_METHOD)]
class TestAttribute extends Interitty\Utils\BaseAttribute
{
}

class TestAttributedObject
{
    #[TestAttribute]
    public function attributedMethod(): void
    {
    }
}

$object = new TestAttributedObject();
$methods = Interitty\Utils\ReflectionObject::getMethodsAttributes($object, TestAttribute::class);
foreach ($methods as $attributte)
{
    $method = $attributte->getReflectionMethod();
}

DateTime 功能

使用 DateTimeDateTimeZone 对象来处理日期和时间是正确的方法,因为有许多时区,即使在同一个时区内也有夏令时和冬令时。因此,为了正确计算日期之间的差异,为了正确地向全世界的人显示相同的日期,这些对象是必不可少的。

标准 Nette\Utils\DateTime 已经提供了许多有用的功能,而这个扩展(Interitty\Utils\DateTime)则增加了更多。

processParseIso8601

DateTime 对象包含一个静态辅助函数 processParseIso8601,该函数验证并解析给定的字符串,该字符串应与 IS0 8601 DateTime 标准 相匹配。结果是包含从给定字符串中的标准化格式的信息或抛出 InvalidArgumentException

示例:processParseIso8601 的使用

代码 DateTime::processParseIso8601('1989-12-17T12:00+00:00'); 将返回以下数组。

[
    'year' => '1989',
    'month' => '12',
    'day' => '17',
    'hour' => '12',
    'minutes' => '00',
    'seconds' => '00',
    'microseconds' => '000000',
    'timeZone' => '+00:00',
    'timeZoneOperation' => '+',
    'timeZoneHours' => '00',
    'timeZoneMinutes' => '00',
]

代码 DateTime::processParseIso8601('1989-12-17 12:00Z'); 将返回以下数组。

[
    'year' => '1989',
    'month' => '12',
    'day' => '17',
    'hour' => '12',
    'minutes' => '00',
    'seconds' => '00',
    'microseconds' => '000000',
    'timeZone' => 'Z',
    'timeZoneMilitary' => 'Z',
]

当给定的字符串不包含时区信息时,代码 DateTime::processParseIso8601('1989-12-17 12:00'); 将返回以下数组。

[
    'year' => '1989',
    'month' => '12',
    'day' => '17',
    'hour' => '12',
    'minutes' => '00',
    'seconds' => '00',
    'microseconds' => '000000',
]

DateTimeFactory

创建对象的新实例的正确方式是通过工厂及其接口,因为这允许其他开发者更容易地重写代码。因此,interitty/utils 搭带了 DateTimeFactoryInterface 和其实现 DateTimeFactory,允许创建 DateTimeDateTimeZone 对象。

工厂实现还允许创建带有适当的 DateTimeZoneDateTime 对象,就像 Nette\Utils\DateTime 一样容易。

示例:DateTimeFactory 的使用

使用默认时区创建表示当前日期和时间的 DateTime 对象。

$dateTimeFactory = new Interitty\Utils\DateTimeFactory();
$dateTime = $dateTimeFactory->create();

使用给定的时区创建表示给定日期和时间的 DateTime 对象。

$dateTimeFactory = new Interitty\Utils\DateTimeFactory();
$dateTime = $dateTimeFactory->create('1989-12-17T23:59:59+02:00');

创建表示给定日期和时间的 DateTime 对象,时区为 Europe/Prague

$dateTimeFactory = new Interitty\Utils\DateTimeFactory();
$dateTime = $dateTimeFactory->create('1989-12-17T23:59:59', 'Europe/Prague');

createFromIso8601

由于 IS0 8601 DateTime 标准 允许以许多变体格式化字符串,因此将给定的字符串正确转换为 DateTime 对象非常困难。因此,DateTimeFactory 包含了一个执行所有这些繁琐工作的辅助函数。目前,它可以管理大约 15 种所谓的“标准有效 DateTime 字符串”的变体。

示例:DateTimeFactory createFromIso8601 的使用

$dateTimeFactory = new Interitty\Utils\DateTimeFactory();
$dateTime = $dateTimeFactory->createFromIso8601('1989-12-17T12:00:00Z');

数组深度项

数据结构可能非常复杂,由内部数组和对象的组合构成,有时可以使用统一方式访问内部部分可能很有用。

示例:使用 Arrays getter

// Access the inner part
$value = Interitty\Utils\Arrays::offsetGet(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey']);

// Default value when access not existing inner part
$value = Interitty\Utils\Arrays::offsetGet([], 'notExisting', 'defaultValue');

示例:使用 Arrays checker

// Check the inner part
Interitty\Utils\Arrays::offsetExists(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey']);

示例:使用 Arrays setter

// Set the value deep inside the inner part
Interitty\Utils\Arrays::offsetSet(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey'], 'another value');

示例:使用 Arrays unsetter

// Unset the value from deep inside the inner part
Interitty\Utils\Arrays::offsetUnset(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey']);

数组深度项

数据结构可能非常复杂,由内部数组和对象的组合构成,有时可以使用统一方式访问内部部分可能很有用。

示例:使用 Arrays getter

// Access the inner part
$value = Interitty\Utils\Arrays::offsetGet(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey']);

// Default value when access not existing inner part
$value = Interitty\Utils\Arrays::offsetGet([], 'notExisting', 'defaultValue');

示例:使用 Arrays checker

// Check the inner part
Interitty\Utils\Arrays::offsetExists(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey']);

示例:使用 Arrays setter

// Set the value deep inside the inner part
Interitty\Utils\Arrays::offsetSet(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey'], 'another value');

示例:使用 Arrays unsetter

// Unset the value from deep inside the inner part
Interitty\Utils\Arrays::offsetUnset(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey']);

文件系统功能

是 Nette 框架实用 文件系统功能集 的扩展。

tempnam

默认的 PHP 函数 tempnam 不能与套接字一起使用,因此也不能与 mikey179/vfsstream 一起使用,并且也不能与文件扩展名一起使用。此扩展解决了这些问题,允许您创建子文件夹并设置创建的文件的掩码。

/** @var string $file Something like /tmp/folder/prefix.1RaND2.php */
$file = Interitty\Utils\FileSystem::tempnam(sys_get_temp_dir(), 'folder' . DIRECTORY_SEPARATOR . 'prefix.php', 0600);

辅助工具

PHP 编程语言具有许多实用功能,这些功能简化了日常的工作。然而,即使在相对简单的情况下,有些任务也可能需要更复杂的代码块。

类型检查器类或对象

与内置的 is_a 函数相同,但除了检查指定的对象或类是否是指定的类型之一。

示例:使用 Helper::isTypeOf

$test = \Interitty\Utils\Helpers::isTypeOf($foo, ['A', 'B']);

类使用获取器

出人意料的是,内置的 class_uses 函数不能解析父类中的特质。

示例:使用 Helper::getClassUses

foreach(\Interitty\Utils\Helpers::getClassUses($foo) as $traitClass) {
    //...
}

独立的 require(类自动加载)处理器

使用内置的 require 函数可能存在访问相同作用域中变量的潜在安全风险。此外,当给定的文件声明了一个类,重复调用将导致错误。相反,如果给定的文件应该包含预期的类但没有,可能会再次出现意外行为。

示例:使用 Helper::processIsolatedRequire

\Interitty\Utils\Helpers::processIsolatedRequire($className, $filePath);

类或对象检查器使用的特质类

有时检查一个类或对象是否使用了一个特质可能会有用,类似于检查它是否实现了接口。

示例:使用 Helper::isTraitUsed

$test = \Interitty\Utils\Helpers::isTraitUsed($foo, \Nette\SmartObject::class);

事件触发

Nette 框架支持简单明了的 事件处理。然而,可能存在需要向潜在订阅者传递引用参数的情况。

示例:使用 Helper::triggerEvent

/**
 * @method void onChange(float &$radius)
 */
class Circle
{
    use Interitty\Utils\SmartObject;

    /** @var array<callable(float): void> */
    public array $onChange = [];

    public function setRadius(float $radius): void
    {
        //$this->onChange($this, $radius); // Native Nette evnet trigger
        $this->triggerEvent('onChange', [&$radius]);
        $this->radius = $radius
    }
}