diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d8bac3e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Exclude Git and CI configuration files from archives. +.git* export-ignore + +# Exclude test-related files from archives. +tests/ export-ignore +phpstan* export-ignore diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index bcedb22..03a23d2 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -4,9 +4,9 @@ on: push: branches: - main + - support/* pull_request: - branches: - - main + workflow_dispatch: jobs: php: diff --git a/.gitignore b/.gitignore index 34d585a..681b17e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,5 @@ -# Exclude all hidden files -.* - -# Except those related to Git (and GitHub) -!.git* - -# Exclude files from composer install -vendor/ +# Ignore Composer installation artifacts; composer.lock is intentionally +# excluded as this is a library - applications depending on this package +# manage their own lock file. +/vendor/ composer.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7be5bb5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,121 @@ +# Changelog + +All notable changes to this library are documented in this file. + +## [Unreleased] + +- **Breaking** Raise minimum PHP version to 8.2 (#63) +- Add strict type declarations (#63) +- Support PHP 8.5 (#61) + +## [v0.14.0] — 2024-07-15 + +- **Breaking** Restrict `Str::symmetricSplit()`'s `$default` parameter to `?string` (#55) +- Support PHP 8.3 + +## [v0.13.0] — 2023-09-21 + +- **Breaking** `Seq` no longer treats function names as needles (#42) +- Add `iterable_value_first()` (#49) +- Add `yield_groups()` to group sorted iterators by callback-derived criteria (#46) +- Fix `Str::symmetricSplit()` to return padded array when subject is `null` (#45) +- Support PHP 8.2 (#39) + + +## [v0.12.1] — 2022-12-13 + +- Refine `Str` method signatures and `null` handling to improve PHP 8.1 compatibility (#37) + +## [v0.12.0] — 2022-06-15 + +- **Breaking** Raise the minimum supported PHP version to 7.2 (#29) +- **Breaking** `Filter::equal()` and `Filter::unequal()` no longer perform wildcard matching; + use `Filter::like()` and `Filter::unlike()` instead (#33) +- **Breaking** Remove mutator and closure support from `Properties` (#31) +- Add iteration support to `Properties` via `IteratorAggregate` (#32) +- Support PHP 8.1 (#29) + +## [v0.11.0] — 2022-03-23 + +- **Breaking** Drop PHP 5.6 support; require PHP ≥ 7.0 (#28) +- Introduce `BaseFilter` trait for simple filter-aware classes (#28) + +## [v0.10.0] — 2021-11-10 + +- Add the `Seq` utility for searching iterables by key or value (#26) +- Fix `Properties` to not mutate values on set (#23); resolve closures before + mutation (#24) +- Support PHP 8 (#21) + +## [v0.9.0] — 2021-03-19 + +- **Breaking** `Filter\Chain` and `Filter\Condition` no longer tolerate dynamic properties; + use dedicated metadata instead (#20) +- Introduce `Filterable` contract interface and `Filters` trait for + filter-aware objects (#19) +- Introduce `Data` class for arbitrary key-value metadata storage (#20) +- Introduce `MetaDataProvider` interface for filter rule metadata (#20) +- Extend `Filter\Chain` with `insertBefore()` and `insertAfter()` (#20) + +## [v0.8.0] — 2021-01-14 + +- Introduce filter system: `Filter` facade and `Filter\Rule`, `Filter\Chain` + (`All`, `Any`, `None`), and `Filter\Condition` (`Equal`, `Unequal`, + `GreaterThan`, `GreaterThanOrEqual`, `LessThan`, `LessThanOrEqual`) (#15) +- Add `random_bytes()` polyfill for PHP < 7.0 (#18) + +## [v0.7.0] — 2020-10-19 + +- Introduce `Properties` trait for dynamic typed property storage (#16) + +## [v0.6.0] — 2020-10-12 + +- Introduce `Translator` contract interface (#14) + +## [v0.5.0] — 2020-03-12 + +- **Breaking** Raise the minimum supported PHP version to 5.6 (#12) +- **Breaking** Refactor contracts and traits: rename `EventEmitter` → `Events`, + `MessageContainer` → `Messages`; supersede `PaginationInterface` with + `Paginatable`; supersede `ValidatorInterface` with `Validator` (#12) +- Introduce new `PluginLoader` interface and `Plugins` trait (#12) +- Introduce `PriorityQueue` class wrapping `SplPriorityQueue` with stable + ordering (#13) + +## [v0.4.0] — 2020-03-10 + +- Add `iterable_key_first()` for retrieving the first key from any iterable (#10) +- Add `Str::symmetricSplit()`, `Str::trimSplit()`, and `Str::startsWith()` + +## [v0.3.0] — 2019-10-16 + +- Introduce `PaginationInterface` (#5) +- Introduce `Str` class with `camel()` case conversion +- Add `is_iterable()` polyfill for PHP < 7.1 + +## [v0.2.0] — 2019-05-16 + +- Introduce `EventEmitter` trait, wrapping + [Evenement](https://github.com/igorw/evenement) (#8) + +## [v0.1.0] — 2019-03-26 + +Initial release providing `AutoloadingPluginLoader`, `MessageContainer` trait, +`ValidatorInterface`, and type/iterable utility functions. + +[Unreleased]: https://github.com/Icinga/ipl-stdlib/compare/v0.14.0...HEAD +[v0.14.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.14.0 +[v0.13.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.13.0 +[v0.12.1]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.12.1 +[v0.12.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.12.0 +[v0.11.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.11.0 +[v0.10.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.10.0 +[v0.9.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.9.0 +[v0.8.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.8.0 +[v0.7.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.7.0 +[v0.6.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.6.0 +[v0.5.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.5.0 +[v0.4.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.4.0 +[v0.3.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.3.0 +[v0.2.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.2.0 +[v0.1.0]: https://github.com/Icinga/ipl-stdlib/releases/tag/v0.1.0 diff --git a/LICENSE b/LICENSE.md similarity index 93% rename from LICENSE rename to LICENSE.md index 58005ec..564413b 100644 --- a/LICENSE +++ b/LICENSE.md @@ -1,6 +1,6 @@ -The MIT License +# MIT License -Copyright (c) 2018 Icinga GmbH https://www.icinga.com +Copyright (c) 2018 Icinga GmbH https://icinga.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 536abf3..ac94294 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,222 @@ # Icinga PHP Library - Standard Library -This is the Stdlib prototype for the Icinga PHP library ([ipl](https://github.com/Icinga/ipl)). -Please do not use this for anything important yet, as all APIs, Interfaces and -paths are still subject to change. +`ipl/stdlib` provides reusable building blocks for Icinga PHP libraries and +applications. It covers declarative filtering, event emission, string and +iterable utilities, lightweight data and message containers, and a +stable priority queue. + +## Installation + +The recommended way to install this library is via +[Composer](https://getcomposer.org): + +```shell +composer require ipl/stdlib +``` + +`ipl/stdlib` requires PHP 8.2 or later with the `openssl` extension. + +## Usage + +### Filter Rows With Declarative Rules + +Build composable filter trees with `ipl\Stdlib\Filter` and evaluate them +against arrays or objects: + +```php +use ipl\Stdlib\Filter; + +$filter = Filter::all( + Filter::equal('problem', '1'), + Filter::none(Filter::equal('handled', '1')), + Filter::like('service', 'www.*') +); + +$row = [ + 'problem' => '1', + 'handled' => '0', + 'service' => 'www.icinga.com', +]; + +if (Filter::match($filter, $row)) { + // The row matches the rule set. +} +``` + +Available condition factories: `equal`, `unequal`, `like`, `unlike`, +`greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual`. +Available logical factories: `all`, `any`, `none`, `not`. + +### Build Filters Incrementally + +When an object needs to collect filter conditions over time, use the `Filters` +trait. It complements the `Filterable` contract and exposes `filter()`, +`orFilter()`, `notFilter()`, and `orNotFilter()`: + +```php +use ipl\Stdlib\Contract\Filterable; +use ipl\Stdlib\Filter; +use ipl\Stdlib\Filters; + +class Query implements Filterable +{ + use Filters; +} + +$query = (new Query()) + ->filter(Filter::equal('problem', '1')) + ->orNotFilter(Filter::equal('handled', '1')); + +$filter = $query->getFilter(); +``` + +## Events + +The `Events` trait wraps [Evenement](https://github.com/igorw/evenement) and +adds event validation. Declare event name constants on the class to give +callers a typo-safe API and to let `isValidEvent()` enforce an explicit +allow-list: + +```php +use ipl\Stdlib\Events; + +class Connection +{ + use Events; + + public const ON_CONNECT = 'connected'; + public const ON_DISCONNECT = 'disconnected'; + + protected function isValidEvent($event): bool + { + return in_array($event, [static::ON_CONNECT, static::ON_DISCONNECT], true); + } + + public function open(): void + { + // ... connect ... + $this->emit(self::ON_CONNECT, [$this]); + } + + public function close(): void + { + // ... disconnect ... + $this->emit(self::ON_DISCONNECT, [$this]); + } +} + +$conn = new Connection(); +$conn->on(Connection::ON_CONNECT, function (Connection $c): void { + echo "Connected\n"; +}); +$conn->on(Connection::ON_DISCONNECT, function (Connection $c): void { + echo "Disconnected\n"; +}); + +$conn->open(); +$conn->close(); +``` + +## Utility Helpers + +### Str + +`Str` offers string utilities that complement PHP's built-in functions. +It converts between naming conventions, splits and trims in one step, and +provides `startsWith` with case-insensitive matching. + +```php +use ipl\Stdlib\Str; + +// Convert snake_case or kebab-case identifiers to camelCase: +Str::camel('host_name'); // 'hostName' +Str::camel('display-name'); // 'displayName' + +// Split on a delimiter and trim whitespace from every part in one pass: +Str::trimSplit(' foo , bar , baz '); // ['foo', 'bar', 'baz'] +Str::trimSplit('root:secret', ':'); // ['root', 'secret'] + +// Always return exactly $limit parts: pads with null if the delimiter is +// absent, and fold any remainder into the last part if there are more +// separators than expected: +[$user, $pass] = Str::symmetricSplit('root', ':', 2); // ['root', null] +[$user, $pass] = Str::symmetricSplit('root:secret:extra', ':', 2); // ['root', 'secret:extra'] + +// Case-insensitive prefix check: +Str::startsWith('Foobar', 'foo', caseSensitive: false); // true +Str::startsWith('foobar', 'foo'); // true +``` + +### Seq + +`Seq` searches arrays, iterators, and generators by value, key, or callback +without first materializing them into arrays. When the second argument to +`find` or `contains` is a non-callable, it is compared by value; pass a +closure to match by predicate instead: + +```php +use ipl\Stdlib\Seq; + +$users = [ + 'alice' => 'admin', + 'bob' => 'viewer', +]; + +Seq::contains($users, 'viewer'); // true + +[$key, $value] = Seq::find($users, 'admin'); // ['alice', 'admin'] + +// Match by predicate — returns as soon as a result is found: +[$key, $value] = Seq::find($users, fn(string $role): bool => $role !== 'admin'); // ['bob', 'viewer'] +``` + +### Iterable Helpers + +```php +use function ipl\Stdlib\iterable_key_first; +use function ipl\Stdlib\iterable_value_first; + +$map = [ + 'id' => 42, + 'name' => 'Alice', +]; + +iterable_key_first($map); // 'id' +iterable_value_first($map); // 42 + +// Works with generators and iterators — does not require an array: +iterable_key_first(new ArrayIterator(['a' => 1])); // 'a' +iterable_key_first([]); // null +``` + +### Grouping With `yield_groups` + +`yield_groups` partitions a pre-sorted traversable into named groups. +The callback must return at least the grouping criterion, but it can +also return a custom value and key. The traversable **must** be sorted +by the grouping criterion before being passed in; results are undefined +otherwise: + +```php +use function ipl\Stdlib\yield_groups; + +foreach (yield_groups($rows, fn(object $row): string => $row->category) as $category => $items) { + // $items contains all rows for $category. +} +``` + +### Other Utility Classes + +- `Data` — mutable key/value store +- `Messages` — collects user-facing messages and supports `sprintf`-style + placeholders +- `PriorityQueue` — extends `SplPriorityQueue` with stable insertion-order for + items at equal priority; iterate non-destructively with `yieldAll()` + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for a list of notable changes. + +## License + +`ipl/stdlib` is licensed under the terms of the [MIT License](LICENSE.md). diff --git a/composer.json b/composer.json index 2bb9df3..626ea5c 100644 --- a/composer.json +++ b/composer.json @@ -1,22 +1,27 @@ { "name": "ipl/stdlib", "description": "ipl Standard Library", - "type": "library", "license": "MIT", + "type": "library", + "require": { + "php": ">=8.2", + "ext-openssl": "*", + "evenement/evenement": "^3.0.1" + }, "autoload": { - "files": ["src/functions_include.php"], "psr-4": { "ipl\\Stdlib\\": "src" - } + }, + "files": [ + "src/functions_include.php" + ] }, "autoload-dev": { "psr-4": { "ipl\\Tests\\Stdlib\\": "tests" } }, - "require": { - "php": ">=8.2", - "ext-openssl": "*", - "evenement/evenement": "^3.0.1" + "config": { + "sort-packages": true } } diff --git a/src/BaseFilter.php b/src/BaseFilter.php index 0b0a946..fb445db 100644 --- a/src/BaseFilter.php +++ b/src/BaseFilter.php @@ -4,9 +4,11 @@ use ipl\Stdlib\Filter\Rule; +/** + * Store and expose a base filter rule + */ trait BaseFilter { - /** @var ?Rule Base filter */ private ?Rule $baseFilter = null; /** diff --git a/src/Contract/Filterable.php b/src/Contract/Filterable.php index a6fcdb3..3fbaeaa 100644 --- a/src/Contract/Filterable.php +++ b/src/Contract/Filterable.php @@ -4,6 +4,9 @@ use ipl\Stdlib\Filter; +/** + * Manage filters on a query or collection + */ interface Filterable { /** diff --git a/src/Contract/Paginatable.php b/src/Contract/Paginatable.php index 1a63005..820afc6 100644 --- a/src/Contract/Paginatable.php +++ b/src/Contract/Paginatable.php @@ -4,6 +4,9 @@ use Countable; +/** + * Support limit and offset for paginated result sets + */ interface Paginatable extends Countable { /** diff --git a/src/Contract/PaginationInterface.php b/src/Contract/PaginationInterface.php index b00fa66..11c26c7 100644 --- a/src/Contract/PaginationInterface.php +++ b/src/Contract/PaginationInterface.php @@ -2,7 +2,11 @@ namespace ipl\Stdlib\Contract; -/** @deprecated Use {@link Paginatable} instead */ +/** + * Deprecated predecessor of {@see Paginatable} + * + * @deprecated Use {@see Paginatable} instead + */ interface PaginationInterface extends Paginatable { } diff --git a/src/Contract/PluginLoader.php b/src/Contract/PluginLoader.php index d081a47..9425542 100644 --- a/src/Contract/PluginLoader.php +++ b/src/Contract/PluginLoader.php @@ -3,10 +3,9 @@ namespace ipl\Stdlib\Contract; /** - * Representation of plugin loaders + * Load plugin class names by plugin name * - * Plugin loaders must implement the {@link load()} method in order to provide the fully qualified class name of a - * plugin to load. + * Implementations must provide the fully qualified class name of a plugin via {@see load()}. */ interface PluginLoader { diff --git a/src/Contract/Translator.php b/src/Contract/Translator.php index 413d37e..bf0b6be 100644 --- a/src/Contract/Translator.php +++ b/src/Contract/Translator.php @@ -3,7 +3,7 @@ namespace ipl\Stdlib\Contract; /** - * Representation of translators + * Translate messages with optional context, domain scope, and plural forms */ interface Translator { diff --git a/src/Contract/Validator.php b/src/Contract/Validator.php index e43821d..9f3a683 100644 --- a/src/Contract/Validator.php +++ b/src/Contract/Validator.php @@ -2,10 +2,13 @@ namespace ipl\Stdlib\Contract; +/** + * Validate values and collect error messages + */ interface Validator { /** - * Get whether the given value is valid + * Check whether the given value is valid * * @param mixed $value * @@ -16,7 +19,7 @@ public function isValid($value); /** * Get the validation error messages * - * @return array + * @return string[] */ public function getMessages(); } diff --git a/src/Contract/ValidatorInterface.php b/src/Contract/ValidatorInterface.php index 36cf55e..c116857 100644 --- a/src/Contract/ValidatorInterface.php +++ b/src/Contract/ValidatorInterface.php @@ -2,7 +2,11 @@ namespace ipl\Stdlib\Contract; -/** @deprecated Use {@link Validator} instead */ +/** + * Deprecated predecessor of {@see Validator} + * + * @deprecated Use {@see Validator} instead + */ interface ValidatorInterface extends Validator { } diff --git a/src/Data.php b/src/Data.php index d54d598..60c212d 100644 --- a/src/Data.php +++ b/src/Data.php @@ -2,6 +2,9 @@ namespace ipl\Stdlib; +/** + * Store and retrieve arbitrary key-value data + */ class Data { /** @var array */ diff --git a/src/EventEmitter.php b/src/EventEmitter.php index 6d189ee..264ae82 100644 --- a/src/EventEmitter.php +++ b/src/EventEmitter.php @@ -2,7 +2,11 @@ namespace ipl\Stdlib; -/** @deprecated Use {@link Events} instead */ +/** + * Deprecated predecessor of {@see Events} + * + * @deprecated Use {@see Events} instead + */ trait EventEmitter { use Events; diff --git a/src/Events.php b/src/Events.php index 3ecae41..2ef2357 100644 --- a/src/Events.php +++ b/src/Events.php @@ -5,18 +5,25 @@ use Evenement\EventEmitterTrait; use InvalidArgumentException; +/** + * Register listeners and emit named events with optional event-name validation + */ trait Events { use EventEmitterTrait { EventEmitterTrait::on as private evenementUnvalidatedOn; } - /** @var array */ + /** @var array */ protected array $eventsEmittedOnce = []; /** + * Emit the given event at most once, ignoring subsequent calls + * * @param string $event * @param array $arguments + * + * @return void */ protected function emitOnce(string $event, array $arguments = []): void { @@ -27,9 +34,14 @@ protected function emitOnce(string $event, array $arguments = []): void } /** + * Register a listener for the given event, validating the event name first + * * @param string $event * @param callable $listener + * * @return $this + * + * @throws InvalidArgumentException If the event name is not valid */ public function on($event, callable $listener): static { @@ -39,6 +51,15 @@ public function on($event, callable $listener): static return $this; } + /** + * Assert that the given event name is valid + * + * @param string $event + * + * @return void + * + * @throws InvalidArgumentException If the event name is not valid + */ protected function assertValidEvent(string $event): void { if (! $this->isValidEvent($event)) { @@ -47,7 +68,10 @@ protected function assertValidEvent(string $event): void } /** + * Check whether the given event name is valid + * * @param string $event + * * @return bool */ public function isValidEvent($event) diff --git a/src/Filter.php b/src/Filter.php index 8450a98..9287007 100644 --- a/src/Filter.php +++ b/src/Filter.php @@ -17,20 +17,25 @@ use ipl\Stdlib\Filter\Unequal; use ipl\Stdlib\Filter\Unlike; +/** + * Build filter rules and evaluate them against rows + */ class Filter { /** - * protected - This is only a factory class + * Create a new Filter + * + * Intentionally protected; use the static factory methods instead. */ protected function __construct() { } /** - * Return whether the given rule matches the given item + * Check whether the given rule matches the given item * * @param Rule $rule - * @param object|array $row + * @param array|object $row * * @return bool */ @@ -46,6 +51,8 @@ public static function match(Rule $rule, array|object $row): bool /** * Create a rule that matches if **all** of the given rules do * + * If no rules are given, the resulting rule always matches. + * * @param Rule ...$rules * * @return Chain @@ -61,7 +68,7 @@ public static function all(Rule ...$rules): Chain * @param All $rules * @param object $row * - * @return bool + * @return bool True if all rules match; always true if no rules are given */ protected function matchAll(All $rules, object $row): bool { @@ -77,6 +84,8 @@ protected function matchAll(All $rules, object $row): bool /** * Create a rule that matches if **any** of the given rules do * + * If no rules are given, the resulting rule never matches. + * * @param Rule ...$rules * * @return Chain @@ -92,7 +101,7 @@ public static function any(Rule ...$rules): Chain * @param Any $rules * @param object $row * - * @return bool + * @return bool True if any rule matches; always false if no rules are given */ protected function matchAny(Any $rules, object $row): bool { @@ -108,6 +117,8 @@ protected function matchAny(Any $rules, object $row): bool /** * Create a rule that matches if **none** of the given rules do * + * If no rules are given, the resulting rule always matches. + * * @param Rule ...$rules * * @return Chain @@ -123,7 +134,7 @@ public static function none(Rule ...$rules): Chain * @param None $rules * @param object $row * - * @return bool + * @return bool True if no rules match; always true if no rules are given */ protected function matchNone(None $rules, object $row): bool { @@ -234,7 +245,7 @@ protected function performEqualityMatch(mixed $value, mixed $rowValue, bool $ign /** @var string|string[] $value {@see self::normalizeTypes} ensures this is the case */ $value = is_array($value) ? array_map('strtolower', $value) - : ($value === null ? null : strtolower($value)); // phpstan is wrong here + : ($value === null ? null : strtolower($value)); // PHPStan incorrectly infers the type here. } if (is_array($value)) { @@ -260,7 +271,7 @@ protected function performSimilarityMatch(mixed $value, mixed $rowValue, bool $i /** @var string|string[] $value {@see self::normalizeTypes} ensures this is the case */ $value = is_array($value) ? array_map('strtolower', $value) - : ($value === null ? null : strtolower($value)); // phpstan is wrong here + : ($value === null ? null : strtolower($value)); // PHPStan incorrectly infers the type here. } if (is_array($value)) { @@ -500,7 +511,7 @@ protected function extractValue(string $column, object $row): mixed * Normalize type of $value to the one of $rowValue * * For details on how this works please see the corresponding test - * {@see \ipl\Tests\Stdlib\FilterTest::testConditionsAreValueTypeAgnostic} + * {@see \ipl\Tests\Stdlib\FilterTest::testConditionsAreValueTypeAgnostic}. * * @param mixed $rowValue * @param mixed $value diff --git a/src/Filter/All.php b/src/Filter/All.php index 67b47b6..b89f3ab 100644 --- a/src/Filter/All.php +++ b/src/Filter/All.php @@ -2,6 +2,9 @@ namespace ipl\Stdlib\Filter; +/** + * Filter chain that matches when all contained rules match + */ class All extends Chain { } diff --git a/src/Filter/Any.php b/src/Filter/Any.php index 5d47ebe..31db673 100644 --- a/src/Filter/Any.php +++ b/src/Filter/Any.php @@ -2,6 +2,9 @@ namespace ipl\Stdlib\Filter; +/** + * Filter chain that matches when at least one contained rule matches + */ class Any extends Chain { } diff --git a/src/Filter/Chain.php b/src/Filter/Chain.php index 0c75d53..cf18a60 100644 --- a/src/Filter/Chain.php +++ b/src/Filter/Chain.php @@ -8,7 +8,11 @@ use OutOfBoundsException; use Traversable; -/** @implements IteratorAggregate */ +/** + * Abstract filter chain that holds an ordered list of rules + * + * @implements IteratorAggregate + */ abstract class Chain implements Rule, MetaDataProvider, IteratorAggregate, Countable { use MetaData; @@ -43,7 +47,7 @@ public function __clone() } /** - * Get an iterator this chain's rules + * Get an iterator for this chain's rules * * @return ArrayIterator */ @@ -72,8 +76,9 @@ public function add(Rule $rule): static * @param Rule $rule * @param Rule $before * - * @throws OutOfBoundsException In case no existing rule is found * @return $this + * + * @throws OutOfBoundsException If the reference rule is not found */ public function insertBefore(Rule $rule, Rule $before): static { @@ -93,8 +98,9 @@ public function insertBefore(Rule $rule, Rule $before): static * @param Rule $rule * @param Rule $after * - * @throws OutOfBoundsException In case no existing rule is found * @return $this + * + * @throws OutOfBoundsException If the reference rule is not found */ public function insertAfter(Rule $rule, Rule $after): static { @@ -126,8 +132,9 @@ public function has(Rule $rule): bool * @param Rule $rule * @param Rule $replacement * - * @throws OutOfBoundsException In case no existing rule is found * @return $this + * + * @throws OutOfBoundsException If the rule to replace is not found */ public function replace(Rule $rule, Rule $replacement): static { @@ -168,11 +175,6 @@ public function isEmpty(): bool return empty($this->rules); } - /** - * Count this chain's rules - * - * @return int - */ public function count(): int { return count($this->rules); diff --git a/src/Filter/Condition.php b/src/Filter/Condition.php index dfb160a..6f47f80 100644 --- a/src/Filter/Condition.php +++ b/src/Filter/Condition.php @@ -2,6 +2,9 @@ namespace ipl\Stdlib\Filter; +/** + * Abstract filter condition matching a column against a value + */ abstract class Condition implements Rule, MetaDataProvider { use MetaData; @@ -9,7 +12,6 @@ abstract class Condition implements Rule, MetaDataProvider /** @var string|string[] */ protected string|array $column = []; - /** @var mixed */ protected mixed $value = null; /** diff --git a/src/Filter/Equal.php b/src/Filter/Equal.php index f5f22de..e5f3e07 100644 --- a/src/Filter/Equal.php +++ b/src/Filter/Equal.php @@ -2,9 +2,11 @@ namespace ipl\Stdlib\Filter; +/** + * Match when the column value equals the filter value + */ class Equal extends Condition { - /** @var bool */ protected bool $ignoreCase = false; /** @@ -20,10 +22,10 @@ public function ignoreCase(): static } /** - * Return whether this rule ignores case - * - * @return bool - */ + * Return whether this rule ignores case + * + * @return bool + */ public function ignoresCase(): bool { return $this->ignoreCase; diff --git a/src/Filter/GreaterThan.php b/src/Filter/GreaterThan.php index fd8190c..c9f332f 100644 --- a/src/Filter/GreaterThan.php +++ b/src/Filter/GreaterThan.php @@ -2,6 +2,9 @@ namespace ipl\Stdlib\Filter; +/** + * Match when the column value is strictly greater than the filter value + */ class GreaterThan extends Condition { } diff --git a/src/Filter/GreaterThanOrEqual.php b/src/Filter/GreaterThanOrEqual.php index 4cd4a73..479db91 100644 --- a/src/Filter/GreaterThanOrEqual.php +++ b/src/Filter/GreaterThanOrEqual.php @@ -2,6 +2,9 @@ namespace ipl\Stdlib\Filter; +/** + * Match when the column value is greater than or equal to the filter value + */ class GreaterThanOrEqual extends Condition { } diff --git a/src/Filter/LessThan.php b/src/Filter/LessThan.php index 297493f..4a2746a 100644 --- a/src/Filter/LessThan.php +++ b/src/Filter/LessThan.php @@ -2,6 +2,9 @@ namespace ipl\Stdlib\Filter; +/** + * Match when the column value is strictly less than the filter value + */ class LessThan extends Condition { } diff --git a/src/Filter/LessThanOrEqual.php b/src/Filter/LessThanOrEqual.php index ef35974..cd8ae41 100644 --- a/src/Filter/LessThanOrEqual.php +++ b/src/Filter/LessThanOrEqual.php @@ -2,6 +2,9 @@ namespace ipl\Stdlib\Filter; +/** + * Match when the column value is less than or equal to the filter value + */ class LessThanOrEqual extends Condition { } diff --git a/src/Filter/Like.php b/src/Filter/Like.php index d720c71..0a40348 100644 --- a/src/Filter/Like.php +++ b/src/Filter/Like.php @@ -2,9 +2,11 @@ namespace ipl\Stdlib\Filter; +/** + * Match when the column value is similar to the filter value, supporting `*` wildcards + */ class Like extends Condition { - /** @var bool */ protected bool $ignoreCase = false; /** @@ -20,10 +22,10 @@ public function ignoreCase(): static } /** - * Return whether this rule ignores case - * - * @return bool - */ + * Return whether this rule ignores case + * + * @return bool + */ public function ignoresCase(): bool { return $this->ignoreCase; diff --git a/src/Filter/MetaData.php b/src/Filter/MetaData.php index f07fdaf..c0e2a29 100644 --- a/src/Filter/MetaData.php +++ b/src/Filter/MetaData.php @@ -4,9 +4,11 @@ use ipl\Stdlib\Data; +/** + * Complement {@see MetaDataProvider} by lazily creating a {@see Data} bag on first access + */ trait MetaData { - /** @var ?Data */ protected ?Data $metaData = null; public function metaData(): Data diff --git a/src/Filter/MetaDataProvider.php b/src/Filter/MetaDataProvider.php index c4f0ab9..df0feb4 100644 --- a/src/Filter/MetaDataProvider.php +++ b/src/Filter/MetaDataProvider.php @@ -4,10 +4,16 @@ use ipl\Stdlib\Data; +/** + * Provide access to arbitrary rule meta data + */ interface MetaDataProvider { /** - * Get this rule's meta data + * Get this rule's metadata + * + * Implementations must return the same bag on every call, creating it on + * first access or upfront. * * @return Data */ diff --git a/src/Filter/None.php b/src/Filter/None.php index a1b14f7..d4eee7a 100644 --- a/src/Filter/None.php +++ b/src/Filter/None.php @@ -2,6 +2,9 @@ namespace ipl\Stdlib\Filter; +/** + * Filter chain that matches when none of the contained rules match + */ class None extends Chain { } diff --git a/src/Filter/Rule.php b/src/Filter/Rule.php index dc83c80..7eb210d 100644 --- a/src/Filter/Rule.php +++ b/src/Filter/Rule.php @@ -2,6 +2,9 @@ namespace ipl\Stdlib\Filter; +/** + * Marker interface for filter rules (chains and conditions) + */ interface Rule { } diff --git a/src/Filter/Unequal.php b/src/Filter/Unequal.php index 7742dfe..bf90fd2 100644 --- a/src/Filter/Unequal.php +++ b/src/Filter/Unequal.php @@ -2,9 +2,11 @@ namespace ipl\Stdlib\Filter; +/** + * Match when the column value does not equal the filter value + */ class Unequal extends Condition { - /** @var bool */ protected bool $ignoreCase = false; /** @@ -20,10 +22,10 @@ public function ignoreCase(): static } /** - * Return whether this rule ignores case - * - * @return bool - */ + * Return whether this rule ignores case + * + * @return bool + */ public function ignoresCase(): bool { return $this->ignoreCase; diff --git a/src/Filter/Unlike.php b/src/Filter/Unlike.php index df8936d..b99c223 100644 --- a/src/Filter/Unlike.php +++ b/src/Filter/Unlike.php @@ -2,9 +2,11 @@ namespace ipl\Stdlib\Filter; +/** + * Match when the column value does not match the filter value, supporting `*` wildcards + */ class Unlike extends Condition { - /** @var bool */ protected bool $ignoreCase = false; /** @@ -20,10 +22,10 @@ public function ignoreCase(): static } /** - * Return whether this rule ignores case - * - * @return bool - */ + * Return whether this rule ignores case + * + * @return bool + */ public function ignoresCase(): bool { return $this->ignoreCase; diff --git a/src/Filters.php b/src/Filters.php index c4b03ad..acd1020 100644 --- a/src/Filters.php +++ b/src/Filters.php @@ -2,9 +2,11 @@ namespace ipl\Stdlib; +/** + * Add and compose filter rules on a query or collection + */ trait Filters { - /** @var ?Filter\Chain */ protected ?Filter\Chain $filter = null; public function getFilter(): Filter\Chain diff --git a/src/MessageContainer.php b/src/MessageContainer.php index 3b383b1..76822ee 100644 --- a/src/MessageContainer.php +++ b/src/MessageContainer.php @@ -2,7 +2,11 @@ namespace ipl\Stdlib; -/** @deprecated Use {@link Messages} instead */ +/** + * Deprecated predecessor of {@see Messages} + * + * @deprecated Use {@see Messages} instead + */ trait MessageContainer { use Messages; diff --git a/src/Messages.php b/src/Messages.php index a728c0f..4b7086d 100644 --- a/src/Messages.php +++ b/src/Messages.php @@ -2,9 +2,12 @@ namespace ipl\Stdlib; +/** + * Store, retrieve, and manage a list of string messages + */ trait Messages { - /** @var array */ + /** @var string[] */ protected array $messages = []; /** @@ -20,7 +23,7 @@ public function hasMessages(): bool /** * Get all messages * - * @return array + * @return string[] */ public function getMessages() { @@ -67,7 +70,7 @@ public function addMessage(string $message, mixed ...$args): static /** * Add the given messages * - * @param array $messages + * @param string[] $messages * * @return $this */ diff --git a/src/Plugins.php b/src/Plugins.php index e70dcb4..d5f59b0 100644 --- a/src/Plugins.php +++ b/src/Plugins.php @@ -5,9 +5,12 @@ use ipl\Stdlib\Contract\PluginLoader; use ipl\Stdlib\Loader\AutoloadingPluginLoader; +/** + * Register plugin loaders by type and resolve plugin class names + */ trait Plugins { - /** @var array Registered plugin loaders by type */ + /** @var array Registered plugin loaders by type */ protected array $pluginLoaders = []; /** @@ -75,7 +78,6 @@ public function addPluginLoader( public function loadPlugin(string $type, string $name): string|false { if ($this->hasPluginLoader($type)) { - /** @var PluginLoader $loader */ foreach ($this->pluginLoaders[$type] as $loader) { $class = $loader->load($name); if ($class) { @@ -87,6 +89,17 @@ public function loadPlugin(string $type, string $name): string|false return false; } + /** + * Add a default plugin loader for the given type + * + * Default loaders are appended after any loaders added via {@see addPluginLoader()}. + * + * @param string $type + * @param PluginLoader|string $loaderOrNamespace + * @param string $postfix + * + * @return $this + */ protected function addDefaultPluginLoader( string $type, PluginLoader|string $loaderOrNamespace, diff --git a/src/PriorityQueue.php b/src/PriorityQueue.php index 1c10736..d423cb3 100644 --- a/src/PriorityQueue.php +++ b/src/PriorityQueue.php @@ -14,13 +14,11 @@ */ class PriorityQueue extends SplPriorityQueue { - /** @var int */ + /** @var int Decreasing insertion counter for stable ordering at equal priorities */ protected int $serial = PHP_INT_MAX; /** - * Inserts an element in the queue by sifting it up. - * - * Maintains insertion order for items with the same priority. + * Insert an element in the queue, maintaining insertion order for equal priorities * * @param TValue $value * @param TPriority $priority @@ -35,11 +33,11 @@ public function insert(mixed $value, mixed $priority): true /** * Yield all items as priority-value pairs * - * @return Generator + * @return Generator */ public function yieldAll(): Generator { - // Clone queue because the SplPriorityQueue acts as a heap and thus items are removed upon iteration + // Clone the queue because SplPriorityQueue acts as a heap and removes items upon iteration. $queue = clone $this; $queue->setExtractFlags(static::EXTR_BOTH); diff --git a/src/Properties.php b/src/Properties.php index 015f0d3..01d659b 100644 --- a/src/Properties.php +++ b/src/Properties.php @@ -2,15 +2,16 @@ namespace ipl\Stdlib; +use Generator; use OutOfBoundsException; use Traversable; /** - * Trait for property access, mutation and array access. + * Trait for property access, mutation, and array access */ trait Properties { - /** @var array */ + /** @var array */ private array $properties = []; /** @@ -38,7 +39,7 @@ public function hasProperty(string $key): bool /** * Set the given properties * - * @param array $properties + * @param array $properties * * @return $this */ @@ -87,7 +88,7 @@ protected function setProperty(string $key, mixed $value): static /** * Iterate over all existing properties * - * @return Traversable + * @return Generator */ public function getIterator(): Traversable { @@ -125,6 +126,8 @@ public function offsetGet(mixed $offset): mixed * * @param mixed $offset * @param mixed $value + * + * @return void */ public function offsetSet(mixed $offset, mixed $value): void { @@ -135,6 +138,8 @@ public function offsetSet(mixed $offset, mixed $value): void * Unset the value for an offset * * @param mixed $offset + * + * @return void */ public function offsetUnset(mixed $offset): void { @@ -148,7 +153,7 @@ public function offsetUnset(mixed $offset): void * e.g. `$value = $object->property;`. * Do not call this method directly. * - * @param mixed $key + * @param string $key * * @return mixed */ @@ -166,6 +171,8 @@ public function __get(string $key): mixed * * @param string $key * @param mixed $value + * + * @return void */ public function __set(string $key, mixed $value): void { @@ -196,6 +203,8 @@ public function __isset(string $key): bool * Do not call this method directly. * * @param string $key + * + * @return void */ public function __unset(string $key): void { diff --git a/src/Seq.php b/src/Seq.php index acc303f..349e870 100644 --- a/src/Seq.php +++ b/src/Seq.php @@ -12,7 +12,7 @@ class Seq /** * Check if the traversable contains the given needle * - * @param iterable $traversable + * @param iterable $traversable * @param mixed $needle Might also be a closure * @param bool $caseSensitive Whether strings should be compared case-sensitive * @@ -26,12 +26,11 @@ public static function contains(iterable $traversable, mixed $needle, bool $case /** * Search in the traversable for the given needle and return its key and value * - * @param iterable $traversable + * @param iterable $traversable * @param mixed $needle Might also be a closure * @param bool $caseSensitive Whether strings should be compared case-sensitive * - * @return array An array with two entries, the first is the key, then the value. - * Both are null if nothing is found. + * @return array{0: mixed, 1: mixed} The found key and value, or [null, null] if nothing is found */ public static function find(iterable $traversable, mixed $needle, bool $caseSensitive = true): array { @@ -66,7 +65,7 @@ public static function find(iterable $traversable, mixed $needle, bool $caseSens /** * Search in the traversable for the given needle and return its key * - * @param iterable $traversable + * @param iterable $traversable * @param mixed $needle Might also be a closure * @param bool $caseSensitive Whether strings should be compared case-sensitive * @@ -80,7 +79,7 @@ public static function findKey(iterable $traversable, mixed $needle, bool $caseS /** * Search in the traversable for the given needle and return its value * - * @param iterable $traversable + * @param iterable $traversable * @param mixed $needle Might also be a closure * @param bool $caseSensitive Whether strings should be compared case-sensitive * diff --git a/src/functions.php b/src/functions.php index 5a829cb..adaa820 100644 --- a/src/functions.php +++ b/src/functions.php @@ -3,10 +3,9 @@ namespace ipl\Stdlib; use Generator; -use InvalidArgumentException; use IteratorIterator; -use Traversable; use stdClass; +use Traversable; /** * Detect and return the PHP type of the given subject @@ -27,7 +26,7 @@ function get_php_type(mixed $subject): string * * @param iterable|stdClass $subject * - * @return array + * @return array */ function arrayval(iterable|stdClass $subject): array { @@ -39,14 +38,14 @@ function arrayval(iterable|stdClass $subject): array return (array) $subject; } - // Works for generators too + // Also works for generators. return iterator_to_array($subject); } /** * Get the first key of an iterable * - * @param iterable $iterable + * @param iterable $iterable * * @return int|string|null The first key of the iterable if it is not empty, null otherwise */ @@ -62,9 +61,9 @@ function iterable_key_first(iterable $iterable): int|string|null /** * Get the first value of an iterable * - * @param iterable $iterable + * @param iterable $iterable * - * @return ?mixed + * @return mixed|null The first value of the iterable if it is not empty, null otherwise */ function iterable_value_first(iterable $iterable): mixed { @@ -84,7 +83,7 @@ function iterable_value_first(iterable $iterable): mixed * @param Traversable $traversable * @param callable(mixed $value, mixed $key): array{0: mixed, 1?: mixed, 2?: mixed} $groupBy * - * @return Generator + * @return Generator */ function yield_groups(Traversable $traversable, callable $groupBy): Generator { diff --git a/src/functions_include.php b/src/functions_include.php index 9a2dc6f..4eda869 100644 --- a/src/functions_include.php +++ b/src/functions_include.php @@ -1,6 +1,6 @@