interitty / utils
Extension of the standard Nette/Utils by some other features that are specific for use in the Interitty projects.
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
Extension of the standard Nette/Utils by some other features that are specific for use in the Interitty projects.
Requirements
- PHP >= 8.3
Installation
The best way to install interitty/utils is using Composer:
composer require interitty/utils
Configuration
The package uses an extension for PHPStan to better predict return types of magic methods.
If the phpstan/extension-installer service is installed, no further
configuration is needed, otherwise it is necessary to add the following configuration to the phpstan.neon
file
in the project root folder.
includes:
- ./vendor/interitty/utils/src/PHPStan/extension.neon
Features
Instead of using standard Nette\Utils\*
classes, there are new Interitty\Utils\*
classes that provide some other features.
Array column helper
The default PHP function array_column
allows working only with arrays
and works with data only for the first level of immersion. However, it may be practical to work with various iterators
and generators that provide data as a DTO object, for example.
At the same time, it may be useful to use data from a deeper level of immersion.
A helper function Interitty\Utils\Arrays::arrayColumn
was created for this purpose, which behaves virtually identically
but without these limitations.
Example: usage of array column helper
$array = [
1 => ['data' => ['key' => 'key1', 'value' => 'value1']],
2 => ['data' => ['key' => 'key2', 'value' => 'value2']],
];
$values = Arrays::arrayColumn($array, ['data', 'value'], ['data', 'key']);
Array to generator helper
Especially for testing purposes, but also at other times it may be useful to be able to pass the provided fields in the form of generator.
Example: usage of array to generator helper
$array = [0 => 1, 'a' => 'b'];
$generator = Interitty\Utils\Arrays::arrayToGenerator($array);
Array sort by nested value
The default PHP function sort can work with a one-dimensional array by its value. However, sorting multidimensional fields according to some nested keys may be practical. For example, sort web pages field by a nested attribute with a creation date or a title.
Example: usage of array sort ascending by nested value
$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']);
As can be seen from the example, there may be situations where the required nested shift key may not always be available.
For this purpose, there is an optional $fallback
parameter that can be used to specify whether to remove such
an element from the array (ArraySorter:: FALLBACK_REMOVE
), leave it in place (ArraySorter::FALLBACK_KEEP
),
move it to the beginning (ArraySorter::FALLBACK_TOP
), or move it to the end (ArraySorter::FALLBACK_BOTTOM
),
which is the default behavior.
Example: usage of array sort descending by nested value
There are many cases where descending sorting may be helpful, like a list of articles sorted from newest to oldest. For this purpose, there is an adequate equivalent to the rsort function.
$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']);
Array topological sort
There are situations when it is necessary to sort fields according to interdependencies determined by an incomplete
set of conditions, which element should be "before" or "after" which. For these purposes,
the Interitty\Utils\ArraySorter::tsort
function is available.
Example: usage of array topological sort
$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
Once some data comes from an unreliable source, it needs to be validated. For this purpose, a helper function
Interitty\Utils\Validators::filterVariable
has been created, which wraps the native php function filter_var
and adds support for more convenient handling of flags and for working with the default value.
It filters data by either validating or sanitizing it. This is especially useful when the data source contains unknown
(or foreign) data. Validation is used to validate or check if the data meets certain qualifications, but will not change
the data itself.
Sanitization will sanitize the data, so it may alter it by removing undesired characters. It doesn't actually validate the data! The behavior of most filters can optionally be tweaked by flags.
For more information see the PHP's filter_var()
function
manual page and Validate filters or
Sanitize filters.
Example: usage of 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 wrapper features
When the array
structure needs to be more like an object
for a while. And thanks to helping from the author of
the PHPStan, the wrapper is also equipped with a fully static type checker that allows you
to declare expected internal attributes and their types using phpdoc and verify the existence of magic methods
accordingly. Of course, there's also full support for deep work, allowing you to easily access
any internal element, whether it's an object or an array.
Example: usage of 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);
Assertable checks
Standard Nette\Utils\Validators methods assert()
and assertField
throws \Nette\Utils\AssertionException
with the exact message about what is wrong. It is very useful
for developers to have these "assertions" wherever there is no possibility of exact typehint
(for example in getters & setters) to easier and faster detection of a source of the problem. However, these additional
checks cost some time, CPU, and memory even when the code is 100% perfect. Fortunately, there is a native PHP solution
in the native assert
function. This function comes with the support
of configuration to completely remove these "assertions" from generated bytecode in the production environment. However,
it requires to have the assertion that returns bool
result.
This extension (Interitty\Utils\Validators
) simply returns true
instead of void
, so it can use it inside of the assert
function.
See the official documentation for more examples of the possible validator's syntax.
Example: usage of Assertable checks
/**
* 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;
}
Extended and translatable exceptions
When treating edge cases and validation conditions, but also during the normal life cycle of the application, exceptions may occur whose information value should reach the user. In case the application is multilingual, it is necessary to work with parameterized text that is passed to the Translator object for translation, in which the parameters are then replaced by a real value.
For this purpose, the Interitty\Exceptions\ExtendedExceptionTrait
trait was created, which allows all the functions
described above, and additionally provides a fluent interface for defining individual exception parameters and
a Interitty\Exceptions\ExtendedExceptionInterface
interface to document this.
In order not to have to create a large number of classes, with all the exceptions used in the application, just to use
the mentioned trait, a helper function Interitty\Exceptions\Exceptions::extend
was created which can extend
the passed exception at runtime.
If the Interitty\Exceptions\Exceptions
class has set Translator using a static setter, this is then passed by the
extend
helper function to all of the created exceptions and used when converting them to a string.
Example: usage of extend helper for the exception object
throw Exceptions::extend(new RuntimeException('Message with :placeholder'))->addData('placeholder', 'value');
Example: usage of extend helper for the exception fluent interface
throw Exceptions::extend(RuntimeException::class)
->setMessage('Message with :placeholder')
->setCode(42)
->addData('placeholder', 'value');
Example: initial setup in Nette based application
services:
application.application:
setup:
- Interitty\Exceptions\Exceptions::setTempDir(%tempDir%)
- Interitty\Exceptions\Exceptions::setTranslator(@Nette\Localization\Translator)
Readable/Writable/Executable validator
Standard Nette\Utils\Validators does not support
the is_readable
, is_writable
and is_executable
.
This extension (Interitty\Utils\Validators
) simply adds them.
Example: usage of Readable/Writable
/**
* 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;
}
Collection
The interitty/utils contain a simple implementation of the Collection with the built-in type check that prevents of add and also gaining a wrong data type.
$collection = new Interitty\Iterators\Collection('string');
$collection->add('foo');
The $type
of the collection defined in __construct
is validated via Nette\Utils\Validators
so it can be more specific than just data type. For example, it can be defined as a collection of strings that should be consisted of up to 42 characters.
$collection = new Interitty\Iterators\Collection('string:..42');
$collection->add('foo');
The collection also allowed to work with the exactly defined integer or string key for each added item.
$collection = new Interitty\Iterators\Collection('integer');
$collection->add(42, 'foo');
$foo = $collection->get('foo');
$collection->delete('foo');
The collection also allows to inform about count of added items.
$collection = new Interitty\Iterators\Collection('integer');
$count = $collection->count();
There is also a possibility to add more than one item at once by adding an array
or any Traversable
object that contains required data.
$data = [
'foo' => 42,
'bar' => 0,
];
$collection = new Interitty\Iterators\Collection('integer');
$collection->addCollection($data);
Reflection
The default implementation of ReflectionObject
cannot easily access a private
variable if it is not a direct variable in the object,
i.e. if it is declared on an ancestor. For this purpose, the Interitty\Utils\ ReflectionObject
extension has been added, which adds
a second optional parameter bool $deep
to the getProperty
function, which can be used to enable support for accessing it.
This is additionally used by the helper functions getNonPublicPropertyValue
, and setNonPublicPropertyValue
which make it easy to access
this functionality. This is still an OOP workaround with a significant performance impact. It should therefore not be used in a production
environment. However, it can still be useful for test creation, for example.
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');
Reflection methods attributes
To access the methods of the object marked by the attribute
and its parameters, there is a helper getMethodsAttributes
. If the attribute is based on a generic
Interitty\Utils\BaseAttribute
, there is an possibility to access the marked method.
#[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 features
Working with DateTime
and DateTimeZone
objects is the right way,
way to work with date and time, because there are many time zones and even in one zone there is summertime and wintertime.
So to correctly calculate the difference between dates, to correctly display the same date to people all over the world, these objects are a must-have.
Standard Nette\Utils\DateTime already comes with many useful features,
and this extension (Interitty\Utils\DateTime
) adds some other.
processParseIso8601
The DateTime
object contains static helper processParseIso8601
that validates and then parses given string, that should match the IS0 8601 standard for DateTime.
The result is an array that contains information from the given string in a standardized format or the InvalidArgumentException
is thrown.
Example: usage of processParseIso8601
The code DateTime::processParseIso8601('1989-12-17T12:00+00:00');
will return the following array.
[
'year' => '1989',
'month' => '12',
'day' => '17',
'hour' => '12',
'minutes' => '00',
'seconds' => '00',
'microseconds' => '000000',
'timeZone' => '+00:00',
'timeZoneOperation' => '+',
'timeZoneHours' => '00',
'timeZoneMinutes' => '00',
]
The code DateTime::processParseIso8601('1989-12-17 12:00Z');
will return the following array.
[
'year' => '1989',
'month' => '12',
'day' => '17',
'hour' => '12',
'minutes' => '00',
'seconds' => '00',
'microseconds' => '000000',
'timeZone' => 'Z',
'timeZoneMilitary' => 'Z',
]
When the given string does not contain the information about the timezone the code DateTime::processParseIso8601('1989-12-17 12:00');
will return the following array.
[
'year' => '1989',
'month' => '12',
'day' => '17',
'hour' => '12',
'minutes' => '00',
'seconds' => '00',
'microseconds' => '000000',
]
DateTimeFactory
The proper way how to create a new instance of an object is thru the factories with their interfaces because it allows other developers to overwrite the code easier.
So the interitty/utils come with the DateTimeFactoryInterface
and his implementation DateTimeFactory
that allows creating the DateTime
and DateTimeZone
objects.
The factory implementation also allows creating DateTime
objects with proper DateTimeZone
easier like the Nette\Utils\DateTime.
Example: usage of DateTimeFactory
Create the DateTime
object representing the current date and time with default TimeZone.
$dateTimeFactory = new Interitty\Utils\DateTimeFactory();
$dateTime = $dateTimeFactory->create();
Create the DateTime
object representing the given date and time with given TimeZone.
$dateTimeFactory = new Interitty\Utils\DateTimeFactory();
$dateTime = $dateTimeFactory->create('1989-12-17T23:59:59+02:00');
Create the DateTime
object representing the given date and time with Europe/Prague
as TimeZone.
$dateTimeFactory = new Interitty\Utils\DateTimeFactory();
$dateTime = $dateTimeFactory->create('1989-12-17T23:59:59', 'Europe/Prague');
createFromIso8601
Because the IS0 8601 standard for DateTime allows formating the string in so many variants,
it is very hard to properly convert the given string into the DateTime
object. Because of that, the DateTimeFactory
contains the helper
that does all the hard work. Currently, it can manage about 15 variants of the so-called "standard valid DateTime string".
Example: usage of DateTimeFactory createFromIso8601
$dateTimeFactory = new Interitty\Utils\DateTimeFactory();
$dateTime = $dateTimeFactory->createFromIso8601('1989-12-17T12:00:00Z');
Array deep item
Data structures can be very complicated by the combination of arrays and objects inside and sometimes it can be useful to have a possibility to access the inner part unified way.
Example: usage of 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');
Example: usage of Arrays checker
// Check the inner part
Interitty\Utils\Arrays::offsetExists(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey']);
Example: usage of Arrays setter
// Set the value deep inside the inner part
Interitty\Utils\Arrays::offsetSet(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey'], 'another value');
Example: usage of Arrays unsetter
// Unset the value from deep inside the inner part
Interitty\Utils\Arrays::offsetUnset(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey']);
Array deep item
Data structures can be very complicated by the combination of arrays and objects inside and sometimes it can be useful to have a possibility to access the inner part unified way.
Example: usage of 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');
Example: usage of Arrays checker
// Check the inner part
Interitty\Utils\Arrays::offsetExists(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey']);
Example: usage of Arrays setter
// Set the value deep inside the inner part
Interitty\Utils\Arrays::offsetSet(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey'], 'another value');
Example: usage of Arrays unsetter
// Unset the value from deep inside the inner part
Interitty\Utils\Arrays::offsetUnset(['objectKey' = (object) ['innerKey' => 'value']], ['objectKey', 'innerKey']);
Filesystem features
An extension to the Nette framework's practical filesystem feature set.
tempnam
The default php function tempnam
cannot work with the sockets
and therefore with the mikey179/vfsstream
, and also cannot work with file extensions.
This extension solves these ailments, allowing you to create a subfolder and set the mask of the file being created.
/** @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);
Helpers
The PHP programming language has many practical features that simplify everyday work. Still, there are situations where even a relatively simple task requires a more complex block of code.
Class or object of type checker
Same as the is_a
built-in function, except that it checks if the specified object or class is at least one of the specified types.
Example: usage of Helpers::isTypeOf
$test = \Interitty\Utils\Helpers::isTypeOf($foo, ['A', 'B']);
Class uses getter
Surprisingly, the built-in class_uses
function does not resolve traits from parent classes.
Example: usage of Helpers::getClassUses
foreach(\Interitty\Utils\Helpers::getClassUses($foo) as $traitClass) {
//...
}
Isolated require (class autoload) processor
Using the built-in require
function may have the potential security risk of accessing variables in the same scope. In addition, when a given file declares a class, repeated calls will cause an error. Conversely, if a given file should contain the expected class but does not, unexpected behavior may again occur
Example: usage of Helpers::processIsolatedRequire
\Interitty\Utils\Helpers::processIsolatedRequire($className, $filePath);
Trait class used by class or object checker
Sometimes it can be useful to be able to check that a class or object uses a trait, similar to checking that it meets an interface.
Example: usage of Helpers::isTraitUsed
$test = \Interitty\Utils\Helpers::isTraitUsed($foo, \Nette\SmartObject::class);
Event trigger
Nette framework supports simple and clear event handling. However, there may be situations when you need to pass reference parameters to potential subscribers.
Example: usage of Helpers::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
}
}