@@ -30,7 +30,7 @@ public static function match(string $pattern, string $subject, ?array &$matches
3030 {
3131 self ::checkOffsetCapture ($ flags , 'matchWithOffsets ' );
3232
33- $ result = preg_match ($ pattern , $ subject , $ matches , $ flags | PREG_UNMATCHED_AS_NULL , $ offset );
33+ $ result = self :: pregMatch ($ pattern , $ subject , $ matches , $ flags | PREG_UNMATCHED_AS_NULL , $ offset );
3434 if ($ result === false ) {
3535 throw PcreException::fromFunction ('preg_match ' , $ pattern );
3636 }
@@ -69,7 +69,7 @@ public static function matchStrictGroups(string $pattern, string $subject, ?arra
6969 */
7070 public static function matchWithOffsets (string $ pattern , string $ subject , ?array &$ matches , int $ flags = 0 , int $ offset = 0 ): int
7171 {
72- $ result = preg_match ($ pattern , $ subject , $ matches , $ flags | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE , $ offset );
72+ $ result = self :: pregMatch ($ pattern , $ subject , $ matches , $ flags | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE , $ offset );
7373 if ($ result === false ) {
7474 throw PcreException::fromFunction ('preg_match ' , $ pattern );
7575 }
@@ -415,4 +415,40 @@ private static function enforceNonNullMatchAll(string $pattern, array $matches,
415415 /** @var array<int|string, list<string>> */
416416 return $ matches ;
417417 }
418+
419+ /**
420+ * @param non-empty-string $pattern
421+ * @param array<string|null> $matches Set by method
422+ * @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE> $flags
423+ * @return 0|1|false
424+ *
425+ * @param-out array<int|string, string|null> $matches
426+ */
427+ private static function pregMatch (string $ pattern , string $ subject , ?array &$ matches = null , int $ flags = 0 , int $ offset = 0 )
428+ {
429+ if (PHP_VERSION_ID >= 70400 ) {
430+ return preg_match ($ pattern , $ subject , $ matches , $ flags , $ offset );
431+ }
432+
433+ // On PHP 7.2 and 7.3, PREG_UNMATCHED_AS_NULL only works correctly in preg_match_all
434+ // as preg_match does not set trailing unmatched groups to null
435+ // e.g. preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, PREG_UNMATCHED_AS_NULL); would
436+ // have $match[4] unset instead of null
437+ //
438+ // So we use preg_match_all here as workaround to ensure old-PHP meets the expectations
439+ // set by the library's documentation
440+ $ result = preg_match_all ($ pattern , $ subject , $ matchesInternal , $ flags , $ offset );
441+ if (!is_int ($ result )) { // PHP < 8 may return null, 8+ returns int|false
442+ throw PcreException::fromFunction ('preg_match ' , $ pattern );
443+ }
444+
445+ if ($ result === 0 ) {
446+ $ matches = [];
447+ } else {
448+ $ matches = array_map (function ($ m ) { return reset ($ m ); }, $ matchesInternal );
449+ $ result = min ($ result , 1 );
450+ }
451+
452+ return $ result ;
453+ }
418454}
0 commit comments