From 8705f91c6d9e9ac95d50f336c5d362692afdac29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jan 2024 08:51:20 +0300 Subject: [PATCH 01/60] Update rector/rector requirement from ^0.18.3 to ^0.19.0 (#124) Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/0.18.3...0.19.0) --- updated-dependencies: - dependency-name: rector/rector dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index df553e2..d2a2537 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "require-dev": { "maglnet/composer-require-checker": "^4.2", "phpunit/phpunit": "^9.5", - "rector/rector": "^0.18.3", + "rector/rector": "^0.19.0", "roave/infection-static-analysis-plugin": "^1.16", "spatie/phpunit-watcher": "^1.23", "vimeo/psalm": "^4.30|^5.8" From 57e7c3e277f997c6bdde11dd01be6e51af1e4555 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 06:33:39 +0300 Subject: [PATCH 02/60] Update rector/rector requirement from ^0.19.0 to ^1.0.0 (#125) Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/0.19.0...1.0.0) --- updated-dependencies: - dependency-name: rector/rector dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d2a2537..8488caf 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "require-dev": { "maglnet/composer-require-checker": "^4.2", "phpunit/phpunit": "^9.5", - "rector/rector": "^0.19.0", + "rector/rector": "^1.0.0", "roave/infection-static-analysis-plugin": "^1.16", "spatie/phpunit-watcher": "^1.23", "vimeo/psalm": "^4.30|^5.8" From 1ffc2634139950bb848ea596e6384e5b414302f5 Mon Sep 17 00:00:00 2001 From: Ihor Sychevskyi Date: Sat, 17 Feb 2024 05:15:35 +0200 Subject: [PATCH 03/60] update irc link (#127) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8488caf..8f3564d 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "issues": "https://github.com/yiisoft/strings/issues", "forum": "https://www.yiiframework.com/forum/", "wiki": "https://www.yiiframework.com/wiki/", - "irc": "irc://irc.freenode.net/yii", + "irc": "ircs://irc.libera.chat:6697/yii", "chat": "https://t.me/yii3en" }, "require": { From da1a17892c729d7bc95e70621431f64aa1fb3f29 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 17 Mar 2024 13:49:04 +0300 Subject: [PATCH 04/60] Add more specific psalm type for result of `StringHelper::base64UrlEncode()` method (#128) --- CHANGELOG.md | 2 +- src/StringHelper.php | 4 ++++ tests/StringHelperTest.php | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8a47e5..4bdbaf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.4.1 under development -- no changes in this release. +- Enh #128: Add more specific psalm type for result of `StringHelper::base64UrlEncode()` method (@vjik) ## 2.4.0 December 22, 2023 diff --git a/src/StringHelper.php b/src/StringHelper.php index 75a1bf4..f395156 100644 --- a/src/StringHelper.php +++ b/src/StringHelper.php @@ -438,6 +438,10 @@ public static function uppercaseFirstCharacterInEachWord(string $string, string * @param string $input The string to encode. * * @return string Encoded string. + * + * @psalm-template T as string + * @psalm-param T $input + * @psalm-return (T is non-empty-string ? non-empty-string : "") */ public static function base64UrlEncode(string $input): string { diff --git a/tests/StringHelperTest.php b/tests/StringHelperTest.php index d412145..619a615 100644 --- a/tests/StringHelperTest.php +++ b/tests/StringHelperTest.php @@ -252,13 +252,14 @@ public function testBase64UrlDecode($output, $base64UrlEncoded): void $this->assertEquals($output, $decoded); } - public function base64UrlEncodedStringsProvider(): array + public static function base64UrlEncodedStringsProvider(): array { return [ 'Regular string' => ['This is an encoded string', 'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw=='], '? and _ characters' => ['subjects?_d=1', 'c3ViamVjdHM_X2Q9MQ=='], '> character' => ['subjects>_d=1', 'c3ViamVjdHM-X2Q9MQ=='], 'Unicode' => ['Это закодированная строка', '0K3RgtC-INC30LDQutC-0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw'], + 'empty string' => ['', ''], ]; } From de94166b27bbc6690ff8743f91ff3d5a018a76a9 Mon Sep 17 00:00:00 2001 From: Luiz Marin <67489841+luizcmarin@users.noreply.github.com> Date: Sat, 18 May 2024 01:36:01 -0300 Subject: [PATCH 05/60] Fix docs (#130) Co-authored-by: Sergei Predvoditelev --- README.md | 44 ++++++-------------------------------------- UPGRADE.md | 2 +- docs/internals.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 39 deletions(-) create mode 100644 docs/internals.md diff --git a/README.md b/README.md index e6ca88e..4f22737 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ The package provides: ## Installation +The package could be installed with [Composer](https://getcomposer.org): + ```shell composer require yiisoft/strings ``` @@ -241,46 +243,12 @@ if ($regexp->matches('second')) { } ``` -## Testing - -### Unit testing - -The package is tested with [PHPUnit](https://phpunit.de/). To run tests: - -```shell -./vendor/bin/phpunit -``` - -### Mutation testing - -The package tests are checked with [Infection](https://infection.github.io/) mutation framework with -[Infection Static Analysis Plugin](https://github.com/Roave/infection-static-analysis-plugin). To run it: - -```shell -./vendor/bin/roave-infection-static-analysis-plugin -``` - -### Static analysis - -The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis: - -```shell -./vendor/bin/psalm -``` - -### Code style - -Use [Rector](https://github.com/rectorphp/rector) to make codebase follow some specific rules or -use either newest or any specific version of PHP: - -```shell -./vendor/bin/rector -``` +## Documentation -### Dependencies +- [Internals](docs/internals.md) -Use [ComposerRequireChecker](https://github.com/maglnet/ComposerRequireChecker) to detect transitive -[Composer](https://getcomposer.org/) dependencies. +If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that. +You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). ## License diff --git a/UPGRADE.md b/UPGRADE.md index dddc9d8..07fc897 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,4 +1,4 @@ -# Upgrading Instructions +# Yii Strings Upgrading Instructions These notes highlight changes that could break your application when you upgrade package from one version to another. diff --git a/docs/internals.md b/docs/internals.md new file mode 100644 index 0000000..087a514 --- /dev/null +++ b/docs/internals.md @@ -0,0 +1,44 @@ +# Internals + +## Unit testing + +The package is tested with [PHPUnit](https://phpunit.de/). To run tests: + +```shell +./vendor/bin/phpunit +``` + +## Mutation testing + +The package tests are checked with [Infection](https://infection.github.io/) mutation framework with +[Infection Static Analysis Plugin](https://github.com/Roave/infection-static-analysis-plugin). To run it: + +```shell +./vendor/bin/roave-infection-static-analysis-plugin +``` + +## Static analysis + +The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis: + +```shell +./vendor/bin/psalm +``` + +## Code style + +Use [Rector](https://github.com/rectorphp/rector) to make codebase follow some specific rules or +use either newest or any specific version of PHP: + +```shell +./vendor/bin/rector +``` + +## Dependencies + +This package uses [composer-require-checker](https://github.com/maglnet/ComposerRequireChecker) to check if +all dependencies are correctly defined in `composer.json`. To run the checker, execute the following command: + +```shell +./vendor/bin/composer-require-checker +``` From 530048a8a9235d24e96613b141041231a2d0aa9d Mon Sep 17 00:00:00 2001 From: Luiz Marin <67489841+luizcmarin@users.noreply.github.com> Date: Wed, 22 May 2024 11:28:49 -0300 Subject: [PATCH 06/60] Fix docs (#131) --- README.md | 6 +++--- composer.json | 12 +++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4f22737..2888d73 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- + Yii

Yii Strings


@@ -21,7 +21,7 @@ The package provides: - `NumericHelper` that has static methods to work with numeric strings; - `Inflector` provides methods such as `toPlural()` or `toSlug()` that derive a new string based on the string given; - `WildcardPattern` is a shell wildcard pattern to match strings against; -- `CombinedRegexp` is a wrapper that optimizes multiple regular expressions matching and +- `CombinedRegexp` is a wrapper that optimizes multiple regular expressions matching and `MemoizedCombinedRegexp` is a decorator that caches results of `CombinedRegexp` to speed up matching. ## Requirements @@ -157,7 +157,7 @@ Overall the inflector has the following method groups. ## WildcardPattern usage -`WildcardPattern` allows a simple POSIX-style string matching. +`WildcardPattern` allows a simple POSIX-style string matching. ```php use \Yiisoft\Strings\WildcardPattern; diff --git a/composer.json b/composer.json index 8f3564d..1a29832 100644 --- a/composer.json +++ b/composer.json @@ -10,13 +10,23 @@ "homepage": "https://www.yiiframework.com/", "license": "BSD-3-Clause", "support": { + "issues": "https://github.com/yiisoft/strings/issues?state=open", "source": "https://github.com/yiisoft/strings", - "issues": "https://github.com/yiisoft/strings/issues", "forum": "https://www.yiiframework.com/forum/", "wiki": "https://www.yiiframework.com/wiki/", "irc": "ircs://irc.libera.chat:6697/yii", "chat": "https://t.me/yii3en" }, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/yiisoft" + }, + { + "type": "github", + "url": "https://github.com/sponsors/yiisoft" + } + ], "require": { "php": "^8.0", "ext-mbstring": "*" From 47a3675d817d98612aeec5083d73a184ecd5239c Mon Sep 17 00:00:00 2001 From: Luiz Marin <67489841+luizcmarin@users.noreply.github.com> Date: Fri, 24 May 2024 10:48:48 -0300 Subject: [PATCH 07/60] Update UPGRADE.md (#132) --- UPGRADE.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 07fc897..e7cb567 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,12 +1,11 @@ -# Yii Strings Upgrading Instructions +# Upgrading Instructions for Yii Strings -These notes highlight changes that could break your application when you upgrade package from one version to another. +This file contains the upgrade notes. These notes highlight changes that could break your +application when you upgrade the package from one version to another. -Upgrading in general is as simple as updating your dependency in your `composer.json` and running `composer update`. -In a big application however there may be more things to consider, which are explained in the following. - -> Note: The following upgrading instructions are cumulative. That is, if you want to upgrade from a version A to version -> C and there is a version B between A and C, you need to follow the instructions for both A and B. +> **Important!** The following upgrading instructions are cumulative. That is, if you want +> to upgrade from version A to version C and there is version B between A and C, you need +> to following the instructions for both A and B. ## Upgrade from 1.2.0 From a7217c82ba4a2e39d4773b4fa6ef040fcb0f55b0 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 26 Aug 2024 21:21:10 +0300 Subject: [PATCH 08/60] Fix readme badges (#133) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2888d73..5f05113 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@

-[![Latest Stable Version](https://poser.pugx.org/yiisoft/strings/v/stable.png)](https://packagist.org/packages/yiisoft/strings) -[![Total Downloads](https://poser.pugx.org/yiisoft/strings/downloads.png)](https://packagist.org/packages/yiisoft/strings) -[![Build Status](https://github.com/yiisoft/strings/workflows/build/badge.svg)](https://github.com/yiisoft/strings/actions) +[![Latest Stable Version](https://poser.pugx.org/yiisoft/strings/v)](https://packagist.org/packages/yiisoft/strings) +[![Total Downloads](https://poser.pugx.org/yiisoft/strings/downloads)](https://packagist.org/packages/yiisoft/strings) +[![Build status](https://github.com/yiisoft/strings/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/strings/actions/workflows/build.yml) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/yiisoft/strings/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/strings/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/strings/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/strings/?branch=master) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fstrings%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/strings/master) From e7a06ae40a8bb9499384b3fd9c2496562fe1d3b1 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 26 Aug 2024 21:27:14 +0300 Subject: [PATCH 09/60] Remove scrutinizer (#134) --- .gitattributes | 1 - .scrutinizer.yml | 35 ----------------------------------- README.md | 3 +-- 3 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 .scrutinizer.yml diff --git a/.gitattributes b/.gitattributes index 5583f36..9f9e9af 100644 --- a/.gitattributes +++ b/.gitattributes @@ -26,7 +26,6 @@ /.editorconfig export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/.scrutinizer.yml export-ignore /phpunit.xml.dist export-ignore /tests export-ignore /docs export-ignore diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 61fb502..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,35 +0,0 @@ -checks: - php: true - -filter: - paths: - - src/ - -build: - image: default-bionic - - environment: - php: - version: 8.0.18 - ini: - xdebug.mode: coverage - - nodes: - analysis: - tests: - override: - - php-scrutinizer-run - - phpunit: - dependencies: - override: - - composer self-update - - composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - - tests: - override: - - command: ./vendor/bin/phpunit --coverage-clover ./coverage.xml - on_node: 1 - coverage: - file: coverage.xml - format: php-clover diff --git a/README.md b/README.md index 5f05113..aafb961 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ [![Latest Stable Version](https://poser.pugx.org/yiisoft/strings/v)](https://packagist.org/packages/yiisoft/strings) [![Total Downloads](https://poser.pugx.org/yiisoft/strings/downloads)](https://packagist.org/packages/yiisoft/strings) [![Build status](https://github.com/yiisoft/strings/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/strings/actions/workflows/build.yml) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/yiisoft/strings/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/strings/?branch=master) -[![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/strings/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/strings/?branch=master) +[![Code coverage](https://codecov.io/gh/yiisoft/strings/graph/badge.svg?token=GEPMBAHNCX)](https://codecov.io/gh/yiisoft/strings) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fstrings%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/strings/master) [![static analysis](https://github.com/yiisoft/strings/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/strings/actions?query=workflow%3A%22static+analysis%22) [![type-coverage](https://shepherd.dev/github/yiisoft/strings/coverage.svg)](https://shepherd.dev/github/yiisoft/strings) From fccb0ef70340630546af7f6156eaffad1c3a5d5b Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 26 Aug 2024 21:30:23 +0300 Subject: [PATCH 10/60] Fix psalm errors (#135) --- composer.json | 2 +- src/AbstractCombinedRegexp.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1a29832..a53433c 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "rector/rector": "^1.0.0", "roave/infection-static-analysis-plugin": "^1.16", "spatie/phpunit-watcher": "^1.23", - "vimeo/psalm": "^4.30|^5.8" + "vimeo/psalm": "^4.30|^5.25" }, "autoload": { "psr-4": { diff --git a/src/AbstractCombinedRegexp.php b/src/AbstractCombinedRegexp.php index e40c91f..169172c 100644 --- a/src/AbstractCombinedRegexp.php +++ b/src/AbstractCombinedRegexp.php @@ -15,7 +15,14 @@ */ abstract class AbstractCombinedRegexp { + /** + * @psalm-suppress MissingClassConstType + */ public const REGEXP_DELIMITER = '/'; + + /** + * @psalm-suppress MissingClassConstType + */ public const QUOTE_REPLACER = '\\/'; /** From 0e09941fe1de00c7521a9e04e689018c64d4cfdb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:51:00 +0300 Subject: [PATCH 11/60] Update rector/rector requirement from ^1.0.0 to ^2.0.3 (#136) Co-authored-by: Sergei Predvoditelev --- composer.json | 2 +- rector.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/composer.json b/composer.json index a53433c..e16642e 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "require-dev": { "maglnet/composer-require-checker": "^4.2", "phpunit/phpunit": "^9.5", - "rector/rector": "^1.0.0", + "rector/rector": "^2.0.3", "roave/infection-static-analysis-plugin": "^1.16", "spatie/phpunit-watcher": "^1.23", "vimeo/psalm": "^4.30|^5.25" diff --git a/rector.php b/rector.php index c80d86e..f55daae 100644 --- a/rector.php +++ b/rector.php @@ -4,7 +4,6 @@ use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector; use Rector\Config\RectorConfig; -use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector; use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector; use Rector\Set\ValueObject\LevelSetList; @@ -24,6 +23,5 @@ $rectorConfig->skip([ ClosureToArrowFunctionRector::class, - JsonThrowOnErrorRector::class, ]); }; From 3230b19fb2c95777ee82b31088c64c6c23dd4a6f Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 19 Jan 2025 11:22:39 +0300 Subject: [PATCH 12/60] Add `StringHelper::matchAnyRegex()` (#137) --- CHANGELOG.md | 1 + README.md | 1 + src/StringHelper.php | 21 +++++++++++ tests/StringHelper/MatchAnyRegexTest.php | 46 ++++++++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 tests/StringHelper/MatchAnyRegexTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bdbaf3..7f78b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.4.1 under development +- New #137: Add `StringHelper::matchAnyRegex()` method as a facade for `CombinedRegexp` (@vjik) - Enh #128: Add more specific psalm type for result of `StringHelper::base64UrlEncode()` method (@vjik) ## 2.4.0 December 22, 2023 diff --git a/README.md b/README.md index aafb961..df04325 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ Overall the helper has the following method groups. ### Other +- matchAnyRegex - parsePath - split diff --git a/src/StringHelper.php b/src/StringHelper.php index f395156..0bf4497 100644 --- a/src/StringHelper.php +++ b/src/StringHelper.php @@ -729,6 +729,27 @@ public static function findBetweenLast(string $string, string $start, ?string $e return mb_substr($string, $startPos, $endPos - $startPos); } + /** + * Checks if a given string matches any of the provided patterns. + * + * Note that patterns should be provided without delimiters on both sides. For example, `te(s|x)t`. + * + * @see https://www.php.net/manual/reference.pcre.pattern.syntax.php + * @see https://www.php.net/manual/reference.pcre.pattern.modifiers.php + * + * @param string $string The string to match against the patterns. + * @param string[] $patterns Regular expressions without delimiters on both sides. + * @param string $flags Flags to apply to all regular expressions. + */ + public static function matchAnyRegex(string $string, array $patterns, string $flags = ''): bool + { + if (empty($patterns)) { + return false; + } + + return (new CombinedRegexp($patterns, $flags))->matches($string); + } + /** * Ensure the input string is a valid UTF-8 string. * diff --git a/tests/StringHelper/MatchAnyRegexTest.php b/tests/StringHelper/MatchAnyRegexTest.php new file mode 100644 index 0000000..a7df6a3 --- /dev/null +++ b/tests/StringHelper/MatchAnyRegexTest.php @@ -0,0 +1,46 @@ +assertSame($expected, $result); + } + + /** + * @testWith [true, "test"] + * [false, "hello"] + */ + public function testWithoutFlags(bool $expected, string $string): void + { + $result = StringHelper::matchAnyRegex($string, ['te[sx]t', 'm(o|a)n']); + $this->assertSame($expected, $result); + } + + public function testWithoutPatterns(): void + { + $result = StringHelper::matchAnyRegex('test', []); + $this->assertFalse($result); + } +} From 5063d70af8b59907261c66a57349ef2c20f5cf2e Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 19 Jan 2025 13:44:53 +0000 Subject: [PATCH 13/60] Release version 2.5.0 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f78b7b..2158561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # Yii Strings Change Log -## 2.4.1 under development +## 2.5.0 January 19, 2025 - New #137: Add `StringHelper::matchAnyRegex()` method as a facade for `CombinedRegexp` (@vjik) -- Enh #128: Add more specific psalm type for result of `StringHelper::base64UrlEncode()` method (@vjik) +- Enh #128: Add more specific psalm type for result of `StringHelper::base64UrlEncode()` method (@vjik) ## 2.4.0 December 22, 2023 From ae45a5cf5c63d3c864d028456d4e3190fd1e9f71 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 19 Jan 2025 13:44:56 +0000 Subject: [PATCH 14/60] Prepare for next release --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2158561..d780445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Yii Strings Change Log +## 2.5.1 under development + +- no changes in this release. + ## 2.5.0 January 19, 2025 - New #137: Add `StringHelper::matchAnyRegex()` method as a facade for `CombinedRegexp` (@vjik) From 2bf12f1bd5eb8b67cd9cdddac1ba438808261d16 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 31 Jan 2025 15:41:46 +0300 Subject: [PATCH 15/60] Improve Rector CI action (#139) --- .github/workflows/rector.yml | 3 ++- composer.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 35411d0..457772a 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -1,5 +1,5 @@ on: - pull_request: + pull_request_target: paths-ignore: - 'docs/**' - 'README.md' @@ -17,6 +17,7 @@ jobs: secrets: token: ${{ secrets.YIISOFT_GITHUB_TOKEN }} with: + repository: ${{ github.event.pull_request.head.repo.full_name }} os: >- ['ubuntu-latest'] php: >- diff --git a/composer.json b/composer.json index e16642e..dbb37b5 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "require-dev": { "maglnet/composer-require-checker": "^4.2", "phpunit/phpunit": "^9.5", - "rector/rector": "^2.0.3", + "rector/rector": "^2.0.7", "roave/infection-static-analysis-plugin": "^1.16", "spatie/phpunit-watcher": "^1.23", "vimeo/psalm": "^4.30|^5.25" From eff507ea0c76d7e4fc08f547ec78df421e86dbc4 Mon Sep 17 00:00:00 2001 From: ferrum_fist Date: Sun, 2 Feb 2025 20:55:30 +0300 Subject: [PATCH 16/60] Explicitly mark nullable parameters --- CHANGELOG.md | 2 +- src/StringHelper.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d780445..d3537f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.5.1 under development -- no changes in this release. +- Bug #138: Explicitly mark nullable parameters (@ferrumfist) ## 2.5.0 January 19, 2025 diff --git a/src/StringHelper.php b/src/StringHelper.php index 0bf4497..fc1a1b2 100644 --- a/src/StringHelper.php +++ b/src/StringHelper.php @@ -67,7 +67,7 @@ public static function byteLength(string|null $input): int * * @see https://www.php.net/manual/en/function.substr.php */ - public static function byteSubstring(string $input, int $start, int $length = null): string + public static function byteSubstring(string $input, int $start, ?int $length = null): string { return mb_substr($input, $start, $length ?? mb_strlen($input, '8bit'), '8bit'); } @@ -133,7 +133,7 @@ public static function directoryName(string $path): string * * @see https://php.net/manual/en/function.mb-substr.php */ - public static function substring(string $string, int $start, int $length = null, string $encoding = 'UTF-8'): string + public static function substring(string $string, int $start, ?int $length = null, string $encoding = 'UTF-8'): string { return mb_substr($string, $start, $length, $encoding); } From 4cdcc85b2f94b29a11da8bde579d645ef1cf4f46 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 6 Feb 2025 20:46:04 +0300 Subject: [PATCH 17/60] Bump minimal required PHP version to 8.1 (#140) --- .github/workflows/build.yml | 2 +- .../workflows/composer-require-checker.yml | 2 +- .github/workflows/static.yml | 2 +- .gitignore | 8 +- CHANGELOG.md | 1 + README.md | 2 +- composer.json | 13 +- phpunit.xml.dist | 28 ++-- rector.php | 25 ++-- src/CombinedRegexp.php | 4 +- src/MemoizedCombinedRegexp.php | 2 +- ...php => AbstractCombinedRegexpTestCase.php} | 19 +-- tests/CombinedRegexpTest.php | 2 +- tests/InflectorTest.php | 19 +-- tests/IsOneOfAssert.php | 2 +- tests/MemoizedCombinedRegexpTest.php | 2 +- tests/NumericHelperTest.php | 13 +- tests/StringHelper/MatchAnyRegexTest.php | 5 +- tests/StringHelperTest.php | 120 ++++++------------ tests/Support/StringableObject.php | 2 +- tests/WildcardPatternTest.php | 25 ++-- 21 files changed, 115 insertions(+), 183 deletions(-) rename tests/{AbstractCombinedRegexpTest.php => AbstractCombinedRegexpTestCase.php} (95%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f980c7f..3feaeef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,4 +29,4 @@ jobs: os: >- ['ubuntu-latest', 'windows-latest'] php: >- - ['8.0', '8.1', '8.2', '8.3'] + ['8.1', '8.2', '8.3'] diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index 5473ec9..a857bce 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -31,4 +31,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.0', '8.1', '8.2', '8.3'] + ['8.1', '8.2', '8.3'] diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 8694d2d..e33eca8 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -29,4 +29,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.0', '8.1', '8.2', '8.3'] + ['8.1', '8.2', '8.3'] diff --git a/.gitignore b/.gitignore index 56da114..5d023ef 100644 --- a/.gitignore +++ b/.gitignore @@ -23,9 +23,7 @@ composer.phar # Mac DS_Store Files .DS_Store -# phpunit itself is not needed -phpunit.phar -# local phpunit config +# PHPUnit +/phpunit.phar /phpunit.xml -# phpunit cache -.phpunit.result.cache +/.phpunit.cache diff --git a/CHANGELOG.md b/CHANGELOG.md index d3537f1..4f201fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2.5.1 under development - Bug #138: Explicitly mark nullable parameters (@ferrumfist) +- Chg #140: Bump minimal required PHP version to 8.1 and minor refactoring (@vjik) ## 2.5.0 January 19, 2025 diff --git a/README.md b/README.md index df04325..f9dad8f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The package provides: ## Requirements -- PHP 8.0 or higher. +- PHP 8.1 or higher. - `mbstring` PHP extension. ## Installation diff --git a/composer.json b/composer.json index dbb37b5..a0d8b45 100644 --- a/composer.json +++ b/composer.json @@ -28,16 +28,16 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "ext-mbstring": "*" }, "require-dev": { - "maglnet/composer-require-checker": "^4.2", - "phpunit/phpunit": "^9.5", + "maglnet/composer-require-checker": "^4.7.1", + "phpunit/phpunit": "^10.5.44", "rector/rector": "^2.0.7", - "roave/infection-static-analysis-plugin": "^1.16", - "spatie/phpunit-watcher": "^1.23", - "vimeo/psalm": "^4.30|^5.25" + "roave/infection-static-analysis-plugin": "^1.35", + "spatie/phpunit-watcher": "^1.24", + "vimeo/psalm": "^5.26.1" }, "autoload": { "psr-4": { @@ -51,6 +51,7 @@ }, "config": { "sort-packages": true, + "bump-after-update": "dev", "allow-plugins": { "infection/extension-installer": true, "composer/package-versions-deprecated": true diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 14acb8c..98b0b95 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,18 +1,20 @@ - - + colors="true" + displayDetailsOnPhpunitDeprecations="true" +> - + @@ -21,9 +23,9 @@ - + - ./src + ./src - + diff --git a/rector.php b/rector.php index f55daae..1f8a952 100644 --- a/rector.php +++ b/rector.php @@ -5,23 +5,18 @@ use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector; use Rector\Config\RectorConfig; use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector; -use Rector\Set\ValueObject\LevelSetList; +use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; -return static function (RectorConfig $rectorConfig): void { - $rectorConfig->paths([ +return RectorConfig::configure() + ->withPaths([ __DIR__ . '/src', __DIR__ . '/tests', - ]); - - // register a single rule - $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); - - // define sets of rules - $rectorConfig->sets([ - LevelSetList::UP_TO_PHP_80, - ]); - - $rectorConfig->skip([ + ]) + ->withPhpSets(php81: true) + ->withRules([ + InlineConstructorDefaultToPropertyRector::class, + ]) + ->withSkip([ ClosureToArrowFunctionRector::class, + NullToStrictStringFuncCallArgRector::class, ]); -}; diff --git a/src/CombinedRegexp.php b/src/CombinedRegexp.php index cba76d2..81fc72c 100644 --- a/src/CombinedRegexp.php +++ b/src/CombinedRegexp.php @@ -28,7 +28,7 @@ final class CombinedRegexp extends AbstractCombinedRegexp /** * @psalm-var non-empty-string */ - private string $compiledPattern; + private readonly string $compiledPattern; /** * @param string[] $patterns Regular expressions to combine. @@ -36,7 +36,7 @@ final class CombinedRegexp extends AbstractCombinedRegexp */ public function __construct( array $patterns, - private string $flags = '' + private readonly string $flags = '' ) { if (empty($patterns)) { throw new InvalidArgumentException('At least one pattern should be specified.'); diff --git a/src/MemoizedCombinedRegexp.php b/src/MemoizedCombinedRegexp.php index 1e49245..f366462 100644 --- a/src/MemoizedCombinedRegexp.php +++ b/src/MemoizedCombinedRegexp.php @@ -18,7 +18,7 @@ final class MemoizedCombinedRegexp extends AbstractCombinedRegexp private array $results = []; public function __construct( - private AbstractCombinedRegexp $decorated, + private readonly AbstractCombinedRegexp $decorated, ) { } diff --git a/tests/AbstractCombinedRegexpTest.php b/tests/AbstractCombinedRegexpTestCase.php similarity index 95% rename from tests/AbstractCombinedRegexpTest.php rename to tests/AbstractCombinedRegexpTestCase.php index 178d075..bf06246 100644 --- a/tests/AbstractCombinedRegexpTest.php +++ b/tests/AbstractCombinedRegexpTestCase.php @@ -4,14 +4,13 @@ namespace Yiisoft\Strings\Tests; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\AbstractCombinedRegexp; -abstract class AbstractCombinedRegexpTest extends TestCase +abstract class AbstractCombinedRegexpTestCase extends TestCase { - /** - * @dataProvider dataMatchAny - */ + #[DataProvider('dataMatchAny')] public function testMatchAny(array $patterns, string $string, bool $expectedResult): void { $regexp = $this->createCombinedRegexp($patterns); @@ -106,9 +105,7 @@ public static function dataMatchAny(): iterable ]; } - /** - * @dataProvider dataMatchingPattern - */ + #[DataProvider('dataMatchingPattern')] public function testMatchingPattern(array $patterns, string $string, string $expectedResult): void { $regexp = $this->createCombinedRegexp($patterns); @@ -153,9 +150,7 @@ public static function dataMatchingPattern(): iterable ]; } - /** - * @dataProvider dataMatchingPatternPosition - */ + #[DataProvider('dataMatchingPatternPosition')] public function testMatchingPatternPosition(array $patterns, string $string, int $expectedResult): void { $regexp = $this->createCombinedRegexp($patterns); @@ -200,9 +195,7 @@ public static function dataMatchingPatternPosition(): iterable ]; } - /** - * @dataProvider dataMatchDifferentDelimiters - */ + #[DataProvider('dataMatchDifferentDelimiters')] public function testMatchDifferentDelimiters( array $patterns, string $flags, diff --git a/tests/CombinedRegexpTest.php b/tests/CombinedRegexpTest.php index 7a59dd2..aa9a4ed 100644 --- a/tests/CombinedRegexpTest.php +++ b/tests/CombinedRegexpTest.php @@ -7,7 +7,7 @@ use Yiisoft\Strings\AbstractCombinedRegexp; use Yiisoft\Strings\CombinedRegexp; -final class CombinedRegexpTest extends AbstractCombinedRegexpTest +final class CombinedRegexpTest extends AbstractCombinedRegexpTestCase { protected function createCombinedRegexp(array $patterns, string $flags = ''): AbstractCombinedRegexp { diff --git a/tests/InflectorTest.php b/tests/InflectorTest.php index d068126..08b6e4e 100644 --- a/tests/InflectorTest.php +++ b/tests/InflectorTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Strings\Tests; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\Inflector; @@ -135,9 +136,7 @@ public function testToWords(): void $this->assertEquals('і це дійсно так!', $inflector->toWords('ІЦеДійсноТак!')); } - /** - * @dataProvider pascalCaseToIdProvider() - */ + #[DataProvider('pascalCaseToIdProvider')] public function testPascalCaseToId(string $expectedResult, array $arguments): void { $inflector = new Inflector(); @@ -162,7 +161,7 @@ public function testToCamelCase(): void $this->assertEquals('ひらがなHepimiz', $inflector->toCamelCase('ひらがな_hepimiz')); } - public function dataToSnakeCase(): array + public static function dataToSnakeCase(): array { return [ [['input' => 'userName'], 'user_name'], @@ -176,9 +175,7 @@ public function dataToSnakeCase(): array ]; } - /** - * @dataProvider dataToSnakeCase - */ + #[DataProvider('dataToSnakeCase')] public function testToSnakeCase(array $arguments, string $expectedOutput): void { $inflector = new Inflector(); @@ -193,7 +190,7 @@ public function testToTable(): void $this->assertEquals('customer_tables', $inflector->classToTable('customerTable')); } - public function toSlugCommonsDataProvider(): array + public static function toSlugCommonsDataProvider(): array { return [ ['', ''], @@ -216,9 +213,7 @@ public function toSlugCommonsDataProvider(): array ]; } - /** - * @dataProvider toSlugCommonsDataProvider - */ + #[DataProvider('toSlugCommonsDataProvider')] public function testToSlugCommons(string $input, string $expected, string $replacement = '-'): void { $inflector = new Inflector(); @@ -489,7 +484,7 @@ private function assertIsOneOf(mixed $actual, array $expected, $message = ''): v self::assertThat($actual, new IsOneOfAssert($expected), $message); } - public function pascalCaseToIdProvider(): array + public static function pascalCaseToIdProvider(): array { return [ ['photo\\album-controller', ['Photo\\AlbumController', '-', false]], diff --git a/tests/IsOneOfAssert.php b/tests/IsOneOfAssert.php index 700c598..429245a 100644 --- a/tests/IsOneOfAssert.php +++ b/tests/IsOneOfAssert.php @@ -11,7 +11,7 @@ */ final class IsOneOfAssert extends Constraint { - public function __construct(private array $allowedValues) + public function __construct(private readonly array $allowedValues) { } diff --git a/tests/MemoizedCombinedRegexpTest.php b/tests/MemoizedCombinedRegexpTest.php index 138e385..e406f0e 100644 --- a/tests/MemoizedCombinedRegexpTest.php +++ b/tests/MemoizedCombinedRegexpTest.php @@ -8,7 +8,7 @@ use Yiisoft\Strings\CombinedRegexp; use Yiisoft\Strings\MemoizedCombinedRegexp; -final class MemoizedCombinedRegexpTest extends AbstractCombinedRegexpTest +final class MemoizedCombinedRegexpTest extends AbstractCombinedRegexpTestCase { protected function createCombinedRegexp(array $patterns, string $flags = ''): AbstractCombinedRegexp { diff --git a/tests/NumericHelperTest.php b/tests/NumericHelperTest.php index 2cc509e..5291a4e 100644 --- a/tests/NumericHelperTest.php +++ b/tests/NumericHelperTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Strings\Tests; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\NumericHelper; use Yiisoft\Strings\Tests\Support\StringableObject; @@ -38,7 +39,7 @@ public function testToOrdinalWithIncorrectType(): void NumericHelper::toOrdinal('bla-bla'); } - public function dataNormalize(): array + public static function dataNormalize(): array { return [ 'French' => ['4 294 967 295,000', '4294967295.000'], @@ -54,9 +55,7 @@ public function dataNormalize(): array ]; } - /** - * @dataProvider dataNormalize - */ + #[DataProvider('dataNormalize')] public function testNormalize(mixed $input, string $expected): void { $this->assertSame($expected, NumericHelper::normalize($input)); @@ -68,7 +67,7 @@ public function testNormalizeWithIncorrectType(): void NumericHelper::normalize([]); } - public function dataIsInteger(): array + public static function dataIsInteger(): array { return [ [new \stdClass(), false], @@ -83,9 +82,7 @@ public function dataIsInteger(): array ]; } - /** - * @dataProvider dataIsInteger - */ + #[DataProvider('dataIsInteger')] public function testIsInteger(mixed $value, bool $expected): void { $this->assertSame($expected, NumericHelper::isInteger($value)); diff --git a/tests/StringHelper/MatchAnyRegexTest.php b/tests/StringHelper/MatchAnyRegexTest.php index a7df6a3..5be92a3 100644 --- a/tests/StringHelper/MatchAnyRegexTest.php +++ b/tests/StringHelper/MatchAnyRegexTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Strings\Tests\StringHelper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\StringHelper; @@ -19,9 +20,7 @@ public static function dataBase(): array ]; } - /** - * @dataProvider dataBase - */ + #[DataProvider('dataBase')] public function testBase(bool $expected, string $string, array $patterns, string $flags = ''): void { $result = StringHelper::matchAnyRegex($string, $patterns, $flags); diff --git a/tests/StringHelperTest.php b/tests/StringHelperTest.php index 619a615..f6f65b1 100644 --- a/tests/StringHelperTest.php +++ b/tests/StringHelperTest.php @@ -5,6 +5,7 @@ namespace Yiisoft\Strings\Tests; use InvalidArgumentException; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\StringHelper; @@ -118,11 +119,7 @@ public function testTruncateWords(): void $this->assertEquals('это строка с неожиданными…', StringHelper::truncateWords(' это строка с неожиданными пробелами ', 4)); } - /** - * @dataProvider providerStartsWith - * - * @param string|null $with - */ + #[DataProvider('providerStartsWith')] public function testStartsWith(bool $result, string $string, ?string $with): void { $this->assertSame($result, StringHelper::startsWith($string, $with)); @@ -131,7 +128,7 @@ public function testStartsWith(bool $result, string $string, ?string $with): voi /** * Rules that should work the same for case-sensitive and case-insensitive `startsWith()`. */ - public function providerStartsWith(): array + public static function providerStartsWith(): array { return [ // positive check @@ -170,11 +167,7 @@ public function testStartsWithIgnoringCase(): void $this->assertTrue(StringHelper::startsWithIgnoringCase('anything', null)); } - /** - * @dataProvider providerEndsWith - * - * @param string|null $with - */ + #[DataProvider('providerEndsWith')] public function testEndsWith(bool $result, string $string, ?string $with): void { // case sensitive version check @@ -184,7 +177,7 @@ public function testEndsWith(bool $result, string $string, ?string $with): void /** * Rules that should work the same for case-sensitive and case-insensitive `endsWith()`. */ - public function providerEndsWith(): array + public static function providerEndsWith(): array { return [ // positive check @@ -231,21 +224,14 @@ public function testCountWords(): void $this->assertEquals(1, StringHelper::countWords(' слово ')); } - /** - * @dataProvider base64UrlEncodedStringsProvider - */ + #[DataProvider('base64UrlEncodedStringsProvider')] public function testBase64UrlEncode(string $input, string $base64UrlEncoded): void { $encoded = StringHelper::base64UrlEncode($input); $this->assertEquals($base64UrlEncoded, $encoded); } - /** - * @dataProvider base64UrlEncodedStringsProvider - * - * @param $output - * @param $base64UrlEncoded - */ + #[DataProvider('base64UrlEncodedStringsProvider')] public function testBase64UrlDecode($output, $base64UrlEncoded): void { $decoded = StringHelper::base64UrlDecode($base64UrlEncoded); @@ -263,7 +249,7 @@ public static function base64UrlEncodedStringsProvider(): array ]; } - public function uppercaseFirstCharacterProvider(): array + public static function uppercaseFirstCharacterProvider(): array { return [ ['foo', 'Foo'], @@ -274,15 +260,13 @@ public function uppercaseFirstCharacterProvider(): array ]; } - /** - * @dataProvider uppercaseFirstCharacterProvider - */ + #[DataProvider('uppercaseFirstCharacterProvider')] public function testUppercaseFirstCharacter(string $string, string $expectedResult): void { $this->assertSame($expectedResult, StringHelper::uppercaseFirstCharacter($string)); } - public function uppercaseFirstCharacterInEachWordProvider(): array + public static function uppercaseFirstCharacterInEachWordProvider(): array { return [ 'Single word' => ['foo', 'Foo'], @@ -293,9 +277,7 @@ public function uppercaseFirstCharacterInEachWordProvider(): array ]; } - /** - * @dataProvider uppercaseFirstCharacterInEachWordProvider - */ + #[DataProvider('uppercaseFirstCharacterInEachWordProvider')] public function testUppercaseFirstCharacterInEachWord(string $string, string $expectedResult): void { $this->assertSame($expectedResult, StringHelper::uppercaseFirstCharacterInEachWord($string)); @@ -348,7 +330,7 @@ public function testLength(): void /** * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/substr_replace.phpt */ - public function replaceSubstringProvider(): array + public static function replaceSubstringProvider(): array { return [ ['trbala ', ['try this', 'bala ', 2]], @@ -361,15 +343,13 @@ public function replaceSubstringProvider(): array ]; } - /** - * @dataProvider replaceSubstringProvider - */ + #[DataProvider('replaceSubstringProvider')] public function testReplaceSubstring(string $expected, array $arguments): void { $this->assertSame($expected, StringHelper::replaceSubstring(...$arguments)); } - public function dataSplit(): array + public static function dataSplit(): array { return [ ['', []], @@ -411,9 +391,7 @@ public function dataSplit(): array ]; } - /** - * @dataProvider dataSplit - */ + #[DataProvider('dataSplit')] public function testSplit(string $string, array $expected): void { $this->assertSame($expected, StringHelper::split($string)); @@ -424,7 +402,7 @@ public function testSplitWithSeparator(): void $this->assertSame(['A', 'B', 'C'], StringHelper::split(' A 2 B3C', '\d')); } - public function dataParsePath(): array + public static function dataParsePath(): array { return [ ['key1.key2.key3', '.', '\\', false, ['key1', 'key2', 'key3']], @@ -470,9 +448,7 @@ public function dataParsePath(): array ]; } - /** - * @dataProvider dataParsePath - */ + #[DataProvider('dataParsePath')] public function testParsePath( string $path, string $delimiter, @@ -508,7 +484,7 @@ public function testParsePathWithDelimiterEqualsEscapeCharacter(): void StringHelper::parsePath('key1.key2.key3', '.', '.'); } - public function dataInvariantTrim(): iterable + public static function dataInvariantTrim(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -526,7 +502,7 @@ public function dataInvariantTrim(): iterable ]; } - public function dataTrim(): iterable + public static function dataTrim(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -552,7 +528,7 @@ public function dataTrim(): iterable ]; } - public function dataLtrim(): iterable + public static function dataLtrim(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -578,7 +554,7 @@ public function dataLtrim(): iterable ]; } - public function dataRtrim(): iterable + public static function dataRtrim(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -600,7 +576,7 @@ public function dataRtrim(): iterable ]; } - public function dataTrimPattern(): iterable + public static function dataTrimPattern(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -641,7 +617,7 @@ public function dataTrimPattern(): iterable ]; } - public function dataLtrimPattern(): iterable + public static function dataLtrimPattern(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -667,7 +643,7 @@ public function dataLtrimPattern(): iterable ]; } - public function dataRtrimPattern(): iterable + public static function dataRtrimPattern(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -708,52 +684,40 @@ public function dataRtrimPattern(): iterable ]; } - /** - * @dataProvider dataInvariantTrim - * @dataProvider dataTrim - */ + #[DataProvider('dataTrim')] + #[DataProvider('dataInvariantTrim')] public function testTrim(string|array $string, string|array $expected): void { $this->assertSame($expected, StringHelper::trim($string)); } - /** - * @dataProvider dataInvariantTrim - * @dataProvider dataLtrim - */ + #[DataProvider('dataLtrim')] + #[DataProvider('dataInvariantTrim')] public function testLtrim(string|array $string, string|array $expected): void { $this->assertSame($expected, StringHelper::ltrim($string)); } - /** - * @dataProvider dataInvariantTrim - * @dataProvider dataRtrim - */ + #[DataProvider('dataRtrim')] + #[DataProvider('dataInvariantTrim')] public function testRtrim(string|array $string, string|array $expected): void { $this->assertSame($expected, StringHelper::rtrim($string)); } - /** - * @dataProvider dataTrimPattern - */ + #[DataProvider('dataTrimPattern')] public function testTrimPattern(string|array $string, string $pattern, string|array $expected): void { $this->assertSame($expected, StringHelper::trim($string, $pattern)); } - /** - * @dataProvider dataLtrimPattern - */ + #[DataProvider('dataLtrimPattern')] public function testLtrimPattern(string|array $string, string $pattern, string|array $expected): void { $this->assertSame($expected, StringHelper::ltrim($string, $pattern)); } - /** - * @dataProvider dataRtrimPattern - */ + #[DataProvider('dataRtrimPattern')] public function testRtrimPattern(string|array $string, string $pattern, string|array $expected): void { $this->assertSame($expected, StringHelper::rtrim($string, $pattern)); @@ -766,15 +730,13 @@ public function testInvalidTrimPattern(): void StringHelper::trim('string', "\xC3\x28"); } - /** - * @dataProvider dataProviderFindBetween - */ + #[DataProvider('dataProviderFindBetween')] public function testFindBetween(string $string, string $start, ?string $end, ?string $expectedResult): void { $this->assertSame($expectedResult, StringHelper::findBetween($string, $start, $end)); } - public function dataProviderFindBetween(): array + public static function dataProviderFindBetween(): array { return [ ['hello world hello', ' hello', ' world', null], // end before start @@ -794,15 +756,13 @@ public function dataProviderFindBetween(): array ]; } - /** - * @dataProvider dataProviderFindBetweenFirst - */ + #[DataProvider('dataProviderFindBetweenFirst')] public function testFindBetweenFirst(string $string, string $start, ?string $end, ?string $expectedResult): void { $this->assertSame($expectedResult, StringHelper::findBetweenFirst($string, $start, $end)); } - public function dataProviderFindBetweenFirst(): array + public static function dataProviderFindBetweenFirst(): array { return [ ['[a][b][c]', '[', ']', 'a'], // normal case @@ -824,15 +784,13 @@ public function dataProviderFindBetweenFirst(): array ]; } - /** - * @dataProvider dataProviderFindBetweenLast - */ + #[DataProvider('dataProviderFindBetweenLast')] public function testFindBetweenLast(string $string, string $start, ?string $end, ?string $expectedResult): void { $this->assertSame($expectedResult, StringHelper::findBetweenLast($string, $start, $end)); } - public function dataProviderFindBetweenLast(): array + public static function dataProviderFindBetweenLast(): array { return [ ['[a][b][c]', '[', ']', 'c'], // normal case diff --git a/tests/Support/StringableObject.php b/tests/Support/StringableObject.php index e4d1614..5c83bb0 100644 --- a/tests/Support/StringableObject.php +++ b/tests/Support/StringableObject.php @@ -9,7 +9,7 @@ final class StringableObject implements Stringable { public function __construct( - private string $string + private readonly string $string ) { } diff --git a/tests/WildcardPatternTest.php b/tests/WildcardPatternTest.php index acee2ac..99b9aee 100644 --- a/tests/WildcardPatternTest.php +++ b/tests/WildcardPatternTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Strings\Tests; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\WildcardPattern; @@ -14,7 +15,7 @@ final class WildcardPatternTest extends TestCase * * @return array test data. */ - public function dataProviderMatchWildcard(): array + public static function dataProviderMatchWildcard(): array { return [ // * @@ -93,9 +94,7 @@ public function dataProviderMatchWildcard(): array ]; } - /** - * @dataProvider dataProviderMatchWildcard - */ + #[DataProvider('dataProviderMatchWildcard')] public function testMatchWildcard(string $pattern, string $string, bool $expectedResult, array $options = []): void { $wildcardPattern = $this->getWildcardPattern($pattern, $options); @@ -126,15 +125,13 @@ public function testImmutability(): void $this->assertNotSame($original, $original->ignoreCase()); } - /** - * @dataProvider isDynamicDataProvider - */ + #[DataProvider('isDynamicDataProvider')] public function testIsDynamic(string $pattern, bool $expected): void { $this->assertSame($expected, WildcardPattern::isDynamic($pattern)); } - public function isDynamicDataProvider(): array + public static function isDynamicDataProvider(): array { return [ 'not-dynamic' => ['just-some-string', false], @@ -145,7 +142,7 @@ public function isDynamicDataProvider(): array ]; } - public function customDelimitersProvider(): array + public static function customDelimitersProvider(): array { return [ 'empty' => ['begin*end', 'begin/end', [], true], @@ -154,16 +151,14 @@ public function customDelimitersProvider(): array ]; } - /** - * @dataProvider customDelimitersProvider - */ + #[DataProvider('customDelimitersProvider')] public function testCustomDelimiters(string $pattern, string $string, array $delimiters, bool $expected): void { $wildcardPattern = $wildcardPattern = new WildcardPattern($pattern, $delimiters); $this->assertSame($expected, $wildcardPattern->match($string)); } - public function quoteDataProvider(): array + public static function quoteDataProvider(): array { return [ 'no-pattern' => ['test', 'test'], @@ -171,9 +166,7 @@ public function quoteDataProvider(): array ]; } - /** - * @dataProvider quoteDataProvider - */ + #[DataProvider('quoteDataProvider')] public function testQuote(string $string, string $expected): void { $this->assertSame($expected, WildcardPattern::quote($string)); From 4a1623d9d2789ae82162fa915a9ae13fd10f91ed Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 7 Feb 2025 14:06:56 +0300 Subject: [PATCH 18/60] Fix `StringHelper::parsePath()` for empty string (#141) --- CHANGELOG.md | 1 + src/StringHelper.php | 4 ++-- tests/StringHelperTest.php | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f201fc..ebb3d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Bug #138: Explicitly mark nullable parameters (@ferrumfist) - Chg #140: Bump minimal required PHP version to 8.1 and minor refactoring (@vjik) +- Bug #141: `StringHelper::parsePath()` for empty string returns path `['']` instead of `[]` before (@vjik) ## 2.5.0 January 19, 2025 diff --git a/src/StringHelper.php b/src/StringHelper.php index fc1a1b2..b9b2293 100644 --- a/src/StringHelper.php +++ b/src/StringHelper.php @@ -489,7 +489,7 @@ public static function split(string $string, string $separator = '\R'): array * * @return string[] * - * @psalm-return list + * @psalm-return non-empty-list */ public static function parsePath( string $path, @@ -510,7 +510,7 @@ public static function parsePath( } if ($path === '') { - return []; + return ['']; } if (!str_contains($path, $delimiter)) { diff --git a/tests/StringHelperTest.php b/tests/StringHelperTest.php index f6f65b1..303392d 100644 --- a/tests/StringHelperTest.php +++ b/tests/StringHelperTest.php @@ -443,8 +443,8 @@ public static function dataParsePath(): array ['.key1.key2', '.', '\\', false, ['', 'key1', 'key2']], ['~key1~key2', '~', '\\', false, ['', 'key1', 'key2']], - ['', '.', '\\', false, []], - ['', '.', '\\', true, []], + ['', '.', '\\', false, ['']], + ['', '.', '\\', true, ['']], ]; } From 59deb2e3003341f97110d5d52bd6fb470adf0db5 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 7 Feb 2025 16:59:08 +0300 Subject: [PATCH 19/60] Check string on a valid UTF-8 in `StringHelper` trim methods (#142) --- CHANGELOG.md | 1 + src/StringHelper.php | 37 ++++++++++++++++++++++++++++++-- tests/StringHelperTest.php | 44 +++++++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebb3d80..05992ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Bug #138: Explicitly mark nullable parameters (@ferrumfist) - Chg #140: Bump minimal required PHP version to 8.1 and minor refactoring (@vjik) +- Bug #142: Check string on a valid UTF-8 in `StringHelper` methods: `trim()`, `ltrim()` and `rtrim()` (@vjik) - Bug #141: `StringHelper::parsePath()` for empty string returns path `['']` instead of `[]` before (@vjik) ## 2.5.0 January 19, 2025 diff --git a/src/StringHelper.php b/src/StringHelper.php index b9b2293..70c5f8b 100644 --- a/src/StringHelper.php +++ b/src/StringHelper.php @@ -580,8 +580,13 @@ public static function parsePath( */ public static function trim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array { + self::ensureUtf8String($string); self::ensureUtf8Pattern($pattern); + /** + * @var string|string[] `$string` is correct UTF-8 string and `$pattern` is correct (it should be passed + * already prepared), so `preg_replace` never returns `null`. + */ return preg_replace("#^[$pattern]+|[$pattern]+$#uD", '', $string); } @@ -603,8 +608,13 @@ public static function trim(string|array $string, string $pattern = self::DEFAUL */ public static function ltrim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array { + self::ensureUtf8String($string); self::ensureUtf8Pattern($pattern); + /** + * @var string|string[] `$string` is correct UTF-8 string and `$pattern` is correct (it should be passed + * already prepared), so `preg_replace` never returns `null`. + */ return preg_replace("#^[$pattern]+#u", '', $string); } @@ -626,8 +636,13 @@ public static function ltrim(string|array $string, string $pattern = self::DEFAU */ public static function rtrim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array { + self::ensureUtf8String($string); self::ensureUtf8Pattern($pattern); + /** + * @var string|string[] `$string` is correct UTF-8 string and `$pattern` is correct (it should be passed + * already prepared), so `preg_replace` never returns `null`. + */ return preg_replace("#[$pattern]+$#uD", '', $string); } @@ -751,9 +766,9 @@ public static function matchAnyRegex(string $string, array $patterns, string $fl } /** - * Ensure the input string is a valid UTF-8 string. + * Ensure the pattern is a valid UTF-8 string. * - * @param string $pattern The input string. + * @param string $pattern The pattern. * * @throws InvalidArgumentException */ @@ -763,4 +778,22 @@ private static function ensureUtf8Pattern(string $pattern): void throw new InvalidArgumentException('Pattern is not a valid UTF-8 string.'); } } + + /** + * Ensure the string is a valid UTF-8 string. + * + * @param array|string $string The string. + * + * @throws InvalidArgumentException + * + * @psalm-param string|string[] $string + */ + private static function ensureUtf8String(string|array $string): void + { + foreach ((array) $string as $s) { + if (!preg_match('##u', $s)) { + throw new InvalidArgumentException('String is not a valid UTF-8 string.'); + } + } + } } diff --git a/tests/StringHelperTest.php b/tests/StringHelperTest.php index 303392d..89bd206 100644 --- a/tests/StringHelperTest.php +++ b/tests/StringHelperTest.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\StringHelper; @@ -726,10 +727,51 @@ public function testRtrimPattern(string|array $string, string $pattern, string|a public function testInvalidTrimPattern(): void { $this->expectException(InvalidArgumentException::class); - + $this->expectExceptionMessage('Pattern is not a valid UTF-8 string.'); StringHelper::trim('string', "\xC3\x28"); } + #[TestWith(["abc\xFF"])] + #[TestWith([['hello', "abc\xFF"]])] + public function testInvalidTrimString(string|array $string): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('String is not a valid UTF-8 string.'); + StringHelper::trim($string); + } + + public function testInvalidLtrimPattern(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Pattern is not a valid UTF-8 string.'); + StringHelper::ltrim('string', "\xC3\x28"); + } + + #[TestWith(["abc\xFF"])] + #[TestWith([['hello', "abc\xFF"]])] + public function testInvalidLtrimString(string|array $string): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('String is not a valid UTF-8 string.'); + StringHelper::ltrim($string); + } + + public function testInvalidRtrimPattern(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Pattern is not a valid UTF-8 string.'); + StringHelper::ltrim('string', "\xC3\x28"); + } + + #[TestWith(["abc\xFF"])] + #[TestWith([['hello', "abc\xFF"]])] + public function testInvalidRtrimString(string|array $string): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('String is not a valid UTF-8 string.'); + StringHelper::rtrim($string); + } + #[DataProvider('dataProviderFindBetween')] public function testFindBetween(string $string, string $start, ?string $end, ?string $expectedResult): void { From 8accf0820371dfd0a92dd4bfb054ef63ca6ad695 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 9 Feb 2025 18:26:28 +0300 Subject: [PATCH 20/60] Add PHP 8.4 (#143) Co-authored-by: Sergei Tigrov --- .github/workflows/build.yml | 2 +- .../workflows/composer-require-checker.yml | 2 +- .github/workflows/mutation.yml | 2 +- .github/workflows/rector.yml | 2 +- .github/workflows/static.yml | 2 +- CHANGELOG.md | 1 + composer.json | 8 +- src/Inflector.php | 80 +++++++++++++------ src/NumericHelper.php | 7 +- src/StringHelper.php | 18 ++++- src/WildcardPattern.php | 2 + 11 files changed, 89 insertions(+), 37 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3feaeef..54dfa76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,4 +29,4 @@ jobs: os: >- ['ubuntu-latest', 'windows-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.1', '8.2', '8.3', '8.4'] diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index a857bce..a93390b 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -31,4 +31,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.1', '8.2', '8.3', '8.4'] diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 8150499..a56769e 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -27,6 +27,6 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.2'] + ['8.4'] secrets: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 457772a..5d6931d 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -21,4 +21,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.3'] + ['8.4'] diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index e33eca8..d03874d 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -29,4 +29,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.1', '8.2', '8.3', '8.4'] diff --git a/CHANGELOG.md b/CHANGELOG.md index 05992ee..9664018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Bug #138: Explicitly mark nullable parameters (@ferrumfist) - Chg #140: Bump minimal required PHP version to 8.1 and minor refactoring (@vjik) +- Chg #143: Change PHP constraint in `composer.json` to `~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0` (@vjik) - Bug #142: Check string on a valid UTF-8 in `StringHelper` methods: `trim()`, `ltrim()` and `rtrim()` (@vjik) - Bug #141: `StringHelper::parsePath()` for empty string returns path `['']` instead of `[]` before (@vjik) diff --git a/composer.json b/composer.json index a0d8b45..8141195 100644 --- a/composer.json +++ b/composer.json @@ -28,16 +28,16 @@ } ], "require": { - "php": "^8.1", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "ext-mbstring": "*" }, "require-dev": { "maglnet/composer-require-checker": "^4.7.1", - "phpunit/phpunit": "^10.5.44", - "rector/rector": "^2.0.7", + "phpunit/phpunit": "^10.5.45", + "rector/rector": "^2.0.8", "roave/infection-static-analysis-plugin": "^1.35", "spatie/phpunit-watcher": "^1.24", - "vimeo/psalm": "^5.26.1" + "vimeo/psalm": "^5.26.1|^6.4.1" }, "autoload": { "psr-4": { diff --git a/src/Inflector.php b/src/Inflector.php index 521fc26..ef15aea 100644 --- a/src/Inflector.php +++ b/src/Inflector.php @@ -435,6 +435,7 @@ public function toPlural(string $input): string } foreach ($this->pluralizeRules as $rule => $replacement) { if (preg_match($rule, $input)) { + /** @var string `$rule` and `$replacement` always correct, so `preg_replace` always returns string */ return preg_replace($rule, $replacement, $input); } } @@ -459,6 +460,7 @@ public function toSingular(string $input): string foreach ($this->singularizeRules as $rule => $replacement) { if (preg_match($rule, $input)) { + /** @var string `$rule` and `$replacement` always correct, so `preg_replace` always returns string */ return preg_replace($rule, $replacement, $input); } } @@ -488,17 +490,21 @@ public function toSentence(string $input, bool $uppercaseAll = false): string * Converts a string into space-separated words. * For example, 'PostTag' will be converted to 'Post Tag'. * - * @param string $input The string to be converted. + * @param string $input The valid UTF-8 string to be converted. * * @return string The resulting words. */ public function toWords(string $input): string { - return mb_strtolower(trim(str_replace([ - '-', - '_', - '.', - ], ' ', preg_replace('/(?pascalCaseToId(preg_replace('/[^\pL\pN]+/u', '_', $input), '_', $strict); + /** + * @var string $input We assume that `$input` is valid UTF-8 string, so `preg_replace()` never returns `false`. + */ + $input = preg_replace('/[^\pL\pN]+/u', '_', $input); + return $this->pascalCaseToId($input, '_', $strict); } /** @@ -635,8 +655,8 @@ public function tableToClass(string $tableName): string * and removes the rest. You may customize characters map via $transliteration property * of the helper. * - * @param string $input An arbitrary string to convert. - * @param string $replacement The replacement to use for spaces. + * @param string $input An arbitrary valid UTF-8 string to convert. + * @param string $replacement The replacement to use for spaces. It must be valid UTF-8 string. * @param bool $lowercase whether to return the string in lowercase or not. Defaults to `true`. * * @return string The converted string. @@ -644,9 +664,21 @@ public function tableToClass(string $tableName): string public function toSlug(string $input, string $replacement = '-', bool $lowercase = true): string { $quotedReplacement = preg_quote($replacement, '/'); - // replace all non words character + + /** + * Replace all non-words character + * + * @var string $input We assume that `$input` and `$replacement` are valid UTF-8 strings, so `preg_replace()` + * never returns `false`. + */ $input = preg_replace('/[^a-zA-Z0-9]+/u', $replacement, $this->toTransliterated($input)); - // remove first and last replacements + + /** + * Remove first and last replacements + * + * @var string $input We assume that `$input` and `$quotedReplacement` are valid UTF-8 strings, so + * `preg_replace()` never returns `false`. + */ $input = preg_replace( "/^(?:$quotedReplacement)+|(?:$quotedReplacement)+$/u" . ($lowercase ? 'i' : ''), '', @@ -665,12 +697,10 @@ public function toSlug(string $input, string $replacement = '-', bool $lowercase * * @noinspection PhpComposerExtensionStubsInspection * - * @param string $input Input string. + * @param string $input Input string. It must be valid UTF-8 string. * @param string|Transliterator|null $transliterator either a {@see \Transliterator} or a string * from which a {@see \Transliterator} can be built. If null, value set with {@see withTransliterator()} * or {@see TRANSLITERATE_LOOSE} is used. - * - * @return string */ public function toTransliterated(string $input, $transliterator = null): string { @@ -679,7 +709,11 @@ public function toTransliterated(string $input, $transliterator = null): string $transliterator = $this->transliterator; } - /* @noinspection PhpComposerExtensionStubsInspection */ + /** + * @noinspection PhpComposerExtensionStubsInspection + * @var string We assume that `$input` are valid UTF-8 strings and `$transliterator` is valid, so + * `preg_replace()` never returns `false`. + */ return transliterator_transliterate($transliterator, $input); } diff --git a/src/NumericHelper.php b/src/NumericHelper.php index 9a665cd..5f99ef3 100644 --- a/src/NumericHelper.php +++ b/src/NumericHelper.php @@ -53,7 +53,7 @@ public static function toOrdinal(mixed $value): string /** * Returns string representation of a number value without thousands separators and with dot as decimal separator. * - * @param bool|float|int|string|Stringable $value + * @param bool|float|int|string|Stringable $value String in `string` or `Stringable` must be valid UTF-8 string. * * @throws InvalidArgumentException if value is not scalar. */ @@ -69,8 +69,11 @@ public static function normalize(mixed $value): string return $value ? '1' : '0'; } - $value = str_replace([' ', ','], ['', '.'], (string)$value); + $value = str_replace([' ', ','], ['', '.'], (string) $value); + /** + * @var string We assume that `$value` is valid UTF-8 string, so `preg_replace()` never returns `false`. + */ return preg_replace('/\.(?=.*\.)/', '', $value); } diff --git a/src/StringHelper.php b/src/StringHelper.php index 70c5f8b..75b7c92 100644 --- a/src/StringHelper.php +++ b/src/StringHelper.php @@ -410,13 +410,16 @@ public static function uppercaseFirstCharacter(string $string, string $encoding /** * Uppercase the first character of each word in a string. * - * @param string $string The string to be processed. + * @param string $string The valid UTF-8 string to be processed. * @param string $encoding The encoding to use, defaults to "UTF-8". * * @see https://php.net/manual/en/function.ucwords.php */ public static function uppercaseFirstCharacterInEachWord(string $string, string $encoding = 'UTF-8'): string { + /** + * @var array $words We assume that `$string` is valid UTF-8 string, so `preg_split()` never returns `false`. + */ $words = preg_split('/\s/u', $string, -1, PREG_SPLIT_NO_EMPTY); $wordsWithUppercaseFirstCharacter = array_map( @@ -466,13 +469,22 @@ public static function base64UrlDecode(string $input): string * Split a string to array with non-empty lines. * Whitespace from the beginning and end of a each line will be stripped. * - * @param string $string The input string. + * @param string $string The input string. It must be valid UTF-8 string. * @param string $separator The boundary string. It is a part of regular expression - * so should be taken into account or properly escaped with {@see preg_quote()}. + * so should be taken into account or properly escaped with {@see preg_quote()}. It must be valid UTF-8 string. */ public static function split(string $string, string $separator = '\R'): array { + /** + * @var string $string We assume that `$string` is valid UTF-8 string, so `preg_replace()` never returns + * `false`. + */ $string = preg_replace('(^\s*|\s*$)', '', $string); + + /** + * @var array We assume that $separator is prepared by `preg_quote()` and $string is valid UTF-8 string, + * so `preg_split()` never returns `false`. + */ return preg_split('~\s*' . $separator . '\s*~u', $string, -1, PREG_SPLIT_NO_EMPTY); } diff --git a/src/WildcardPattern.php b/src/WildcardPattern.php index 95e5206..f5b0e27 100644 --- a/src/WildcardPattern.php +++ b/src/WildcardPattern.php @@ -80,6 +80,7 @@ public function ignoreCase(bool $flag = true): self */ public static function isDynamic(string $pattern): bool { + /** @var string $pattern `$rule` and `$replacement` always correct, so `preg_replace` always returns string */ $pattern = preg_replace('/\\\\./', '', $pattern); return preg_match('/[*{?\[]/', $pattern) === 1; } @@ -93,6 +94,7 @@ public static function isDynamic(string $pattern): bool */ public static function quote(string $string): string { + /** @var string `$rule` and `$replacement` always correct, so `preg_replace` always returns string */ return preg_replace('#([\\\\?*\\[\\]])#', '\\\\$1', $string); } From eeb087d96ab8ae35c5b17daafe5d8ad9522880ab Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 9 Feb 2025 16:30:52 +0000 Subject: [PATCH 21/60] Release version 2.6.0 --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9664018..f4d74ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,12 @@ # Yii Strings Change Log -## 2.5.1 under development +## 2.6.0 February 09, 2025 -- Bug #138: Explicitly mark nullable parameters (@ferrumfist) - Chg #140: Bump minimal required PHP version to 8.1 and minor refactoring (@vjik) - Chg #143: Change PHP constraint in `composer.json` to `~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0` (@vjik) -- Bug #142: Check string on a valid UTF-8 in `StringHelper` methods: `trim()`, `ltrim()` and `rtrim()` (@vjik) +- Bug #138: Explicitly mark nullable parameters (@ferrumfist) - Bug #141: `StringHelper::parsePath()` for empty string returns path `['']` instead of `[]` before (@vjik) +- Bug #142: Check string on a valid UTF-8 in `StringHelper` methods: `trim()`, `ltrim()` and `rtrim()` (@vjik) ## 2.5.0 January 19, 2025 From 220caa78ad2da6a79a5b9710cb5a02b7ded358bc Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 9 Feb 2025 16:30:54 +0000 Subject: [PATCH 22/60] Prepare for next release --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d74ff..bb63c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Yii Strings Change Log +## 2.6.1 under development + +- no changes in this release. + ## 2.6.0 February 09, 2025 - Chg #140: Bump minimal required PHP version to 8.1 and minor refactoring (@vjik) From 284f7a7e8372fb2157c9e201bf25adb28836ac2d Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 25 Jul 2025 15:47:50 +0300 Subject: [PATCH 23/60] Fix psalm errors + Update dev deps (#145) --- composer.json | 9 ++++----- psalm.xml | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 8141195..80e222e 100644 --- a/composer.json +++ b/composer.json @@ -28,16 +28,16 @@ } ], "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "8.1 - 8.4", "ext-mbstring": "*" }, "require-dev": { "maglnet/composer-require-checker": "^4.7.1", - "phpunit/phpunit": "^10.5.45", - "rector/rector": "^2.0.8", + "phpunit/phpunit": "^10.5.48", + "rector/rector": "^2.1.2", "roave/infection-static-analysis-plugin": "^1.35", "spatie/phpunit-watcher": "^1.24", - "vimeo/psalm": "^5.26.1|^6.4.1" + "vimeo/psalm": "^5.26.1 || ^6.13" }, "autoload": { "psr-4": { @@ -51,7 +51,6 @@ }, "config": { "sort-packages": true, - "bump-after-update": "dev", "allow-plugins": { "infection/extension-installer": true, "composer/package-versions-deprecated": true diff --git a/psalm.xml b/psalm.xml index d091d59..77a366b 100644 --- a/psalm.xml +++ b/psalm.xml @@ -3,6 +3,8 @@ errorLevel="1" findUnusedBaselineEntry="true" findUnusedCode="false" + ensureOverrideAttribute="false" + strictBinaryOperands="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" From 5d597329ec518ce6a53fc8dd9cac96174ada0206 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 29 Jul 2025 14:32:54 +0300 Subject: [PATCH 24/60] Add benchmarks for Inflector (#144) Co-authored-by: samdark <47294+samdark@users.noreply.github.com> Co-authored-by: Sergei Predvoditelev --- .gitignore | 3 + composer.json | 3 +- phpbench.json | 14 +++ tests/benchmarks/CombinedRegexpBench.php | 75 +++++++++++++++ tests/benchmarks/InflectorBench.php | 109 ++++++++++++++++++++++ tests/benchmarks/NumericHelperBench.php | 36 +++++++ tests/benchmarks/StringHelperBench.php | 103 ++++++++++++++++++++ tests/benchmarks/WildcardPatternBench.php | 43 +++++++++ 8 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 phpbench.json create mode 100644 tests/benchmarks/CombinedRegexpBench.php create mode 100644 tests/benchmarks/InflectorBench.php create mode 100644 tests/benchmarks/NumericHelperBench.php create mode 100644 tests/benchmarks/StringHelperBench.php create mode 100644 tests/benchmarks/WildcardPatternBench.php diff --git a/.gitignore b/.gitignore index 5d023ef..d2d29e5 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ composer.phar /phpunit.phar /phpunit.xml /.phpunit.cache + +# PHPBench +.phpbench diff --git a/composer.json b/composer.json index 80e222e..275b3d8 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ "rector/rector": "^2.1.2", "roave/infection-static-analysis-plugin": "^1.35", "spatie/phpunit-watcher": "^1.24", - "vimeo/psalm": "^5.26.1 || ^6.13" + "vimeo/psalm": "^5.26.1 || ^6.13", + "phpbench/phpbench": "^1.4.1" }, "autoload": { "psr-4": { diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 0000000..c4443bd --- /dev/null +++ b/phpbench.json @@ -0,0 +1,14 @@ +{ + "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", + "runner.bootstrap": "vendor/autoload.php", + "runner.path": "tests/benchmarks", + "runner.php_config": { + "memory_limit": "1G" + }, + "report.generators": { + "default": { + "generator": "composite", + "reports": ["aggregate", "env"] + } + } +} diff --git a/tests/benchmarks/CombinedRegexpBench.php b/tests/benchmarks/CombinedRegexpBench.php new file mode 100644 index 0000000..bd14c1c --- /dev/null +++ b/tests/benchmarks/CombinedRegexpBench.php @@ -0,0 +1,75 @@ +combinedRegexp = new CombinedRegexp($patterns); + $this->memoizedCombinedRegexp = new MemoizedCombinedRegexp($this->combinedRegexp); + } + + public function benchMatches(): void + { + $this->combinedRegexp->matches('string'); + $this->combinedRegexp->matches('123'); + $this->combinedRegexp->matches('String'); + } + + public function benchGetMatchingPattern(): void + { + $this->combinedRegexp->getMatchingPattern('string'); + $this->combinedRegexp->getMatchingPattern('123'); + $this->combinedRegexp->getMatchingPattern('String'); + } + + public function benchGetMatchingPatternPosition(): void + { + $this->combinedRegexp->getMatchingPatternPosition('string'); + $this->combinedRegexp->getMatchingPatternPosition('123'); + $this->combinedRegexp->getMatchingPatternPosition('String'); + } + + public function benchMemoizedMatches(): void + { + $this->memoizedCombinedRegexp->matches('string'); + $this->memoizedCombinedRegexp->matches('123'); + $this->memoizedCombinedRegexp->matches('String'); + } + + public function benchMemoizedGetMatchingPattern(): void + { + $this->memoizedCombinedRegexp->getMatchingPattern('string'); + $this->memoizedCombinedRegexp->getMatchingPattern('123'); + $this->memoizedCombinedRegexp->getMatchingPattern('String'); + } + + public function benchMemoizedGetMatchingPatternPosition(): void + { + $this->memoizedCombinedRegexp->getMatchingPatternPosition('string'); + $this->memoizedCombinedRegexp->getMatchingPatternPosition('123'); + $this->memoizedCombinedRegexp->getMatchingPatternPosition('String'); + } +} diff --git a/tests/benchmarks/InflectorBench.php b/tests/benchmarks/InflectorBench.php new file mode 100644 index 0000000..1937128 --- /dev/null +++ b/tests/benchmarks/InflectorBench.php @@ -0,0 +1,109 @@ +inflector = new Inflector(); + } + + public function benchToPlural(): void + { + $this->inflector->toPlural('apple'); + $this->inflector->toPlural('book'); + $this->inflector->toPlural('child'); + $this->inflector->toPlural('person'); + } + + public function benchToSingular(): void + { + $this->inflector->toSingular('apples'); + $this->inflector->toSingular('books'); + $this->inflector->toSingular('children'); + $this->inflector->toSingular('people'); + } + + public function benchToSentence(): void + { + $this->inflector->toSentence('some_string_to_convert'); + $this->inflector->toSentence('someStringToConvert'); + } + + public function benchToWords(): void + { + $this->inflector->toWords('some_string_to_convert'); + $this->inflector->toWords('someStringToConvert'); + } + + public function benchPascalCaseToId(): void + { + $this->inflector->pascalCaseToId('SomeString'); + $this->inflector->pascalCaseToId('SomeOtherString'); + } + + public function benchToPascalCase(): void + { + $this->inflector->toPascalCase('some string'); + $this->inflector->toPascalCase('some_other_string'); + } + + public function benchToCamelCase(): void + { + $this->inflector->toCamelCase('some string'); + $this->inflector->toCamelCase('some_other_string'); + } + + public function benchToSnakeCase(): void + { + $this->inflector->toSnakeCase('SomeString'); + $this->inflector->toSnakeCase('some other string'); + } + + public function benchToHumanReadable(): void + { + $this->inflector->toHumanReadable('some_string'); + $this->inflector->toHumanReadable('SomeString'); + } + + public function benchClassToTable(): void + { + $this->inflector->classToTable('SomeClass'); + $this->inflector->classToTable('Another\\ClassName'); + } + + public function benchTableToClass(): void + { + $this->inflector->tableToClass('some_table'); + $this->inflector->tableToClass('another_table_name'); + } + + public function benchToSlug(): void + { + $this->inflector->toSlug('some string to slug'); + $this->inflector->toSlug('some other_string-to slug'); + } + + public function benchToTransliterated(): void + { + $this->inflector->toTransliterated('some string to transliterate'); + $this->inflector->toTransliterated('Українська мова'); + $this->inflector->toTransliterated('Русский язык'); + $this->inflector->toTransliterated('日本語って本当にかっこいいですね'); + } +} diff --git a/tests/benchmarks/NumericHelperBench.php b/tests/benchmarks/NumericHelperBench.php new file mode 100644 index 0000000..50469a8 --- /dev/null +++ b/tests/benchmarks/NumericHelperBench.php @@ -0,0 +1,36 @@ +b', '', ''); + } + + public function benchMatchAnyRegex(): void + { + StringHelper::matchAnyRegex('string', ['/[a-z]+/', '/[0-9]+/']); + } +} diff --git a/tests/benchmarks/WildcardPatternBench.php b/tests/benchmarks/WildcardPatternBench.php new file mode 100644 index 0000000..4326d48 --- /dev/null +++ b/tests/benchmarks/WildcardPatternBench.php @@ -0,0 +1,43 @@ +wildcardPattern = new WildcardPattern('*a*'); + } + + public function benchMatch(): void + { + $this->wildcardPattern->match('banana'); + $this->wildcardPattern->match('apple'); + $this->wildcardPattern->match('orange'); + } + + public function benchIsDynamic(): void + { + WildcardPattern::isDynamic('*a*'); + WildcardPattern::isDynamic('test'); + } + + public function benchQuote(): void + { + WildcardPattern::quote('*a*'); + } +} From 2d5fea1f6e1b437996d6f3b994943b9065db1665 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 Aug 2025 08:10:21 +0300 Subject: [PATCH 25/60] Fix #119: Fix incorrect pluralization of words ending in "tion", "sion" and "gion" Co-authored-by: Alexander Makarov --- CHANGELOG.md | 2 +- src/Inflector.php | 7 ++++++- tests/InflectorTest.php | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb63c4d..1ef0048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.6.1 under development -- no changes in this release. +- Bug #119: Fix incorrect pluralization of words ending in "tion", "sion" and "gion" (@samdark) ## 2.6.0 February 09, 2025 diff --git a/src/Inflector.php b/src/Inflector.php index ef15aea..91ac2cd 100644 --- a/src/Inflector.php +++ b/src/Inflector.php @@ -101,7 +101,9 @@ final class Inflector '/(alias)$/i' => '\1es', '/(ax|cris|test)is$/i' => '\1es', '/(currenc)y$/' => '\1ies', - '/on$/i' => 'a', + '/(tion|sion|gion)$/i' => '\1s', + '/menon$/i' => 'mena', + '/maton$/i' => 'mata', '/s$/' => 's', '/^$/' => '', '/$/' => 's', @@ -145,6 +147,8 @@ final class Inflector '/(^analy)ses$/i' => '\1sis', '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', '/criteria$/i' => 'criterion', + '/mena$/i' => 'menon', + '/mata$/i' => 'maton', '/([ti])a$/i' => '\1um', '/(p)eople$/i' => '\1\2erson', '/(m)en$/i' => '\1an', @@ -209,6 +213,7 @@ final class Inflector 'corps' => 'corps', 'corpus' => 'corpuses', 'cow' => 'cows', + 'criterion' => 'criteria', 'curve' => 'curves', 'debris' => 'debris', 'diabetes' => 'diabetes', diff --git a/tests/InflectorTest.php b/tests/InflectorTest.php index 08b6e4e..ecdf50c 100644 --- a/tests/InflectorTest.php +++ b/tests/InflectorTest.php @@ -39,6 +39,21 @@ private function getTestDataForToPlural(): array 'analysis' => 'analyses', 'datum' => 'data', 'schema' => 'schemas', + 'nation' => 'nations', + 'action' => 'actions', + 'creation' => 'creations', + 'decision' => 'decisions', + 'vision' => 'visions', + 'explosion' => 'explosions', + 'legion' => 'legions', + 'region' => 'regions', + 'information' => 'information', // special case - already ends in 'tion' but is uncountable + 'education' => 'educations', + 'attention' => 'attentions', + 'function' => 'functions', + 'junction' => 'junctions', + 'baron' => 'barons', + 'demon' => 'demons', ]; } From e4aa788185bfac46a7ed8f7c667a0844546385e9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:24:31 +0300 Subject: [PATCH 26/60] Fix #3: Add `StringHelper::truncateWordsByLength()` method to truncate text by character length while preserving word boundaries Co-authored-by: Sergei Predvoditelev Co-authored-by: Alexander Makarov --- CHANGELOG.md | 1 + README.md | 1 + src/StringHelper.php | 51 +++++++++++++++++++++++++++++++++++--- tests/StringHelperTest.php | 27 ++++++++++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ef0048..e701ba9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.6.1 under development +- New #3: Add `StringHelper::truncateWordsByLength()` method to truncate text by character length while preserving word boundaries (@samdark) - Bug #119: Fix incorrect pluralization of words ending in "tion", "sion" and "gion" (@samdark) ## 2.6.0 February 09, 2025 diff --git a/README.md b/README.md index f9dad8f..383e03d 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Overall the helper has the following method groups. - truncateMiddle - truncateEnd - truncateWords +- truncateWordsByLength - trim - ltrim - rtrim diff --git a/src/StringHelper.php b/src/StringHelper.php index 75b7c92..2f4dd77 100644 --- a/src/StringHelper.php +++ b/src/StringHelper.php @@ -15,6 +15,7 @@ use function implode; use function max; use function mb_strlen; +use function mb_strpos; use function mb_strrpos; use function mb_strtolower; use function mb_strtoupper; @@ -223,7 +224,7 @@ public static function startsWithIgnoringCase(string $input, string|null $with): * Binary and multibyte safe. * * @param string $input Input string to check. - * @param string|null $with Part to search inside of the $string. + * @param string|null $with Part to search inside the $string. * * @return bool Returns true if first input ends with second input, false otherwise. */ @@ -237,7 +238,7 @@ public static function endsWith(string $input, string|null $with): bool * Binary and multibyte safe. * * @param string $input Input string to check. - * @param string|null $with Part to search inside of the $string. + * @param string|null $with Part to search inside the $string. * * @return bool Returns true if first input ends with second input, false otherwise. */ @@ -342,6 +343,50 @@ public static function truncateWords(string $input, int $count, string $trimMark return $input; } + /** + * Truncates a string to the specified character length while preserving word boundaries. + * + * @param string $input The string to truncate. + * @param int $length Maximum length of the truncated string including trim marker. + * @param string $trimMarker String to append to the end of truncated string. + * @param string $encoding The encoding to use, defaults to "UTF-8". + * + * @return string The truncated string. + */ + public static function truncateWordsByLength( + string $input, + int $length, + string $trimMarker = '…', + string $encoding = 'UTF-8', + ): string { + $input = trim($input); + if ($input === '') { + return ''; + } + + if (mb_strlen($input, $encoding) <= $length) { + return $input; + } + + $markerLength = mb_strlen($trimMarker, $encoding); + if ($length <= $markerLength) { + return mb_substr($trimMarker, 0, $length, $encoding); + } + + $truncated = mb_substr($input, 0, $length - $markerLength, $encoding); + + // Prefer not to break words if there's a space within the snippet. + $lastSpace = mb_strrpos($truncated, ' ', 0, $encoding); + if ($lastSpace !== false) { + $cut = rtrim(mb_substr($truncated, 0, $lastSpace, $encoding)); + return $cut === '' + ? mb_substr($trimMarker, 0, $length, $encoding) + : $cut . $trimMarker; + } + + return $truncated . $trimMarker; + } + /** * Get string length. * @@ -467,7 +512,7 @@ public static function base64UrlDecode(string $input): string /** * Split a string to array with non-empty lines. - * Whitespace from the beginning and end of a each line will be stripped. + * Whitespace from the beginning and end of each line will be stripped. * * @param string $string The input string. It must be valid UTF-8 string. * @param string $separator The boundary string. It is a part of regular expression diff --git a/tests/StringHelperTest.php b/tests/StringHelperTest.php index 89bd206..3630743 100644 --- a/tests/StringHelperTest.php +++ b/tests/StringHelperTest.php @@ -120,6 +120,33 @@ public function testTruncateWords(): void $this->assertEquals('это строка с неожиданными…', StringHelper::truncateWords(' это строка с неожиданными пробелами ', 4)); } + #[DataProvider('provideTruncateWordsByLength')] + public function testTruncateWordsByLength(string $expected, string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): void + { + $this->assertSame($expected, StringHelper::truncateWordsByLength($input, $length, $trimMarker, $encoding)); + } + + public static function provideTruncateWordsByLength(): array + { + return [ + 'basic truncation' => ['Do you like drink…', 'Do you like drink coffee at work?', 20], + 'string shorter than limit should return as-is' => ['Short text', 'Short text', 20], + 'string exactly at limit should return as-is' => ['Exact length text', 'Exact length text', 17], + 'custom trim marker' => ['Do you like drink!!!', 'Do you like drink coffee at work?', 23, '!!!'], + 'multibyte characters' => ['это тестовая…', 'это тестовая multibyte строка', 15], + 'no spaces (single word) - should break the word' => ['verylongwo…', 'verylongword', 11], + 'very short limit with marker' => ['A…', 'A long sentence', 2], + 'limit with the same length as marker' => ['…', 'Some text', 1], + 'empty string' => ['', '', 10], + 'spaces that exceed limit should truncate to empty' => ['', ' ', 3], + 'text with trailing spaces should be trimmed' => ['Hello world', 'Hello world ', 15], + 'multiple words that fit exactly' => ['Hello…', 'Hello world', 6], + 'space at start should add marker' => ['Hel…', ' Hello', 4], + 'spaces should be trimmed' => ['Hello', ' Hello ', 100], + 'nested spaces should be trimmed' => ['Hello…', 'Hello World', 8], + ]; + } + #[DataProvider('providerStartsWith')] public function testStartsWith(bool $result, string $string, ?string $with): void { From c8770364932bab9cea4f25f6a77f75eb37991eec Mon Sep 17 00:00:00 2001 From: Maksim Spirkov <63721828+max-s-lab@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:35:23 +0400 Subject: [PATCH 27/60] Add `NumericHelper::convertHumanReadableSizeToBytes()` method (#148) --- .gitignore | 3 ++ CHANGELOG.md | 1 + README.md | 1 + src/NumericHelper.php | 75 +++++++++++++++++++++++++++++++++ tests/NumericHelperTest.php | 83 +++++++++++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+) diff --git a/.gitignore b/.gitignore index d2d29e5..0f8b69d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # phpstorm project files .idea +# visual studio code project files +.vscode + # netbeans project files nbproject diff --git a/CHANGELOG.md b/CHANGELOG.md index e701ba9..ae578ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - New #3: Add `StringHelper::truncateWordsByLength()` method to truncate text by character length while preserving word boundaries (@samdark) - Bug #119: Fix incorrect pluralization of words ending in "tion", "sion" and "gion" (@samdark) +- New #148: Add `NumericHelper::convertHumanReadableSizeToBytes()` method (@max-s-lab) ## 2.6.0 February 09, 2025 diff --git a/README.md b/README.md index 383e03d..a0ea44a 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ The following methods are available: - toOrdinal - normalize - isInteger +- convertHumanReadableSizeToBytes ## Inflector usage diff --git a/src/NumericHelper.php b/src/NumericHelper.php index 5f99ef3..6aaccef 100644 --- a/src/NumericHelper.php +++ b/src/NumericHelper.php @@ -16,12 +16,45 @@ use function is_scalar; use function preg_replace; use function str_replace; +use function substr; /** * Provides static methods to work with numeric strings. */ final class NumericHelper { + /** + * @psalm-var array> + */ + private const FILESYSTEM_SIZE_POSTFIXES = [ + 3 => [ + 'KiB' => 1024, + 'MiB' => 1048576, + 'GiB' => 1073741824, + 'TiB' => 1099511627776, + 'PiB' => 1125899906842624, + ], + 2 => [ + 'kB' => 1000, + 'MB' => 1000000, + 'GB' => 1000000000, + 'TB' => 1000000000000, + 'PB' => 1000000000000000, + ], + 1 => [ + 'k' => 1024, + 'K' => 1024, + 'm' => 1048576, + 'M' => 1048576, + 'g' => 1073741824, + 'G' => 1073741824, + 't' => 1099511627776, + 'T' => 1099511627776, + 'p' => 1125899906842624, + 'P' => 1125899906842624, + ], + ]; + /** * Converts number to its ordinal English form. For example, converts 13 to 13th, 2 to 2nd etc. * @@ -86,4 +119,46 @@ public static function isInteger(mixed $value): bool { return filter_var($value, FILTER_VALIDATE_INT) !== false; } + + /** + * Converts human readable size to bytes. + * + * @param string $string Human readable size. Examples: `1024`, `1kB`, `1.5M`, `1GiB`. Full + * list of supported postfixes in {@see FILESYSTEM_SIZE_POSTFIXES}. + + * Note: This parameter must be less than `8192P` on 64-bit systems and `2G` on 32-bit systems. + * + * @throws InvalidArgumentException when the string is invalid. + * + * @return int The number of bytes equivalent to the specified string. + * + * @see https://www.gnu.org/software/coreutils/manual/html_node/Block-size.html + */ + public static function convertHumanReadableSizeToBytes(string $string): int + { + if (is_numeric($string)) { + return (int) $string; + } + + foreach (self::FILESYSTEM_SIZE_POSTFIXES as $postfixLength => $postfixes) { + $postfix = substr($string, -$postfixLength); + if ($postfix === '' || preg_match('/\\d/', $postfix) === 1) { + continue; + } + + $numericPart = substr($string, 0, -$postfixLength); + if (!is_numeric($numericPart)) { + throw new InvalidArgumentException("Incorrect input string: $string"); + } + + $postfixMultiplier = $postfixes[$postfix] ?? null; + if ($postfixMultiplier === null) { + throw new InvalidArgumentException("Not supported postfix '$postfix' in input string: $string"); + } + + return (int) ((float) $numericPart * $postfixMultiplier); + } + + throw new InvalidArgumentException("Incorrect input string: $string"); + } } diff --git a/tests/NumericHelperTest.php b/tests/NumericHelperTest.php index 5291a4e..79a02a7 100644 --- a/tests/NumericHelperTest.php +++ b/tests/NumericHelperTest.php @@ -87,4 +87,87 @@ public function testIsInteger(mixed $value, bool $expected): void { $this->assertSame($expected, NumericHelper::isInteger($value)); } + + public static function dataConvertHumanReadableSizeToBytes(): array + { + return [ + // Only numbers + ['1024', 1024], + ['9223372036854775807', 9223372036854775807], + // Single-character postfix + ['512K', 524288], + ['512k', 524288], + ['2.5k', 2560], + ['2.5K', 2560], + ['128M', 134217728], + ['128m', 134217728], + ['4.5m', 4718592], + ['4.5M', 4718592], + ['2G', 2147483648], + ['2g', 2147483648], + ['2.5g', 2684354560], + ['2.5G', 2684354560], + ['1.1G', 1181116006], + ['2t', 2199023255552], + ['2T', 2199023255552], + ['6.5t', 7146825580544], + ['6.5T', 7146825580544], + ['3p', 3377699720527872], + ['3P', 3377699720527872], + ['3.5p', 3940649673949184], + ['3.5P', 3940649673949184], + // Two-character postfix + ['2kB', 2000], + ['2.5kB', 2500], + ['1MB', 1000000], + ['3.3MB', 3300000], + ['6GB', 6000000000], + ['7.4GB', 7400000000], + ['4TB', 4000000000000], + ['4.9TB', 4900000000000], + ['7PB', 7000000000000000], + ['7.7PB', 7700000000000000], + // Three-character postfix + ['512KiB', 524288], + ['2.5KiB', 2560], + ['128MiB', 134217728], + ['4.5MiB', 4718592], + ['2GiB', 2147483648], + ['2.5GiB', 2684354560], + ['2TiB', 2199023255552], + ['6.5TiB', 7146825580544], + ['3PiB', 3377699720527872], + ['3.5PiB', 3940649673949184], + ]; + } + + #[DataProvider('dataConvertHumanReadableSizeToBytes')] + public function testConvertHumanReadableSizeToBytes(string $string, int $expected): void + { + $this->assertSame($expected, NumericHelper::convertHumanReadableSizeToBytes($string)); + } + + public static function dataConvertHumanReadableSizeToBytesWithInvalidStrings(): array + { + return [ + ['12cKib', 'Incorrect input string: 12cKib'], + ['12Kcb', 'Not supported postfix \'Kcb\' in input string: 12Kcb'], + ['1c2kB', 'Incorrect input string: 1c2kB'], + ['12Kc', 'Not supported postfix \'Kc\' in input string: 12Kc'], + ['1c2k', 'Incorrect input string: 1c2k'], + ['123n', 'Not supported postfix \'n\' in input string: 123n'], + ['k', 'Incorrect input string: k'], + ['K', 'Incorrect input string: K'], + ['m', 'Incorrect input string: m'], + ['M', 'Incorrect input string: M'], + ['', 'Incorrect input string: '], + ]; + } + + #[DataProvider('dataConvertHumanReadableSizeToBytesWithInvalidStrings')] + public function testConvertHumanReadableSizeToBytesWithInvalidStrings(string $string, string $message): void + { + $this->expectExceptionObject(new \InvalidArgumentException($message)); + NumericHelper::convertHumanReadableSizeToBytes($string); + } } From 34bba9e4ee60940a4e2f8bcabc64d6fd9ea8d900 Mon Sep 17 00:00:00 2001 From: Maksim Spirkov <63721828+mspirkov@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:19:00 +0400 Subject: [PATCH 28/60] Minor edits in `NumericHelper` (#149) Co-authored-by: Sergei Predvoditelev --- CHANGELOG.md | 2 +- src/NumericHelper.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae578ba..5dcdf52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - New #3: Add `StringHelper::truncateWordsByLength()` method to truncate text by character length while preserving word boundaries (@samdark) - Bug #119: Fix incorrect pluralization of words ending in "tion", "sion" and "gion" (@samdark) -- New #148: Add `NumericHelper::convertHumanReadableSizeToBytes()` method (@max-s-lab) +- New #148, #149: Add `NumericHelper::convertHumanReadableSizeToBytes()` method (@mspirkov) ## 2.6.0 February 09, 2025 diff --git a/src/NumericHelper.php b/src/NumericHelper.php index 6aaccef..7380ad8 100644 --- a/src/NumericHelper.php +++ b/src/NumericHelper.php @@ -14,6 +14,7 @@ use function is_bool; use function is_numeric; use function is_scalar; +use function preg_match; use function preg_replace; use function str_replace; use function substr; From bce4a7c1b8280ce4aa922334ad0203c351a2851a Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 23 Nov 2025 08:57:11 +0300 Subject: [PATCH 29/60] Add PHP 8.5 support (#150) --- .github/workflows/build.yml | 2 +- .github/workflows/composer-require-checker.yml | 2 +- .github/workflows/mutation.yml | 5 +++-- .github/workflows/rector.yml | 2 +- CHANGELOG.md | 4 +++- README.md | 2 +- composer.json | 14 ++++++++++---- tools/.gitignore | 2 ++ tools/infection/composer.json | 10 ++++++++++ tools/psalm/composer.json | 5 +++++ 10 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 tools/.gitignore create mode 100644 tools/infection/composer.json create mode 100644 tools/psalm/composer.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54dfa76..19b694d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,4 +29,4 @@ jobs: os: >- ['ubuntu-latest', 'windows-latest'] php: >- - ['8.1', '8.2', '8.3', '8.4'] + ['8.1', '8.2', '8.3', '8.4', '8.5'] diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index a93390b..d2ef508 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -31,4 +31,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3', '8.4'] + ['8.1', '8.2', '8.3', '8.4', '8.5'] diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index a56769e..cc40daa 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -22,11 +22,12 @@ name: mutation test jobs: mutation: - uses: yiisoft/actions/.github/workflows/roave-infection.yml@master + uses: yiisoft/actions/.github/workflows/infection.yml@master with: os: >- ['ubuntu-latest'] php: >- - ['8.4'] + ['8.5'] + infection-args: "--ignore-msi-with-no-mutations" secrets: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 5d6931d..20f7d89 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -21,4 +21,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.4'] + ['8.5'] diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dcdf52..3870171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ ## 2.6.1 under development -- New #3: Add `StringHelper::truncateWordsByLength()` method to truncate text by character length while preserving word boundaries (@samdark) +- New #3: Add `StringHelper::truncateWordsByLength()` method to truncate text by character length while preserving word + boundaries (@samdark) - Bug #119: Fix incorrect pluralization of words ending in "tion", "sion" and "gion" (@samdark) - New #148, #149: Add `NumericHelper::convertHumanReadableSizeToBytes()` method (@mspirkov) +- Enh #150: Add PHP 8.5 support (@vjik) ## 2.6.0 February 09, 2025 diff --git a/README.md b/README.md index a0ea44a..2d16529 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The package provides: ## Requirements -- PHP 8.1 or higher. +- PHP 8.1 - 8.5. - `mbstring` PHP extension. ## Installation diff --git a/composer.json b/composer.json index 275b3d8..4c47136 100644 --- a/composer.json +++ b/composer.json @@ -28,16 +28,15 @@ } ], "require": { - "php": "8.1 - 8.4", + "php": "8.1 - 8.5", "ext-mbstring": "*" }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", "maglnet/composer-require-checker": "^4.7.1", "phpunit/phpunit": "^10.5.48", "rector/rector": "^2.1.2", - "roave/infection-static-analysis-plugin": "^1.35", "spatie/phpunit-watcher": "^1.24", - "vimeo/psalm": "^5.26.1 || ^6.13", "phpbench/phpbench": "^1.4.1" }, "autoload": { @@ -53,10 +52,17 @@ "config": { "sort-packages": true, "allow-plugins": { - "infection/extension-installer": true, + "bamarni/composer-bin-plugin": true, "composer/package-versions-deprecated": true } }, + "extra": { + "bamarni-bin": { + "bin-links": true, + "target-directory": "tools", + "forward-command": true + } + }, "scripts": { "test": "phpunit --testdox --no-interaction", "test-watch": "phpunit-watcher watch" diff --git a/tools/.gitignore b/tools/.gitignore new file mode 100644 index 0000000..cf452dc --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1,2 @@ +/*/vendor +/*/composer.lock diff --git a/tools/infection/composer.json b/tools/infection/composer.json new file mode 100644 index 0000000..18be2ea --- /dev/null +++ b/tools/infection/composer.json @@ -0,0 +1,10 @@ +{ + "require-dev": { + "infection/infection": "^0.26 || ^0.31.9" + }, + "config": { + "allow-plugins": { + "infection/extension-installer": true + } + } +} diff --git a/tools/psalm/composer.json b/tools/psalm/composer.json new file mode 100644 index 0000000..9751a9f --- /dev/null +++ b/tools/psalm/composer.json @@ -0,0 +1,5 @@ +{ + "require-dev": { + "vimeo/psalm": "^5.26.1 || ^6.5" + } +} From 9bc7fea56374619cccd4587848029fe97f98bb33 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 23 Nov 2025 21:00:58 +0300 Subject: [PATCH 30/60] Release version 2.7.0 --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3870171..4770201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,12 @@ # Yii Strings Change Log -## 2.6.1 under development +## 2.7.0 November 23, 2025 +- New #148, #149: Add `NumericHelper::convertHumanReadableSizeToBytes()` method (@mspirkov) - New #3: Add `StringHelper::truncateWordsByLength()` method to truncate text by character length while preserving word boundaries (@samdark) -- Bug #119: Fix incorrect pluralization of words ending in "tion", "sion" and "gion" (@samdark) -- New #148, #149: Add `NumericHelper::convertHumanReadableSizeToBytes()` method (@mspirkov) - Enh #150: Add PHP 8.5 support (@vjik) +- Bug #119: Fix incorrect pluralization of words ending in "tion", "sion" and "gion" (@samdark) ## 2.6.0 February 09, 2025 From 434a7535d5a4f16deb2ad28c330462bdb68235d4 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 23 Nov 2025 21:01:03 +0300 Subject: [PATCH 31/60] Prepare for next release --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4770201..1099cbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Yii Strings Change Log +## 2.7.1 under development + +- no changes in this release. + ## 2.7.0 November 23, 2025 - New #148, #149: Add `NumericHelper::convertHumanReadableSizeToBytes()` method (@mspirkov) From abc8ec6b3ae4ec1773376662f69bd722920a7b07 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 7 Dec 2025 00:50:47 +0300 Subject: [PATCH 32/60] Use default FUNDING and ISSUE_TEMPLATE (#152) --- .github/FUNDING.yml | 4 ---- .github/ISSUE_TEMPLATE.md | 16 ---------------- 2 files changed, 20 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index f0dc531..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -# These are supported funding model platforms - -open_collective: yiisoft -github: [yiisoft] diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index b748c2d..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,16 +0,0 @@ - - -### What steps will reproduce the problem? - -### What is the expected result? - -### What do you get instead? - - -### Additional info - -| Q | A -| ---------------- | --- -| Version | 1.0.? -| PHP version | -| Operating system | From 8b5bc076236dfd25c8fed1fdd87a22963da51aad Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 20 Jan 2026 12:26:05 +0300 Subject: [PATCH 33/60] Fix #156: Add `NumericHelper::trimDecimalZeros()` Co-authored-by: samdark <47294+samdark@users.noreply.github.com> Co-authored-by: Sergei Tigrov Co-authored-by: Sergei Predvoditelev --- CHANGELOG.md | 2 +- README.md | 1 + src/NumericHelper.php | 39 ++++++++++++++++++++++++- tests/NumericHelperTest.php | 38 ++++++++++++++++++++++-- tests/benchmarks/NumericHelperBench.php | 12 ++++++++ 5 files changed, 87 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1099cbe..b868d9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.7.1 under development -- no changes in this release. +- New #156: Add `NumericHelper::trimDecimalZeros()` (@samdark, @vjik) ## 2.7.0 November 23, 2025 diff --git a/README.md b/README.md index 2d16529..d768fed 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ The following methods are available: - normalize - isInteger - convertHumanReadableSizeToBytes +- trimDecimalZeros ## Inflector usage diff --git a/src/NumericHelper.php b/src/NumericHelper.php index 7380ad8..bf68d64 100644 --- a/src/NumericHelper.php +++ b/src/NumericHelper.php @@ -122,7 +122,7 @@ public static function isInteger(mixed $value): bool } /** - * Converts human readable size to bytes. + * Converts human-readable size to bytes. * * @param string $string Human readable size. Examples: `1024`, `1kB`, `1.5M`, `1GiB`. Full * list of supported postfixes in {@see FILESYSTEM_SIZE_POSTFIXES}. @@ -162,4 +162,41 @@ public static function convertHumanReadableSizeToBytes(string $string): int throw new InvalidArgumentException("Incorrect input string: $string"); } + + /** + * Trims spaces and trailing decimal zeros from a numeric string. + * + * If the fractional part consists only of zeros, the decimal dot separator is removed as well. + * The value that is `null` is returned as-is. + * + * @param string|null $value Numeric string or null. + * + * @return string|null The input string with spaces, trailing decimal zeros (and a trailing decimal + * dot separator, if any) removed, or `null` if the input was `null`. + * + * @see is_numeric() + */ + public static function trimDecimalZeros(?string $value): ?string + { + if ($value === null) { + return null; + } + + if (!is_numeric($value)) { + throw new InvalidArgumentException( + sprintf('Value must be numeric string or null. "%s" given.', $value) + ); + } + + $value = trim($value); + + if (!str_contains($value, '.')) { + return $value; + } + + $value = rtrim($value, '0'); + $value = rtrim($value, '.'); + + return $value ?: '0'; + } } diff --git a/tests/NumericHelperTest.php b/tests/NumericHelperTest.php index 79a02a7..460bfce 100644 --- a/tests/NumericHelperTest.php +++ b/tests/NumericHelperTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Strings\Tests; +use InvalidArgumentException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\NumericHelper; @@ -35,7 +36,7 @@ public function testToOrdinal(): void public function testToOrdinalWithIncorrectType(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); NumericHelper::toOrdinal('bla-bla'); } @@ -63,7 +64,7 @@ public function testNormalize(mixed $input, string $expected): void public function testNormalizeWithIncorrectType(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); NumericHelper::normalize([]); } @@ -167,7 +168,38 @@ public static function dataConvertHumanReadableSizeToBytesWithInvalidStrings(): #[DataProvider('dataConvertHumanReadableSizeToBytesWithInvalidStrings')] public function testConvertHumanReadableSizeToBytesWithInvalidStrings(string $string, string $message): void { - $this->expectExceptionObject(new \InvalidArgumentException($message)); + $this->expectExceptionObject(new InvalidArgumentException($message)); NumericHelper::convertHumanReadableSizeToBytes($string); } + + public static function dataTrimDecimalZeros(): array + { + return [ + 'no decimals in integer with zeros' => ['390', '390'], + 'all zeros' => ['390.000', '390'], + 'no zeros' => ['3.14', '3.14'], + 'some zeros' => ['42.010', '42.01'], + 'zeros' => ['0.0', '0'], + 'decimal only' => ['.5', '.5'], + 'decimal zero' => ['.0', '0'], + 'start with zero' => ['0.25', '0.25'], + 'negative' => ['-3.000', '-3'], + 'null' => [null, null], + 'starts with zero' => ['02471', '02471'], + 'exponent' => ['1337e0', '1337e0'], + 'spaces' => ['3.140 ', '3.14'], + ]; + } + + #[DataProvider('dataTrimDecimalZeros')] + public function testTrimDecimalZeros(?string $input, ?string $expected): void + { + $this->assertSame($expected, NumericHelper::trimDecimalZeros($input)); + } + + public function trimDecimalZerosWithNonNumericString(): void + { + $this->expectException(InvalidArgumentException::class); + NumericHelper::trimDecimalZeros('hello'); + } } diff --git a/tests/benchmarks/NumericHelperBench.php b/tests/benchmarks/NumericHelperBench.php index 50469a8..50081bf 100644 --- a/tests/benchmarks/NumericHelperBench.php +++ b/tests/benchmarks/NumericHelperBench.php @@ -33,4 +33,16 @@ public function benchNormalize(): void NumericHelper::normalize('1,000,000.123'); NumericHelper::normalize('1 000 000,123'); } + + public function benchTrimDecimalZeros(): void + { + NumericHelper::trimDecimalZeros('390'); + NumericHelper::trimDecimalZeros('390.000'); + NumericHelper::trimDecimalZeros('3.14'); + NumericHelper::trimDecimalZeros('42.010'); + NumericHelper::trimDecimalZeros('0.0'); + NumericHelper::trimDecimalZeros('.5'); + NumericHelper::trimDecimalZeros('0.25'); + NumericHelper::trimDecimalZeros('-3.000'); + } } From 73ccafd27d71025a160ccf4a4f9898eecc7c9351 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Jun 2026 00:10:43 +0300 Subject: [PATCH 34/60] Harden GitHub workflows --- .github/workflows/bc.yml | 5 ++++- .github/workflows/build.yml | 5 ++++- .github/workflows/composer-require-checker.yml | 5 ++++- .github/workflows/mutation.yml | 5 ++++- .github/workflows/rector.yml | 5 ++++- .github/workflows/static.yml | 5 ++++- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index 00041a9..bdfe214 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -23,9 +23,12 @@ on: name: backwards compatibility +permissions: + contents: read + jobs: roave_bc_check: - uses: yiisoft/actions/.github/workflows/bc.yml@master + uses: yiisoft/actions/.github/workflows/bc.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 with: os: >- ['ubuntu-latest'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f980c7f..d30d533 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,9 +22,12 @@ on: name: build +permissions: + contents: read + jobs: phpunit: - uses: yiisoft/actions/.github/workflows/phpunit.yml@master + uses: yiisoft/actions/.github/workflows/phpunit.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 with: os: >- ['ubuntu-latest', 'windows-latest'] diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index 5473ec9..e231b98 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -24,9 +24,12 @@ on: name: Composer require checker +permissions: + contents: read + jobs: composer-require-checker: - uses: yiisoft/actions/.github/workflows/composer-require-checker.yml@master + uses: yiisoft/actions/.github/workflows/composer-require-checker.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 with: os: >- ['ubuntu-latest'] diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 8150499..2f07320 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -20,9 +20,12 @@ on: name: mutation test +permissions: + contents: read + jobs: mutation: - uses: yiisoft/actions/.github/workflows/roave-infection.yml@master + uses: yiisoft/actions/.github/workflows/roave-infection.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 with: os: >- ['ubuntu-latest'] diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 35411d0..da9413a 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -11,9 +11,12 @@ on: name: rector +permissions: + contents: read + jobs: rector: - uses: yiisoft/actions/.github/workflows/rector.yml@master + uses: yiisoft/actions/.github/workflows/rector.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 secrets: token: ${{ secrets.YIISOFT_GITHUB_TOKEN }} with: diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 8694d2d..c2c06a1 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -22,9 +22,12 @@ on: name: static analysis +permissions: + contents: read + jobs: psalm: - uses: yiisoft/actions/.github/workflows/psalm.yml@master + uses: yiisoft/actions/.github/workflows/psalm.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 with: os: >- ['ubuntu-latest'] From 8eb30b2ccda1671ef9fa3d74b9ad5d6b5d151f8b Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Jun 2026 02:10:21 +0300 Subject: [PATCH 35/60] Add zizmorify configuration --- .github/dependabot.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index db86156..6cc0071 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,16 +1,8 @@ version: 2 updates: - # Maintain dependencies for GitHub Actions. - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" - # Too noisy. See https://github.community/t/increase-if-necessary-for-github-actions-in-dependabot/179581 - open-pull-requests-limit: 0 - - # Maintain dependencies for Composer - - package-ecosystem: "composer" - directory: "/" - schedule: - interval: "daily" - versioning-strategy: increase-if-necessary + interval: "weekly" + cooldown: + default-days: 7 From 29a6d768beba5516a768bd565fc1be6c4cf7bbe2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Jun 2026 02:33:43 +0300 Subject: [PATCH 36/60] Add zizmorify workflow --- .github/workflows/zizmor.yml | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..e9b7e06 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,37 @@ +name: GitHub Actions Security Analysis with zizmor 🌈 + +on: + push: + branches: + - main + paths: + - '.github/**.yml' + - '.github/**.yaml' + pull_request: + paths: + - '.github/**.yml' + - '.github/**.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + zizmor: + name: Run zizmor 🌈 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run zizmor 🌈 + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + with: + advanced-security: false + annotations: true + persona: 'pedantic' From 14254b8f901ea38256ca398e92d2f13672357c65 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Jun 2026 12:02:35 +0300 Subject: [PATCH 37/60] Restore Dependabot non-GitHub-Actions updates --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6cc0071..b185d42 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,11 @@ version: 2 updates: + # Maintain dependencies for GitHub Actions. + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "daily" + versioning-strategy: increase-if-necessary - package-ecosystem: "github-actions" directory: "/" schedule: From 0ebdd185d87abcad12aef4d307a4902b340733ec Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Jun 2026 12:11:17 +0300 Subject: [PATCH 38/60] Restore Dependabot non-GitHub-Actions updates --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b185d42..988aa61 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,8 @@ version: 2 updates: # Maintain dependencies for GitHub Actions. + + # Maintain dependencies for Composer - package-ecosystem: "composer" directory: "/" schedule: From a1ee0d0dd0dc4f31f6ff32ddc80f626ae9840adf Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Jun 2026 12:18:16 +0300 Subject: [PATCH 39/60] Restore Dependabot non-GitHub-Actions updates --- .github/dependabot.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 988aa61..41b2db6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,5 @@ version: 2 updates: - # Maintain dependencies for GitHub Actions. # Maintain dependencies for Composer - package-ecosystem: "composer" From 8bb22d8e4997b5f72ab4a9cdac0fbb771105ecc2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Jun 2026 12:25:29 +0300 Subject: [PATCH 40/60] Restore Dependabot non-GitHub-Actions updates --- .github/dependabot.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 41b2db6..0088948 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,5 @@ version: 2 updates: - # Maintain dependencies for Composer - package-ecosystem: "composer" directory: "/" From 34819c28fa017305e0f9e2d2f0d21a76cd1975c2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Jun 2026 12:43:30 +0300 Subject: [PATCH 41/60] Remove Rector pull_request_target inputs --- .github/workflows/rector.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index da9413a..a62cbd6 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -17,8 +17,6 @@ permissions: jobs: rector: uses: yiisoft/actions/.github/workflows/rector.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 - secrets: - token: ${{ secrets.YIISOFT_GITHUB_TOKEN }} with: os: >- ['ubuntu-latest'] From 954fd6442a243ed23d56c7855512ff92a42ee752 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Jun 2026 12:43:34 +0300 Subject: [PATCH 42/60] Remove Rector pull_request_target inputs --- .github/workflows/rector.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 20f7d89..136b0d1 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -14,10 +14,7 @@ name: rector jobs: rector: uses: yiisoft/actions/.github/workflows/rector.yml@master - secrets: - token: ${{ secrets.YIISOFT_GITHUB_TOKEN }} with: - repository: ${{ github.event.pull_request.head.repo.full_name }} os: >- ['ubuntu-latest'] php: >- From a67e2cc707239a4dcfe9616d875a8ab74ffb46b9 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 20 Jun 2026 11:40:37 +0300 Subject: [PATCH 43/60] Use master for yiisoft actions --- .github/workflows/bc.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/composer-require-checker.yml | 2 +- .github/workflows/mutation.yml | 2 +- .github/workflows/rector.yml | 2 +- .github/workflows/static.yml | 2 +- .github/zizmor.yml | 5 +++++ 7 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .github/zizmor.yml diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index bdfe214..b269391 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -28,7 +28,7 @@ permissions: jobs: roave_bc_check: - uses: yiisoft/actions/.github/workflows/bc.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 + uses: yiisoft/actions/.github/workflows/bc.yml@master with: os: >- ['ubuntu-latest'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d30d533..d3a7abc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ permissions: jobs: phpunit: - uses: yiisoft/actions/.github/workflows/phpunit.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 + uses: yiisoft/actions/.github/workflows/phpunit.yml@master with: os: >- ['ubuntu-latest', 'windows-latest'] diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index e231b98..6c2e161 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -29,7 +29,7 @@ permissions: jobs: composer-require-checker: - uses: yiisoft/actions/.github/workflows/composer-require-checker.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 + uses: yiisoft/actions/.github/workflows/composer-require-checker.yml@master with: os: >- ['ubuntu-latest'] diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 2f07320..1f7ad3d 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -25,7 +25,7 @@ permissions: jobs: mutation: - uses: yiisoft/actions/.github/workflows/roave-infection.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 + uses: yiisoft/actions/.github/workflows/roave-infection.yml@master with: os: >- ['ubuntu-latest'] diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index a62cbd6..98a6ff7 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -16,7 +16,7 @@ permissions: jobs: rector: - uses: yiisoft/actions/.github/workflows/rector.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 + uses: yiisoft/actions/.github/workflows/rector.yml@master with: os: >- ['ubuntu-latest'] diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index c2c06a1..f176c8e 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -27,7 +27,7 @@ permissions: jobs: psalm: - uses: yiisoft/actions/.github/workflows/psalm.yml@ab62d6b3b0e0cff6c9724ec5a395bedb41c639a2 + uses: yiisoft/actions/.github/workflows/psalm.yml@master with: os: >- ['ubuntu-latest'] diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..85ca798 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,5 @@ +rules: + unpinned-uses: + config: + policies: + "yiisoft/*": any From dad98de2594e1de9ba8b6a1fcf47f5436bae5a06 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 20 Jun 2026 11:40:42 +0300 Subject: [PATCH 44/60] Use master for yiisoft actions --- .github/zizmor.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/zizmor.yml diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..85ca798 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,5 @@ +rules: + unpinned-uses: + config: + policies: + "yiisoft/*": any From c62e98997335659d6a88fdd773e1fce95891bfa2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 20 Jun 2026 12:10:13 +0300 Subject: [PATCH 45/60] Use master for yiisoft actions --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index db86156..7da1f95 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,8 @@ updates: open-pull-requests-limit: 0 # Maintain dependencies for Composer + ignore: + - dependency-name: "yiisoft/*" - package-ecosystem: "composer" directory: "/" schedule: From a483badbd02144672e46dad84327a825027a1ac5 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 20 Jun 2026 12:10:18 +0300 Subject: [PATCH 46/60] Use master for yiisoft actions --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0088948..0510f85 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,5 @@ updates: interval: "weekly" cooldown: default-days: 7 + ignore: + - dependency-name: "yiisoft/*" From dedfc9d0880163eb7c4952cbe01a7092e6a1808e Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 21 Jun 2026 15:06:49 +0300 Subject: [PATCH 47/60] Remove redundant zizmor config --- .github/zizmor.yml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .github/zizmor.yml diff --git a/.github/zizmor.yml b/.github/zizmor.yml deleted file mode 100644 index 85ca798..0000000 --- a/.github/zizmor.yml +++ /dev/null @@ -1,5 +0,0 @@ -rules: - unpinned-uses: - config: - policies: - "yiisoft/*": any From 34fd79af71f429defca053e172f02d6f15bb5cdd Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 24 Jun 2026 16:07:43 +0300 Subject: [PATCH 48/60] Remove redundant dependabot change --- .github/dependabot.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7da1f95..db86156 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,8 +9,6 @@ updates: open-pull-requests-limit: 0 # Maintain dependencies for Composer - ignore: - - dependency-name: "yiisoft/*" - package-ecosystem: "composer" directory: "/" schedule: From 9a1c47ea64a91fc10fe368b94be8538231cd5710 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 24 Jun 2026 16:38:20 +0300 Subject: [PATCH 49/60] Revert "Remove redundant dependabot change" This reverts commit 34fd79af71f429defca053e172f02d6f15bb5cdd. --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index db86156..7da1f95 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,8 @@ updates: open-pull-requests-limit: 0 # Maintain dependencies for Composer + ignore: + - dependency-name: "yiisoft/*" - package-ecosystem: "composer" directory: "/" schedule: From 76284671a85c898624d2f5437c0f1241c57a4441 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 24 Jun 2026 18:00:51 +0300 Subject: [PATCH 50/60] Normalize Dependabot GitHub Actions updates --- .github/dependabot.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2796240..6d4f88a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,18 +1,8 @@ version: 2 updates: - # Maintain dependencies for Composer - ignore: - - dependency-name: "yiisoft/*" - - package-ecosystem: "composer" + # Maintain dependencies for GitHub Actions. + - package-ecosystem: "github-actions"\n directory: "/"\n schedule:\n interval: "weekly"\n cooldown:\n default-days: 7\n ignore:\n - dependency-name: "yiisoft/*"\n - package-ecosystem: "composer" directory: "/" schedule: interval: "daily" versioning-strategy: increase-if-necessary - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - cooldown: - default-days: 7 - ignore: - - dependency-name: "yiisoft/*" From 73e5a594f70c0e1f48849fb483a66b9ecdf160b9 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 24 Jun 2026 22:40:08 +0300 Subject: [PATCH 51/60] Fix Dependabot GitHub Actions updates --- .github/dependabot.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6d4f88a..a47ec9b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,17 @@ version: 2 updates: # Maintain dependencies for GitHub Actions. - - package-ecosystem: "github-actions"\n directory: "/"\n schedule:\n interval: "weekly"\n cooldown:\n default-days: 7\n ignore:\n - dependency-name: "yiisoft/*"\n - package-ecosystem: "composer" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + ignore: + - dependency-name: "yiisoft/*" + + # Maintain dependencies for Composer + - package-ecosystem: "composer" directory: "/" schedule: interval: "daily" From af6dcf4396ecacf04bd7c48b1d04d6e6f3ee5413 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 25 Jun 2026 00:46:18 +0300 Subject: [PATCH 52/60] Fix zizmor workflow findings --- .github/dependabot.yml | 2 ++ .github/workflows/zizmor.yml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a47ec9b..10f7e30 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,4 +15,6 @@ updates: directory: "/" schedule: interval: "daily" + cooldown: + default-days: 7 versioning-strategy: increase-if-necessary diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 9465846..430255d 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -14,8 +14,8 @@ on: - '.github/**.yaml' permissions: - actions: read - contents: read + actions: read # Required by zizmor when reading workflow metadata through the API. + contents: read # Required to read workflow files. jobs: zizmor: From 521853ed749e181f551f0581ce41b60b22eac049 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 2 Jul 2026 23:31:48 +0300 Subject: [PATCH 53/60] Fix hardened workflow checks --- .github/workflows/rector.yml | 2 +- .github/workflows/zizmor.yml | 30 ++++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 2e4ba9e..69e39cc 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -1,5 +1,5 @@ on: - pull_request_target: + pull_request: paths-ignore: - 'docs/**' - 'README.md' diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 430255d..0861478 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -1,4 +1,4 @@ -name: GitHub Actions Security Analysis with zizmor 🌈 +name: GitHub Actions Security Analysis with zizmor on: push: @@ -19,4 +19,30 @@ permissions: jobs: zizmor: - uses: yiisoft/actions/.github/workflows/zizmor.yml@master + name: Run zizmor + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Create zizmor configuration + run: | + cat > .zizmor-shared.yml <<'YAML' + rules: + unpinned-uses: + config: + policies: + "yiisoft/*": any + YAML + + - name: Run zizmor + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + with: + advanced-security: false + annotations: true + config: .zizmor-shared.yml + inputs: .github + min-severity: high + persona: 'pedantic' From 5c4e8998cf1c5d87cd714cdc3a3d20330c871910 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 3 Jul 2026 00:07:58 +0300 Subject: [PATCH 54/60] Use shared zizmor workflow --- .github/workflows/zizmor.yml | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 0861478..430255d 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -1,4 +1,4 @@ -name: GitHub Actions Security Analysis with zizmor +name: GitHub Actions Security Analysis with zizmor 🌈 on: push: @@ -19,30 +19,4 @@ permissions: jobs: zizmor: - name: Run zizmor - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Create zizmor configuration - run: | - cat > .zizmor-shared.yml <<'YAML' - rules: - unpinned-uses: - config: - policies: - "yiisoft/*": any - YAML - - - name: Run zizmor - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 - with: - advanced-security: false - annotations: true - config: .zizmor-shared.yml - inputs: .github - min-severity: high - persona: 'pedantic' + uses: yiisoft/actions/.github/workflows/zizmor.yml@master From 8936c1f9a5177c023044f745190b56fbd1140563 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 3 Jul 2026 01:41:08 +0300 Subject: [PATCH 55/60] Run BC check on PHP 8.4 --- .github/workflows/bc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index b269391..0577a84 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -33,4 +33,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1'] + ['8.4'] From bf37e0ff6010c02c8f69d79be54d2816704171a9 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Mon, 6 Jul 2026 13:32:41 +0300 Subject: [PATCH 56/60] Revert PHP changes --- rector.php | 27 ++- src/AbstractCombinedRegexp.php | 7 - src/CombinedRegexp.php | 4 +- src/Inflector.php | 89 +++----- src/MemoizedCombinedRegexp.php | 2 +- src/NumericHelper.php | 120 +---------- src/StringHelper.php | 139 ++---------- src/WildcardPattern.php | 2 - ...ase.php => AbstractCombinedRegexpTest.php} | 19 +- tests/CombinedRegexpTest.php | 2 +- tests/InflectorTest.php | 34 ++- tests/IsOneOfAssert.php | 2 +- tests/MemoizedCombinedRegexpTest.php | 2 +- tests/NumericHelperTest.php | 132 +----------- tests/StringHelper/MatchAnyRegexTest.php | 45 ---- tests/StringHelperTest.php | 198 ++++++++---------- tests/Support/StringableObject.php | 2 +- tests/WildcardPatternTest.php | 25 ++- tests/benchmarks/CombinedRegexpBench.php | 75 ------- tests/benchmarks/InflectorBench.php | 109 ---------- tests/benchmarks/NumericHelperBench.php | 48 ----- tests/benchmarks/StringHelperBench.php | 103 --------- tests/benchmarks/WildcardPatternBench.php | 43 ---- 23 files changed, 199 insertions(+), 1030 deletions(-) rename tests/{AbstractCombinedRegexpTestCase.php => AbstractCombinedRegexpTest.php} (95%) delete mode 100644 tests/StringHelper/MatchAnyRegexTest.php delete mode 100644 tests/benchmarks/CombinedRegexpBench.php delete mode 100644 tests/benchmarks/InflectorBench.php delete mode 100644 tests/benchmarks/NumericHelperBench.php delete mode 100644 tests/benchmarks/StringHelperBench.php delete mode 100644 tests/benchmarks/WildcardPatternBench.php diff --git a/rector.php b/rector.php index 1f8a952..c80d86e 100644 --- a/rector.php +++ b/rector.php @@ -4,19 +4,26 @@ use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector; use Rector\Config\RectorConfig; +use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector; use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector; -use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; +use Rector\Set\ValueObject\LevelSetList; -return RectorConfig::configure() - ->withPaths([ +return static function (RectorConfig $rectorConfig): void { + $rectorConfig->paths([ __DIR__ . '/src', __DIR__ . '/tests', - ]) - ->withPhpSets(php81: true) - ->withRules([ - InlineConstructorDefaultToPropertyRector::class, - ]) - ->withSkip([ + ]); + + // register a single rule + $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); + + // define sets of rules + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_80, + ]); + + $rectorConfig->skip([ ClosureToArrowFunctionRector::class, - NullToStrictStringFuncCallArgRector::class, + JsonThrowOnErrorRector::class, ]); +}; diff --git a/src/AbstractCombinedRegexp.php b/src/AbstractCombinedRegexp.php index 169172c..e40c91f 100644 --- a/src/AbstractCombinedRegexp.php +++ b/src/AbstractCombinedRegexp.php @@ -15,14 +15,7 @@ */ abstract class AbstractCombinedRegexp { - /** - * @psalm-suppress MissingClassConstType - */ public const REGEXP_DELIMITER = '/'; - - /** - * @psalm-suppress MissingClassConstType - */ public const QUOTE_REPLACER = '\\/'; /** diff --git a/src/CombinedRegexp.php b/src/CombinedRegexp.php index 81fc72c..cba76d2 100644 --- a/src/CombinedRegexp.php +++ b/src/CombinedRegexp.php @@ -28,7 +28,7 @@ final class CombinedRegexp extends AbstractCombinedRegexp /** * @psalm-var non-empty-string */ - private readonly string $compiledPattern; + private string $compiledPattern; /** * @param string[] $patterns Regular expressions to combine. @@ -36,7 +36,7 @@ final class CombinedRegexp extends AbstractCombinedRegexp */ public function __construct( array $patterns, - private readonly string $flags = '' + private string $flags = '' ) { if (empty($patterns)) { throw new InvalidArgumentException('At least one pattern should be specified.'); diff --git a/src/Inflector.php b/src/Inflector.php index beba938..07678d0 100644 --- a/src/Inflector.php +++ b/src/Inflector.php @@ -101,9 +101,7 @@ final class Inflector '/(alias)$/i' => '\1es', '/(ax|cris|test)is$/i' => '\1es', '/(currenc)y$/' => '\1ies', - '/(tion|sion|gion)$/i' => '\1s', - '/menon$/i' => 'mena', - '/maton$/i' => 'mata', + '/on$/i' => 'a', '/s$/' => 's', '/^$/' => '', '/$/' => 's', @@ -147,8 +145,6 @@ final class Inflector '/(^analy)ses$/i' => '\1sis', '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', '/criteria$/i' => 'criterion', - '/mena$/i' => 'menon', - '/mata$/i' => 'maton', '/([ti])a$/i' => '\1um', '/(p)eople$/i' => '\1\2erson', '/(m)en$/i' => '\1an', @@ -213,7 +209,6 @@ final class Inflector 'corps' => 'corps', 'corpus' => 'corpuses', 'cow' => 'cows', - 'criterion' => 'criteria', 'curve' => 'curves', 'debris' => 'debris', 'diabetes' => 'diabetes', @@ -440,7 +435,6 @@ public function toPlural(string $input): string } foreach ($this->pluralizeRules as $rule => $replacement) { if (preg_match($rule, $input)) { - /** @var string `$rule` and `$replacement` always correct, so `preg_replace` always returns string */ return preg_replace($rule, $replacement, $input); } } @@ -465,7 +459,6 @@ public function toSingular(string $input): string foreach ($this->singularizeRules as $rule => $replacement) { if (preg_match($rule, $input)) { - /** @var string `$rule` and `$replacement` always correct, so `preg_replace` always returns string */ return preg_replace($rule, $replacement, $input); } } @@ -495,21 +488,17 @@ public function toSentence(string $input, bool $uppercaseAll = false): string * Converts a string into space-separated words. * For example, 'PostTag' will be converted to 'Post Tag'. * - * @param string $input The valid UTF-8 string to be converted. + * @param string $input The string to be converted. * * @return string The resulting words. */ public function toWords(string $input): string { - /** - * @var string $words We assume that `$input` is valid UTF-8 string, so `preg_replace()` never returns `false`. - */ - $words = preg_replace('/(?pascalCaseToId($input, '_', $strict); + return $this->pascalCaseToId(preg_replace('/[^\pL\pN]+/u', '_', $input), '_', $strict); } /** @@ -660,8 +635,8 @@ public function tableToClass(string $tableName): string * and removes the rest. You may customize characters map via $transliteration property * of the helper. * - * @param string $input An arbitrary valid UTF-8 string to convert. - * @param string $replacement The replacement to use for spaces. It must be valid UTF-8 string. + * @param string $input An arbitrary string to convert. + * @param string $replacement The replacement to use for spaces. * @param bool $lowercase whether to return the string in lowercase or not. Defaults to `true`. * * @return string The converted string. @@ -669,21 +644,9 @@ public function tableToClass(string $tableName): string public function toSlug(string $input, string $replacement = '-', bool $lowercase = true): string { $quotedReplacement = preg_quote($replacement, '/'); - - /** - * Replace all non-words character - * - * @var string $input We assume that `$input` and `$replacement` are valid UTF-8 strings, so `preg_replace()` - * never returns `false`. - */ + // replace all non words character $input = preg_replace('/[^a-zA-Z0-9]+/u', $replacement, $this->toTransliterated($input)); - - /** - * Remove first and last replacements - * - * @var string $input We assume that `$input` and `$quotedReplacement` are valid UTF-8 strings, so - * `preg_replace()` never returns `false`. - */ + // remove first and last replacements $input = preg_replace( "/^(?:$quotedReplacement)+|(?:$quotedReplacement)+$/u" . ($lowercase ? 'i' : ''), '', @@ -702,10 +665,12 @@ public function toSlug(string $input, string $replacement = '-', bool $lowercase * * @noinspection PhpComposerExtensionStubsInspection * - * @param string $input Input string. It must be valid UTF-8 string. + * @param string $input Input string. * @param string|Transliterator|null $transliterator either a {@see \Transliterator} or a string * from which a {@see \Transliterator} can be built. If null, value set with {@see withTransliterator()} * or {@see TRANSLITERATE_LOOSE} is used. + * + * @return string */ public function toTransliterated(string $input, $transliterator = null): string { @@ -714,11 +679,7 @@ public function toTransliterated(string $input, $transliterator = null): string $transliterator = $this->transliterator; } - /** - * @noinspection PhpComposerExtensionStubsInspection - * @var string We assume that `$input` are valid UTF-8 strings and `$transliterator` is valid, so - * `preg_replace()` never returns `false`. - */ + /* @noinspection PhpComposerExtensionStubsInspection */ return transliterator_transliterate($transliterator, $input); } diff --git a/src/MemoizedCombinedRegexp.php b/src/MemoizedCombinedRegexp.php index f366462..1e49245 100644 --- a/src/MemoizedCombinedRegexp.php +++ b/src/MemoizedCombinedRegexp.php @@ -18,7 +18,7 @@ final class MemoizedCombinedRegexp extends AbstractCombinedRegexp private array $results = []; public function __construct( - private readonly AbstractCombinedRegexp $decorated, + private AbstractCombinedRegexp $decorated, ) { } diff --git a/src/NumericHelper.php b/src/NumericHelper.php index bf68d64..9a665cd 100644 --- a/src/NumericHelper.php +++ b/src/NumericHelper.php @@ -14,48 +14,14 @@ use function is_bool; use function is_numeric; use function is_scalar; -use function preg_match; use function preg_replace; use function str_replace; -use function substr; /** * Provides static methods to work with numeric strings. */ final class NumericHelper { - /** - * @psalm-var array> - */ - private const FILESYSTEM_SIZE_POSTFIXES = [ - 3 => [ - 'KiB' => 1024, - 'MiB' => 1048576, - 'GiB' => 1073741824, - 'TiB' => 1099511627776, - 'PiB' => 1125899906842624, - ], - 2 => [ - 'kB' => 1000, - 'MB' => 1000000, - 'GB' => 1000000000, - 'TB' => 1000000000000, - 'PB' => 1000000000000000, - ], - 1 => [ - 'k' => 1024, - 'K' => 1024, - 'm' => 1048576, - 'M' => 1048576, - 'g' => 1073741824, - 'G' => 1073741824, - 't' => 1099511627776, - 'T' => 1099511627776, - 'p' => 1125899906842624, - 'P' => 1125899906842624, - ], - ]; - /** * Converts number to its ordinal English form. For example, converts 13 to 13th, 2 to 2nd etc. * @@ -87,7 +53,7 @@ public static function toOrdinal(mixed $value): string /** * Returns string representation of a number value without thousands separators and with dot as decimal separator. * - * @param bool|float|int|string|Stringable $value String in `string` or `Stringable` must be valid UTF-8 string. + * @param bool|float|int|string|Stringable $value * * @throws InvalidArgumentException if value is not scalar. */ @@ -103,11 +69,8 @@ public static function normalize(mixed $value): string return $value ? '1' : '0'; } - $value = str_replace([' ', ','], ['', '.'], (string) $value); + $value = str_replace([' ', ','], ['', '.'], (string)$value); - /** - * @var string We assume that `$value` is valid UTF-8 string, so `preg_replace()` never returns `false`. - */ return preg_replace('/\.(?=.*\.)/', '', $value); } @@ -120,83 +83,4 @@ public static function isInteger(mixed $value): bool { return filter_var($value, FILTER_VALIDATE_INT) !== false; } - - /** - * Converts human-readable size to bytes. - * - * @param string $string Human readable size. Examples: `1024`, `1kB`, `1.5M`, `1GiB`. Full - * list of supported postfixes in {@see FILESYSTEM_SIZE_POSTFIXES}. - - * Note: This parameter must be less than `8192P` on 64-bit systems and `2G` on 32-bit systems. - * - * @throws InvalidArgumentException when the string is invalid. - * - * @return int The number of bytes equivalent to the specified string. - * - * @see https://www.gnu.org/software/coreutils/manual/html_node/Block-size.html - */ - public static function convertHumanReadableSizeToBytes(string $string): int - { - if (is_numeric($string)) { - return (int) $string; - } - - foreach (self::FILESYSTEM_SIZE_POSTFIXES as $postfixLength => $postfixes) { - $postfix = substr($string, -$postfixLength); - if ($postfix === '' || preg_match('/\\d/', $postfix) === 1) { - continue; - } - - $numericPart = substr($string, 0, -$postfixLength); - if (!is_numeric($numericPart)) { - throw new InvalidArgumentException("Incorrect input string: $string"); - } - - $postfixMultiplier = $postfixes[$postfix] ?? null; - if ($postfixMultiplier === null) { - throw new InvalidArgumentException("Not supported postfix '$postfix' in input string: $string"); - } - - return (int) ((float) $numericPart * $postfixMultiplier); - } - - throw new InvalidArgumentException("Incorrect input string: $string"); - } - - /** - * Trims spaces and trailing decimal zeros from a numeric string. - * - * If the fractional part consists only of zeros, the decimal dot separator is removed as well. - * The value that is `null` is returned as-is. - * - * @param string|null $value Numeric string or null. - * - * @return string|null The input string with spaces, trailing decimal zeros (and a trailing decimal - * dot separator, if any) removed, or `null` if the input was `null`. - * - * @see is_numeric() - */ - public static function trimDecimalZeros(?string $value): ?string - { - if ($value === null) { - return null; - } - - if (!is_numeric($value)) { - throw new InvalidArgumentException( - sprintf('Value must be numeric string or null. "%s" given.', $value) - ); - } - - $value = trim($value); - - if (!str_contains($value, '.')) { - return $value; - } - - $value = rtrim($value, '0'); - $value = rtrim($value, '.'); - - return $value ?: '0'; - } } diff --git a/src/StringHelper.php b/src/StringHelper.php index 2f4dd77..75a1bf4 100644 --- a/src/StringHelper.php +++ b/src/StringHelper.php @@ -15,7 +15,6 @@ use function implode; use function max; use function mb_strlen; -use function mb_strpos; use function mb_strrpos; use function mb_strtolower; use function mb_strtoupper; @@ -68,7 +67,7 @@ public static function byteLength(string|null $input): int * * @see https://www.php.net/manual/en/function.substr.php */ - public static function byteSubstring(string $input, int $start, ?int $length = null): string + public static function byteSubstring(string $input, int $start, int $length = null): string { return mb_substr($input, $start, $length ?? mb_strlen($input, '8bit'), '8bit'); } @@ -134,7 +133,7 @@ public static function directoryName(string $path): string * * @see https://php.net/manual/en/function.mb-substr.php */ - public static function substring(string $string, int $start, ?int $length = null, string $encoding = 'UTF-8'): string + public static function substring(string $string, int $start, int $length = null, string $encoding = 'UTF-8'): string { return mb_substr($string, $start, $length, $encoding); } @@ -224,7 +223,7 @@ public static function startsWithIgnoringCase(string $input, string|null $with): * Binary and multibyte safe. * * @param string $input Input string to check. - * @param string|null $with Part to search inside the $string. + * @param string|null $with Part to search inside of the $string. * * @return bool Returns true if first input ends with second input, false otherwise. */ @@ -238,7 +237,7 @@ public static function endsWith(string $input, string|null $with): bool * Binary and multibyte safe. * * @param string $input Input string to check. - * @param string|null $with Part to search inside the $string. + * @param string|null $with Part to search inside of the $string. * * @return bool Returns true if first input ends with second input, false otherwise. */ @@ -343,50 +342,6 @@ public static function truncateWords(string $input, int $count, string $trimMark return $input; } - /** - * Truncates a string to the specified character length while preserving word boundaries. - * - * @param string $input The string to truncate. - * @param int $length Maximum length of the truncated string including trim marker. - * @param string $trimMarker String to append to the end of truncated string. - * @param string $encoding The encoding to use, defaults to "UTF-8". - * - * @return string The truncated string. - */ - public static function truncateWordsByLength( - string $input, - int $length, - string $trimMarker = '…', - string $encoding = 'UTF-8', - ): string { - $input = trim($input); - if ($input === '') { - return ''; - } - - if (mb_strlen($input, $encoding) <= $length) { - return $input; - } - - $markerLength = mb_strlen($trimMarker, $encoding); - if ($length <= $markerLength) { - return mb_substr($trimMarker, 0, $length, $encoding); - } - - $truncated = mb_substr($input, 0, $length - $markerLength, $encoding); - - // Prefer not to break words if there's a space within the snippet. - $lastSpace = mb_strrpos($truncated, ' ', 0, $encoding); - if ($lastSpace !== false) { - $cut = rtrim(mb_substr($truncated, 0, $lastSpace, $encoding)); - return $cut === '' - ? mb_substr($trimMarker, 0, $length, $encoding) - : $cut . $trimMarker; - } - - return $truncated . $trimMarker; - } - /** * Get string length. * @@ -455,16 +410,13 @@ public static function uppercaseFirstCharacter(string $string, string $encoding /** * Uppercase the first character of each word in a string. * - * @param string $string The valid UTF-8 string to be processed. + * @param string $string The string to be processed. * @param string $encoding The encoding to use, defaults to "UTF-8". * * @see https://php.net/manual/en/function.ucwords.php */ public static function uppercaseFirstCharacterInEachWord(string $string, string $encoding = 'UTF-8'): string { - /** - * @var array $words We assume that `$string` is valid UTF-8 string, so `preg_split()` never returns `false`. - */ $words = preg_split('/\s/u', $string, -1, PREG_SPLIT_NO_EMPTY); $wordsWithUppercaseFirstCharacter = array_map( @@ -486,10 +438,6 @@ public static function uppercaseFirstCharacterInEachWord(string $string, string * @param string $input The string to encode. * * @return string Encoded string. - * - * @psalm-template T as string - * @psalm-param T $input - * @psalm-return (T is non-empty-string ? non-empty-string : "") */ public static function base64UrlEncode(string $input): string { @@ -512,24 +460,15 @@ public static function base64UrlDecode(string $input): string /** * Split a string to array with non-empty lines. - * Whitespace from the beginning and end of each line will be stripped. + * Whitespace from the beginning and end of a each line will be stripped. * - * @param string $string The input string. It must be valid UTF-8 string. + * @param string $string The input string. * @param string $separator The boundary string. It is a part of regular expression - * so should be taken into account or properly escaped with {@see preg_quote()}. It must be valid UTF-8 string. + * so should be taken into account or properly escaped with {@see preg_quote()}. */ public static function split(string $string, string $separator = '\R'): array { - /** - * @var string $string We assume that `$string` is valid UTF-8 string, so `preg_replace()` never returns - * `false`. - */ $string = preg_replace('(^\s*|\s*$)', '', $string); - - /** - * @var array We assume that $separator is prepared by `preg_quote()` and $string is valid UTF-8 string, - * so `preg_split()` never returns `false`. - */ return preg_split('~\s*' . $separator . '\s*~u', $string, -1, PREG_SPLIT_NO_EMPTY); } @@ -546,7 +485,7 @@ public static function split(string $string, string $separator = '\R'): array * * @return string[] * - * @psalm-return non-empty-list + * @psalm-return list */ public static function parsePath( string $path, @@ -567,7 +506,7 @@ public static function parsePath( } if ($path === '') { - return ['']; + return []; } if (!str_contains($path, $delimiter)) { @@ -637,13 +576,8 @@ public static function parsePath( */ public static function trim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array { - self::ensureUtf8String($string); self::ensureUtf8Pattern($pattern); - /** - * @var string|string[] `$string` is correct UTF-8 string and `$pattern` is correct (it should be passed - * already prepared), so `preg_replace` never returns `null`. - */ return preg_replace("#^[$pattern]+|[$pattern]+$#uD", '', $string); } @@ -665,13 +599,8 @@ public static function trim(string|array $string, string $pattern = self::DEFAUL */ public static function ltrim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array { - self::ensureUtf8String($string); self::ensureUtf8Pattern($pattern); - /** - * @var string|string[] `$string` is correct UTF-8 string and `$pattern` is correct (it should be passed - * already prepared), so `preg_replace` never returns `null`. - */ return preg_replace("#^[$pattern]+#u", '', $string); } @@ -693,13 +622,8 @@ public static function ltrim(string|array $string, string $pattern = self::DEFAU */ public static function rtrim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array { - self::ensureUtf8String($string); self::ensureUtf8Pattern($pattern); - /** - * @var string|string[] `$string` is correct UTF-8 string and `$pattern` is correct (it should be passed - * already prepared), so `preg_replace` never returns `null`. - */ return preg_replace("#[$pattern]+$#uD", '', $string); } @@ -802,30 +726,9 @@ public static function findBetweenLast(string $string, string $start, ?string $e } /** - * Checks if a given string matches any of the provided patterns. + * Ensure the input string is a valid UTF-8 string. * - * Note that patterns should be provided without delimiters on both sides. For example, `te(s|x)t`. - * - * @see https://www.php.net/manual/reference.pcre.pattern.syntax.php - * @see https://www.php.net/manual/reference.pcre.pattern.modifiers.php - * - * @param string $string The string to match against the patterns. - * @param string[] $patterns Regular expressions without delimiters on both sides. - * @param string $flags Flags to apply to all regular expressions. - */ - public static function matchAnyRegex(string $string, array $patterns, string $flags = ''): bool - { - if (empty($patterns)) { - return false; - } - - return (new CombinedRegexp($patterns, $flags))->matches($string); - } - - /** - * Ensure the pattern is a valid UTF-8 string. - * - * @param string $pattern The pattern. + * @param string $pattern The input string. * * @throws InvalidArgumentException */ @@ -835,22 +738,4 @@ private static function ensureUtf8Pattern(string $pattern): void throw new InvalidArgumentException('Pattern is not a valid UTF-8 string.'); } } - - /** - * Ensure the string is a valid UTF-8 string. - * - * @param array|string $string The string. - * - * @throws InvalidArgumentException - * - * @psalm-param string|string[] $string - */ - private static function ensureUtf8String(string|array $string): void - { - foreach ((array) $string as $s) { - if (!preg_match('##u', $s)) { - throw new InvalidArgumentException('String is not a valid UTF-8 string.'); - } - } - } } diff --git a/src/WildcardPattern.php b/src/WildcardPattern.php index f5b0e27..95e5206 100644 --- a/src/WildcardPattern.php +++ b/src/WildcardPattern.php @@ -80,7 +80,6 @@ public function ignoreCase(bool $flag = true): self */ public static function isDynamic(string $pattern): bool { - /** @var string $pattern `$rule` and `$replacement` always correct, so `preg_replace` always returns string */ $pattern = preg_replace('/\\\\./', '', $pattern); return preg_match('/[*{?\[]/', $pattern) === 1; } @@ -94,7 +93,6 @@ public static function isDynamic(string $pattern): bool */ public static function quote(string $string): string { - /** @var string `$rule` and `$replacement` always correct, so `preg_replace` always returns string */ return preg_replace('#([\\\\?*\\[\\]])#', '\\\\$1', $string); } diff --git a/tests/AbstractCombinedRegexpTestCase.php b/tests/AbstractCombinedRegexpTest.php similarity index 95% rename from tests/AbstractCombinedRegexpTestCase.php rename to tests/AbstractCombinedRegexpTest.php index bf06246..178d075 100644 --- a/tests/AbstractCombinedRegexpTestCase.php +++ b/tests/AbstractCombinedRegexpTest.php @@ -4,13 +4,14 @@ namespace Yiisoft\Strings\Tests; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\AbstractCombinedRegexp; -abstract class AbstractCombinedRegexpTestCase extends TestCase +abstract class AbstractCombinedRegexpTest extends TestCase { - #[DataProvider('dataMatchAny')] + /** + * @dataProvider dataMatchAny + */ public function testMatchAny(array $patterns, string $string, bool $expectedResult): void { $regexp = $this->createCombinedRegexp($patterns); @@ -105,7 +106,9 @@ public static function dataMatchAny(): iterable ]; } - #[DataProvider('dataMatchingPattern')] + /** + * @dataProvider dataMatchingPattern + */ public function testMatchingPattern(array $patterns, string $string, string $expectedResult): void { $regexp = $this->createCombinedRegexp($patterns); @@ -150,7 +153,9 @@ public static function dataMatchingPattern(): iterable ]; } - #[DataProvider('dataMatchingPatternPosition')] + /** + * @dataProvider dataMatchingPatternPosition + */ public function testMatchingPatternPosition(array $patterns, string $string, int $expectedResult): void { $regexp = $this->createCombinedRegexp($patterns); @@ -195,7 +200,9 @@ public static function dataMatchingPatternPosition(): iterable ]; } - #[DataProvider('dataMatchDifferentDelimiters')] + /** + * @dataProvider dataMatchDifferentDelimiters + */ public function testMatchDifferentDelimiters( array $patterns, string $flags, diff --git a/tests/CombinedRegexpTest.php b/tests/CombinedRegexpTest.php index aa9a4ed..7a59dd2 100644 --- a/tests/CombinedRegexpTest.php +++ b/tests/CombinedRegexpTest.php @@ -7,7 +7,7 @@ use Yiisoft\Strings\AbstractCombinedRegexp; use Yiisoft\Strings\CombinedRegexp; -final class CombinedRegexpTest extends AbstractCombinedRegexpTestCase +final class CombinedRegexpTest extends AbstractCombinedRegexpTest { protected function createCombinedRegexp(array $patterns, string $flags = ''): AbstractCombinedRegexp { diff --git a/tests/InflectorTest.php b/tests/InflectorTest.php index 2b17ade..68cf25b 100644 --- a/tests/InflectorTest.php +++ b/tests/InflectorTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\Strings\Tests; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\Inflector; @@ -39,21 +38,6 @@ private function getTestDataForToPlural(): array 'analysis' => 'analyses', 'datum' => 'data', 'schema' => 'schemas', - 'nation' => 'nations', - 'action' => 'actions', - 'creation' => 'creations', - 'decision' => 'decisions', - 'vision' => 'visions', - 'explosion' => 'explosions', - 'legion' => 'legions', - 'region' => 'regions', - 'information' => 'information', // special case - already ends in 'tion' but is uncountable - 'education' => 'educations', - 'attention' => 'attentions', - 'function' => 'functions', - 'junction' => 'junctions', - 'baron' => 'barons', - 'demon' => 'demons', ]; } @@ -151,7 +135,9 @@ public function testToWords(): void $this->assertEquals('і це дійсно так!', $inflector->toWords('ІЦеДійсноТак!')); } - #[DataProvider('pascalCaseToIdProvider')] + /** + * @dataProvider pascalCaseToIdProvider() + */ public function testPascalCaseToId(string $expectedResult, array $arguments): void { $inflector = new Inflector(); @@ -176,7 +162,7 @@ public function testToCamelCase(): void $this->assertEquals('ひらがなHepimiz', $inflector->toCamelCase('ひらがな_hepimiz')); } - public static function dataToSnakeCase(): array + public function dataToSnakeCase(): array { return [ [['input' => 'userName'], 'user_name'], @@ -190,7 +176,9 @@ public static function dataToSnakeCase(): array ]; } - #[DataProvider('dataToSnakeCase')] + /** + * @dataProvider dataToSnakeCase + */ public function testToSnakeCase(array $arguments, string $expectedOutput): void { $inflector = new Inflector(); @@ -205,7 +193,7 @@ public function testToTable(): void $this->assertEquals('customer_tables', $inflector->classToTable('customerTable')); } - public static function toSlugCommonsDataProvider(): array + public function toSlugCommonsDataProvider(): array { return [ ['', ''], @@ -228,7 +216,9 @@ public static function toSlugCommonsDataProvider(): array ]; } - #[DataProvider('toSlugCommonsDataProvider')] + /** + * @dataProvider toSlugCommonsDataProvider + */ public function testToSlugCommons(string $input, string $expected, string $replacement = '-'): void { $inflector = new Inflector(); @@ -499,7 +489,7 @@ private function assertIsOneOf(mixed $actual, array $expected, $message = ''): v self::assertThat($actual, new IsOneOfAssert($expected), $message); } - public static function pascalCaseToIdProvider(): array + public function pascalCaseToIdProvider(): array { return [ ['photo\\album-controller', ['Photo\\AlbumController', '-', false]], diff --git a/tests/IsOneOfAssert.php b/tests/IsOneOfAssert.php index 429245a..700c598 100644 --- a/tests/IsOneOfAssert.php +++ b/tests/IsOneOfAssert.php @@ -11,7 +11,7 @@ */ final class IsOneOfAssert extends Constraint { - public function __construct(private readonly array $allowedValues) + public function __construct(private array $allowedValues) { } diff --git a/tests/MemoizedCombinedRegexpTest.php b/tests/MemoizedCombinedRegexpTest.php index e406f0e..138e385 100644 --- a/tests/MemoizedCombinedRegexpTest.php +++ b/tests/MemoizedCombinedRegexpTest.php @@ -8,7 +8,7 @@ use Yiisoft\Strings\CombinedRegexp; use Yiisoft\Strings\MemoizedCombinedRegexp; -final class MemoizedCombinedRegexpTest extends AbstractCombinedRegexpTestCase +final class MemoizedCombinedRegexpTest extends AbstractCombinedRegexpTest { protected function createCombinedRegexp(array $patterns, string $flags = ''): AbstractCombinedRegexp { diff --git a/tests/NumericHelperTest.php b/tests/NumericHelperTest.php index 460bfce..2cc509e 100644 --- a/tests/NumericHelperTest.php +++ b/tests/NumericHelperTest.php @@ -4,8 +4,6 @@ namespace Yiisoft\Strings\Tests; -use InvalidArgumentException; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\NumericHelper; use Yiisoft\Strings\Tests\Support\StringableObject; @@ -36,11 +34,11 @@ public function testToOrdinal(): void public function testToOrdinalWithIncorrectType(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); NumericHelper::toOrdinal('bla-bla'); } - public static function dataNormalize(): array + public function dataNormalize(): array { return [ 'French' => ['4 294 967 295,000', '4294967295.000'], @@ -56,7 +54,9 @@ public static function dataNormalize(): array ]; } - #[DataProvider('dataNormalize')] + /** + * @dataProvider dataNormalize + */ public function testNormalize(mixed $input, string $expected): void { $this->assertSame($expected, NumericHelper::normalize($input)); @@ -64,11 +64,11 @@ public function testNormalize(mixed $input, string $expected): void public function testNormalizeWithIncorrectType(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); NumericHelper::normalize([]); } - public static function dataIsInteger(): array + public function dataIsInteger(): array { return [ [new \stdClass(), false], @@ -83,123 +83,11 @@ public static function dataIsInteger(): array ]; } - #[DataProvider('dataIsInteger')] + /** + * @dataProvider dataIsInteger + */ public function testIsInteger(mixed $value, bool $expected): void { $this->assertSame($expected, NumericHelper::isInteger($value)); } - - public static function dataConvertHumanReadableSizeToBytes(): array - { - return [ - // Only numbers - ['1024', 1024], - ['9223372036854775807', 9223372036854775807], - // Single-character postfix - ['512K', 524288], - ['512k', 524288], - ['2.5k', 2560], - ['2.5K', 2560], - ['128M', 134217728], - ['128m', 134217728], - ['4.5m', 4718592], - ['4.5M', 4718592], - ['2G', 2147483648], - ['2g', 2147483648], - ['2.5g', 2684354560], - ['2.5G', 2684354560], - ['1.1G', 1181116006], - ['2t', 2199023255552], - ['2T', 2199023255552], - ['6.5t', 7146825580544], - ['6.5T', 7146825580544], - ['3p', 3377699720527872], - ['3P', 3377699720527872], - ['3.5p', 3940649673949184], - ['3.5P', 3940649673949184], - // Two-character postfix - ['2kB', 2000], - ['2.5kB', 2500], - ['1MB', 1000000], - ['3.3MB', 3300000], - ['6GB', 6000000000], - ['7.4GB', 7400000000], - ['4TB', 4000000000000], - ['4.9TB', 4900000000000], - ['7PB', 7000000000000000], - ['7.7PB', 7700000000000000], - // Three-character postfix - ['512KiB', 524288], - ['2.5KiB', 2560], - ['128MiB', 134217728], - ['4.5MiB', 4718592], - ['2GiB', 2147483648], - ['2.5GiB', 2684354560], - ['2TiB', 2199023255552], - ['6.5TiB', 7146825580544], - ['3PiB', 3377699720527872], - ['3.5PiB', 3940649673949184], - ]; - } - - #[DataProvider('dataConvertHumanReadableSizeToBytes')] - public function testConvertHumanReadableSizeToBytes(string $string, int $expected): void - { - $this->assertSame($expected, NumericHelper::convertHumanReadableSizeToBytes($string)); - } - - public static function dataConvertHumanReadableSizeToBytesWithInvalidStrings(): array - { - return [ - ['12cKib', 'Incorrect input string: 12cKib'], - ['12Kcb', 'Not supported postfix \'Kcb\' in input string: 12Kcb'], - ['1c2kB', 'Incorrect input string: 1c2kB'], - ['12Kc', 'Not supported postfix \'Kc\' in input string: 12Kc'], - ['1c2k', 'Incorrect input string: 1c2k'], - ['123n', 'Not supported postfix \'n\' in input string: 123n'], - ['k', 'Incorrect input string: k'], - ['K', 'Incorrect input string: K'], - ['m', 'Incorrect input string: m'], - ['M', 'Incorrect input string: M'], - ['', 'Incorrect input string: '], - ]; - } - - #[DataProvider('dataConvertHumanReadableSizeToBytesWithInvalidStrings')] - public function testConvertHumanReadableSizeToBytesWithInvalidStrings(string $string, string $message): void - { - $this->expectExceptionObject(new InvalidArgumentException($message)); - NumericHelper::convertHumanReadableSizeToBytes($string); - } - - public static function dataTrimDecimalZeros(): array - { - return [ - 'no decimals in integer with zeros' => ['390', '390'], - 'all zeros' => ['390.000', '390'], - 'no zeros' => ['3.14', '3.14'], - 'some zeros' => ['42.010', '42.01'], - 'zeros' => ['0.0', '0'], - 'decimal only' => ['.5', '.5'], - 'decimal zero' => ['.0', '0'], - 'start with zero' => ['0.25', '0.25'], - 'negative' => ['-3.000', '-3'], - 'null' => [null, null], - 'starts with zero' => ['02471', '02471'], - 'exponent' => ['1337e0', '1337e0'], - 'spaces' => ['3.140 ', '3.14'], - ]; - } - - #[DataProvider('dataTrimDecimalZeros')] - public function testTrimDecimalZeros(?string $input, ?string $expected): void - { - $this->assertSame($expected, NumericHelper::trimDecimalZeros($input)); - } - - public function trimDecimalZerosWithNonNumericString(): void - { - $this->expectException(InvalidArgumentException::class); - NumericHelper::trimDecimalZeros('hello'); - } } diff --git a/tests/StringHelper/MatchAnyRegexTest.php b/tests/StringHelper/MatchAnyRegexTest.php deleted file mode 100644 index 5be92a3..0000000 --- a/tests/StringHelper/MatchAnyRegexTest.php +++ /dev/null @@ -1,45 +0,0 @@ -assertSame($expected, $result); - } - - /** - * @testWith [true, "test"] - * [false, "hello"] - */ - public function testWithoutFlags(bool $expected, string $string): void - { - $result = StringHelper::matchAnyRegex($string, ['te[sx]t', 'm(o|a)n']); - $this->assertSame($expected, $result); - } - - public function testWithoutPatterns(): void - { - $result = StringHelper::matchAnyRegex('test', []); - $this->assertFalse($result); - } -} diff --git a/tests/StringHelperTest.php b/tests/StringHelperTest.php index 3630743..d412145 100644 --- a/tests/StringHelperTest.php +++ b/tests/StringHelperTest.php @@ -5,8 +5,6 @@ namespace Yiisoft\Strings\Tests; use InvalidArgumentException; -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\StringHelper; @@ -120,34 +118,11 @@ public function testTruncateWords(): void $this->assertEquals('это строка с неожиданными…', StringHelper::truncateWords(' это строка с неожиданными пробелами ', 4)); } - #[DataProvider('provideTruncateWordsByLength')] - public function testTruncateWordsByLength(string $expected, string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): void - { - $this->assertSame($expected, StringHelper::truncateWordsByLength($input, $length, $trimMarker, $encoding)); - } - - public static function provideTruncateWordsByLength(): array - { - return [ - 'basic truncation' => ['Do you like drink…', 'Do you like drink coffee at work?', 20], - 'string shorter than limit should return as-is' => ['Short text', 'Short text', 20], - 'string exactly at limit should return as-is' => ['Exact length text', 'Exact length text', 17], - 'custom trim marker' => ['Do you like drink!!!', 'Do you like drink coffee at work?', 23, '!!!'], - 'multibyte characters' => ['это тестовая…', 'это тестовая multibyte строка', 15], - 'no spaces (single word) - should break the word' => ['verylongwo…', 'verylongword', 11], - 'very short limit with marker' => ['A…', 'A long sentence', 2], - 'limit with the same length as marker' => ['…', 'Some text', 1], - 'empty string' => ['', '', 10], - 'spaces that exceed limit should truncate to empty' => ['', ' ', 3], - 'text with trailing spaces should be trimmed' => ['Hello world', 'Hello world ', 15], - 'multiple words that fit exactly' => ['Hello…', 'Hello world', 6], - 'space at start should add marker' => ['Hel…', ' Hello', 4], - 'spaces should be trimmed' => ['Hello', ' Hello ', 100], - 'nested spaces should be trimmed' => ['Hello…', 'Hello World', 8], - ]; - } - - #[DataProvider('providerStartsWith')] + /** + * @dataProvider providerStartsWith + * + * @param string|null $with + */ public function testStartsWith(bool $result, string $string, ?string $with): void { $this->assertSame($result, StringHelper::startsWith($string, $with)); @@ -156,7 +131,7 @@ public function testStartsWith(bool $result, string $string, ?string $with): voi /** * Rules that should work the same for case-sensitive and case-insensitive `startsWith()`. */ - public static function providerStartsWith(): array + public function providerStartsWith(): array { return [ // positive check @@ -195,7 +170,11 @@ public function testStartsWithIgnoringCase(): void $this->assertTrue(StringHelper::startsWithIgnoringCase('anything', null)); } - #[DataProvider('providerEndsWith')] + /** + * @dataProvider providerEndsWith + * + * @param string|null $with + */ public function testEndsWith(bool $result, string $string, ?string $with): void { // case sensitive version check @@ -205,7 +184,7 @@ public function testEndsWith(bool $result, string $string, ?string $with): void /** * Rules that should work the same for case-sensitive and case-insensitive `endsWith()`. */ - public static function providerEndsWith(): array + public function providerEndsWith(): array { return [ // positive check @@ -252,32 +231,38 @@ public function testCountWords(): void $this->assertEquals(1, StringHelper::countWords(' слово ')); } - #[DataProvider('base64UrlEncodedStringsProvider')] + /** + * @dataProvider base64UrlEncodedStringsProvider + */ public function testBase64UrlEncode(string $input, string $base64UrlEncoded): void { $encoded = StringHelper::base64UrlEncode($input); $this->assertEquals($base64UrlEncoded, $encoded); } - #[DataProvider('base64UrlEncodedStringsProvider')] + /** + * @dataProvider base64UrlEncodedStringsProvider + * + * @param $output + * @param $base64UrlEncoded + */ public function testBase64UrlDecode($output, $base64UrlEncoded): void { $decoded = StringHelper::base64UrlDecode($base64UrlEncoded); $this->assertEquals($output, $decoded); } - public static function base64UrlEncodedStringsProvider(): array + public function base64UrlEncodedStringsProvider(): array { return [ 'Regular string' => ['This is an encoded string', 'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw=='], '? and _ characters' => ['subjects?_d=1', 'c3ViamVjdHM_X2Q9MQ=='], '> character' => ['subjects>_d=1', 'c3ViamVjdHM-X2Q9MQ=='], 'Unicode' => ['Это закодированная строка', '0K3RgtC-INC30LDQutC-0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw'], - 'empty string' => ['', ''], ]; } - public static function uppercaseFirstCharacterProvider(): array + public function uppercaseFirstCharacterProvider(): array { return [ ['foo', 'Foo'], @@ -288,13 +273,15 @@ public static function uppercaseFirstCharacterProvider(): array ]; } - #[DataProvider('uppercaseFirstCharacterProvider')] + /** + * @dataProvider uppercaseFirstCharacterProvider + */ public function testUppercaseFirstCharacter(string $string, string $expectedResult): void { $this->assertSame($expectedResult, StringHelper::uppercaseFirstCharacter($string)); } - public static function uppercaseFirstCharacterInEachWordProvider(): array + public function uppercaseFirstCharacterInEachWordProvider(): array { return [ 'Single word' => ['foo', 'Foo'], @@ -305,7 +292,9 @@ public static function uppercaseFirstCharacterInEachWordProvider(): array ]; } - #[DataProvider('uppercaseFirstCharacterInEachWordProvider')] + /** + * @dataProvider uppercaseFirstCharacterInEachWordProvider + */ public function testUppercaseFirstCharacterInEachWord(string $string, string $expectedResult): void { $this->assertSame($expectedResult, StringHelper::uppercaseFirstCharacterInEachWord($string)); @@ -358,7 +347,7 @@ public function testLength(): void /** * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/substr_replace.phpt */ - public static function replaceSubstringProvider(): array + public function replaceSubstringProvider(): array { return [ ['trbala ', ['try this', 'bala ', 2]], @@ -371,13 +360,15 @@ public static function replaceSubstringProvider(): array ]; } - #[DataProvider('replaceSubstringProvider')] + /** + * @dataProvider replaceSubstringProvider + */ public function testReplaceSubstring(string $expected, array $arguments): void { $this->assertSame($expected, StringHelper::replaceSubstring(...$arguments)); } - public static function dataSplit(): array + public function dataSplit(): array { return [ ['', []], @@ -419,7 +410,9 @@ public static function dataSplit(): array ]; } - #[DataProvider('dataSplit')] + /** + * @dataProvider dataSplit + */ public function testSplit(string $string, array $expected): void { $this->assertSame($expected, StringHelper::split($string)); @@ -430,7 +423,7 @@ public function testSplitWithSeparator(): void $this->assertSame(['A', 'B', 'C'], StringHelper::split(' A 2 B3C', '\d')); } - public static function dataParsePath(): array + public function dataParsePath(): array { return [ ['key1.key2.key3', '.', '\\', false, ['key1', 'key2', 'key3']], @@ -471,12 +464,14 @@ public static function dataParsePath(): array ['.key1.key2', '.', '\\', false, ['', 'key1', 'key2']], ['~key1~key2', '~', '\\', false, ['', 'key1', 'key2']], - ['', '.', '\\', false, ['']], - ['', '.', '\\', true, ['']], + ['', '.', '\\', false, []], + ['', '.', '\\', true, []], ]; } - #[DataProvider('dataParsePath')] + /** + * @dataProvider dataParsePath + */ public function testParsePath( string $path, string $delimiter, @@ -512,7 +507,7 @@ public function testParsePathWithDelimiterEqualsEscapeCharacter(): void StringHelper::parsePath('key1.key2.key3', '.', '.'); } - public static function dataInvariantTrim(): iterable + public function dataInvariantTrim(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -530,7 +525,7 @@ public static function dataInvariantTrim(): iterable ]; } - public static function dataTrim(): iterable + public function dataTrim(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -556,7 +551,7 @@ public static function dataTrim(): iterable ]; } - public static function dataLtrim(): iterable + public function dataLtrim(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -582,7 +577,7 @@ public static function dataLtrim(): iterable ]; } - public static function dataRtrim(): iterable + public function dataRtrim(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -604,7 +599,7 @@ public static function dataRtrim(): iterable ]; } - public static function dataTrimPattern(): iterable + public function dataTrimPattern(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -645,7 +640,7 @@ public static function dataTrimPattern(): iterable ]; } - public static function dataLtrimPattern(): iterable + public function dataLtrimPattern(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -671,7 +666,7 @@ public static function dataLtrimPattern(): iterable ]; } - public static function dataRtrimPattern(): iterable + public function dataRtrimPattern(): iterable { $base = 'Здесь我' . self::WS['nbsp'] . '-' . self::WS['thsp'] . 'Multibyte我' . self::WS['lsep'] . 'Строка 👍🏻'; @@ -712,40 +707,52 @@ public static function dataRtrimPattern(): iterable ]; } - #[DataProvider('dataTrim')] - #[DataProvider('dataInvariantTrim')] + /** + * @dataProvider dataInvariantTrim + * @dataProvider dataTrim + */ public function testTrim(string|array $string, string|array $expected): void { $this->assertSame($expected, StringHelper::trim($string)); } - #[DataProvider('dataLtrim')] - #[DataProvider('dataInvariantTrim')] + /** + * @dataProvider dataInvariantTrim + * @dataProvider dataLtrim + */ public function testLtrim(string|array $string, string|array $expected): void { $this->assertSame($expected, StringHelper::ltrim($string)); } - #[DataProvider('dataRtrim')] - #[DataProvider('dataInvariantTrim')] + /** + * @dataProvider dataInvariantTrim + * @dataProvider dataRtrim + */ public function testRtrim(string|array $string, string|array $expected): void { $this->assertSame($expected, StringHelper::rtrim($string)); } - #[DataProvider('dataTrimPattern')] + /** + * @dataProvider dataTrimPattern + */ public function testTrimPattern(string|array $string, string $pattern, string|array $expected): void { $this->assertSame($expected, StringHelper::trim($string, $pattern)); } - #[DataProvider('dataLtrimPattern')] + /** + * @dataProvider dataLtrimPattern + */ public function testLtrimPattern(string|array $string, string $pattern, string|array $expected): void { $this->assertSame($expected, StringHelper::ltrim($string, $pattern)); } - #[DataProvider('dataRtrimPattern')] + /** + * @dataProvider dataRtrimPattern + */ public function testRtrimPattern(string|array $string, string $pattern, string|array $expected): void { $this->assertSame($expected, StringHelper::rtrim($string, $pattern)); @@ -754,58 +761,19 @@ public function testRtrimPattern(string|array $string, string $pattern, string|a public function testInvalidTrimPattern(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Pattern is not a valid UTF-8 string.'); - StringHelper::trim('string', "\xC3\x28"); - } - - #[TestWith(["abc\xFF"])] - #[TestWith([['hello', "abc\xFF"]])] - public function testInvalidTrimString(string|array $string): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('String is not a valid UTF-8 string.'); - StringHelper::trim($string); - } - - public function testInvalidLtrimPattern(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Pattern is not a valid UTF-8 string.'); - StringHelper::ltrim('string', "\xC3\x28"); - } - #[TestWith(["abc\xFF"])] - #[TestWith([['hello', "abc\xFF"]])] - public function testInvalidLtrimString(string|array $string): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('String is not a valid UTF-8 string.'); - StringHelper::ltrim($string); - } - - public function testInvalidRtrimPattern(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Pattern is not a valid UTF-8 string.'); - StringHelper::ltrim('string', "\xC3\x28"); - } - - #[TestWith(["abc\xFF"])] - #[TestWith([['hello', "abc\xFF"]])] - public function testInvalidRtrimString(string|array $string): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('String is not a valid UTF-8 string.'); - StringHelper::rtrim($string); + StringHelper::trim('string', "\xC3\x28"); } - #[DataProvider('dataProviderFindBetween')] + /** + * @dataProvider dataProviderFindBetween + */ public function testFindBetween(string $string, string $start, ?string $end, ?string $expectedResult): void { $this->assertSame($expectedResult, StringHelper::findBetween($string, $start, $end)); } - public static function dataProviderFindBetween(): array + public function dataProviderFindBetween(): array { return [ ['hello world hello', ' hello', ' world', null], // end before start @@ -825,13 +793,15 @@ public static function dataProviderFindBetween(): array ]; } - #[DataProvider('dataProviderFindBetweenFirst')] + /** + * @dataProvider dataProviderFindBetweenFirst + */ public function testFindBetweenFirst(string $string, string $start, ?string $end, ?string $expectedResult): void { $this->assertSame($expectedResult, StringHelper::findBetweenFirst($string, $start, $end)); } - public static function dataProviderFindBetweenFirst(): array + public function dataProviderFindBetweenFirst(): array { return [ ['[a][b][c]', '[', ']', 'a'], // normal case @@ -853,13 +823,15 @@ public static function dataProviderFindBetweenFirst(): array ]; } - #[DataProvider('dataProviderFindBetweenLast')] + /** + * @dataProvider dataProviderFindBetweenLast + */ public function testFindBetweenLast(string $string, string $start, ?string $end, ?string $expectedResult): void { $this->assertSame($expectedResult, StringHelper::findBetweenLast($string, $start, $end)); } - public static function dataProviderFindBetweenLast(): array + public function dataProviderFindBetweenLast(): array { return [ ['[a][b][c]', '[', ']', 'c'], // normal case diff --git a/tests/Support/StringableObject.php b/tests/Support/StringableObject.php index 5c83bb0..e4d1614 100644 --- a/tests/Support/StringableObject.php +++ b/tests/Support/StringableObject.php @@ -9,7 +9,7 @@ final class StringableObject implements Stringable { public function __construct( - private readonly string $string + private string $string ) { } diff --git a/tests/WildcardPatternTest.php b/tests/WildcardPatternTest.php index 99b9aee..acee2ac 100644 --- a/tests/WildcardPatternTest.php +++ b/tests/WildcardPatternTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\Strings\Tests; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Strings\WildcardPattern; @@ -15,7 +14,7 @@ final class WildcardPatternTest extends TestCase * * @return array test data. */ - public static function dataProviderMatchWildcard(): array + public function dataProviderMatchWildcard(): array { return [ // * @@ -94,7 +93,9 @@ public static function dataProviderMatchWildcard(): array ]; } - #[DataProvider('dataProviderMatchWildcard')] + /** + * @dataProvider dataProviderMatchWildcard + */ public function testMatchWildcard(string $pattern, string $string, bool $expectedResult, array $options = []): void { $wildcardPattern = $this->getWildcardPattern($pattern, $options); @@ -125,13 +126,15 @@ public function testImmutability(): void $this->assertNotSame($original, $original->ignoreCase()); } - #[DataProvider('isDynamicDataProvider')] + /** + * @dataProvider isDynamicDataProvider + */ public function testIsDynamic(string $pattern, bool $expected): void { $this->assertSame($expected, WildcardPattern::isDynamic($pattern)); } - public static function isDynamicDataProvider(): array + public function isDynamicDataProvider(): array { return [ 'not-dynamic' => ['just-some-string', false], @@ -142,7 +145,7 @@ public static function isDynamicDataProvider(): array ]; } - public static function customDelimitersProvider(): array + public function customDelimitersProvider(): array { return [ 'empty' => ['begin*end', 'begin/end', [], true], @@ -151,14 +154,16 @@ public static function customDelimitersProvider(): array ]; } - #[DataProvider('customDelimitersProvider')] + /** + * @dataProvider customDelimitersProvider + */ public function testCustomDelimiters(string $pattern, string $string, array $delimiters, bool $expected): void { $wildcardPattern = $wildcardPattern = new WildcardPattern($pattern, $delimiters); $this->assertSame($expected, $wildcardPattern->match($string)); } - public static function quoteDataProvider(): array + public function quoteDataProvider(): array { return [ 'no-pattern' => ['test', 'test'], @@ -166,7 +171,9 @@ public static function quoteDataProvider(): array ]; } - #[DataProvider('quoteDataProvider')] + /** + * @dataProvider quoteDataProvider + */ public function testQuote(string $string, string $expected): void { $this->assertSame($expected, WildcardPattern::quote($string)); diff --git a/tests/benchmarks/CombinedRegexpBench.php b/tests/benchmarks/CombinedRegexpBench.php deleted file mode 100644 index bd14c1c..0000000 --- a/tests/benchmarks/CombinedRegexpBench.php +++ /dev/null @@ -1,75 +0,0 @@ -combinedRegexp = new CombinedRegexp($patterns); - $this->memoizedCombinedRegexp = new MemoizedCombinedRegexp($this->combinedRegexp); - } - - public function benchMatches(): void - { - $this->combinedRegexp->matches('string'); - $this->combinedRegexp->matches('123'); - $this->combinedRegexp->matches('String'); - } - - public function benchGetMatchingPattern(): void - { - $this->combinedRegexp->getMatchingPattern('string'); - $this->combinedRegexp->getMatchingPattern('123'); - $this->combinedRegexp->getMatchingPattern('String'); - } - - public function benchGetMatchingPatternPosition(): void - { - $this->combinedRegexp->getMatchingPatternPosition('string'); - $this->combinedRegexp->getMatchingPatternPosition('123'); - $this->combinedRegexp->getMatchingPatternPosition('String'); - } - - public function benchMemoizedMatches(): void - { - $this->memoizedCombinedRegexp->matches('string'); - $this->memoizedCombinedRegexp->matches('123'); - $this->memoizedCombinedRegexp->matches('String'); - } - - public function benchMemoizedGetMatchingPattern(): void - { - $this->memoizedCombinedRegexp->getMatchingPattern('string'); - $this->memoizedCombinedRegexp->getMatchingPattern('123'); - $this->memoizedCombinedRegexp->getMatchingPattern('String'); - } - - public function benchMemoizedGetMatchingPatternPosition(): void - { - $this->memoizedCombinedRegexp->getMatchingPatternPosition('string'); - $this->memoizedCombinedRegexp->getMatchingPatternPosition('123'); - $this->memoizedCombinedRegexp->getMatchingPatternPosition('String'); - } -} diff --git a/tests/benchmarks/InflectorBench.php b/tests/benchmarks/InflectorBench.php deleted file mode 100644 index 1937128..0000000 --- a/tests/benchmarks/InflectorBench.php +++ /dev/null @@ -1,109 +0,0 @@ -inflector = new Inflector(); - } - - public function benchToPlural(): void - { - $this->inflector->toPlural('apple'); - $this->inflector->toPlural('book'); - $this->inflector->toPlural('child'); - $this->inflector->toPlural('person'); - } - - public function benchToSingular(): void - { - $this->inflector->toSingular('apples'); - $this->inflector->toSingular('books'); - $this->inflector->toSingular('children'); - $this->inflector->toSingular('people'); - } - - public function benchToSentence(): void - { - $this->inflector->toSentence('some_string_to_convert'); - $this->inflector->toSentence('someStringToConvert'); - } - - public function benchToWords(): void - { - $this->inflector->toWords('some_string_to_convert'); - $this->inflector->toWords('someStringToConvert'); - } - - public function benchPascalCaseToId(): void - { - $this->inflector->pascalCaseToId('SomeString'); - $this->inflector->pascalCaseToId('SomeOtherString'); - } - - public function benchToPascalCase(): void - { - $this->inflector->toPascalCase('some string'); - $this->inflector->toPascalCase('some_other_string'); - } - - public function benchToCamelCase(): void - { - $this->inflector->toCamelCase('some string'); - $this->inflector->toCamelCase('some_other_string'); - } - - public function benchToSnakeCase(): void - { - $this->inflector->toSnakeCase('SomeString'); - $this->inflector->toSnakeCase('some other string'); - } - - public function benchToHumanReadable(): void - { - $this->inflector->toHumanReadable('some_string'); - $this->inflector->toHumanReadable('SomeString'); - } - - public function benchClassToTable(): void - { - $this->inflector->classToTable('SomeClass'); - $this->inflector->classToTable('Another\\ClassName'); - } - - public function benchTableToClass(): void - { - $this->inflector->tableToClass('some_table'); - $this->inflector->tableToClass('another_table_name'); - } - - public function benchToSlug(): void - { - $this->inflector->toSlug('some string to slug'); - $this->inflector->toSlug('some other_string-to slug'); - } - - public function benchToTransliterated(): void - { - $this->inflector->toTransliterated('some string to transliterate'); - $this->inflector->toTransliterated('Українська мова'); - $this->inflector->toTransliterated('Русский язык'); - $this->inflector->toTransliterated('日本語って本当にかっこいいですね'); - } -} diff --git a/tests/benchmarks/NumericHelperBench.php b/tests/benchmarks/NumericHelperBench.php deleted file mode 100644 index 50081bf..0000000 --- a/tests/benchmarks/NumericHelperBench.php +++ /dev/null @@ -1,48 +0,0 @@ -b', '', ''); - } - - public function benchMatchAnyRegex(): void - { - StringHelper::matchAnyRegex('string', ['/[a-z]+/', '/[0-9]+/']); - } -} diff --git a/tests/benchmarks/WildcardPatternBench.php b/tests/benchmarks/WildcardPatternBench.php deleted file mode 100644 index 4326d48..0000000 --- a/tests/benchmarks/WildcardPatternBench.php +++ /dev/null @@ -1,43 +0,0 @@ -wildcardPattern = new WildcardPattern('*a*'); - } - - public function benchMatch(): void - { - $this->wildcardPattern->match('banana'); - $this->wildcardPattern->match('apple'); - $this->wildcardPattern->match('orange'); - } - - public function benchIsDynamic(): void - { - WildcardPattern::isDynamic('*a*'); - WildcardPattern::isDynamic('test'); - } - - public function benchQuote(): void - { - WildcardPattern::quote('*a*'); - } -} From cc6993cde8c6b121f8ca9da780a271ca1c3bb433 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Mon, 6 Jul 2026 13:38:44 +0300 Subject: [PATCH 57/60] Revert unrelated workflow changes --- .gitattributes | 1 + .github/FUNDING.yml | 4 ++ .github/ISSUE_TEMPLATE.md | 16 +++++ .github/dependabot.yml | 2 - .github/workflows/bc.yml | 2 +- .github/workflows/build.yml | 2 +- .../workflows/composer-require-checker.yml | 2 +- .github/workflows/mutation.yml | 5 +- .github/workflows/rector.yml | 2 +- .github/workflows/static.yml | 2 +- .gitignore | 14 ++-- .scrutinizer.yml | 35 ++++++++++ CHANGELOG.md | 24 ------- README.md | 65 ++++++++++++++----- UPGRADE.md | 13 ++-- composer.json | 37 +++-------- docs/internals.md | 44 ------------- phpbench.json | 14 ---- phpunit.xml.dist | 28 ++++---- psalm.xml | 2 - tools/.gitignore | 2 - tools/infection/composer.json | 10 --- tools/psalm/composer.json | 5 -- 23 files changed, 145 insertions(+), 186 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .scrutinizer.yml delete mode 100644 docs/internals.md delete mode 100644 phpbench.json delete mode 100644 tools/.gitignore delete mode 100644 tools/infection/composer.json delete mode 100644 tools/psalm/composer.json diff --git a/.gitattributes b/.gitattributes index 9f9e9af..5583f36 100644 --- a/.gitattributes +++ b/.gitattributes @@ -26,6 +26,7 @@ /.editorconfig export-ignore /.gitattributes export-ignore /.gitignore export-ignore +/.scrutinizer.yml export-ignore /phpunit.xml.dist export-ignore /tests export-ignore /docs export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..f0dc531 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +open_collective: yiisoft +github: [yiisoft] diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..b748c2d --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,16 @@ + + +### What steps will reproduce the problem? + +### What is the expected result? + +### What do you get instead? + + +### Additional info + +| Q | A +| ---------------- | --- +| Version | 1.0.? +| PHP version | +| Operating system | diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 10f7e30..a47ec9b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,4 @@ updates: directory: "/" schedule: interval: "daily" - cooldown: - default-days: 7 versioning-strategy: increase-if-necessary diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index 0577a84..b269391 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -33,4 +33,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.4'] + ['8.1'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 35abbae..d3a7abc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,4 +32,4 @@ jobs: os: >- ['ubuntu-latest', 'windows-latest'] php: >- - ['8.1', '8.2', '8.3', '8.4', '8.5'] + ['8.0', '8.1', '8.2', '8.3'] diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index 1a56fef..6c2e161 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -34,4 +34,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3', '8.4', '8.5'] + ['8.0', '8.1', '8.2', '8.3'] diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index ce83871..1f7ad3d 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -25,12 +25,11 @@ permissions: jobs: mutation: - uses: yiisoft/actions/.github/workflows/infection.yml@master + uses: yiisoft/actions/.github/workflows/roave-infection.yml@master with: os: >- ['ubuntu-latest'] php: >- - ['8.5'] - infection-args: "--ignore-msi-with-no-mutations" + ['8.2'] secrets: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 69e39cc..98a6ff7 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -21,4 +21,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.5'] + ['8.3'] diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index cab5775..f176c8e 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -32,4 +32,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3', '8.4'] + ['8.0', '8.1', '8.2', '8.3'] diff --git a/.gitignore b/.gitignore index 0f8b69d..56da114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ # phpstorm project files .idea -# visual studio code project files -.vscode - # netbeans project files nbproject @@ -26,10 +23,9 @@ composer.phar # Mac DS_Store Files .DS_Store -# PHPUnit -/phpunit.phar +# phpunit itself is not needed +phpunit.phar +# local phpunit config /phpunit.xml -/.phpunit.cache - -# PHPBench -.phpbench +# phpunit cache +.phpunit.result.cache diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..61fb502 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,35 @@ +checks: + php: true + +filter: + paths: + - src/ + +build: + image: default-bionic + + environment: + php: + version: 8.0.18 + ini: + xdebug.mode: coverage + + nodes: + analysis: + tests: + override: + - php-scrutinizer-run + + phpunit: + dependencies: + override: + - composer self-update + - composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + tests: + override: + - command: ./vendor/bin/phpunit --coverage-clover ./coverage.xml + on_node: 1 + coverage: + file: coverage.xml + format: php-clover diff --git a/CHANGELOG.md b/CHANGELOG.md index aa82df6..7ea8f41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,30 +3,6 @@ ## 3.0.0 under development - Chg #112: Change default value of `$strict` parameter in `Inflector::toSnakeCase()` to `false` (@vjik) -## 2.7.1 under development - -- New #156: Add `NumericHelper::trimDecimalZeros()` (@samdark, @vjik) - -## 2.7.0 November 23, 2025 - -- New #148, #149: Add `NumericHelper::convertHumanReadableSizeToBytes()` method (@mspirkov) -- New #3: Add `StringHelper::truncateWordsByLength()` method to truncate text by character length while preserving word - boundaries (@samdark) -- Enh #150: Add PHP 8.5 support (@vjik) -- Bug #119: Fix incorrect pluralization of words ending in "tion", "sion" and "gion" (@samdark) - -## 2.6.0 February 09, 2025 - -- Chg #140: Bump minimal required PHP version to 8.1 and minor refactoring (@vjik) -- Chg #143: Change PHP constraint in `composer.json` to `~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0` (@vjik) -- Bug #138: Explicitly mark nullable parameters (@ferrumfist) -- Bug #141: `StringHelper::parsePath()` for empty string returns path `['']` instead of `[]` before (@vjik) -- Bug #142: Check string on a valid UTF-8 in `StringHelper` methods: `trim()`, `ltrim()` and `rtrim()` (@vjik) - -## 2.5.0 January 19, 2025 - -- New #137: Add `StringHelper::matchAnyRegex()` method as a facade for `CombinedRegexp` (@vjik) -- Enh #128: Add more specific psalm type for result of `StringHelper::base64UrlEncode()` method (@vjik) ## 2.4.0 December 22, 2023 diff --git a/README.md b/README.md index d768fed..e6ca88e 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@

- Yii +

Yii Strings


-[![Latest Stable Version](https://poser.pugx.org/yiisoft/strings/v)](https://packagist.org/packages/yiisoft/strings) -[![Total Downloads](https://poser.pugx.org/yiisoft/strings/downloads)](https://packagist.org/packages/yiisoft/strings) -[![Build status](https://github.com/yiisoft/strings/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/strings/actions/workflows/build.yml) -[![Code coverage](https://codecov.io/gh/yiisoft/strings/graph/badge.svg?token=GEPMBAHNCX)](https://codecov.io/gh/yiisoft/strings) +[![Latest Stable Version](https://poser.pugx.org/yiisoft/strings/v/stable.png)](https://packagist.org/packages/yiisoft/strings) +[![Total Downloads](https://poser.pugx.org/yiisoft/strings/downloads.png)](https://packagist.org/packages/yiisoft/strings) +[![Build Status](https://github.com/yiisoft/strings/workflows/build/badge.svg)](https://github.com/yiisoft/strings/actions) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/yiisoft/strings/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/strings/?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/strings/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/strings/?branch=master) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fstrings%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/strings/master) [![static analysis](https://github.com/yiisoft/strings/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/strings/actions?query=workflow%3A%22static+analysis%22) [![type-coverage](https://shepherd.dev/github/yiisoft/strings/coverage.svg)](https://shepherd.dev/github/yiisoft/strings) @@ -20,18 +21,16 @@ The package provides: - `NumericHelper` that has static methods to work with numeric strings; - `Inflector` provides methods such as `toPlural()` or `toSlug()` that derive a new string based on the string given; - `WildcardPattern` is a shell wildcard pattern to match strings against; -- `CombinedRegexp` is a wrapper that optimizes multiple regular expressions matching and +- `CombinedRegexp` is a wrapper that optimizes multiple regular expressions matching and `MemoizedCombinedRegexp` is a decorator that caches results of `CombinedRegexp` to speed up matching. ## Requirements -- PHP 8.1 - 8.5. +- PHP 8.0 or higher. - `mbstring` PHP extension. ## Installation -The package could be installed with [Composer](https://getcomposer.org): - ```shell composer require yiisoft/strings ``` @@ -74,7 +73,6 @@ Overall the helper has the following method groups. - truncateMiddle - truncateEnd - truncateWords -- truncateWordsByLength - trim - ltrim - rtrim @@ -98,7 +96,6 @@ Overall the helper has the following method groups. ### Other -- matchAnyRegex - parsePath - split @@ -115,8 +112,6 @@ The following methods are available: - toOrdinal - normalize - isInteger -- convertHumanReadableSizeToBytes -- trimDecimalZeros ## Inflector usage @@ -160,7 +155,7 @@ Overall the inflector has the following method groups. ## WildcardPattern usage -`WildcardPattern` allows a simple POSIX-style string matching. +`WildcardPattern` allows a simple POSIX-style string matching. ```php use \Yiisoft\Strings\WildcardPattern; @@ -246,12 +241,46 @@ if ($regexp->matches('second')) { } ``` -## Documentation +## Testing + +### Unit testing + +The package is tested with [PHPUnit](https://phpunit.de/). To run tests: + +```shell +./vendor/bin/phpunit +``` + +### Mutation testing + +The package tests are checked with [Infection](https://infection.github.io/) mutation framework with +[Infection Static Analysis Plugin](https://github.com/Roave/infection-static-analysis-plugin). To run it: + +```shell +./vendor/bin/roave-infection-static-analysis-plugin +``` + +### Static analysis + +The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis: + +```shell +./vendor/bin/psalm +``` + +### Code style + +Use [Rector](https://github.com/rectorphp/rector) to make codebase follow some specific rules or +use either newest or any specific version of PHP: + +```shell +./vendor/bin/rector +``` -- [Internals](docs/internals.md) +### Dependencies -If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that. -You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). +Use [ComposerRequireChecker](https://github.com/maglnet/ComposerRequireChecker) to detect transitive +[Composer](https://getcomposer.org/) dependencies. ## License diff --git a/UPGRADE.md b/UPGRADE.md index 42dc833..3d9851e 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,11 +1,12 @@ -# Upgrading Instructions for Yii Strings +# Upgrading Instructions -This file contains the upgrade notes. These notes highlight changes that could break your -application when you upgrade the package from one version to another. +These notes highlight changes that could break your application when you upgrade package from one version to another. -> **Important!** The following upgrading instructions are cumulative. That is, if you want -> to upgrade from version A to version C and there is version B between A and C, you need -> to following the instructions for both A and B. +Upgrading in general is as simple as updating your dependency in your `composer.json` and running `composer update`. +In a big application however there may be more things to consider, which are explained in the following. + +> Note: The following upgrading instructions are cumulative. That is, if you want to upgrade from a version A to version +> C and there is a version B between A and C, you need to follow the instructions for both A and B. ## Upgrade from 2.x to 3.x diff --git a/composer.json b/composer.json index 4c47136..df553e2 100644 --- a/composer.json +++ b/composer.json @@ -10,34 +10,24 @@ "homepage": "https://www.yiiframework.com/", "license": "BSD-3-Clause", "support": { - "issues": "https://github.com/yiisoft/strings/issues?state=open", "source": "https://github.com/yiisoft/strings", + "issues": "https://github.com/yiisoft/strings/issues", "forum": "https://www.yiiframework.com/forum/", "wiki": "https://www.yiiframework.com/wiki/", - "irc": "ircs://irc.libera.chat:6697/yii", + "irc": "irc://irc.freenode.net/yii", "chat": "https://t.me/yii3en" }, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/yiisoft" - }, - { - "type": "github", - "url": "https://github.com/sponsors/yiisoft" - } - ], "require": { - "php": "8.1 - 8.5", + "php": "^8.0", "ext-mbstring": "*" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "maglnet/composer-require-checker": "^4.7.1", - "phpunit/phpunit": "^10.5.48", - "rector/rector": "^2.1.2", - "spatie/phpunit-watcher": "^1.24", - "phpbench/phpbench": "^1.4.1" + "maglnet/composer-require-checker": "^4.2", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.18.3", + "roave/infection-static-analysis-plugin": "^1.16", + "spatie/phpunit-watcher": "^1.23", + "vimeo/psalm": "^4.30|^5.8" }, "autoload": { "psr-4": { @@ -52,17 +42,10 @@ "config": { "sort-packages": true, "allow-plugins": { - "bamarni/composer-bin-plugin": true, + "infection/extension-installer": true, "composer/package-versions-deprecated": true } }, - "extra": { - "bamarni-bin": { - "bin-links": true, - "target-directory": "tools", - "forward-command": true - } - }, "scripts": { "test": "phpunit --testdox --no-interaction", "test-watch": "phpunit-watcher watch" diff --git a/docs/internals.md b/docs/internals.md deleted file mode 100644 index 087a514..0000000 --- a/docs/internals.md +++ /dev/null @@ -1,44 +0,0 @@ -# Internals - -## Unit testing - -The package is tested with [PHPUnit](https://phpunit.de/). To run tests: - -```shell -./vendor/bin/phpunit -``` - -## Mutation testing - -The package tests are checked with [Infection](https://infection.github.io/) mutation framework with -[Infection Static Analysis Plugin](https://github.com/Roave/infection-static-analysis-plugin). To run it: - -```shell -./vendor/bin/roave-infection-static-analysis-plugin -``` - -## Static analysis - -The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis: - -```shell -./vendor/bin/psalm -``` - -## Code style - -Use [Rector](https://github.com/rectorphp/rector) to make codebase follow some specific rules or -use either newest or any specific version of PHP: - -```shell -./vendor/bin/rector -``` - -## Dependencies - -This package uses [composer-require-checker](https://github.com/maglnet/ComposerRequireChecker) to check if -all dependencies are correctly defined in `composer.json`. To run the checker, execute the following command: - -```shell -./vendor/bin/composer-require-checker -``` diff --git a/phpbench.json b/phpbench.json deleted file mode 100644 index c4443bd..0000000 --- a/phpbench.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", - "runner.bootstrap": "vendor/autoload.php", - "runner.path": "tests/benchmarks", - "runner.php_config": { - "memory_limit": "1G" - }, - "report.generators": { - "default": { - "generator": "composite", - "reports": ["aggregate", "env"] - } - } -} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 98b0b95..14acb8c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,18 @@ - + executionOrder="random" + resolveDependencies="true"> - + @@ -23,9 +21,9 @@ - + - ./src + ./src - + diff --git a/psalm.xml b/psalm.xml index 77a366b..d091d59 100644 --- a/psalm.xml +++ b/psalm.xml @@ -3,8 +3,6 @@ errorLevel="1" findUnusedBaselineEntry="true" findUnusedCode="false" - ensureOverrideAttribute="false" - strictBinaryOperands="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" diff --git a/tools/.gitignore b/tools/.gitignore deleted file mode 100644 index cf452dc..0000000 --- a/tools/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/*/vendor -/*/composer.lock diff --git a/tools/infection/composer.json b/tools/infection/composer.json deleted file mode 100644 index 18be2ea..0000000 --- a/tools/infection/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "require-dev": { - "infection/infection": "^0.26 || ^0.31.9" - }, - "config": { - "allow-plugins": { - "infection/extension-installer": true - } - } -} diff --git a/tools/psalm/composer.json b/tools/psalm/composer.json deleted file mode 100644 index 9751a9f..0000000 --- a/tools/psalm/composer.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "require-dev": { - "vimeo/psalm": "^5.26.1 || ^6.5" - } -} From 352f07db3745f0046e906e65f3360e78a63083ad Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Mon, 6 Jul 2026 13:44:10 +0300 Subject: [PATCH 58/60] Run BC check on PHP 8.4 --- .github/workflows/bc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index b269391..0577a84 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -33,4 +33,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1'] + ['8.4'] From d7a4dfdeb53b40e18f55d7df89eb7cefbe71855a Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Mon, 6 Jul 2026 13:45:53 +0300 Subject: [PATCH 59/60] Allow BC checker install on PHP 8.4 --- .github/workflows/bc.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index 0577a84..0e4777e 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -34,3 +34,5 @@ jobs: ['ubuntu-latest'] php: >- ['8.4'] + composer-require-options: >- + --with-all-dependencies --no-progress --ansi --ignore-platform-req=php+ From ca2728e31d68b8346e45d15babfeb8d3adb5e1d7 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Mon, 6 Jul 2026 13:50:12 +0300 Subject: [PATCH 60/60] Make BC checker installable on PHP 8.4 --- .github/workflows/bc.yml | 2 -- composer.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index 0e4777e..0577a84 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -34,5 +34,3 @@ jobs: ['ubuntu-latest'] php: >- ['8.4'] - composer-require-options: >- - --with-all-dependencies --no-progress --ansi --ignore-platform-req=php+ diff --git a/composer.json b/composer.json index df553e2..4503cb3 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "rector/rector": "^0.18.3", "roave/infection-static-analysis-plugin": "^1.16", "spatie/phpunit-watcher": "^1.23", - "vimeo/psalm": "^4.30|^5.8" + "vimeo/psalm": "^4.30|^5.8|^6.16" }, "autoload": { "psr-4": {