Repository files navigation

composer/pcre

PCRE wrapping library that offers type-safe preg_* replacements.

This library gives you a way to ensure preg_* functions do not fail silently, returning unexpected nulls that may not be handled.

As of 3.0 this library enforces PREG_UNMATCHED_AS_NULL usage for all matching and replaceCallback functions, read more below to understand the implications.

It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it simplifies and reduces the possible return values from all the preg_* functions which are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a PHPStan extension for parsing regular expressions and giving you even better output types.

This library is a thin wrapper around preg_* functions with some limitations. If you are looking for a richer API to handle regular expressions have a look at rawr/t-regx instead.

Continuous Integration

Installation

Install the latest version with:

$ composer require composer/pcre

Requirements

  • PHP 7.4.0 is required for 3.x versions
  • PHP 7.2.0 is required for 2.x versions
  • PHP 5.3.2 is required for 1.x versions

Basic usage

Instead of:

if (preg_match('{fo+}', $string, $matches)) { ... }
if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... }
if (preg_match_all('{fo+}', $string, $matches)) { ... }
$newString = preg_replace('{fo+}', 'bar', $string);
$newString = preg_replace_callback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = preg_grep('{[a-z]}', $elements);
$array = preg_split('{[a-z]+}', $string);

You can now call these on the Preg class:

useComposer\Pcre\Preg;
if (Preg::match('{fo+}', $string, $matches)) { ... }
if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... }
if (Preg::matchAll('{fo+}', $string, $matches)) { ... }
$newString = Preg::replace('{fo+}', 'bar', $string);
$newString = Preg::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = Preg::grep('{[a-z]}', $elements);
$array = Preg::split('{[a-z]+}', $string);

The main difference is if anything fails to match/replace/.., it will throw a Composer\Pcre\PcreException instead of returning null (or false in some cases), so you can now use the return values safely relying on the fact that they can only be strings (for replace), ints (for match) or arrays (for grep/split).

Additionally the Preg class provides match methods that return bool rather than int, for stricter type safety when the number of pattern matches is not useful:

useComposer\Pcre\Preg;
if (Preg::isMatch('{fo+}', $string, $matches)) // boolif (Preg::isMatchAll('{fo+}', $string, $matches)) // bool

Finally the Preg class provides a few *StrictGroups method variants that ensure match groups are always present and thus non-nullable, making it easier to write type-safe code:

useComposer\Pcre\Preg;
// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throwif (Preg::matchStrictGroups('{fo+}', $string, $matches))
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))

Note: This is generally safe to use as long as you do not have optional subpatterns (i.e. (something)? or (something)* or branches with a | that result in some groups not being matched at all). A subpattern that can match an empty string like (.*) is not optional, it will be present as an empty string in the matches. A non-matching subpattern, even if optional like (?:foo)? will anyway not be present in matches so it is also not a problem to use these with *StrictGroups methods.

If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the Regex class:

useComposer\Pcre\Regex;
// this is useful when you are just interested in knowing if something matched// as it returns a bool instead of int(1/0) for match$bool = Regex::isMatch('{fo+}', $string);
$result = Regex::match('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchWithOffsets('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchAll('{fo+}', $string);
if ($result->matched && $result->count > 3) { something($result->matches); }
$newString = Regex::replace('{fo+}', 'bar', $string)->result;
$newString = Regex::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string)->result;
$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result;

Note that preg_grep and preg_split are only callable via the Preg class as they do not have complex return types warranting a specific result object.

See the MatchResult, MatchWithOffsetsResult, MatchAllResult, MatchAllWithOffsetsResult, and ReplaceResult class sources for more details.

Restrictions / Limitations

Due to type safety requirements a few restrictions are in place.

  • matching using PREG_OFFSET_CAPTURE is made available via matchWithOffsets and matchAllWithOffsets. You cannot pass the flag to match/matchAll.
  • Preg::split will also reject PREG_SPLIT_OFFSET_CAPTURE and you should use splitWithOffsets instead.
  • matchAll rejects PREG_SET_ORDER as it also changes the shape of the returned matches. There is no alternative provided as you can fairly easily code around it.
  • preg_filter is not supported as it has a rather crazy API, most likely you should rather use Preg::grep in combination with some loop and Preg::replace.
  • replace, replaceCallback and replaceCallbackArray do not support an array $subject, only simple strings.
  • As of 2.0, the library always uses PREG_UNMATCHED_AS_NULL for matching, which offers much saner/more predictable results. As of 3.0 the flag is also set for replaceCallback and replaceCallbackArray.

PREG_UNMATCHED_AS_NULL

As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all match* and isMatch* functions. As of 3.0 it is also done for replaceCallback and replaceCallbackArray.

This means your matches will always contain all matching groups, either as null if unmatched or as string if it matched.

The advantages in clarity and predictability are clearer if you compare the two outputs of running this with and without PREG_UNMATCHED_AS_NULL in $flags:

preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
no flagPREG_UNMATCHED_AS_NULL
array (size=4)array (size=5)
0 => string 'ac' (length=2)0 => string 'ac' (length=2)
1 => string 'a' (length=1)1 => string 'a' (length=1)
2 => string '' (length=0)2 => null
3 => string 'c' (length=1)3 => string 'c' (length=1)
4 => null
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 allgroup 2 is null when unmatched and a string if it matched, easy to check for
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 itgroup 4 is always set, and null in this case as there was no match, easy to check for with $m[4] !== null

PHPStan Extension

To use the PHPStan extension if you do not use phpstan/extension-installer you can include vendor/composer/pcre/extension.neon in your PHPStan config.

The extension provides much better type information for $matches as well as regex validation where possible.

License

composer/pcre is licensed under the MIT License, see the LICENSE file for details.

About

PCRE wrapping library that offers type-safe preg_* replacements.

Resources

Contributing

Security policy

Stars

711 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

composer/pcre

PCRE wrapping library that offers type-safe preg_* replacements.

This library gives you a way to ensure preg_* functions do not fail silently, returning unexpected nulls that may not be handled.

As of 3.0 this library enforces PREG_UNMATCHED_AS_NULL usage for all matching and replaceCallback functions, read more below to understand the implications.

It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it simplifies and reduces the possible return values from all the preg_* functions which are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a PHPStan extension for parsing regular expressions and giving you even better output types.

This library is a thin wrapper around preg_* functions with some limitations. If you are looking for a richer API to handle regular expressions have a look at rawr/t-regx instead.

Continuous Integration

Installation

Install the latest version with:

$ composer require composer/pcre

Requirements

  • PHP 7.4.0 is required for 3.x versions
  • PHP 7.2.0 is required for 2.x versions
  • PHP 5.3.2 is required for 1.x versions

Basic usage

Instead of:

if (preg_match('{fo+}', $string, $matches)) { ... }
if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... }
if (preg_match_all('{fo+}', $string, $matches)) { ... }
$newString = preg_replace('{fo+}', 'bar', $string);
$newString = preg_replace_callback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = preg_grep('{[a-z]}', $elements);
$array = preg_split('{[a-z]+}', $string);

You can now call these on the Preg class:

useComposer\Pcre\Preg;
if (Preg::match('{fo+}', $string, $matches)) { ... }
if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... }
if (Preg::matchAll('{fo+}', $string, $matches)) { ... }
$newString = Preg::replace('{fo+}', 'bar', $string);
$newString = Preg::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = Preg::grep('{[a-z]}', $elements);
$array = Preg::split('{[a-z]+}', $string);

The main difference is if anything fails to match/replace/.., it will throw a Composer\Pcre\PcreException instead of returning null (or false in some cases), so you can now use the return values safely relying on the fact that they can only be strings (for replace), ints (for match) or arrays (for grep/split).

Additionally the Preg class provides match methods that return bool rather than int, for stricter type safety when the number of pattern matches is not useful:

useComposer\Pcre\Preg;
if (Preg::isMatch('{fo+}', $string, $matches)) // boolif (Preg::isMatchAll('{fo+}', $string, $matches)) // bool

Finally the Preg class provides a few *StrictGroups method variants that ensure match groups are always present and thus non-nullable, making it easier to write type-safe code:

useComposer\Pcre\Preg;
// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throwif (Preg::matchStrictGroups('{fo+}', $string, $matches))
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))

Note: This is generally safe to use as long as you do not have optional subpatterns (i.e. (something)? or (something)* or branches with a | that result in some groups not being matched at all). A subpattern that can match an empty string like (.*) is not optional, it will be present as an empty string in the matches. A non-matching subpattern, even if optional like (?:foo)? will anyway not be present in matches so it is also not a problem to use these with *StrictGroups methods.

If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the Regex class:

useComposer\Pcre\Regex;
// this is useful when you are just interested in knowing if something matched// as it returns a bool instead of int(1/0) for match$bool = Regex::isMatch('{fo+}', $string);
$result = Regex::match('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchWithOffsets('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchAll('{fo+}', $string);
if ($result->matched && $result->count > 3) { something($result->matches); }
$newString = Regex::replace('{fo+}', 'bar', $string)->result;
$newString = Regex::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string)->result;
$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result;

Note that preg_grep and preg_split are only callable via the Preg class as they do not have complex return types warranting a specific result object.

See the MatchResult, MatchWithOffsetsResult, MatchAllResult, MatchAllWithOffsetsResult, and ReplaceResult class sources for more details.

Restrictions / Limitations

Due to type safety requirements a few restrictions are in place.

  • matching using PREG_OFFSET_CAPTURE is made available via matchWithOffsets and matchAllWithOffsets. You cannot pass the flag to match/matchAll.
  • Preg::split will also reject PREG_SPLIT_OFFSET_CAPTURE and you should use splitWithOffsets instead.
  • matchAll rejects PREG_SET_ORDER as it also changes the shape of the returned matches. There is no alternative provided as you can fairly easily code around it.
  • preg_filter is not supported as it has a rather crazy API, most likely you should rather use Preg::grep in combination with some loop and Preg::replace.
  • replace, replaceCallback and replaceCallbackArray do not support an array $subject, only simple strings.
  • As of 2.0, the library always uses PREG_UNMATCHED_AS_NULL for matching, which offers much saner/more predictable results. As of 3.0 the flag is also set for replaceCallback and replaceCallbackArray.

PREG_UNMATCHED_AS_NULL

As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all match* and isMatch* functions. As of 3.0 it is also done for replaceCallback and replaceCallbackArray.

This means your matches will always contain all matching groups, either as null if unmatched or as string if it matched.

The advantages in clarity and predictability are clearer if you compare the two outputs of running this with and without PREG_UNMATCHED_AS_NULL in $flags:

preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
no flagPREG_UNMATCHED_AS_NULL
array (size=4)array (size=5)
0 => string 'ac' (length=2)0 => string 'ac' (length=2)
1 => string 'a' (length=1)1 => string 'a' (length=1)
2 => string '' (length=0)2 => null
3 => string 'c' (length=1)3 => string 'c' (length=1)
4 => null
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 allgroup 2 is null when unmatched and a string if it matched, easy to check for
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 itgroup 4 is always set, and null in this case as there was no match, easy to check for with $m[4] !== null

PHPStan Extension

To use the PHPStan extension if you do not use phpstan/extension-installer you can include vendor/composer/pcre/extension.neon in your PHPStan config.

The extension provides much better type information for $matches as well as regex validation where possible.

License

composer/pcre is licensed under the MIT License, see the LICENSE file for details.

About

PCRE wrapping library that offers type-safe preg_* replacements.

Resources

Contributing

Security policy

Stars

711 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

composer/pcre

PCRE wrapping library that offers type-safe preg_* replacements.

This library gives you a way to ensure preg_* functions do not fail silently, returning unexpected nulls that may not be handled.

As of 3.0 this library enforces PREG_UNMATCHED_AS_NULL usage for all matching and replaceCallback functions, read more below to understand the implications.

It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it simplifies and reduces the possible return values from all the preg_* functions which are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a PHPStan extension for parsing regular expressions and giving you even better output types.

This library is a thin wrapper around preg_* functions with some limitations. If you are looking for a richer API to handle regular expressions have a look at rawr/t-regx instead.

Continuous Integration

Installation

Install the latest version with:

$ composer require composer/pcre

Requirements

  • PHP 7.4.0 is required for 3.x versions
  • PHP 7.2.0 is required for 2.x versions
  • PHP 5.3.2 is required for 1.x versions

Basic usage

Instead of:

if (preg_match('{fo+}', $string, $matches)) { ... }
if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... }
if (preg_match_all('{fo+}', $string, $matches)) { ... }
$newString = preg_replace('{fo+}', 'bar', $string);
$newString = preg_replace_callback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = preg_grep('{[a-z]}', $elements);
$array = preg_split('{[a-z]+}', $string);

You can now call these on the Preg class:

useComposer\Pcre\Preg;
if (Preg::match('{fo+}', $string, $matches)) { ... }
if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... }
if (Preg::matchAll('{fo+}', $string, $matches)) { ... }
$newString = Preg::replace('{fo+}', 'bar', $string);
$newString = Preg::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = Preg::grep('{[a-z]}', $elements);
$array = Preg::split('{[a-z]+}', $string);

The main difference is if anything fails to match/replace/.., it will throw a Composer\Pcre\PcreException instead of returning null (or false in some cases), so you can now use the return values safely relying on the fact that they can only be strings (for replace), ints (for match) or arrays (for grep/split).

Additionally the Preg class provides match methods that return bool rather than int, for stricter type safety when the number of pattern matches is not useful:

useComposer\Pcre\Preg;
if (Preg::isMatch('{fo+}', $string, $matches)) // boolif (Preg::isMatchAll('{fo+}', $string, $matches)) // bool

Finally the Preg class provides a few *StrictGroups method variants that ensure match groups are always present and thus non-nullable, making it easier to write type-safe code:

useComposer\Pcre\Preg;
// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throwif (Preg::matchStrictGroups('{fo+}', $string, $matches))
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))

Note: This is generally safe to use as long as you do not have optional subpatterns (i.e. (something)? or (something)* or branches with a | that result in some groups not being matched at all). A subpattern that can match an empty string like (.*) is not optional, it will be present as an empty string in the matches. A non-matching subpattern, even if optional like (?:foo)? will anyway not be present in matches so it is also not a problem to use these with *StrictGroups methods.

If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the Regex class:

useComposer\Pcre\Regex;
// this is useful when you are just interested in knowing if something matched// as it returns a bool instead of int(1/0) for match$bool = Regex::isMatch('{fo+}', $string);
$result = Regex::match('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchWithOffsets('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchAll('{fo+}', $string);
if ($result->matched && $result->count > 3) { something($result->matches); }
$newString = Regex::replace('{fo+}', 'bar', $string)->result;
$newString = Regex::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string)->result;
$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result;

Note that preg_grep and preg_split are only callable via the Preg class as they do not have complex return types warranting a specific result object.

See the MatchResult, MatchWithOffsetsResult, MatchAllResult, MatchAllWithOffsetsResult, and ReplaceResult class sources for more details.

Restrictions / Limitations

Due to type safety requirements a few restrictions are in place.

  • matching using PREG_OFFSET_CAPTURE is made available via matchWithOffsets and matchAllWithOffsets. You cannot pass the flag to match/matchAll.
  • Preg::split will also reject PREG_SPLIT_OFFSET_CAPTURE and you should use splitWithOffsets instead.
  • matchAll rejects PREG_SET_ORDER as it also changes the shape of the returned matches. There is no alternative provided as you can fairly easily code around it.
  • preg_filter is not supported as it has a rather crazy API, most likely you should rather use Preg::grep in combination with some loop and Preg::replace.
  • replace, replaceCallback and replaceCallbackArray do not support an array $subject, only simple strings.
  • As of 2.0, the library always uses PREG_UNMATCHED_AS_NULL for matching, which offers much saner/more predictable results. As of 3.0 the flag is also set for replaceCallback and replaceCallbackArray.

PREG_UNMATCHED_AS_NULL

As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all match* and isMatch* functions. As of 3.0 it is also done for replaceCallback and replaceCallbackArray.

This means your matches will always contain all matching groups, either as null if unmatched or as string if it matched.

The advantages in clarity and predictability are clearer if you compare the two outputs of running this with and without PREG_UNMATCHED_AS_NULL in $flags:

preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
no flagPREG_UNMATCHED_AS_NULL
array (size=4)array (size=5)
0 => string 'ac' (length=2)0 => string 'ac' (length=2)
1 => string 'a' (length=1)1 => string 'a' (length=1)
2 => string '' (length=0)2 => null
3 => string 'c' (length=1)3 => string 'c' (length=1)
4 => null
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 allgroup 2 is null when unmatched and a string if it matched, easy to check for
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 itgroup 4 is always set, and null in this case as there was no match, easy to check for with $m[4] !== null

PHPStan Extension

To use the PHPStan extension if you do not use phpstan/extension-installer you can include vendor/composer/pcre/extension.neon in your PHPStan config.

The extension provides much better type information for $matches as well as regex validation where possible.

License

composer/pcre is licensed under the MIT License, see the LICENSE file for details.

About

PCRE wrapping library that offers type-safe preg_* replacements.

Resources

Contributing

Security policy

Stars

711 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

composer/pcre

PCRE wrapping library that offers type-safe preg_* replacements.

This library gives you a way to ensure preg_* functions do not fail silently, returning unexpected nulls that may not be handled.

As of 3.0 this library enforces PREG_UNMATCHED_AS_NULL usage for all matching and replaceCallback functions, read more below to understand the implications.

It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it simplifies and reduces the possible return values from all the preg_* functions which are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a PHPStan extension for parsing regular expressions and giving you even better output types.

This library is a thin wrapper around preg_* functions with some limitations. If you are looking for a richer API to handle regular expressions have a look at rawr/t-regx instead.

Continuous Integration

Installation

Install the latest version with:

$ composer require composer/pcre

Requirements

  • PHP 7.4.0 is required for 3.x versions
  • PHP 7.2.0 is required for 2.x versions
  • PHP 5.3.2 is required for 1.x versions

Basic usage

Instead of:

if (preg_match('{fo+}', $string, $matches)) { ... }
if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... }
if (preg_match_all('{fo+}', $string, $matches)) { ... }
$newString = preg_replace('{fo+}', 'bar', $string);
$newString = preg_replace_callback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = preg_grep('{[a-z]}', $elements);
$array = preg_split('{[a-z]+}', $string);

You can now call these on the Preg class:

useComposer\Pcre\Preg;
if (Preg::match('{fo+}', $string, $matches)) { ... }
if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... }
if (Preg::matchAll('{fo+}', $string, $matches)) { ... }
$newString = Preg::replace('{fo+}', 'bar', $string);
$newString = Preg::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = Preg::grep('{[a-z]}', $elements);
$array = Preg::split('{[a-z]+}', $string);

The main difference is if anything fails to match/replace/.., it will throw a Composer\Pcre\PcreException instead of returning null (or false in some cases), so you can now use the return values safely relying on the fact that they can only be strings (for replace), ints (for match) or arrays (for grep/split).

Additionally the Preg class provides match methods that return bool rather than int, for stricter type safety when the number of pattern matches is not useful:

useComposer\Pcre\Preg;
if (Preg::isMatch('{fo+}', $string, $matches)) // boolif (Preg::isMatchAll('{fo+}', $string, $matches)) // bool

Finally the Preg class provides a few *StrictGroups method variants that ensure match groups are always present and thus non-nullable, making it easier to write type-safe code:

useComposer\Pcre\Preg;
// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throwif (Preg::matchStrictGroups('{fo+}', $string, $matches))
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))

Note: This is generally safe to use as long as you do not have optional subpatterns (i.e. (something)? or (something)* or branches with a | that result in some groups not being matched at all). A subpattern that can match an empty string like (.*) is not optional, it will be present as an empty string in the matches. A non-matching subpattern, even if optional like (?:foo)? will anyway not be present in matches so it is also not a problem to use these with *StrictGroups methods.

If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the Regex class:

useComposer\Pcre\Regex;
// this is useful when you are just interested in knowing if something matched// as it returns a bool instead of int(1/0) for match$bool = Regex::isMatch('{fo+}', $string);
$result = Regex::match('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchWithOffsets('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchAll('{fo+}', $string);
if ($result->matched && $result->count > 3) { something($result->matches); }
$newString = Regex::replace('{fo+}', 'bar', $string)->result;
$newString = Regex::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string)->result;
$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result;

Note that preg_grep and preg_split are only callable via the Preg class as they do not have complex return types warranting a specific result object.

See the MatchResult, MatchWithOffsetsResult, MatchAllResult, MatchAllWithOffsetsResult, and ReplaceResult class sources for more details.

Restrictions / Limitations

Due to type safety requirements a few restrictions are in place.

  • matching using PREG_OFFSET_CAPTURE is made available via matchWithOffsets and matchAllWithOffsets. You cannot pass the flag to match/matchAll.
  • Preg::split will also reject PREG_SPLIT_OFFSET_CAPTURE and you should use splitWithOffsets instead.
  • matchAll rejects PREG_SET_ORDER as it also changes the shape of the returned matches. There is no alternative provided as you can fairly easily code around it.
  • preg_filter is not supported as it has a rather crazy API, most likely you should rather use Preg::grep in combination with some loop and Preg::replace.
  • replace, replaceCallback and replaceCallbackArray do not support an array $subject, only simple strings.
  • As of 2.0, the library always uses PREG_UNMATCHED_AS_NULL for matching, which offers much saner/more predictable results. As of 3.0 the flag is also set for replaceCallback and replaceCallbackArray.

PREG_UNMATCHED_AS_NULL

As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all match* and isMatch* functions. As of 3.0 it is also done for replaceCallback and replaceCallbackArray.

This means your matches will always contain all matching groups, either as null if unmatched or as string if it matched.

The advantages in clarity and predictability are clearer if you compare the two outputs of running this with and without PREG_UNMATCHED_AS_NULL in $flags:

preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
no flagPREG_UNMATCHED_AS_NULL
array (size=4)array (size=5)
0 => string 'ac' (length=2)0 => string 'ac' (length=2)
1 => string 'a' (length=1)1 => string 'a' (length=1)
2 => string '' (length=0)2 => null
3 => string 'c' (length=1)3 => string 'c' (length=1)
4 => null
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 allgroup 2 is null when unmatched and a string if it matched, easy to check for
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 itgroup 4 is always set, and null in this case as there was no match, easy to check for with $m[4] !== null

PHPStan Extension

To use the PHPStan extension if you do not use phpstan/extension-installer you can include vendor/composer/pcre/extension.neon in your PHPStan config.

The extension provides much better type information for $matches as well as regex validation where possible.

License

composer/pcre is licensed under the MIT License, see the LICENSE file for details.

About

PCRE wrapping library that offers type-safe preg_* replacements.

Resources

Contributing

Security policy

Stars

711 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

composer/pcre

PCRE wrapping library that offers type-safe preg_* replacements.

This library gives you a way to ensure preg_* functions do not fail silently, returning unexpected nulls that may not be handled.

As of 3.0 this library enforces PREG_UNMATCHED_AS_NULL usage for all matching and replaceCallback functions, read more below to understand the implications.

It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it simplifies and reduces the possible return values from all the preg_* functions which are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a PHPStan extension for parsing regular expressions and giving you even better output types.

This library is a thin wrapper around preg_* functions with some limitations. If you are looking for a richer API to handle regular expressions have a look at rawr/t-regx instead.

Continuous Integration

Installation

Install the latest version with:

$ composer require composer/pcre

Requirements

  • PHP 7.4.0 is required for 3.x versions
  • PHP 7.2.0 is required for 2.x versions
  • PHP 5.3.2 is required for 1.x versions

Basic usage

Instead of:

if (preg_match('{fo+}', $string, $matches)) { ... }
if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... }
if (preg_match_all('{fo+}', $string, $matches)) { ... }
$newString = preg_replace('{fo+}', 'bar', $string);
$newString = preg_replace_callback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = preg_grep('{[a-z]}', $elements);
$array = preg_split('{[a-z]+}', $string);

You can now call these on the Preg class:

useComposer\Pcre\Preg;
if (Preg::match('{fo+}', $string, $matches)) { ... }
if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... }
if (Preg::matchAll('{fo+}', $string, $matches)) { ... }
$newString = Preg::replace('{fo+}', 'bar', $string);
$newString = Preg::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = Preg::grep('{[a-z]}', $elements);
$array = Preg::split('{[a-z]+}', $string);

The main difference is if anything fails to match/replace/.., it will throw a Composer\Pcre\PcreException instead of returning null (or false in some cases), so you can now use the return values safely relying on the fact that they can only be strings (for replace), ints (for match) or arrays (for grep/split).

Additionally the Preg class provides match methods that return bool rather than int, for stricter type safety when the number of pattern matches is not useful:

useComposer\Pcre\Preg;
if (Preg::isMatch('{fo+}', $string, $matches)) // boolif (Preg::isMatchAll('{fo+}', $string, $matches)) // bool

Finally the Preg class provides a few *StrictGroups method variants that ensure match groups are always present and thus non-nullable, making it easier to write type-safe code:

useComposer\Pcre\Preg;
// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throwif (Preg::matchStrictGroups('{fo+}', $string, $matches))
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))

Note: This is generally safe to use as long as you do not have optional subpatterns (i.e. (something)? or (something)* or branches with a | that result in some groups not being matched at all). A subpattern that can match an empty string like (.*) is not optional, it will be present as an empty string in the matches. A non-matching subpattern, even if optional like (?:foo)? will anyway not be present in matches so it is also not a problem to use these with *StrictGroups methods.

If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the Regex class:

useComposer\Pcre\Regex;
// this is useful when you are just interested in knowing if something matched// as it returns a bool instead of int(1/0) for match$bool = Regex::isMatch('{fo+}', $string);
$result = Regex::match('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchWithOffsets('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchAll('{fo+}', $string);
if ($result->matched && $result->count > 3) { something($result->matches); }
$newString = Regex::replace('{fo+}', 'bar', $string)->result;
$newString = Regex::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string)->result;
$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result;

Note that preg_grep and preg_split are only callable via the Preg class as they do not have complex return types warranting a specific result object.

See the MatchResult, MatchWithOffsetsResult, MatchAllResult, MatchAllWithOffsetsResult, and ReplaceResult class sources for more details.

Restrictions / Limitations

Due to type safety requirements a few restrictions are in place.

  • matching using PREG_OFFSET_CAPTURE is made available via matchWithOffsets and matchAllWithOffsets. You cannot pass the flag to match/matchAll.
  • Preg::split will also reject PREG_SPLIT_OFFSET_CAPTURE and you should use splitWithOffsets instead.
  • matchAll rejects PREG_SET_ORDER as it also changes the shape of the returned matches. There is no alternative provided as you can fairly easily code around it.
  • preg_filter is not supported as it has a rather crazy API, most likely you should rather use Preg::grep in combination with some loop and Preg::replace.
  • replace, replaceCallback and replaceCallbackArray do not support an array $subject, only simple strings.
  • As of 2.0, the library always uses PREG_UNMATCHED_AS_NULL for matching, which offers much saner/more predictable results. As of 3.0 the flag is also set for replaceCallback and replaceCallbackArray.

PREG_UNMATCHED_AS_NULL

As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all match* and isMatch* functions. As of 3.0 it is also done for replaceCallback and replaceCallbackArray.

This means your matches will always contain all matching groups, either as null if unmatched or as string if it matched.

The advantages in clarity and predictability are clearer if you compare the two outputs of running this with and without PREG_UNMATCHED_AS_NULL in $flags:

preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
no flagPREG_UNMATCHED_AS_NULL
array (size=4)array (size=5)
0 => string 'ac' (length=2)0 => string 'ac' (length=2)
1 => string 'a' (length=1)1 => string 'a' (length=1)
2 => string '' (length=0)2 => null
3 => string 'c' (length=1)3 => string 'c' (length=1)
4 => null
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 allgroup 2 is null when unmatched and a string if it matched, easy to check for
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 itgroup 4 is always set, and null in this case as there was no match, easy to check for with $m[4] !== null

PHPStan Extension

To use the PHPStan extension if you do not use phpstan/extension-installer you can include vendor/composer/pcre/extension.neon in your PHPStan config.

The extension provides much better type information for $matches as well as regex validation where possible.

License

composer/pcre is licensed under the MIT License, see the LICENSE file for details.

About

PCRE wrapping library that offers type-safe preg_* replacements.

Resources

Contributing

Security policy

Stars

711 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

composer/pcre

PCRE wrapping library that offers type-safe preg_* replacements.

This library gives you a way to ensure preg_* functions do not fail silently, returning unexpected nulls that may not be handled.

As of 3.0 this library enforces PREG_UNMATCHED_AS_NULL usage for all matching and replaceCallback functions, read more below to understand the implications.

It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it simplifies and reduces the possible return values from all the preg_* functions which are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a PHPStan extension for parsing regular expressions and giving you even better output types.

This library is a thin wrapper around preg_* functions with some limitations. If you are looking for a richer API to handle regular expressions have a look at rawr/t-regx instead.

Continuous Integration

Installation

Install the latest version with:

$ composer require composer/pcre

Requirements

  • PHP 7.4.0 is required for 3.x versions
  • PHP 7.2.0 is required for 2.x versions
  • PHP 5.3.2 is required for 1.x versions

Basic usage

Instead of:

if (preg_match('{fo+}', $string, $matches)) { ... }
if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... }
if (preg_match_all('{fo+}', $string, $matches)) { ... }
$newString = preg_replace('{fo+}', 'bar', $string);
$newString = preg_replace_callback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = preg_grep('{[a-z]}', $elements);
$array = preg_split('{[a-z]+}', $string);

You can now call these on the Preg class:

useComposer\Pcre\Preg;
if (Preg::match('{fo+}', $string, $matches)) { ... }
if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... }
if (Preg::matchAll('{fo+}', $string, $matches)) { ... }
$newString = Preg::replace('{fo+}', 'bar', $string);
$newString = Preg::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = Preg::grep('{[a-z]}', $elements);
$array = Preg::split('{[a-z]+}', $string);

The main difference is if anything fails to match/replace/.., it will throw a Composer\Pcre\PcreException instead of returning null (or false in some cases), so you can now use the return values safely relying on the fact that they can only be strings (for replace), ints (for match) or arrays (for grep/split).

Additionally the Preg class provides match methods that return bool rather than int, for stricter type safety when the number of pattern matches is not useful:

useComposer\Pcre\Preg;
if (Preg::isMatch('{fo+}', $string, $matches)) // boolif (Preg::isMatchAll('{fo+}', $string, $matches)) // bool

Finally the Preg class provides a few *StrictGroups method variants that ensure match groups are always present and thus non-nullable, making it easier to write type-safe code:

useComposer\Pcre\Preg;
// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throwif (Preg::matchStrictGroups('{fo+}', $string, $matches))
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))

Note: This is generally safe to use as long as you do not have optional subpatterns (i.e. (something)? or (something)* or branches with a | that result in some groups not being matched at all). A subpattern that can match an empty string like (.*) is not optional, it will be present as an empty string in the matches. A non-matching subpattern, even if optional like (?:foo)? will anyway not be present in matches so it is also not a problem to use these with *StrictGroups methods.

If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the Regex class:

useComposer\Pcre\Regex;
// this is useful when you are just interested in knowing if something matched// as it returns a bool instead of int(1/0) for match$bool = Regex::isMatch('{fo+}', $string);
$result = Regex::match('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchWithOffsets('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchAll('{fo+}', $string);
if ($result->matched && $result->count > 3) { something($result->matches); }
$newString = Regex::replace('{fo+}', 'bar', $string)->result;
$newString = Regex::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string)->result;
$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result;

Note that preg_grep and preg_split are only callable via the Preg class as they do not have complex return types warranting a specific result object.

See the MatchResult, MatchWithOffsetsResult, MatchAllResult, MatchAllWithOffsetsResult, and ReplaceResult class sources for more details.

Restrictions / Limitations

Due to type safety requirements a few restrictions are in place.

  • matching using PREG_OFFSET_CAPTURE is made available via matchWithOffsets and matchAllWithOffsets. You cannot pass the flag to match/matchAll.
  • Preg::split will also reject PREG_SPLIT_OFFSET_CAPTURE and you should use splitWithOffsets instead.
  • matchAll rejects PREG_SET_ORDER as it also changes the shape of the returned matches. There is no alternative provided as you can fairly easily code around it.
  • preg_filter is not supported as it has a rather crazy API, most likely you should rather use Preg::grep in combination with some loop and Preg::replace.
  • replace, replaceCallback and replaceCallbackArray do not support an array $subject, only simple strings.
  • As of 2.0, the library always uses PREG_UNMATCHED_AS_NULL for matching, which offers much saner/more predictable results. As of 3.0 the flag is also set for replaceCallback and replaceCallbackArray.

PREG_UNMATCHED_AS_NULL

As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all match* and isMatch* functions. As of 3.0 it is also done for replaceCallback and replaceCallbackArray.

This means your matches will always contain all matching groups, either as null if unmatched or as string if it matched.

The advantages in clarity and predictability are clearer if you compare the two outputs of running this with and without PREG_UNMATCHED_AS_NULL in $flags:

preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
no flagPREG_UNMATCHED_AS_NULL
array (size=4)array (size=5)
0 => string 'ac' (length=2)0 => string 'ac' (length=2)
1 => string 'a' (length=1)1 => string 'a' (length=1)
2 => string '' (length=0)2 => null
3 => string 'c' (length=1)3 => string 'c' (length=1)
4 => null
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 allgroup 2 is null when unmatched and a string if it matched, easy to check for
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 itgroup 4 is always set, and null in this case as there was no match, easy to check for with $m[4] !== null

PHPStan Extension

To use the PHPStan extension if you do not use phpstan/extension-installer you can include vendor/composer/pcre/extension.neon in your PHPStan config.

The extension provides much better type information for $matches as well as regex validation where possible.

License

composer/pcre is licensed under the MIT License, see the LICENSE file for details.

About

PCRE wrapping library that offers type-safe preg_* replacements.

Resources

Contributing

Security policy

Stars

711 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

composer/pcre

PCRE wrapping library that offers type-safe preg_* replacements.

This library gives you a way to ensure preg_* functions do not fail silently, returning unexpected nulls that may not be handled.

As of 3.0 this library enforces PREG_UNMATCHED_AS_NULL usage for all matching and replaceCallback functions, read more below to understand the implications.

It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it simplifies and reduces the possible return values from all the preg_* functions which are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a PHPStan extension for parsing regular expressions and giving you even better output types.

This library is a thin wrapper around preg_* functions with some limitations. If you are looking for a richer API to handle regular expressions have a look at rawr/t-regx instead.

Continuous Integration

Installation

Install the latest version with:

$ composer require composer/pcre

Requirements

  • PHP 7.4.0 is required for 3.x versions
  • PHP 7.2.0 is required for 2.x versions
  • PHP 5.3.2 is required for 1.x versions

Basic usage

Instead of:

if (preg_match('{fo+}', $string, $matches)) { ... }
if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... }
if (preg_match_all('{fo+}', $string, $matches)) { ... }
$newString = preg_replace('{fo+}', 'bar', $string);
$newString = preg_replace_callback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = preg_grep('{[a-z]}', $elements);
$array = preg_split('{[a-z]+}', $string);

You can now call these on the Preg class:

useComposer\Pcre\Preg;
if (Preg::match('{fo+}', $string, $matches)) { ... }
if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... }
if (Preg::matchAll('{fo+}', $string, $matches)) { ... }
$newString = Preg::replace('{fo+}', 'bar', $string);
$newString = Preg::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = Preg::grep('{[a-z]}', $elements);
$array = Preg::split('{[a-z]+}', $string);

The main difference is if anything fails to match/replace/.., it will throw a Composer\Pcre\PcreException instead of returning null (or false in some cases), so you can now use the return values safely relying on the fact that they can only be strings (for replace), ints (for match) or arrays (for grep/split).

Additionally the Preg class provides match methods that return bool rather than int, for stricter type safety when the number of pattern matches is not useful:

useComposer\Pcre\Preg;
if (Preg::isMatch('{fo+}', $string, $matches)) // boolif (Preg::isMatchAll('{fo+}', $string, $matches)) // bool

Finally the Preg class provides a few *StrictGroups method variants that ensure match groups are always present and thus non-nullable, making it easier to write type-safe code:

useComposer\Pcre\Preg;
// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throwif (Preg::matchStrictGroups('{fo+}', $string, $matches))
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))

Note: This is generally safe to use as long as you do not have optional subpatterns (i.e. (something)? or (something)* or branches with a | that result in some groups not being matched at all). A subpattern that can match an empty string like (.*) is not optional, it will be present as an empty string in the matches. A non-matching subpattern, even if optional like (?:foo)? will anyway not be present in matches so it is also not a problem to use these with *StrictGroups methods.

If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the Regex class:

useComposer\Pcre\Regex;
// this is useful when you are just interested in knowing if something matched// as it returns a bool instead of int(1/0) for match$bool = Regex::isMatch('{fo+}', $string);
$result = Regex::match('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchWithOffsets('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchAll('{fo+}', $string);
if ($result->matched && $result->count > 3) { something($result->matches); }
$newString = Regex::replace('{fo+}', 'bar', $string)->result;
$newString = Regex::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string)->result;
$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result;

Note that preg_grep and preg_split are only callable via the Preg class as they do not have complex return types warranting a specific result object.

See the MatchResult, MatchWithOffsetsResult, MatchAllResult, MatchAllWithOffsetsResult, and ReplaceResult class sources for more details.

Restrictions / Limitations

Due to type safety requirements a few restrictions are in place.

  • matching using PREG_OFFSET_CAPTURE is made available via matchWithOffsets and matchAllWithOffsets. You cannot pass the flag to match/matchAll.
  • Preg::split will also reject PREG_SPLIT_OFFSET_CAPTURE and you should use splitWithOffsets instead.
  • matchAll rejects PREG_SET_ORDER as it also changes the shape of the returned matches. There is no alternative provided as you can fairly easily code around it.
  • preg_filter is not supported as it has a rather crazy API, most likely you should rather use Preg::grep in combination with some loop and Preg::replace.
  • replace, replaceCallback and replaceCallbackArray do not support an array $subject, only simple strings.
  • As of 2.0, the library always uses PREG_UNMATCHED_AS_NULL for matching, which offers much saner/more predictable results. As of 3.0 the flag is also set for replaceCallback and replaceCallbackArray.

PREG_UNMATCHED_AS_NULL

As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all match* and isMatch* functions. As of 3.0 it is also done for replaceCallback and replaceCallbackArray.

This means your matches will always contain all matching groups, either as null if unmatched or as string if it matched.

The advantages in clarity and predictability are clearer if you compare the two outputs of running this with and without PREG_UNMATCHED_AS_NULL in $flags:

preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
no flagPREG_UNMATCHED_AS_NULL
array (size=4)array (size=5)
0 => string 'ac' (length=2)0 => string 'ac' (length=2)
1 => string 'a' (length=1)1 => string 'a' (length=1)
2 => string '' (length=0)2 => null
3 => string 'c' (length=1)3 => string 'c' (length=1)
4 => null
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 allgroup 2 is null when unmatched and a string if it matched, easy to check for
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 itgroup 4 is always set, and null in this case as there was no match, easy to check for with $m[4] !== null

PHPStan Extension

To use the PHPStan extension if you do not use phpstan/extension-installer you can include vendor/composer/pcre/extension.neon in your PHPStan config.

The extension provides much better type information for $matches as well as regex validation where possible.

License

composer/pcre is licensed under the MIT License, see the LICENSE file for details.

About

PCRE wrapping library that offers type-safe preg_* replacements.

Resources

Contributing

Security policy

Stars

711 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

composer/pcre

PCRE wrapping library that offers type-safe preg_* replacements.

This library gives you a way to ensure preg_* functions do not fail silently, returning unexpected nulls that may not be handled.

As of 3.0 this library enforces PREG_UNMATCHED_AS_NULL usage for all matching and replaceCallback functions, read more below to understand the implications.

It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it simplifies and reduces the possible return values from all the preg_* functions which are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a PHPStan extension for parsing regular expressions and giving you even better output types.

This library is a thin wrapper around preg_* functions with some limitations. If you are looking for a richer API to handle regular expressions have a look at rawr/t-regx instead.

Continuous Integration

Installation

Install the latest version with:

$ composer require composer/pcre

Requirements

  • PHP 7.4.0 is required for 3.x versions
  • PHP 7.2.0 is required for 2.x versions
  • PHP 5.3.2 is required for 1.x versions

Basic usage

Instead of:

if (preg_match('{fo+}', $string, $matches)) { ... }
if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... }
if (preg_match_all('{fo+}', $string, $matches)) { ... }
$newString = preg_replace('{fo+}', 'bar', $string);
$newString = preg_replace_callback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = preg_grep('{[a-z]}', $elements);
$array = preg_split('{[a-z]+}', $string);

You can now call these on the Preg class:

useComposer\Pcre\Preg;
if (Preg::match('{fo+}', $string, $matches)) { ... }
if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... }
if (Preg::matchAll('{fo+}', $string, $matches)) { ... }
$newString = Preg::replace('{fo+}', 'bar', $string);
$newString = Preg::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string);
$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
$filtered = Preg::grep('{[a-z]}', $elements);
$array = Preg::split('{[a-z]+}', $string);

The main difference is if anything fails to match/replace/.., it will throw a Composer\Pcre\PcreException instead of returning null (or false in some cases), so you can now use the return values safely relying on the fact that they can only be strings (for replace), ints (for match) or arrays (for grep/split).

Additionally the Preg class provides match methods that return bool rather than int, for stricter type safety when the number of pattern matches is not useful:

useComposer\Pcre\Preg;
if (Preg::isMatch('{fo+}', $string, $matches)) // boolif (Preg::isMatchAll('{fo+}', $string, $matches)) // bool

Finally the Preg class provides a few *StrictGroups method variants that ensure match groups are always present and thus non-nullable, making it easier to write type-safe code:

useComposer\Pcre\Preg;
// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throwif (Preg::matchStrictGroups('{fo+}', $string, $matches))
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))

Note: This is generally safe to use as long as you do not have optional subpatterns (i.e. (something)? or (something)* or branches with a | that result in some groups not being matched at all). A subpattern that can match an empty string like (.*) is not optional, it will be present as an empty string in the matches. A non-matching subpattern, even if optional like (?:foo)? will anyway not be present in matches so it is also not a problem to use these with *StrictGroups methods.

If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the Regex class:

useComposer\Pcre\Regex;
// this is useful when you are just interested in knowing if something matched// as it returns a bool instead of int(1/0) for match$bool = Regex::isMatch('{fo+}', $string);
$result = Regex::match('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchWithOffsets('{fo+}', $string);
if ($result->matched) { something($result->matches); }
$result = Regex::matchAll('{fo+}', $string);
if ($result->matched && $result->count > 3) { something($result->matches); }
$newString = Regex::replace('{fo+}', 'bar', $string)->result;
$newString = Regex::replaceCallback('{fo+}', function ($match) { returnstrtoupper($match[0]); }, $string)->result;
$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result;

Note that preg_grep and preg_split are only callable via the Preg class as they do not have complex return types warranting a specific result object.

See the MatchResult, MatchWithOffsetsResult, MatchAllResult, MatchAllWithOffsetsResult, and ReplaceResult class sources for more details.

Restrictions / Limitations

Due to type safety requirements a few restrictions are in place.

  • matching using PREG_OFFSET_CAPTURE is made available via matchWithOffsets and matchAllWithOffsets. You cannot pass the flag to match/matchAll.
  • Preg::split will also reject PREG_SPLIT_OFFSET_CAPTURE and you should use splitWithOffsets instead.
  • matchAll rejects PREG_SET_ORDER as it also changes the shape of the returned matches. There is no alternative provided as you can fairly easily code around it.
  • preg_filter is not supported as it has a rather crazy API, most likely you should rather use Preg::grep in combination with some loop and Preg::replace.
  • replace, replaceCallback and replaceCallbackArray do not support an array $subject, only simple strings.
  • As of 2.0, the library always uses PREG_UNMATCHED_AS_NULL for matching, which offers much saner/more predictable results. As of 3.0 the flag is also set for replaceCallback and replaceCallbackArray.

PREG_UNMATCHED_AS_NULL

As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all match* and isMatch* functions. As of 3.0 it is also done for replaceCallback and replaceCallbackArray.

This means your matches will always contain all matching groups, either as null if unmatched or as string if it matched.

The advantages in clarity and predictability are clearer if you compare the two outputs of running this with and without PREG_UNMATCHED_AS_NULL in $flags:

preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
no flagPREG_UNMATCHED_AS_NULL
array (size=4)array (size=5)
0 => string 'ac' (length=2)0 => string 'ac' (length=2)
1 => string 'a' (length=1)1 => string 'a' (length=1)
2 => string '' (length=0)2 => null
3 => string 'c' (length=1)3 => string 'c' (length=1)
4 => null
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 allgroup 2 is null when unmatched and a string if it matched, easy to check for
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 itgroup 4 is always set, and null in this case as there was no match, easy to check for with $m[4] !== null

PHPStan Extension

To use the PHPStan extension if you do not use phpstan/extension-installer you can include vendor/composer/pcre/extension.neon in your PHPStan config.

The extension provides much better type information for $matches as well as regex validation where possible.

License

composer/pcre is licensed under the MIT License, see the LICENSE file for details.

About

PCRE wrapping library that offers type-safe preg_* replacements.

Resources

Contributing

Security policy

Stars

711 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages