Skip to content

Commit 2e56743

Browse files
authored
Switch to use PREG_UNMATCHED_AS_NULL always, drop PHP < 7.2, fixes #1 (#9)
* Switch to use PREG_UNMATCHED_AS_NULL always, drop PHP < 7.2, fixes #1 * Migrate to scalar hints and add return types * Mark consts internal
1 parent 121c9ed commit 2e56743

31 files changed

+311
-671
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ jobs:
1717
strategy:
1818
matrix:
1919
php-version:
20-
- "5.3"
21-
- "5.4"
22-
- "5.5"
23-
- "5.6"
24-
- "7.0"
25-
- "7.1"
2620
- "7.2"
2721
- "7.3"
2822
- "7.4"

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
matrix:
1515
php-version:
16-
- "5.3"
16+
- "7.2"
1717
- "8.1"
1818

1919
steps:

.github/workflows/phpstan.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ jobs:
1717
strategy:
1818
matrix:
1919
php-version:
20-
- "7.4"
20+
- "7.2"
21+
- "8.1"
2122

2223
steps:
2324
- name: "Checkout"
@@ -43,8 +44,8 @@ jobs:
4344
- name: "Install latest dependencies"
4445
run: "composer update ${{ env.COMPOSER_FLAGS }}"
4546

46-
- name: Run PHPStan
47-
# Locked to phpunit 7.5 here as newer ones have void return types which break inheritance
48-
run: |
49-
composer require --dev phpunit/phpunit:^7.5.20 --with-all-dependencies ${{ env.COMPOSER_FLAGS }}
50-
vendor/bin/phpstan analyse
47+
- name: "Initialize PHPUnit sources"
48+
run: "vendor/bin/simple-phpunit --filter NO_TEST_JUST_AUTOLOAD_THANKS"
49+
50+
- name: "Run PHPStan"
51+
run: "composer phpstan"

README.md

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ PCRE wrapping library that offers type-safe `preg_*` replacements.
66
This library gives you a way to ensure `preg_*` functions do not fail silently, returning
77
unexpected `null`s that may not be handled.
88

9-
It also makes it easier to work with static analysis tools like PHPStan or Psalm as it
9+
As of 2.0 this library also enforces [`PREG_UNMATCHED_AS_NULL`](#preg_unmatched_as_null) usage for all matching functions, [read more below](#preg_unmatched_as_null) to understand the implications.
10+
11+
It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it
1012
simplifies and reduces the possible return values from all the `preg_*` functions which
1113
are quite packed with edge cases.
1214

@@ -122,11 +124,39 @@ Due to type safety requirements a few restrictions are in place.
122124
use `Preg::grep` in combination with some loop and `Preg::replace`.
123125
- `replace`, `replaceCallback` and `replaceCallbackArray` do not support an array `$subject`,
124126
only simple strings.
125-
- in 2.x, we plan to always implicitly use `PREG_UNMATCHED_AS_NULL` for matching, which offers much
126-
saner/predictable results. This is currently not doable due to the PHP version requirement and to
127-
keep things working the same across all PHP versions. If you use the library on a PHP 7.2+ project
128-
however we highly recommend using `PREG_UNMATCHED_AS_NULL` with all `match*` and `replaceCallback*`
129-
methods.
127+
- As of 2.0, the library always uses `PREG_UNMATCHED_AS_NULL` for matching, which offers [much
128+
saner/more predictable results](#preg_unmatched_as_null). 3.x will also use the flag for
129+
`replaceCallback` and `replaceCallbackArray`. This is currently not doable due to the PHP
130+
version requirement and to keep things working the same across all PHP versions. If you use
131+
the library on a PHP 7.4+ project however we highly recommend already passing
132+
`PREG_UNMATCHED_AS_NULL` to `replaceCallback` and `replaceCallbackArray`.
133+
134+
#### PREG_UNMATCHED_AS_NULL
135+
136+
As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all `match*` and `isMatch*`
137+
functions (unfortunately `preg_replace_callback[_array]` only support this from PHP 7.4
138+
onwards so we cannot do the same there yet).
139+
140+
This means your matches will always contain all matching groups, either as null if unmatched
141+
or as string if it matched.
142+
143+
The advantages in clarity and predictability are clearer if you compare the two outputs of
144+
running this with and without PREG_UNMATCHED_AS_NULL in $flags:
145+
146+
```php
147+
preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
148+
```
149+
150+
| no flag | PREG_UNMATCHED_AS_NULL |
151+
| --- | --- |
152+
| array (size=4) | array (size=5) |
153+
| 0 => string 'ac' (length=2) | 0 => string 'ac' (length=2) |
154+
| 1 => string 'a' (length=1) | 1 => string 'a' (length=1) |
155+
| 2 => string '' (length=0) | 2 => null |
156+
| 3 => string 'c' (length=1) | 3 => string 'c' (length=1) |
157+
| | 4 => null |
158+
| group 2 (any unmatched group preceding one that matched) is set to `''`. You cannot tell if it matched an empty string or did not match at all | group 2 is `null` when unmatched and a string if it matched, easy to check for |
159+
| group 4 (any optional group without a matching one following) is missing altogether. So you have to check with `isset()`, but really you want `isset($m[4]) && $m[4] !== ''` for safety unless you are very careful to check that a non-optional group follows it | group 4 is always set, and null in this case as there was no match, easy to check for with `$m[4] !== null` |
130160

131161
License
132162
-------

phpstan-baseline.neon

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: "#^Method Composer\\\\Pcre\\\\Preg\\:\\:matchAll\\(\\) should return int\\<0, max\\> but returns int\\.$#"
5+
count: 1
6+
path: src/Preg.php
7+
8+
-
9+
message: "#^Method Composer\\\\Pcre\\\\Preg\\:\\:matchAllWithOffsets\\(\\) should return int\\<0, max\\> but returns int\\.$#"
10+
count: 1
11+
path: src/Preg.php
12+
13+
-
14+
message: "#^Strict comparison using \\=\\=\\= between int and null will always evaluate to false\\.$#"
15+
count: 2
16+
path: src/Preg.php
17+
18+
-
19+
message: "#^Function preg_replace_callback invoked with 6 parameters, 3\\-5 required\\.$#"
20+
count: 1
21+
path: src/Preg.php
22+
23+
-
24+
message: "#^Function preg_replace_callback_array invoked with 5 parameters, 2\\-4 required\\.$#"
25+
count: 1
26+
path: src/Preg.php
27+

phpstan.neon.dist

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,12 @@ parameters:
44
- src
55
- tests
66

7+
reportUnmatchedIgnoredErrors: false
8+
treatPhpDocTypesAsCertain: false
9+
10+
bootstrapFiles:
11+
- tests/phpstan-locate-phpunit-autoloader.php
12+
713
includes:
8-
- vendor/phpstan/phpstan-strict-rules/rules.neon
14+
- vendor/phpstan/phpstan-strict-rules/rules.neon
15+
- phpstan-baseline.neon

src/PcreException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static function fromFunction($function, $pattern)
3535
*/
3636
private static function pcreLastErrorMessage($code)
3737
{
38-
if (PHP_VERSION_ID >= 80000) {
38+
if (function_exists('preg_last_error_msg')) {
3939
return preg_last_error_msg();
4040
}
4141

0 commit comments

Comments
 (0)