├── .phpunit-watcher.yml
├── .styleci.yml
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── UPGRADE.md
├── composer-require-checker.json
├── composer.json
├── infection.json.dist
├── psalm.xml
├── rector.php
└── src
├── AbstractCombinedRegexp.php
├── CombinedRegexp.php
├── Inflector.php
├── MemoizedCombinedRegexp.php
├── NumericHelper.php
├── StringHelper.php
└── WildcardPattern.php
/.phpunit-watcher.yml:
--------------------------------------------------------------------------------
1 | watch:
2 | directories:
3 | - src
4 | - tests
5 | fileMask: '*.php'
6 | notifications:
7 | passingTests: false
8 | failingTests: false
9 | phpunit:
10 | binaryPath: vendor/bin/phpunit
11 | timeout: 180
12 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: psr12
2 | risky: true
3 |
4 | version: 8.1
5 |
6 | finder:
7 | exclude:
8 | - docs
9 | - vendor
10 |
11 | enabled:
12 | - alpha_ordered_traits
13 | - array_indentation
14 | - array_push
15 | - combine_consecutive_issets
16 | - combine_consecutive_unsets
17 | - combine_nested_dirname
18 | - declare_strict_types
19 | - dir_constant
20 | - fully_qualified_strict_types
21 | - function_to_constant
22 | - hash_to_slash_comment
23 | - is_null
24 | - logical_operators
25 | - magic_constant_casing
26 | - magic_method_casing
27 | - method_separation
28 | - modernize_types_casting
29 | - native_function_casing
30 | - native_function_type_declaration_casing
31 | - no_alias_functions
32 | - no_empty_comment
33 | - no_empty_phpdoc
34 | - no_empty_statement
35 | - no_extra_block_blank_lines
36 | - no_short_bool_cast
37 | - no_superfluous_elseif
38 | - no_unneeded_control_parentheses
39 | - no_unneeded_curly_braces
40 | - no_unneeded_final_method
41 | - no_unset_cast
42 | - no_unused_imports
43 | - no_unused_lambda_imports
44 | - no_useless_else
45 | - no_useless_return
46 | - normalize_index_brace
47 | - php_unit_dedicate_assert
48 | - php_unit_dedicate_assert_internal_type
49 | - php_unit_expectation
50 | - php_unit_mock
51 | - php_unit_mock_short_will_return
52 | - php_unit_namespaced
53 | - php_unit_no_expectation_annotation
54 | - phpdoc_no_empty_return
55 | - phpdoc_no_useless_inheritdoc
56 | - phpdoc_order
57 | - phpdoc_property
58 | - phpdoc_scalar
59 | - phpdoc_singular_inheritdoc
60 | - phpdoc_trim
61 | - phpdoc_trim_consecutive_blank_line_separation
62 | - phpdoc_type_to_var
63 | - phpdoc_types
64 | - phpdoc_types_order
65 | - print_to_echo
66 | - regular_callable_call
67 | - return_assignment
68 | - self_accessor
69 | - self_static_accessor
70 | - set_type_to_cast
71 | - short_array_syntax
72 | - short_list_syntax
73 | - simplified_if_return
74 | - single_quote
75 | - standardize_not_equals
76 | - ternary_to_null_coalescing
77 | - trailing_comma_in_multiline_array
78 | - unalign_double_arrow
79 | - unalign_equals
80 | - empty_loop_body_braces
81 | - integer_literal_case
82 | - union_type_without_spaces
83 |
84 | disabled:
85 | - function_declaration
86 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii Strings Change Log
2 |
3 | ## 2.6.1 under development
4 |
5 | - no changes in this release.
6 |
7 | ## 2.6.0 February 09, 2025
8 |
9 | - Chg #140: Bump minimal required PHP version to 8.1 and minor refactoring (@vjik)
10 | - Chg #143: Change PHP constraint in `composer.json` to `~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0` (@vjik)
11 | - Bug #138: Explicitly mark nullable parameters (@ferrumfist)
12 | - Bug #141: `StringHelper::parsePath()` for empty string returns path `['']` instead of `[]` before (@vjik)
13 | - Bug #142: Check string on a valid UTF-8 in `StringHelper` methods: `trim()`, `ltrim()` and `rtrim()` (@vjik)
14 |
15 | ## 2.5.0 January 19, 2025
16 |
17 | - New #137: Add `StringHelper::matchAnyRegex()` method as a facade for `CombinedRegexp` (@vjik)
18 | - Enh #128: Add more specific psalm type for result of `StringHelper::base64UrlEncode()` method (@vjik)
19 |
20 | ## 2.4.0 December 22, 2023
21 |
22 | - New #118: Add `findBetween()`, `findBetweenFirst()` and `findBetweenLast()` methods to `StringHelper` to retrieve
23 | a substring that lies between two strings (@salehhashemi1992)
24 | - Enh #121: Don't use regexp if there is no delimeter in the path in `StringHelper::parsePath()` (@viktorprogger)
25 |
26 | ## 2.3.1 October 30, 2023
27 |
28 | - Enh #117: `WildcardPatters` uses memoization and accelerates ~2 times on repeated calls (@viktorprogger)
29 |
30 | ## 2.3.0 October 23, 2023
31 |
32 | - Enh #114: Add stringable object support to `NumericHelper::normalize()` (@vjik)
33 |
34 | ## 2.2.0 September 20, 2023
35 |
36 | - New #102, #106: Add `CombinedRegexp` class (@xepozz, @vjik)
37 | - New #103: Add `MemoizedCombinedRegexp` decorator that caches results of `CombinedRegexp` (@xepozz)
38 | - New #104: Add methods `StringHelper::trim()`, `StringHelper::ltrim()`, `StringHelper::rtrim()` (@olegbaturin)
39 | - Enh #103: Raise required PHP version to `^8.0` (@xepozz)
40 | - Enh #106: Using fully-qualified function calls to improve performance (@vjik)
41 | - Enh #111: Minor refactoring (@Tigrov)
42 | - Enh #83: Make minor refactoring with Rector help (@vjik)
43 | - Enh #92: Add `$strict` parameter to `Inflector::toSnakeCase()` method (@arogachev)
44 |
45 | ## 2.1.2 July 27, 2023
46 |
47 | - Bug #105: Fix incorrect split UTF-8 strings in `StringHelper::split()` method (@vjik)
48 |
49 | ## 2.1.1 April 28, 2023
50 |
51 | - Enh #85: Improve `StringHelper::parsePath()` method annotation (@vjik)
52 |
53 | ## 2.1.0 August 20, 2022
54 |
55 | - New #75: Add method `Inflector::toSnakeCase()` that convert word to "snake_case" (@soodssr)
56 | - New #81: Add `StringHelper::parsePath()` method (@arogachev, @vjik)
57 |
58 | ## 2.0.0 February 10, 2021
59 |
60 | - Chg #67: Remove `\Yiisoft\Strings\WildcardPattern::withoutEscape()` (@samdark)
61 | - Chg #67: Remove `\Yiisoft\Strings\WildcardPattern::withExactLeadingPeriod()` (@samdark)
62 | - Enh #67: Add `**`, match anything including `/`, to `\Yiisoft\Strings\WildcardPattern`, remove `withExactSlashes()` and `withEnding()` (@samdark)
63 | - Enh #67: Allow specifying delimiters for `*` (@samdark)
64 | - Enh #67: Add `\Yiisoft\Strings\WildcardPattern::isDynamic()` (@samdark)
65 | - Enh #67: Add `\Yiisoft\Strings\WildcardPattern::quote()` (@samdark)
66 |
67 | ## 1.2.0 January 22, 2021
68 |
69 | - Enh #62: Add method `StringHelper::split()` that split a string to array with non-empty lines (@vjik)
70 | - Enh #63: Add method `NumericHelper::isInteger()` that checks whether the given string is an integer number (@vjik)
71 | - Enh #64: Add support of a boolean values to `NumericHelper::normalize()` (@vjik)
72 |
73 | ## 1.1.0 November 13, 2020
74 |
75 | - Enh #52: Allow turning off options in `WildcardPattern` (@vjik)
76 | - Enh #51: Add an option `withEnding()` to `WildcardPattern` for match ending of testing string (@vjik)
77 | - Bug #44: `NumericHelper::toOrdinal()` throws an error for numbers with fractional part (@vjik)
78 |
79 | ## 1.0.1 October 12, 2020
80 |
81 | - Enh #40: Use `str_starts_with()` and `str_ends_with()` if available (@viktorprogger)
82 | - Bug #43: `NumericHelper::normalize()` throws an error for `float` or `int` values in PHP 8 (@vjik)
83 |
84 | ## 1.0.0 August 31, 2020
85 |
86 | - Initial release.
87 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/)
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Yii Strings
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/strings)
10 | [](https://packagist.org/packages/yiisoft/strings)
11 | [](https://github.com/yiisoft/strings/actions/workflows/build.yml)
12 | [](https://codecov.io/gh/yiisoft/strings)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/strings/master)
14 | [](https://github.com/yiisoft/strings/actions?query=workflow%3A%22static+analysis%22)
15 | [](https://shepherd.dev/github/yiisoft/strings)
16 |
17 | The package provides:
18 |
19 | - `StringHelper` that has static methods to work with strings;
20 | - `NumericHelper` that has static methods to work with numeric strings;
21 | - `Inflector` provides methods such as `toPlural()` or `toSlug()` that derive a new string based on the string given;
22 | - `WildcardPattern` is a shell wildcard pattern to match strings against;
23 | - `CombinedRegexp` is a wrapper that optimizes multiple regular expressions matching and
24 | `MemoizedCombinedRegexp` is a decorator that caches results of `CombinedRegexp` to speed up matching.
25 |
26 | ## Requirements
27 |
28 | - PHP 8.1 or higher.
29 | - `mbstring` PHP extension.
30 |
31 | ## Installation
32 |
33 | The package could be installed with [Composer](https://getcomposer.org):
34 |
35 | ```shell
36 | composer require yiisoft/strings
37 | ```
38 |
39 | ## StringHelper usage
40 |
41 | String helper methods are static so usage is like the following:
42 |
43 | ```php
44 | echo \Yiisoft\Strings\StringHelper::countWords('Strings are cool!'); // 3
45 | ```
46 |
47 | Overall the helper has the following method groups.
48 |
49 | ### Bytes
50 |
51 | - byteLength
52 | - byteSubstring
53 |
54 | ### File paths
55 |
56 | - baseName
57 | - directoryName
58 |
59 | ### Substrings
60 |
61 | - substring
62 | - replaceSubstring
63 | - startsWith
64 | - startsWithIgnoringCase
65 | - endsWith
66 | - endsWithIgnoringCase
67 | - findBetween
68 | - findBetweenFirst
69 | - findBetweenLast
70 |
71 | ### Truncation
72 |
73 | - truncateBegin
74 | - truncateMiddle
75 | - truncateEnd
76 | - truncateWords
77 | - trim
78 | - ltrim
79 | - rtrim
80 |
81 | ### Counting
82 |
83 | - length
84 | - countWords
85 |
86 | ### Lowercase and uppercase
87 |
88 | - lowercase
89 | - uppercase
90 | - uppercaseFirstCharacter
91 | - uppercaseFirstCharacterInEachWord
92 |
93 | ### URL friendly base64
94 |
95 | - base64UrlEncode
96 | - base64UrlDecode
97 |
98 | ### Other
99 |
100 | - matchAnyRegex
101 | - parsePath
102 | - split
103 |
104 | ## NumericHelper usage
105 |
106 | Numeric helper methods are static so usage is like the following:
107 |
108 | ```php
109 | echo \Yiisoft\Strings\NumericHelper::toOrdinal(3); // 3rd
110 | ```
111 |
112 | The following methods are available:
113 |
114 | - toOrdinal
115 | - normalize
116 | - isInteger
117 |
118 | ## Inflector usage
119 |
120 | ```php
121 | echo (new \Yiisoft\Strings\Inflector())
122 | ->withoutIntl()
123 | ->toSlug('Strings are cool!'); // strings-are-cool
124 | ```
125 |
126 | Overall the inflector has the following method groups.
127 |
128 | ### Plurals and singulars
129 |
130 | - toPlural
131 | - toSingular
132 |
133 | ### Transliteration
134 |
135 | - toTransliterated
136 |
137 | ### Case conversion
138 |
139 | - pascalCaseToId
140 | - toPascalCase
141 | - toCamelCase
142 |
143 | ### Words and sentences
144 |
145 | - toSentence
146 | - toWords
147 | - toHumanReadable
148 |
149 | ### Classes and database tables
150 |
151 | - classToTable
152 | - tableToClass
153 |
154 | ### URLs
155 |
156 | - toSlug
157 |
158 | ## WildcardPattern usage
159 |
160 | `WildcardPattern` allows a simple POSIX-style string matching.
161 |
162 | ```php
163 | use \Yiisoft\Strings\WildcardPattern;
164 |
165 | $startsWithTest = new WildcardPattern('test*');
166 | if ($startsWithTest->match('testIfThisIsTrue')) {
167 | echo 'It starts with "test"!';
168 | }
169 | ```
170 |
171 | The following characters are special in the pattern:
172 |
173 | - `\` escapes other special characters if usage of escape character is not turned off.
174 | - `*` matches any string, including the empty string, except delimiters (`/` and `\` by default).
175 | - `**` matches any string, including the empty string and delimiters.
176 | - `?` matches any single character.
177 | - `[seq]` matches any character in seq.
178 | - `[a-z]` matches any character from a to z.
179 | - `[!seq]` matches any character not in seq.
180 | - `[[:alnum:]]` matches [POSIX style character classes](https://www.php.net/manual/en/regexp.reference.character-classes.php).
181 |
182 | `ignoreCase()` could be called before doing a `match()` to get a case-insensitive match:
183 |
184 | ```php
185 | use \Yiisoft\Strings\WildcardPattern;
186 |
187 | $startsWithTest = new WildcardPattern('test*');
188 | if ($startsWithTest
189 | ->ignoreCase()
190 | ->match('tEStIfThisIsTrue')) {
191 | echo 'It starts with "test"!';
192 | }
193 | ```
194 |
195 | ## CombinedRegexp usage
196 |
197 | `CombinedRegexp` optimizes matching multiple regular expressions.
198 |
199 | ```php
200 | use \Yiisoft\Strings\CombinedRegexp;
201 |
202 | $patterns = [
203 | 'first',
204 | 'second',
205 | '^a\d$',
206 | ];
207 | $regexp = new CombinedRegexp($patterns, 'i');
208 | $regexp->matches('a5'); // true – matches the third pattern
209 | $regexp->matches('A5'); // true – matches the third pattern because of `i` flag that is applied to all regular expressions
210 | $regexp->getMatchingPattern('a5'); // '^a\d$' – the pattern that matched
211 | $regexp->getMatchingPatternPosition('a5'); // 2 – the index of the pattern in the array
212 | $regexp->getCompiledPattern(); // '~(?|first|second()|^a\d$()())~'
213 | ```
214 |
215 | ## MemoizedCombinedRegexp usage
216 |
217 | `MemoizedCombinedRegexp` caches results of `CombinedRegexp` in memory.
218 | It is useful when the same incoming string are matching multiple times or different methods of `CombinedRegexp` are called.
219 |
220 | ```php
221 | use \Yiisoft\Strings\CombinedRegexp;
222 | use \Yiisoft\Strings\MemoizedCombinedRegexp;
223 |
224 | $patterns = [
225 | 'first',
226 | 'second',
227 | '^a\d$',
228 | ];
229 | $regexp = new MemoizedCombinedRegexp(new CombinedRegexp($patterns, 'i'));
230 | $regexp->matches('a5'); // Fires `preg_match` inside the `CombinedRegexp`.
231 | $regexp->matches('first'); // Fires `preg_match` inside the `CombinedRegexp`.
232 | $regexp->matches('a5'); // Does not fire `preg_match` inside the `CombinedRegexp` because the result is cached.
233 | $regexp->getMatchingPattern('a5'); // The result is cached so no `preg_match` is fired.
234 | $regexp->getMatchingPatternPosition('a5'); // The result is cached so no `preg_match` is fired.
235 |
236 | // The following code fires only once matching mechanism.
237 | if ($regexp->matches('second')) {
238 | echo sprintf(
239 | 'Matched the pattern "%s" which is on the position "%s" in the expressions list.',
240 | $regexp->getMatchingPattern('second'),
241 | $regexp->getMatchingPatternPosition('second'),
242 | );
243 | }
244 | ```
245 |
246 | ## Documentation
247 |
248 | - [Internals](docs/internals.md)
249 |
250 | 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.
251 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community).
252 |
253 | ## License
254 |
255 | The Yii Strings is free software. It is released under the terms of the BSD License.
256 | Please see [`LICENSE`](./LICENSE.md) for more information.
257 |
258 | Maintained by [Yii Software](https://www.yiiframework.com/).
259 |
260 | ## Support the project
261 |
262 | [](https://opencollective.com/yiisoft)
263 |
264 | ## Follow updates
265 |
266 | [](https://www.yiiframework.com/)
267 | [](https://twitter.com/yiiframework)
268 | [](https://t.me/yii3en)
269 | [](https://www.facebook.com/groups/yiitalk)
270 | [](https://yiiframework.com/go/slack)
271 |
--------------------------------------------------------------------------------
/UPGRADE.md:
--------------------------------------------------------------------------------
1 | # Upgrading Instructions for Yii Strings
2 |
3 | This file contains the upgrade notes. These notes highlight changes that could break your
4 | application when you upgrade the package from one version to another.
5 |
6 | > **Important!** The following upgrading instructions are cumulative. That is, if you want
7 | > to upgrade from version A to version C and there is version B between A and C, you need
8 | > to following the instructions for both A and B.
9 |
10 | ## Upgrade from 1.2.0
11 |
12 | `\Yiisoft\Strings\WildCardPattern` was changed.
13 |
14 | - `\Yiisoft\Strings\WildcardPattern::withExactSlashes()` was removed. `*` now always doesn't match `/`.
15 | - `**` was introduced to match anything including `/`.
16 | - `\Yiisoft\Strings\WildcardPattern::withExactLeadingPeriod()` was removed. There is no replacement for old behavior.
17 | - `\Yiisoft\Strings\WildcardPattern::withEnding()` was removed.
18 | - `\Yiisoft\Strings\WildcardPattern::withoutEscape()` was removed.
19 |
20 | To fix possible issues:
21 |
22 | - Remove `withExactSlashes()` calls.
23 | - Replace `*` with `**` in patterns if you need to match `/` as well.
24 | - If `withEnding()` was used, add `**` to the beginning of the pattern.
25 | - If `withoutEscape()` was used, escape `\` in patterns with another `\`.
26 | Likely `\Yiisoft\Strings\WildcardPattern::quote()` may be of help.
27 |
--------------------------------------------------------------------------------
/composer-require-checker.json:
--------------------------------------------------------------------------------
1 | {
2 | "symbol-whitelist": [
3 | "transliterator_transliterate",
4 | "filter_var",
5 | "FILTER_VALIDATE_INT"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/strings",
3 | "type": "library",
4 | "description": "Yii Strings Helper",
5 | "keywords": [
6 | "yii",
7 | "helper",
8 | "string"
9 | ],
10 | "homepage": "https://www.yiiframework.com/",
11 | "license": "BSD-3-Clause",
12 | "support": {
13 | "issues": "https://github.com/yiisoft/strings/issues?state=open",
14 | "source": "https://github.com/yiisoft/strings",
15 | "forum": "https://www.yiiframework.com/forum/",
16 | "wiki": "https://www.yiiframework.com/wiki/",
17 | "irc": "ircs://irc.libera.chat:6697/yii",
18 | "chat": "https://t.me/yii3en"
19 | },
20 | "funding": [
21 | {
22 | "type": "opencollective",
23 | "url": "https://opencollective.com/yiisoft"
24 | },
25 | {
26 | "type": "github",
27 | "url": "https://github.com/sponsors/yiisoft"
28 | }
29 | ],
30 | "require": {
31 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
32 | "ext-mbstring": "*"
33 | },
34 | "require-dev": {
35 | "maglnet/composer-require-checker": "^4.7.1",
36 | "phpunit/phpunit": "^10.5.45",
37 | "rector/rector": "^2.0.8",
38 | "roave/infection-static-analysis-plugin": "^1.35",
39 | "spatie/phpunit-watcher": "^1.24",
40 | "vimeo/psalm": "^5.26.1|^6.4.1"
41 | },
42 | "autoload": {
43 | "psr-4": {
44 | "Yiisoft\\Strings\\": "src"
45 | }
46 | },
47 | "autoload-dev": {
48 | "psr-4": {
49 | "Yiisoft\\Strings\\Tests\\": "tests"
50 | }
51 | },
52 | "config": {
53 | "sort-packages": true,
54 | "bump-after-update": "dev",
55 | "allow-plugins": {
56 | "infection/extension-installer": true,
57 | "composer/package-versions-deprecated": true
58 | }
59 | },
60 | "scripts": {
61 | "test": "phpunit --testdox --no-interaction",
62 | "test-watch": "phpunit-watcher watch"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "directories": [
4 | "src"
5 | ]
6 | },
7 | "logs": {
8 | "text": "php:\/\/stderr",
9 | "stryker": {
10 | "report": "master"
11 | }
12 | },
13 | "mutators": {
14 | "@default": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | withPaths([
12 | __DIR__ . '/src',
13 | __DIR__ . '/tests',
14 | ])
15 | ->withPhpSets(php81: true)
16 | ->withRules([
17 | InlineConstructorDefaultToPropertyRector::class,
18 | ])
19 | ->withSkip([
20 | ClosureToArrowFunctionRector::class,
21 | NullToStrictStringFuncCallArgRector::class,
22 | ]);
23 |
--------------------------------------------------------------------------------
/src/AbstractCombinedRegexp.php:
--------------------------------------------------------------------------------
1 | getCompiledPattern(),
70 | $string,
71 | )
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/CombinedRegexp.php:
--------------------------------------------------------------------------------
1 | patterns = array_values($patterns);
46 | $this->compiledPattern = $this->compilePatterns($this->patterns) . $this->flags;
47 | }
48 |
49 | /**
50 | * @return string The compiled pattern.
51 | */
52 | public function getCompiledPattern(): string
53 | {
54 | return $this->compiledPattern;
55 | }
56 |
57 | /**
58 | * Returns `true` whether the given string matches any of the patterns, `false` - otherwise.
59 | */
60 | public function matches(string $string): bool
61 | {
62 | return preg_match($this->compiledPattern, $string) === 1;
63 | }
64 |
65 | /**
66 | * Returns pattern that matches the given string.
67 | * @throws Exception if the string does not match any of the patterns.
68 | */
69 | public function getMatchingPattern(string $string): string
70 | {
71 | return $this->patterns[$this->getMatchingPatternPosition($string)];
72 | }
73 |
74 | /**
75 | * Returns position of the pattern that matches the given string.
76 | * @throws Exception if the string does not match any of the patterns.
77 | */
78 | public function getMatchingPatternPosition(string $string): int
79 | {
80 | $match = preg_match($this->compiledPattern, $string, $matches);
81 | if ($match !== 1) {
82 | $this->throwFailedMatchException($string);
83 | }
84 |
85 | return count($matches) - 1;
86 | }
87 |
88 | /**
89 | * @param string[] $patterns
90 | *
91 | * @psalm-param list $patterns
92 | * @psalm-return non-empty-string
93 | */
94 | private function compilePatterns(array $patterns): string
95 | {
96 | $quotedPatterns = [];
97 |
98 | /**
99 | * Possible mutant escaping, but it's ok for our case.
100 | * It doesn't matter where to place `()` in the pattern:
101 | * https://regex101.com/r/lE1Q1S/1, https://regex101.com/r/rWg7Fj/1
102 | */
103 | foreach ($patterns as $i => $pattern) {
104 | $quotedPatterns[] = $pattern . str_repeat('()', $i);
105 | }
106 |
107 | $combinedRegexps = '(?|' . strtr(
108 | implode('|', $quotedPatterns),
109 | [self::REGEXP_DELIMITER => self::QUOTE_REPLACER]
110 | ) . ')';
111 |
112 | return self::REGEXP_DELIMITER . $combinedRegexps . self::REGEXP_DELIMITER;
113 | }
114 |
115 | public function getPatterns(): array
116 | {
117 | return $this->patterns;
118 | }
119 |
120 | public function getFlags(): string
121 | {
122 | return $this->flags;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/Inflector.php:
--------------------------------------------------------------------------------
1 |
74 | * The keys are the regular expressions and the values are the corresponding replacements.
75 | */
76 | private array $pluralizeRules = [
77 | '/([nrlm]ese|deer|fish|sheep|measles|ois|pox|media)$/i' => '\1',
78 | '/^(sea[- ]bass)$/i' => '\1',
79 | '/(m)ove$/i' => '\1oves',
80 | '/(f)oot$/i' => '\1eet',
81 | '/(h)uman$/i' => '\1umans',
82 | '/(s)tatus$/i' => '\1tatuses',
83 | '/(s)taff$/i' => '\1taff',
84 | '/(t)ooth$/i' => '\1eeth',
85 | '/(quiz)$/i' => '\1zes',
86 | '/^(ox)$/i' => '\1\2en',
87 | '/([m|l])ouse$/i' => '\1ice',
88 | '/(matr|vert|ind)(ix|ex)$/i' => '\1ices',
89 | '/(x|ch|ss|sh)$/i' => '\1es',
90 | '/([^aeiouy]|qu)y$/i' => '\1ies',
91 | '/(hive)$/i' => '\1s',
92 | '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves',
93 | '/sis$/i' => 'ses',
94 | '/([ti])um$/i' => '\1a',
95 | '/(p)erson$/i' => '\1eople',
96 | '/(m)an$/i' => '\1en',
97 | '/(c)hild$/i' => '\1hildren',
98 | '/(buffal|tomat|potat|ech|her|vet)o$/i' => '\1oes',
99 | '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$/i' => '\1i',
100 | '/us$/i' => 'uses',
101 | '/(alias)$/i' => '\1es',
102 | '/(ax|cris|test)is$/i' => '\1es',
103 | '/(currenc)y$/' => '\1ies',
104 | '/on$/i' => 'a',
105 | '/s$/' => 's',
106 | '/^$/' => '',
107 | '/$/' => 's',
108 | ];
109 |
110 | /**
111 | * @var string[] The rules for converting a word into its singular form.
112 | *
113 | * @psalm-var non-empty-array
114 | * The keys are the regular expressions and the values are the corresponding replacements.
115 | */
116 | private array $singularizeRules = [
117 | '/([nrlm]ese|deer|fish|sheep|measles|ois|pox|media|ss)$/i' => '\1',
118 | '/^(sea[- ]bass)$/i' => '\1',
119 | '/(s)tatuses$/i' => '\1tatus',
120 | '/(f)eet$/i' => '\1oot',
121 | '/(t)eeth$/i' => '\1ooth',
122 | '/^(.*)(menu)s$/i' => '\1\2',
123 | '/(quiz)zes$/i' => '\\1',
124 | '/(matr)ices$/i' => '\1ix',
125 | '/(vert|ind)ices$/i' => '\1ex',
126 | '/^(ox)en/i' => '\1',
127 | '/(alias)(es)*$/i' => '\1',
128 | '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us',
129 | '/([ftw]ax)es/i' => '\1',
130 | '/(cris|ax|test)es$/i' => '\1is',
131 | '/(shoe|slave)s$/i' => '\1',
132 | '/(o)es$/i' => '\1',
133 | '/ouses$/i' => 'ouse',
134 | '/([^a])uses$/i' => '\1us',
135 | '/([m|l])ice$/i' => '\1ouse',
136 | '/(x|ch|ss|sh)es$/i' => '\1',
137 | '/(m)ovies$/i' => '\1\2ovie',
138 | '/(s)eries$/i' => '\1\2eries',
139 | '/([^aeiouy]|qu)ies$/i' => '\1y',
140 | '/([lr])ves$/i' => '\1f',
141 | '/(tive)s$/i' => '\1',
142 | '/(hive)s$/i' => '\1',
143 | '/(drive)s$/i' => '\1',
144 | '/([^fo])ves$/i' => '\1fe',
145 | '/(^analy)ses$/i' => '\1sis',
146 | '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
147 | '/criteria$/i' => 'criterion',
148 | '/([ti])a$/i' => '\1um',
149 | '/(p)eople$/i' => '\1\2erson',
150 | '/(m)en$/i' => '\1an',
151 | '/(c)hildren$/i' => '\1\2hild',
152 | '/(n)ews$/i' => '\1\2ews',
153 | '/(n)etherlands$/i' => '\1\2etherlands',
154 | '/eaus$/i' => 'eau',
155 | '/(currenc)ies$/i' => '\1y',
156 | '/^(.*us)$/i' => '\\1',
157 | '/s$/i' => '',
158 | ];
159 |
160 | /**
161 | * @psalm-var array
162 | *
163 | * @var string[] The special rules for converting a word between its plural form and singular form.
164 | * The keys are the special words in singular form, and the values are the corresponding plural form.
165 | */
166 | private array $specialRules = [
167 | 'Amoyese' => 'Amoyese',
168 | 'Borghese' => 'Borghese',
169 | 'Congoese' => 'Congoese',
170 | 'Faroese' => 'Faroese',
171 | 'Foochowese' => 'Foochowese',
172 | 'Genevese' => 'Genevese',
173 | 'Genoese' => 'Genoese',
174 | 'Gilbertese' => 'Gilbertese',
175 | 'Hottentotese' => 'Hottentotese',
176 | 'Kiplingese' => 'Kiplingese',
177 | 'Kongoese' => 'Kongoese',
178 | 'Lucchese' => 'Lucchese',
179 | 'Maltese' => 'Maltese',
180 | 'Nankingese' => 'Nankingese',
181 | 'Niasese' => 'Niasese',
182 | 'Pekingese' => 'Pekingese',
183 | 'Piedmontese' => 'Piedmontese',
184 | 'Pistoiese' => 'Pistoiese',
185 | 'Portuguese' => 'Portuguese',
186 | 'Sarawakese' => 'Sarawakese',
187 | 'Shavese' => 'Shavese',
188 | 'Vermontese' => 'Vermontese',
189 | 'Wenchowese' => 'Wenchowese',
190 | 'Yengeese' => 'Yengeese',
191 | 'atlas' => 'atlases',
192 | 'beef' => 'beefs',
193 | 'bison' => 'bison',
194 | 'bream' => 'bream',
195 | 'breeches' => 'breeches',
196 | 'britches' => 'britches',
197 | 'brother' => 'brothers',
198 | 'buffalo' => 'buffalo',
199 | 'cafe' => 'cafes',
200 | 'cantus' => 'cantus',
201 | 'carp' => 'carp',
202 | 'chassis' => 'chassis',
203 | 'child' => 'children',
204 | 'clippers' => 'clippers',
205 | 'cod' => 'cod',
206 | 'coitus' => 'coitus',
207 | 'contretemps' => 'contretemps',
208 | 'cookie' => 'cookies',
209 | 'corps' => 'corps',
210 | 'corpus' => 'corpuses',
211 | 'cow' => 'cows',
212 | 'curve' => 'curves',
213 | 'debris' => 'debris',
214 | 'diabetes' => 'diabetes',
215 | 'djinn' => 'djinn',
216 | 'eland' => 'eland',
217 | 'elk' => 'elk',
218 | 'equipment' => 'equipment',
219 | 'flounder' => 'flounder',
220 | 'foe' => 'foes',
221 | 'gallows' => 'gallows',
222 | 'ganglion' => 'ganglions',
223 | 'genie' => 'genies',
224 | 'genus' => 'genera',
225 | 'graffiti' => 'graffiti',
226 | 'graffito' => 'graffiti',
227 | 'headquarters' => 'headquarters',
228 | 'herpes' => 'herpes',
229 | 'hijinks' => 'hijinks',
230 | 'hoof' => 'hoofs',
231 | 'information' => 'information',
232 | 'innings' => 'innings',
233 | 'jackanapes' => 'jackanapes',
234 | 'loaf' => 'loaves',
235 | 'mackerel' => 'mackerel',
236 | 'man' => 'men',
237 | 'mews' => 'mews',
238 | 'money' => 'monies',
239 | 'mongoose' => 'mongooses',
240 | 'moose' => 'moose',
241 | 'move' => 'moves',
242 | 'mumps' => 'mumps',
243 | 'mythos' => 'mythoi',
244 | 'news' => 'news',
245 | 'nexus' => 'nexus',
246 | 'niche' => 'niches',
247 | 'numen' => 'numina',
248 | 'occiput' => 'occiputs',
249 | 'octopus' => 'octopuses',
250 | 'opus' => 'opuses',
251 | 'ox' => 'oxen',
252 | 'pasta' => 'pasta',
253 | 'penis' => 'penises',
254 | 'pincers' => 'pincers',
255 | 'pliers' => 'pliers',
256 | 'proceedings' => 'proceedings',
257 | 'rabies' => 'rabies',
258 | 'rhinoceros' => 'rhinoceros',
259 | 'rice' => 'rice',
260 | 'salmon' => 'salmon',
261 | 'scissors' => 'scissors',
262 | 'series' => 'series',
263 | 'sex' => 'sexes',
264 | 'shears' => 'shears',
265 | 'siemens' => 'siemens',
266 | 'soliloquy' => 'soliloquies',
267 | 'species' => 'species',
268 | 'swine' => 'swine',
269 | 'testes' => 'testes',
270 | 'testis' => 'testes',
271 | 'trilby' => 'trilbys',
272 | 'trousers' => 'trousers',
273 | 'trout' => 'trout',
274 | 'tuna' => 'tuna',
275 | 'turf' => 'turfs',
276 | 'wave' => 'waves',
277 | 'whiting' => 'whiting',
278 | 'wildebeest' => 'wildebeest',
279 | ];
280 |
281 | /**
282 | * @var string[] Fallback map for transliteration used by {@see toTransliterated()} when intl isn't available or
283 | * turned off with {@see withoutIntl()}.
284 | */
285 | private array $transliterationMap = [
286 | 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'AE', 'Ç' => 'C',
287 | 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
288 | 'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ő' => 'O',
289 | 'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ű' => 'U', 'Ý' => 'Y', 'Þ' => 'TH',
290 | 'ß' => 'ss',
291 | 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae', 'ç' => 'c',
292 | 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
293 | 'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ő' => 'o',
294 | 'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ű' => 'u', 'ý' => 'y', 'þ' => 'th',
295 | 'ÿ' => 'y',
296 | ];
297 |
298 | /**
299 | * @var string|Transliterator Either a {@see Transliterator}, or a string from which a {@see Transliterator}
300 | * can be built for transliteration. Used by {@see toTransliterated()} when intl is available.
301 | * Defaults to {@see TRANSLITERATE_LOOSE}.
302 | *
303 | * @see https://secure.php.net/manual/en/transliterator.transliterate.php
304 | */
305 | private string|Transliterator $transliterator = self::TRANSLITERATE_LOOSE;
306 |
307 | private bool $withoutIntl = false;
308 |
309 | /**
310 | * @param string[] $rules The rules for converting a word into its plural form.
311 | *
312 | * The keys are the regular expressions and the values are the corresponding replacements.
313 | * @psalm-param non-empty-array $rules
314 | *
315 | * @return self
316 | */
317 | public function withPluralizeRules(array $rules): self
318 | {
319 | $new = clone $this;
320 | $new->pluralizeRules = $rules;
321 | return $new;
322 | }
323 |
324 | /**
325 | * @return string[] The rules for converting a word into its plural form.
326 | * The keys are the regular expressions and the values are the corresponding replacements.
327 | */
328 | public function getPluralizeRules(): array
329 | {
330 | return $this->pluralizeRules;
331 | }
332 |
333 | /**
334 | * @param string[] $rules The rules for converting a word into its singular form.
335 | * The keys are the regular expressions and the values are the corresponding replacements.
336 | *
337 | * @psalm-param non-empty-array $rules
338 | *
339 | * @return self
340 | */
341 | public function withSingularizeRules(array $rules): self
342 | {
343 | $new = clone $this;
344 | $new->singularizeRules = $rules;
345 | return $new;
346 | }
347 |
348 | /**
349 | * @return string[] The rules for converting a word into its singular form.
350 | * The keys are the regular expressions and the values are the corresponding replacements.
351 | */
352 | public function getSingularizeRules(): array
353 | {
354 | return $this->singularizeRules;
355 | }
356 |
357 | /**
358 | * @param string[] $rules The special rules for converting a word between its plural form and singular form.
359 | *
360 | * @psalm-param array $rules
361 | * The keys are the special words in singular form, and the values are the corresponding plural form.
362 | *
363 | * @return self
364 | */
365 | public function withSpecialRules(array $rules): self
366 | {
367 | $new = clone $this;
368 | $new->specialRules = $rules;
369 | return $new;
370 | }
371 |
372 | /**
373 | * @return string[] The special rules for converting a word between its plural form and singular form.
374 | * The keys are the special words in singular form, and the values are the corresponding plural form.
375 | */
376 | public function getSpecialRules(): array
377 | {
378 | return $this->specialRules;
379 | }
380 |
381 | /**
382 | * @param string|Transliterator $transliterator Either a {@see \Transliterator}, or a string from which
383 | * a {@see \Transliterator} can be built for transliteration. Used by {@see toTransliterated()} when intl is available.
384 | * Defaults to {@see TRANSLITERATE_LOOSE}.
385 | *
386 | * @return self
387 | *
388 | * @see https://secure.php.net/manual/en/transliterator.transliterate.php
389 | */
390 | public function withTransliterator($transliterator): self
391 | {
392 | $new = clone $this;
393 | $new->transliterator = $transliterator;
394 | return $new;
395 | }
396 |
397 | /**
398 | * @param string[] $transliterationMap Fallback map for transliteration used by {@see toTransliterated()} when intl
399 | * isn't available or turned off with {@see withoutIntl()}.
400 | *
401 | * @return $this
402 | */
403 | public function withTransliterationMap(array $transliterationMap): self
404 | {
405 | $new = clone $this;
406 | $new->transliterationMap = $transliterationMap;
407 | return $new;
408 | }
409 |
410 | /**
411 | * Disables usage of intl for {@see toTransliterated()}.
412 | *
413 | * @return self
414 | */
415 | public function withoutIntl(): self
416 | {
417 | $new = clone $this;
418 | $new->withoutIntl = true;
419 | return $new;
420 | }
421 |
422 | /**
423 | * Converts a word to its plural form.
424 | * Note that this is for English only!
425 | * For example, "apple" will become "apples", and "child" will become "children".
426 | *
427 | * @param string $input The word to be pluralized.
428 | *
429 | * @return string The pluralized word.
430 | */
431 | public function toPlural(string $input): string
432 | {
433 | if (isset($this->specialRules[$input])) {
434 | return $this->specialRules[$input];
435 | }
436 | foreach ($this->pluralizeRules as $rule => $replacement) {
437 | if (preg_match($rule, $input)) {
438 | /** @var string `$rule` and `$replacement` always correct, so `preg_replace` always returns string */
439 | return preg_replace($rule, $replacement, $input);
440 | }
441 | }
442 |
443 | return $input;
444 | }
445 |
446 | /**
447 | * Returns the singular of the $word.
448 | *
449 | * @param string $input The english word to singularize.
450 | *
451 | * @return string Singular noun.
452 | */
453 | public function toSingular(string $input): string
454 | {
455 | $result = array_search($input, $this->specialRules, true);
456 |
457 | if ($result !== false) {
458 | return $result;
459 | }
460 |
461 | foreach ($this->singularizeRules as $rule => $replacement) {
462 | if (preg_match($rule, $input)) {
463 | /** @var string `$rule` and `$replacement` always correct, so `preg_replace` always returns string */
464 | return preg_replace($rule, $replacement, $input);
465 | }
466 | }
467 |
468 | return $input;
469 | }
470 |
471 | /**
472 | * Converts an underscored or PascalCase word into a English
473 | * sentence.
474 | *
475 | * @param string $input The string to titleize.
476 | * @param bool $uppercaseAll Whether to set all words to uppercase.
477 | *
478 | * @return string
479 | */
480 | public function toSentence(string $input, bool $uppercaseAll = false): string
481 | {
482 | $input = $this->toHumanReadable($this->pascalCaseToId($input, '_'), $uppercaseAll);
483 |
484 | return $uppercaseAll
485 | ? StringHelper::uppercaseFirstCharacterInEachWord($input)
486 | : StringHelper::uppercaseFirstCharacter($input);
487 | }
488 |
489 | /**
490 | * Converts a string into space-separated words.
491 | * For example, 'PostTag' will be converted to 'Post Tag'.
492 | *
493 | * @param string $input The valid UTF-8 string to be converted.
494 | *
495 | * @return string The resulting words.
496 | */
497 | public function toWords(string $input): string
498 | {
499 | /**
500 | * @var string $words We assume that `$input` is valid UTF-8 string, so `preg_replace()` never returns `false`.
501 | */
502 | $words = preg_replace('/(?toPascalCase($input);
599 |
600 | return mb_strtolower(mb_substr($input, 0, 1)) . mb_substr($input, 1);
601 | }
602 |
603 | /**
604 | * Returns given word as "snake_cased".
605 | *
606 | * Converts a word like "userName" to "user_name".
607 | * It will remove non-alphanumeric character from the word,
608 | * so "who's online" will be converted to "who_s_online".
609 | *
610 | * @param string $input The word to convert. It must be valid UTF-8 string.
611 | * @param bool $strict Whether to insert a separator between two consecutive uppercase chars, defaults to true.
612 | *
613 | * @return string The "snake_cased" string.
614 | */
615 | public function toSnakeCase(string $input, bool $strict = true): string
616 | {
617 | /**
618 | * @var string $input We assume that `$input` is valid UTF-8 string, so `preg_replace()` never returns `false`.
619 | */
620 | $input = preg_replace('/[^\pL\pN]+/u', '_', $input);
621 | return $this->pascalCaseToId($input, '_', $strict);
622 | }
623 |
624 | /**
625 | * Converts a class name to its table name (pluralized) naming conventions.
626 | *
627 | * For example, converts "Car" to "cars", "Person" to "people", and "ActionLog" to "action_log".
628 | *
629 | * @param string $className the class name for getting related table_name.
630 | *
631 | * @return string
632 | */
633 | public function classToTable(string $className): string
634 | {
635 | return $this->toPlural($this->pascalCaseToId($className, '_'));
636 | }
637 |
638 | /**
639 | * Converts a table name to its class name.
640 | *
641 | * For example, converts "cars" to "Car", "people" to "Person", and "action_log" to "ActionLog".
642 | *
643 | * @return string
644 | */
645 | public function tableToClass(string $tableName): string
646 | {
647 | return $this->toPascalCase($this->toSingular($tableName));
648 | }
649 |
650 | /**
651 | * Returns a string with all spaces converted to given replacement,
652 | * non word characters removed and the rest of characters transliterated.
653 | *
654 | * If intl extension isn't available uses fallback that converts latin characters only
655 | * and removes the rest. You may customize characters map via $transliteration property
656 | * of the helper.
657 | *
658 | * @param string $input An arbitrary valid UTF-8 string to convert.
659 | * @param string $replacement The replacement to use for spaces. It must be valid UTF-8 string.
660 | * @param bool $lowercase whether to return the string in lowercase or not. Defaults to `true`.
661 | *
662 | * @return string The converted string.
663 | */
664 | public function toSlug(string $input, string $replacement = '-', bool $lowercase = true): string
665 | {
666 | $quotedReplacement = preg_quote($replacement, '/');
667 |
668 | /**
669 | * Replace all non-words character
670 | *
671 | * @var string $input We assume that `$input` and `$replacement` are valid UTF-8 strings, so `preg_replace()`
672 | * never returns `false`.
673 | */
674 | $input = preg_replace('/[^a-zA-Z0-9]+/u', $replacement, $this->toTransliterated($input));
675 |
676 | /**
677 | * Remove first and last replacements
678 | *
679 | * @var string $input We assume that `$input` and `$quotedReplacement` are valid UTF-8 strings, so
680 | * `preg_replace()` never returns `false`.
681 | */
682 | $input = preg_replace(
683 | "/^(?:$quotedReplacement)+|(?:$quotedReplacement)+$/u" . ($lowercase ? 'i' : ''),
684 | '',
685 | $input,
686 | );
687 |
688 | return $lowercase ? strtolower($input) : $input;
689 | }
690 |
691 | /**
692 | * Returns transliterated version of a string.
693 | *
694 | * If intl extension isn't available uses fallback that converts latin characters only
695 | * and removes the rest. You may customize characters map via $transliteration property
696 | * of the helper.
697 | *
698 | * @noinspection PhpComposerExtensionStubsInspection
699 | *
700 | * @param string $input Input string. It must be valid UTF-8 string.
701 | * @param string|Transliterator|null $transliterator either a {@see \Transliterator} or a string
702 | * from which a {@see \Transliterator} can be built. If null, value set with {@see withTransliterator()}
703 | * or {@see TRANSLITERATE_LOOSE} is used.
704 | */
705 | public function toTransliterated(string $input, $transliterator = null): string
706 | {
707 | if ($this->useIntl()) {
708 | if ($transliterator === null) {
709 | $transliterator = $this->transliterator;
710 | }
711 |
712 | /**
713 | * @noinspection PhpComposerExtensionStubsInspection
714 | * @var string We assume that `$input` are valid UTF-8 strings and `$transliterator` is valid, so
715 | * `preg_replace()` never returns `false`.
716 | */
717 | return transliterator_transliterate($transliterator, $input);
718 | }
719 |
720 | return strtr($input, $this->transliterationMap);
721 | }
722 |
723 | /**
724 | * @return bool If intl extension should be used.
725 | */
726 | private function useIntl(): bool
727 | {
728 | return $this->withoutIntl === false && extension_loaded('intl');
729 | }
730 | }
731 |
--------------------------------------------------------------------------------
/src/MemoizedCombinedRegexp.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | private array $results = [];
19 |
20 | public function __construct(
21 | private readonly AbstractCombinedRegexp $decorated,
22 | ) {
23 | }
24 |
25 | public function getCompiledPattern(): string
26 | {
27 | return $this->decorated->getCompiledPattern();
28 | }
29 |
30 | public function matches(string $string): bool
31 | {
32 | $this->evaluate($string);
33 |
34 | return $this->results[$string]['matches'];
35 | }
36 |
37 | public function getMatchingPattern(string $string): string
38 | {
39 | $this->evaluate($string);
40 |
41 | return $this->getPatterns()[$this->getMatchingPatternPosition($string)];
42 | }
43 |
44 | public function getMatchingPatternPosition(string $string): int
45 | {
46 | $this->evaluate($string);
47 |
48 | return $this->results[$string]['position'] ?? $this->throwFailedMatchException($string);
49 | }
50 |
51 | private function evaluate(string $string): void
52 | {
53 | if (isset($this->results[$string])) {
54 | return;
55 | }
56 | try {
57 | $position = $this->decorated->getMatchingPatternPosition($string);
58 |
59 | $this->results[$string]['matches'] = true;
60 | $this->results[$string]['position'] = $position;
61 | } catch (\Exception) {
62 | $this->results[$string]['matches'] = false;
63 | }
64 | }
65 |
66 | public function getPatterns(): array
67 | {
68 | return $this->decorated->getPatterns();
69 | }
70 |
71 | public function getFlags(): string
72 | {
73 | return $this->decorated->getFlags();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/NumericHelper.php:
--------------------------------------------------------------------------------
1 | $value . 'st',
47 | 2 => $value . 'nd',
48 | 3 => $value . 'rd',
49 | default => $value . 'th',
50 | };
51 | }
52 |
53 | /**
54 | * Returns string representation of a number value without thousands separators and with dot as decimal separator.
55 | *
56 | * @param bool|float|int|string|Stringable $value String in `string` or `Stringable` must be valid UTF-8 string.
57 | *
58 | * @throws InvalidArgumentException if value is not scalar.
59 | */
60 | public static function normalize(mixed $value): string
61 | {
62 | /** @psalm-suppress DocblockTypeContradiction */
63 | if (!is_scalar($value) && !$value instanceof Stringable) {
64 | $type = gettype($value);
65 | throw new InvalidArgumentException("Value must be scalar. $type given.");
66 | }
67 |
68 | if (is_bool($value)) {
69 | return $value ? '1' : '0';
70 | }
71 |
72 | $value = str_replace([' ', ','], ['', '.'], (string) $value);
73 |
74 | /**
75 | * @var string We assume that `$value` is valid UTF-8 string, so `preg_replace()` never returns `false`.
76 | */
77 | return preg_replace('/\.(?=.*\.)/', '', $value);
78 | }
79 |
80 | /**
81 | * Checks whether the given string is an integer number.
82 | *
83 | * Require Filter PHP extension ({@see https://www.php.net/manual/intro.filter.php}).
84 | */
85 | public static function isInteger(mixed $value): bool
86 | {
87 | return filter_var($value, FILTER_VALIDATE_INT) !== false;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/StringHelper.php:
--------------------------------------------------------------------------------
1 | 0 && mb_substr($path, -$length) === $suffix) {
94 | $path = mb_substr($path, 0, -$length);
95 | }
96 | $path = rtrim(str_replace('\\', '/', $path), '/\\');
97 | $position = mb_strrpos($path, '/');
98 | if ($position !== false) {
99 | return mb_substr($path, $position + 1);
100 | }
101 |
102 | return $path;
103 | }
104 |
105 | /**
106 | * Returns parent directory's path.
107 | * This method is similar to `dirname()` except that it will treat
108 | * both \ and / as directory separators, independent of the operating system.
109 | *
110 | * @param string $path A path string.
111 | *
112 | * @return string The parent directory's path.
113 | *
114 | * @see https://www.php.net/manual/en/function.basename.php
115 | */
116 | public static function directoryName(string $path): string
117 | {
118 | $position = mb_strrpos(str_replace('\\', '/', $path), '/');
119 | if ($position !== false) {
120 | return mb_substr($path, 0, $position);
121 | }
122 |
123 | return '';
124 | }
125 |
126 | /**
127 | * Get part of string.
128 | *
129 | * @param string $string To get substring from.
130 | * @param int $start Character to start at.
131 | * @param int|null $length Number of characters to get.
132 | * @param string $encoding The encoding to use, defaults to "UTF-8".
133 | *
134 | * @see https://php.net/manual/en/function.mb-substr.php
135 | */
136 | public static function substring(string $string, int $start, ?int $length = null, string $encoding = 'UTF-8'): string
137 | {
138 | return mb_substr($string, $start, $length, $encoding);
139 | }
140 |
141 | /**
142 | * Replace text within a portion of a string.
143 | *
144 | * @param string $string The input string.
145 | * @param string $replacement The replacement string.
146 | * @param int $start Position to begin replacing substring at.
147 | * If start is non-negative, the replacing will begin at the start'th offset into string.
148 | * If start is negative, the replacing will begin at the start'th character from the end of string.
149 | * @param int|null $length Length of the substring to be replaced.
150 | * If given and is positive, it represents the length of the portion of string which is to be replaced.
151 | * If it is negative, it represents the number of characters from the end of string at which to stop replacing.
152 | * If it is not given, then it will default to the length of the string; i.e. end the replacing at the end of string.
153 | * If length is zero then this function will have the effect of inserting replacement into string at the given start offset.
154 | * @param string $encoding The encoding to use, defaults to "UTF-8".
155 | */
156 | public static function replaceSubstring(
157 | string $string,
158 | string $replacement,
159 | int $start,
160 | int|null $length = null,
161 | string $encoding = 'UTF-8',
162 | ): string {
163 | $stringLength = mb_strlen($string, $encoding);
164 |
165 | if ($start < 0) {
166 | $start = max(0, $stringLength + $start);
167 | } elseif ($start > $stringLength) {
168 | $start = $stringLength;
169 | }
170 |
171 | if ($length !== null && $length < 0) {
172 | $length = max(0, $stringLength - $start + $length);
173 | } elseif ($length === null || $length > $stringLength) {
174 | $length = $stringLength;
175 | }
176 |
177 | if (($start + $length) > $stringLength) {
178 | $length = $stringLength - $start;
179 | }
180 |
181 | return mb_substr($string, 0, $start, $encoding)
182 | . $replacement
183 | . mb_substr($string, $start + $length, $stringLength - $start - $length, $encoding);
184 | }
185 |
186 | /**
187 | * Check if given string starts with specified substring.
188 | * Binary and multibyte safe.
189 | *
190 | * @param string $input Input string.
191 | * @param string|null $with Part to search inside the $string.
192 | *
193 | * @return bool Returns true if first input starts with second input, false otherwise.
194 | */
195 | public static function startsWith(string $input, string|null $with): bool
196 | {
197 | return $with === null || str_starts_with($input, $with);
198 | }
199 |
200 | /**
201 | * Check if given string starts with specified substring ignoring case.
202 | * Binary and multibyte safe.
203 | *
204 | * @param string $input Input string.
205 | * @param string|null $with Part to search inside the $string.
206 | *
207 | * @return bool Returns true if first input starts with second input, false otherwise.
208 | */
209 | public static function startsWithIgnoringCase(string $input, string|null $with): bool
210 | {
211 | $bytes = self::byteLength($with);
212 |
213 | if ($bytes === 0) {
214 | return true;
215 | }
216 |
217 | /** @psalm-suppress PossiblyNullArgument */
218 | return self::lowercase(self::substring($input, 0, $bytes, '8bit')) === self::lowercase($with);
219 | }
220 |
221 | /**
222 | * Check if given string ends with specified substring.
223 | * Binary and multibyte safe.
224 | *
225 | * @param string $input Input string to check.
226 | * @param string|null $with Part to search inside of the $string.
227 | *
228 | * @return bool Returns true if first input ends with second input, false otherwise.
229 | */
230 | public static function endsWith(string $input, string|null $with): bool
231 | {
232 | return $with === null || str_ends_with($input, $with);
233 | }
234 |
235 | /**
236 | * Check if given string ends with specified substring.
237 | * Binary and multibyte safe.
238 | *
239 | * @param string $input Input string to check.
240 | * @param string|null $with Part to search inside of the $string.
241 | *
242 | * @return bool Returns true if first input ends with second input, false otherwise.
243 | */
244 | public static function endsWithIgnoringCase(string $input, string|null $with): bool
245 | {
246 | $bytes = self::byteLength($with);
247 |
248 | if ($bytes === 0) {
249 | return true;
250 | }
251 |
252 | /** @psalm-suppress PossiblyNullArgument */
253 | return self::lowercase(mb_substr($input, -$bytes, mb_strlen($input, '8bit'), '8bit')) === self::lowercase($with);
254 | }
255 |
256 | /**
257 | * Truncates a string from the beginning to the number of characters specified.
258 | *
259 | * @param string $input String to process.
260 | * @param int $length Maximum length of the truncated string including trim marker.
261 | * @param string $trimMarker String to append to the beginning.
262 | * @param string $encoding The encoding to use, defaults to "UTF-8".
263 | */
264 | public static function truncateBegin(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
265 | {
266 | $inputLength = mb_strlen($input, $encoding);
267 |
268 | if ($inputLength <= $length) {
269 | return $input;
270 | }
271 |
272 | $trimMarkerLength = mb_strlen($trimMarker, $encoding);
273 | return self::replaceSubstring($input, $trimMarker, 0, -$length + $trimMarkerLength, $encoding);
274 | }
275 |
276 | /**
277 | * Truncates a string in the middle. Keeping start and end.
278 | * `StringHelper::truncateMiddle('Hello world number 2', 8)` produces "Hell…r 2".
279 | *
280 | * @param string $input The string to truncate.
281 | * @param int $length Maximum length of the truncated string including trim marker.
282 | * @param string $trimMarker String to append in the middle of truncated string.
283 | * @param string $encoding The encoding to use, defaults to "UTF-8".
284 | *
285 | * @return string The truncated string.
286 | */
287 | public static function truncateMiddle(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
288 | {
289 | $inputLength = mb_strlen($input, $encoding);
290 |
291 | if ($inputLength <= $length) {
292 | return $input;
293 | }
294 |
295 | $trimMarkerLength = mb_strlen($trimMarker, $encoding);
296 | $start = (int)ceil(($length - $trimMarkerLength) / 2);
297 | $end = $length - $start - $trimMarkerLength;
298 |
299 | return self::replaceSubstring($input, $trimMarker, $start, -$end, $encoding);
300 | }
301 |
302 | /**
303 | * Truncates a string from the end to the number of characters specified.
304 | *
305 | * @param string $input The string to truncate.
306 | * @param int $length Maximum length of the truncated string including trim marker.
307 | * @param string $trimMarker String to append to the end of truncated string.
308 | * @param string $encoding The encoding to use, defaults to "UTF-8".
309 | *
310 | * @return string The truncated string.
311 | */
312 | public static function truncateEnd(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
313 | {
314 | $inputLength = mb_strlen($input, $encoding);
315 |
316 | if ($inputLength <= $length) {
317 | return $input;
318 | }
319 |
320 | $trimMarkerLength = mb_strlen($trimMarker, $encoding);
321 | return rtrim(mb_substr($input, 0, $length - $trimMarkerLength, $encoding)) . $trimMarker;
322 | }
323 |
324 | /**
325 | * Truncates a string to the number of words specified.
326 | *
327 | * @param string $input The string to truncate.
328 | * @param int $count How many words from original string to include into truncated string.
329 | * @param string $trimMarker String to append to the end of truncated string.
330 | *
331 | * @return string The truncated string.
332 | */
333 | public static function truncateWords(string $input, int $count, string $trimMarker = '…'): string
334 | {
335 | /** @psalm-var list $words */
336 | $words = preg_split('/(\s+)/u', trim($input), -1, PREG_SPLIT_DELIM_CAPTURE);
337 | if (count($words) / 2 > $count) {
338 | $words = array_slice($words, 0, ($count * 2) - 1);
339 | return implode('', $words) . $trimMarker;
340 | }
341 |
342 | return $input;
343 | }
344 |
345 | /**
346 | * Get string length.
347 | *
348 | * @param string $string String to calculate length for.
349 | * @param string $encoding The encoding to use, defaults to "UTF-8".
350 | *
351 | * @see https://php.net/manual/en/function.mb-strlen.php
352 | */
353 | public static function length(string $string, string $encoding = 'UTF-8'): int
354 | {
355 | return mb_strlen($string, $encoding);
356 | }
357 |
358 | /**
359 | * Counts words in a string.
360 | */
361 | public static function countWords(string $input): int
362 | {
363 | /** @var array $words */
364 | $words = preg_split('/\s+/u', $input, -1, PREG_SPLIT_NO_EMPTY);
365 | return count($words);
366 | }
367 |
368 | /**
369 | * Make a string lowercase.
370 | *
371 | * @param string $string String to process.
372 | * @param string $encoding The encoding to use, defaults to "UTF-8".
373 | *
374 | * @see https://php.net/manual/en/function.mb-strtolower.php
375 | */
376 | public static function lowercase(string $string, string $encoding = 'UTF-8'): string
377 | {
378 | return mb_strtolower($string, $encoding);
379 | }
380 |
381 | /**
382 | * Make a string uppercase.
383 | *
384 | * @param string $string String to process.
385 | * @param string $encoding The encoding to use, defaults to "UTF-8".
386 | *
387 | * @see https://php.net/manual/en/function.mb-strtoupper.php
388 | */
389 | public static function uppercase(string $string, string $encoding = 'UTF-8'): string
390 | {
391 | return mb_strtoupper($string, $encoding);
392 | }
393 |
394 | /**
395 | * Make a string's first character uppercase.
396 | *
397 | * @param string $string The string to be processed.
398 | * @param string $encoding The encoding to use, defaults to "UTF-8".
399 | *
400 | * @see https://php.net/manual/en/function.ucfirst.php
401 | */
402 | public static function uppercaseFirstCharacter(string $string, string $encoding = 'UTF-8'): string
403 | {
404 | $firstCharacter = self::substring($string, 0, 1, $encoding);
405 | $rest = self::substring($string, 1, null, $encoding);
406 |
407 | return self::uppercase($firstCharacter, $encoding) . $rest;
408 | }
409 |
410 | /**
411 | * Uppercase the first character of each word in a string.
412 | *
413 | * @param string $string The valid UTF-8 string to be processed.
414 | * @param string $encoding The encoding to use, defaults to "UTF-8".
415 | *
416 | * @see https://php.net/manual/en/function.ucwords.php
417 | */
418 | public static function uppercaseFirstCharacterInEachWord(string $string, string $encoding = 'UTF-8'): string
419 | {
420 | /**
421 | * @var array $words We assume that `$string` is valid UTF-8 string, so `preg_split()` never returns `false`.
422 | */
423 | $words = preg_split('/\s/u', $string, -1, PREG_SPLIT_NO_EMPTY);
424 |
425 | $wordsWithUppercaseFirstCharacter = array_map(
426 | static fn (string $word) => self::uppercaseFirstCharacter($word, $encoding),
427 | $words
428 | );
429 |
430 | return implode(' ', $wordsWithUppercaseFirstCharacter);
431 | }
432 |
433 | /**
434 | * Encodes string into "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
435 | *
436 | * > Note: Base 64 padding `=` may be at the end of the returned string.
437 | * > `=` is not transparent to URL encoding.
438 | *
439 | * @see https://tools.ietf.org/html/rfc4648#page-7
440 | *
441 | * @param string $input The string to encode.
442 | *
443 | * @return string Encoded string.
444 | *
445 | * @psalm-template T as string
446 | * @psalm-param T $input
447 | * @psalm-return (T is non-empty-string ? non-empty-string : "")
448 | */
449 | public static function base64UrlEncode(string $input): string
450 | {
451 | return strtr(base64_encode($input), '+/', '-_');
452 | }
453 |
454 | /**
455 | * Decodes "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
456 | *
457 | * @see https://tools.ietf.org/html/rfc4648#page-7
458 | *
459 | * @param string $input Encoded string.
460 | *
461 | * @return string Decoded string.
462 | */
463 | public static function base64UrlDecode(string $input): string
464 | {
465 | return base64_decode(strtr($input, '-_', '+/'));
466 | }
467 |
468 | /**
469 | * Split a string to array with non-empty lines.
470 | * Whitespace from the beginning and end of a each line will be stripped.
471 | *
472 | * @param string $string The input string. It must be valid UTF-8 string.
473 | * @param string $separator The boundary string. It is a part of regular expression
474 | * so should be taken into account or properly escaped with {@see preg_quote()}. It must be valid UTF-8 string.
475 | */
476 | public static function split(string $string, string $separator = '\R'): array
477 | {
478 | /**
479 | * @var string $string We assume that `$string` is valid UTF-8 string, so `preg_replace()` never returns
480 | * `false`.
481 | */
482 | $string = preg_replace('(^\s*|\s*$)', '', $string);
483 |
484 | /**
485 | * @var array We assume that $separator is prepared by `preg_quote()` and $string is valid UTF-8 string,
486 | * so `preg_split()` never returns `false`.
487 | */
488 | return preg_split('~\s*' . $separator . '\s*~u', $string, -1, PREG_SPLIT_NO_EMPTY);
489 | }
490 |
491 | /**
492 | * @param string $path The path of where do you want to write a value to `$array`. The path can be described by
493 | * a string when each key should be separated by delimiter. If a path item contains delimiter, it can be escaped
494 | * with "\" (backslash) or a custom delimiter can be used.
495 | * @param string $delimiter A separator, used to parse string key for embedded object property retrieving. Defaults
496 | * to "." (dot).
497 | * @param string $escapeCharacter An escape character, used to escape delimiter. Defaults to "\" (backslash).
498 | * @param bool $preserveDelimiterEscaping Whether to preserve delimiter escaping in the items of final array (in
499 | * case of using string as an input). When `false`, "\" (backslashes) are removed. For a "." as delimiter, "."
500 | * becomes "\.". Defaults to `false`.
501 | *
502 | * @return string[]
503 | *
504 | * @psalm-return non-empty-list
505 | */
506 | public static function parsePath(
507 | string $path,
508 | string $delimiter = '.',
509 | string $escapeCharacter = '\\',
510 | bool $preserveDelimiterEscaping = false
511 | ): array {
512 | if (strlen($delimiter) !== 1) {
513 | throw new InvalidArgumentException('Only 1 character is allowed for delimiter.');
514 | }
515 |
516 | if (strlen($escapeCharacter) !== 1) {
517 | throw new InvalidArgumentException('Only 1 escape character is allowed.');
518 | }
519 |
520 | if ($delimiter === $escapeCharacter) {
521 | throw new InvalidArgumentException('Delimiter and escape character must be different.');
522 | }
523 |
524 | if ($path === '') {
525 | return [''];
526 | }
527 |
528 | if (!str_contains($path, $delimiter)) {
529 | if ($preserveDelimiterEscaping) {
530 | return [$path];
531 | }
532 |
533 | return [str_replace($escapeCharacter . $escapeCharacter, $escapeCharacter, $path)];
534 | }
535 |
536 | /** @psalm-var non-empty-list $matches */
537 | $matches = preg_split(
538 | sprintf(
539 | '/(?%1$s%1$s)*)%2$s/',
540 | preg_quote($escapeCharacter, '/'),
541 | preg_quote($delimiter, '/')
542 | ),
543 | $path,
544 | -1,
545 | PREG_SPLIT_OFFSET_CAPTURE
546 | );
547 | $result = [];
548 | $countResults = count($matches);
549 | for ($i = 1; $i < $countResults; $i++) {
550 | $l = $matches[$i][1] - $matches[$i - 1][1] - strlen($matches[$i - 1][0]) - 1;
551 | $result[] = $matches[$i - 1][0] . ($l > 0 ? str_repeat($escapeCharacter, $l) : '');
552 | }
553 | $result[] = $matches[$countResults - 1][0];
554 |
555 | if ($preserveDelimiterEscaping === true) {
556 | return $result;
557 | }
558 |
559 | return array_map(
560 | static fn (string $key): string => str_replace(
561 | [
562 | $escapeCharacter . $escapeCharacter,
563 | $escapeCharacter . $delimiter,
564 | ],
565 | [
566 | $escapeCharacter,
567 | $delimiter,
568 | ],
569 | $key
570 | ),
571 | $result
572 | );
573 | }
574 |
575 | /**
576 | * Strip Unicode whitespace (with Unicode symbol property White_Space=yes) or other characters from the beginning and end of a string.
577 | * Input string and pattern are treated as UTF-8.
578 | *
579 | * @see https://en.wikipedia.org/wiki/Whitespace_character#Unicode
580 | * @see https://www.php.net/manual/function.preg-replace
581 | *
582 | * @param string|string[] $string The string or an array with strings.
583 | * @param string $pattern PCRE regex pattern to search for, as UTF-8 string. Use {@see preg_quote()} to quote `$pattern` if it contains
584 | * special regular expression characters.
585 | *
586 | * @psalm-template TKey of array-key
587 | * @psalm-param string|array $string
588 | * @psalm-param non-empty-string $pattern
589 | * @psalm-return ($string is array ? array : string)
590 | *
591 | * @return string|string[]
592 | */
593 | public static function trim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array
594 | {
595 | self::ensureUtf8String($string);
596 | self::ensureUtf8Pattern($pattern);
597 |
598 | /**
599 | * @var string|string[] `$string` is correct UTF-8 string and `$pattern` is correct (it should be passed
600 | * already prepared), so `preg_replace` never returns `null`.
601 | */
602 | return preg_replace("#^[$pattern]+|[$pattern]+$#uD", '', $string);
603 | }
604 |
605 | /**
606 | * Strip Unicode whitespace (with Unicode symbol property White_Space=yes) or other characters from the beginning of a string.
607 | *
608 | * @see self::trim()
609 | *
610 | * @param string|string[] $string The string or an array with strings.
611 | * @param string $pattern PCRE regex pattern to search for, as UTF-8 string. Use {@see preg_quote()} to quote `$pattern` if it contains
612 | * special regular expression characters.
613 | *
614 | * @psalm-template TKey of array-key
615 | * @psalm-param string|array $string
616 | * @psalm-param non-empty-string $pattern
617 | * @psalm-return ($string is array ? array : string)
618 | *
619 | * @return string|string[]
620 | */
621 | public static function ltrim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array
622 | {
623 | self::ensureUtf8String($string);
624 | self::ensureUtf8Pattern($pattern);
625 |
626 | /**
627 | * @var string|string[] `$string` is correct UTF-8 string and `$pattern` is correct (it should be passed
628 | * already prepared), so `preg_replace` never returns `null`.
629 | */
630 | return preg_replace("#^[$pattern]+#u", '', $string);
631 | }
632 |
633 | /**
634 | * Strip Unicode whitespace (with Unicode symbol property White_Space=yes) or other characters from the end of a string.
635 | *
636 | * @see self::trim()
637 | *
638 | * @param string|string[] $string The string or an array with strings.
639 | * @param string $pattern PCRE regex pattern to search for, as UTF-8 string. Use {@see preg_quote()} to quote `$pattern` if it contains
640 | * special regular expression characters.
641 | *
642 | * @psalm-template TKey of array-key
643 | * @psalm-param string|array $string
644 | * @psalm-param non-empty-string $pattern
645 | * @psalm-return ($string is array ? array : string)
646 | *
647 | * @return string|string[]
648 | */
649 | public static function rtrim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array
650 | {
651 | self::ensureUtf8String($string);
652 | self::ensureUtf8Pattern($pattern);
653 |
654 | /**
655 | * @var string|string[] `$string` is correct UTF-8 string and `$pattern` is correct (it should be passed
656 | * already prepared), so `preg_replace` never returns `null`.
657 | */
658 | return preg_replace("#[$pattern]+$#uD", '', $string);
659 | }
660 |
661 | /**
662 | * Returns the portion of the string that lies between the first occurrence of the `$start` string
663 | * and the last occurrence of the `$end` string after that.
664 | *
665 | * @param string $string The input string.
666 | * @param string $start The string marking the start of the portion to extract.
667 | * @param string|null $end The string marking the end of the portion to extract.
668 | * If the `$end` string is not provided, it defaults to the value of the `$start` string.
669 | * @return string|null The portion of the string between the first occurrence of
670 | * `$start` and the last occurrence of `$end`, or null if either `$start` or `$end` cannot be found.
671 | */
672 | public static function findBetween(string $string, string $start, ?string $end = null): ?string
673 | {
674 | if ($end === null) {
675 | $end = $start;
676 | }
677 |
678 | $startPos = mb_strpos($string, $start);
679 |
680 | if ($startPos === false) {
681 | return null;
682 | }
683 |
684 | $startPos += mb_strlen($start);
685 | $endPos = mb_strrpos($string, $end, $startPos);
686 |
687 | if ($endPos === false) {
688 | return null;
689 | }
690 |
691 | return mb_substr($string, $startPos, $endPos - $startPos);
692 | }
693 |
694 | /**
695 | * Returns the portion of the string between the initial occurrence of the '$start' string
696 | * and the next occurrence of the '$end' string.
697 | *
698 | * @param string $string The input string.
699 | * @param string $start The string marking the beginning of the segment to extract.
700 | * @param string|null $end The string marking the termination of the segment.
701 | * If the '$end' string is not provided, it defaults to the value of the '$start' string.
702 | * @return string|null Extracted segment, or null if '$start' or '$end' is not present.
703 | */
704 | public static function findBetweenFirst(string $string, string $start, ?string $end = null): ?string
705 | {
706 | if ($end === null) {
707 | $end = $start;
708 | }
709 |
710 | $startPos = mb_strpos($string, $start);
711 |
712 | if ($startPos === false) {
713 | return null;
714 | }
715 |
716 | $startPos += mb_strlen($start);
717 | $endPos = mb_strpos($string, $end, $startPos);
718 |
719 | if ($endPos === false) {
720 | return null;
721 | }
722 |
723 | return mb_substr($string, $startPos, $endPos - $startPos);
724 | }
725 |
726 | /**
727 | * Returns the portion of the string between the latest '$start' string
728 | * and the subsequent '$end' string.
729 | *
730 | * @param string $string The input string.
731 | * @param string $start The string marking the beginning of the segment to extract.
732 | * @param string|null $end The string marking the termination of the segment.
733 | * If the '$end' string is not provided, it defaults to the value of the '$start' string.
734 | * @return string|null Extracted segment, or null if '$start' or '$end' is not present.
735 | */
736 | public static function findBetweenLast(string $string, string $start, ?string $end = null): ?string
737 | {
738 | if ($end === null) {
739 | $end = $start;
740 | }
741 |
742 | $endPos = mb_strrpos($string, $end);
743 |
744 | if ($endPos === false) {
745 | return null;
746 | }
747 |
748 | $startPos = mb_strrpos(mb_substr($string, 0, $endPos), $start);
749 |
750 | if ($startPos === false) {
751 | return null;
752 | }
753 |
754 | $startPos += mb_strlen($start);
755 |
756 | return mb_substr($string, $startPos, $endPos - $startPos);
757 | }
758 |
759 | /**
760 | * Checks if a given string matches any of the provided patterns.
761 | *
762 | * Note that patterns should be provided without delimiters on both sides. For example, `te(s|x)t`.
763 | *
764 | * @see https://www.php.net/manual/reference.pcre.pattern.syntax.php
765 | * @see https://www.php.net/manual/reference.pcre.pattern.modifiers.php
766 | *
767 | * @param string $string The string to match against the patterns.
768 | * @param string[] $patterns Regular expressions without delimiters on both sides.
769 | * @param string $flags Flags to apply to all regular expressions.
770 | */
771 | public static function matchAnyRegex(string $string, array $patterns, string $flags = ''): bool
772 | {
773 | if (empty($patterns)) {
774 | return false;
775 | }
776 |
777 | return (new CombinedRegexp($patterns, $flags))->matches($string);
778 | }
779 |
780 | /**
781 | * Ensure the pattern is a valid UTF-8 string.
782 | *
783 | * @param string $pattern The pattern.
784 | *
785 | * @throws InvalidArgumentException
786 | */
787 | private static function ensureUtf8Pattern(string $pattern): void
788 | {
789 | if (!preg_match('##u', $pattern)) {
790 | throw new InvalidArgumentException('Pattern is not a valid UTF-8 string.');
791 | }
792 | }
793 |
794 | /**
795 | * Ensure the string is a valid UTF-8 string.
796 | *
797 | * @param array|string $string The string.
798 | *
799 | * @throws InvalidArgumentException
800 | *
801 | * @psalm-param string|string[] $string
802 | */
803 | private static function ensureUtf8String(string|array $string): void
804 | {
805 | foreach ((array) $string as $s) {
806 | if (!preg_match('##u', $s)) {
807 | throw new InvalidArgumentException('String is not a valid UTF-8 string.');
808 | }
809 | }
810 | }
811 | }
812 |
--------------------------------------------------------------------------------
/src/WildcardPattern.php:
--------------------------------------------------------------------------------
1 | pattern === '**') {
55 | return true;
56 | }
57 |
58 | return preg_match($this->getPatternPrepared(), $string) === 1;
59 | }
60 |
61 | /**
62 | * Make pattern case insensitive.
63 | */
64 | public function ignoreCase(bool $flag = true): self
65 | {
66 | $new = clone $this;
67 | $new->patternPrepared = null;
68 | $new->ignoreCase = $flag;
69 |
70 | return $new;
71 | }
72 |
73 | /**
74 | * Returns whether the pattern contains a dynamic part i.e.
75 | * has unescaped "*", "{", "?", or "[" character.
76 | *
77 | * @param string $pattern The pattern to check.
78 | *
79 | * @return bool Whether the pattern contains a dynamic part.
80 | */
81 | public static function isDynamic(string $pattern): bool
82 | {
83 | /** @var string $pattern `$rule` and `$replacement` always correct, so `preg_replace` always returns string */
84 | $pattern = preg_replace('/\\\\./', '', $pattern);
85 | return preg_match('/[*{?\[]/', $pattern) === 1;
86 | }
87 |
88 | /**
89 | * Escapes pattern characters in a string.
90 | *
91 | * @param string $string Source string.
92 | *
93 | * @return string String with pattern characters escaped.
94 | */
95 | public static function quote(string $string): string
96 | {
97 | /** @var string `$rule` and `$replacement` always correct, so `preg_replace` always returns string */
98 | return preg_replace('#([\\\\?*\\[\\]])#', '\\\\$1', $string);
99 | }
100 |
101 | /**
102 | * @return non-empty-string
103 | */
104 | private function getPatternPrepared(): string
105 | {
106 | if ($this->patternPrepared !== null) {
107 | return $this->patternPrepared;
108 | }
109 |
110 | $replacements = [
111 | '\*\*' => '.*',
112 | '\\\\\\\\' => '\\\\',
113 | '\\\\\\*' => '[*]',
114 | '\\\\\\?' => '[?]',
115 | '\\\\\\[' => '[\[]',
116 | '\\\\\\]' => '[\]]',
117 | ];
118 |
119 | if ($this->delimiters === []) {
120 | $replacements += [
121 | '\*' => '.*',
122 | '\?' => '?',
123 | ];
124 | } else {
125 | $notDelimiters = '[^' . preg_quote(implode('', $this->delimiters), '#') . ']';
126 | $replacements += [
127 | '\*' => "$notDelimiters*",
128 | '\?' => $notDelimiters,
129 | ];
130 | }
131 |
132 | $replacements += [
133 | '\[\!' => '[^',
134 | '\[' => '[',
135 | '\]' => ']',
136 | '\-' => '-',
137 | ];
138 |
139 | $pattern = strtr(preg_quote($this->pattern, '#'), $replacements);
140 | $pattern = '#^' . $pattern . '$#us';
141 |
142 | if ($this->ignoreCase) {
143 | $pattern .= 'i';
144 | }
145 |
146 | $this->patternPrepared = $pattern;
147 |
148 | return $this->patternPrepared;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------