interitty/utils

Extension of the standard Nette/Utils by some other features that are specific for use in the Interitty projects.

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

README

Extension of the standard Nette/Utils by some other features that are specific for use in the Interitty projects.

Requirements

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
    }
}