interitty / utils
通过一些特定于Interitty项目使用的功能扩展了标准Nette/Utils。
Requires
- php: ~8.3
- dg/composer-cleaner: ~2.2
- nette/utils: ~4.0
Requires (Dev)
- interitty/code-checker: ~1.0
- interitty/phpunit: ~1.0
- nette/php-generator: ~4.1
README
通过一些特定于Interitty项目使用的功能扩展了标准的Nette/Utils。
需求
- PHP >= 8.3
安装
使用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_readable
、is_writable
和 is_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
,可以用来启用对它的访问支持。
这还由辅助函数 getNonPublicPropertyValue
和 setNonPublicPropertyValue
使用,这些函数使访问此功能变得简单。这仍然是一个具有重大性能影响的 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 功能
使用 DateTime
和 DateTimeZone
对象来处理日期和时间是正确的方法,因为有许多时区,即使在同一个时区内也有夏令时和冬令时。因此,为了正确计算日期之间的差异,为了正确地向全世界的人显示相同的日期,这些对象是必不可少的。
标准 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
,允许创建 DateTime
和 DateTimeZone
对象。
工厂实现还允许创建带有适当的 DateTimeZone
的 DateTime
对象,就像 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
}
}