@@ -38,6 +38,25 @@ public static function match(string $pattern, string $subject, ?array &$matches
3838 return $ result ;
3939 }
4040
41+ /**
42+ * Variant of `match()` which outputs non-null matches (or throws)
43+ *
44+ * @param non-empty-string $pattern
45+ * @param array<string> $matches Set by method
46+ * @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
47+ * @return 0|1
48+ * @throws UnexpectedNullMatchException
49+ *
50+ * @param-out array<int|string, string> $matches
51+ */
52+ public static function matchStrictGroups (string $ pattern , string $ subject , ?array &$ matches = null , int $ flags = 0 , int $ offset = 0 ): int
53+ {
54+ $ result = self ::match ($ pattern , $ subject , $ matchesInternal , $ flags , $ offset );
55+ $ matches = self ::enforceNonNullMatches ($ pattern , $ matchesInternal , 'match ' );
56+
57+ return $ result ;
58+ }
59+
4160 /**
4261 * Runs preg_match with PREG_OFFSET_CAPTURE
4362 *
@@ -61,18 +80,15 @@ public static function matchWithOffsets(string $pattern, string $subject, ?array
6180 /**
6281 * @param non-empty-string $pattern
6382 * @param array<int|string, list<string|null>> $matches Set by method
64- * @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER > $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
83+ * @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
6584 * @return 0|positive-int
6685 *
6786 * @param-out array<int|string, list<string|null>> $matches
6887 */
6988 public static function matchAll (string $ pattern , string $ subject , ?array &$ matches = null , int $ flags = 0 , int $ offset = 0 ): int
7089 {
7190 self ::checkOffsetCapture ($ flags , 'matchAllWithOffsets ' );
72-
73- if (($ flags & PREG_SET_ORDER ) !== 0 ) {
74- throw new \InvalidArgumentException ('PREG_SET_ORDER is not supported as it changes the type of $matches ' );
75- }
91+ self ::checkSetOrder ($ flags );
7692
7793 $ result = preg_match_all ($ pattern , $ subject , $ matches , $ flags | PREG_UNMATCHED_AS_NULL , $ offset );
7894 if (!is_int ($ result )) { // PHP < 8 may return null, 8+ returns int|false
@@ -82,6 +98,25 @@ public static function matchAll(string $pattern, string $subject, ?array &$match
8298 return $ result ;
8399 }
84100
101+ /**
102+ * Variant of `match()` which outputs non-null matches (or throws)
103+ *
104+ * @param non-empty-string $pattern
105+ * @param array<int|string, list<string|null>> $matches Set by method
106+ * @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
107+ * @return 0|positive-int
108+ * @throws UnexpectedNullMatchException
109+ *
110+ * @param-out array<int|string, list<string>> $matches
111+ */
112+ public static function matchAllStrictGroups (string $ pattern , string $ subject , ?array &$ matches = null , int $ flags = 0 , int $ offset = 0 ): int
113+ {
114+ $ result = self ::matchAll ($ pattern , $ subject , $ matchesInternal , $ flags , $ offset );
115+ $ matches = self ::enforceNonNullMatchAll ($ pattern , $ matchesInternal , 'matchAll ' );
116+
117+ return $ result ;
118+ }
119+
85120 /**
86121 * Runs preg_match_all with PREG_OFFSET_CAPTURE
87122 *
@@ -94,6 +129,8 @@ public static function matchAll(string $pattern, string $subject, ?array &$match
94129 */
95130 public static function matchAllWithOffsets (string $ pattern , string $ subject , ?array &$ matches , int $ flags = 0 , int $ offset = 0 ): int
96131 {
132+ self ::checkSetOrder ($ flags );
133+
97134 $ result = preg_match_all ($ pattern , $ subject , $ matches , $ flags | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE , $ offset );
98135 if (!is_int ($ result )) { // PHP < 8 may return null, 8+ returns int|false
99136 throw PcreException::fromFunction ('preg_match_all ' , $ pattern );
@@ -233,6 +270,8 @@ public static function grep(string $pattern, array $array, int $flags = 0): arra
233270 }
234271
235272 /**
273+ * Variant of match() which returns a bool instead of int
274+ *
236275 * @param non-empty-string $pattern
237276 * @param array<string|null> $matches Set by method
238277 * @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
@@ -245,6 +284,23 @@ public static function isMatch(string $pattern, string $subject, ?array &$matche
245284 }
246285
247286 /**
287+ * Variant of `isMatch()` which outputs non-null matches (or throws)
288+ *
289+ * @param non-empty-string $pattern
290+ * @param array<string> $matches Set by method
291+ * @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
292+ * @throws UnexpectedNullMatchException
293+ *
294+ * @param-out array<int|string, string> $matches
295+ */
296+ public static function isMatchStrictGroups (string $ pattern , string $ subject , ?array &$ matches = null , int $ flags = 0 , int $ offset = 0 ): bool
297+ {
298+ return (bool ) self ::matchStrictGroups ($ pattern , $ subject , $ matches , $ flags , $ offset );
299+ }
300+
301+ /**
302+ * Variant of matchAll() which returns a bool instead of int
303+ *
248304 * @param non-empty-string $pattern
249305 * @param array<int|string, list<string|null>> $matches Set by method
250306 * @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
@@ -257,6 +313,22 @@ public static function isMatchAll(string $pattern, string $subject, ?array &$mat
257313 }
258314
259315 /**
316+ * Variant of `isMatchAll()` which outputs non-null matches (or throws)
317+ *
318+ * @param non-empty-string $pattern
319+ * @param array<int|string, list<string>> $matches Set by method
320+ * @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
321+ *
322+ * @param-out array<int|string, list<string>> $matches
323+ */
324+ public static function isMatchAllStrictGroups (string $ pattern , string $ subject , ?array &$ matches = null , int $ flags = 0 , int $ offset = 0 ): bool
325+ {
326+ return (bool ) self ::matchAllStrictGroups ($ pattern , $ subject , $ matches , $ flags , $ offset );
327+ }
328+
329+ /**
330+ * Variant of matchWithOffsets() which returns a bool instead of int
331+ *
260332 * Runs preg_match with PREG_OFFSET_CAPTURE
261333 *
262334 * @param non-empty-string $pattern
@@ -271,6 +343,8 @@ public static function isMatchWithOffsets(string $pattern, string $subject, ?arr
271343 }
272344
273345 /**
346+ * Variant of matchAllWithOffsets() which returns a bool instead of int
347+ *
274348 * Runs preg_match_all with PREG_OFFSET_CAPTURE
275349 *
276350 * @param non-empty-string $pattern
@@ -290,4 +364,47 @@ private static function checkOffsetCapture(int $flags, string $useFunctionName):
290364 throw new \InvalidArgumentException ('PREG_OFFSET_CAPTURE is not supported as it changes the type of $matches, use ' . $ useFunctionName . '() instead ' );
291365 }
292366 }
367+
368+ private static function checkSetOrder (int $ flags ): void
369+ {
370+ if (($ flags & PREG_SET_ORDER ) !== 0 ) {
371+ throw new \InvalidArgumentException ('PREG_SET_ORDER is not supported as it changes the type of $matches ' );
372+ }
373+ }
374+
375+ /**
376+ * @param array<int|string, string|null> $matches
377+ * @return array<int|string, string>
378+ * @throws UnexpectedNullMatchException
379+ */
380+ private static function enforceNonNullMatches (string $ pattern , array $ matches , string $ variantMethod )
381+ {
382+ foreach ($ matches as $ group => $ match ) {
383+ if (null === $ match ) {
384+ throw new UnexpectedNullMatchException ('Pattern " ' .$ pattern .'" had an unexpected unmatched group " ' .$ group .'", make sure the pattern always matches or use ' .$ variantMethod .'() instead. ' );
385+ }
386+ }
387+
388+ /** @var array<string> */
389+ return $ matches ;
390+ }
391+
392+ /**
393+ * @param array<int|string, list<string|null>> $matches
394+ * @return array<int|string, list<string>>
395+ * @throws UnexpectedNullMatchException
396+ */
397+ private static function enforceNonNullMatchAll (string $ pattern , array $ matches , string $ variantMethod )
398+ {
399+ foreach ($ matches as $ group => $ groupMatches ) {
400+ foreach ($ groupMatches as $ match ) {
401+ if (null === $ match ) {
402+ throw new UnexpectedNullMatchException ('Pattern " ' .$ pattern .'" had an unexpected unmatched group " ' .$ group .'", make sure the pattern always matches or use ' .$ variantMethod .'() instead. ' );
403+ }
404+ }
405+ }
406+
407+ /** @var array<int|string, list<string>> */
408+ return $ matches ;
409+ }
293410}
0 commit comments