Table of Contents

PHP RFC: Prepared Literal String Matching and Replacement

Introduction

PHP provides several functions for searching and replacing literal substrings, including strpos(), str_contains(), str_replace(), and strtr(). These functions work well for many common tasks, but PHP does not provide a built-in way to prepare a set of literal patterns once and reuse the compiled search structure across multiple input strings.

Applications that check text against large keyword dictionaries, identify multiple terms, or perform repeated substitutions must currently choose between repeated searches, regular expressions, and userland implementations of multi-pattern matching algorithms.

This RFC proposes two classes, Str\Matcher and Str\Replacer, for prepared literal substring search and replacement.

<?php
 
$matcher = new Str\Matcher(['error', 'timeout', 'connection refused']);
 
if ($matcher->matches($message)) {
    // Handle a matching message.
}
 
$replacer = new Str\Replacer([
    'cat' => 'dog',
    'cats' => 'dogs',
]);
 
echo $replacer->replace('cats and cat');
// dogs and dog

Both classes prepare an internal matching automaton during construction and can reuse it across multiple subjects.

The implementation uses the Aho-Corasick algorithm. Matching is byte-oriented, binary-safe, and case-sensitive.

Proposal

The RFC introduces two independent final classes in the Str namespace, together with a result class.

Str\Matcher

Str\Matcher prepares a set of literal keywords and provides operations for checking their presence, counting occurrences, and retrieving matching keywords or individual matches.

The proposed API is:

<?php
 
namespace Str;
 
final class Matcher
{
    public function __construct(array $keywords) {}
 
    public function matches(string $subject): bool {}
 
    public function countMatches(string $subject): int {}
 
    /** @return list<string> */
    public function getMatchedKeywords(string $subject): array {}
 
    /** @return list<MatchResult> */
    public function matchAll(string $subject): array {}
 
    /** @return list<string> */
    public function getKeywords(): array {}
}
 
final class MatchResult
{
    private function __construct() {}
 
    public readonly int $offset;
    public readonly int $length;
    public readonly string $keyword;
}

The constructor accepts an array of non-empty strings. Duplicate keywords are removed while preserving their original order.

The methods have the following semantics:

Offsets and lengths are measured in bytes.

Str\Replacer

Str\Replacer prepares an associative array mapping literal substrings to their replacements.

<?php
 
namespace Str;
 
final class Replacer
{
    public function __construct(array $replacements) {}
 
    /** @param int $count */
    public function replace(string $subject, &$count = null): string {}
 
    /** @return array<string|int, string> */
    public function getReplacements(): array {}
}

Replacement follows the literal-array form of strtr():

getReplacements() returns the original replacement mapping as a PHP array.

Matching semantics and limitations

Matching is performed on raw bytes. The implementation does not perform Unicode normalization, case folding, locale-dependent comparisons, or word-boundary detection.

Empty dictionaries are accepted. Empty keywords are rejected.

Matcher requires string keywords, and Replacer requires string replacement values. Integer replacement-map keys are interpreted as their string representations.

Prepared objects cannot be cloned or serialized, and repeated construction is not permitted.

These restrictions are part of the current implementation and are open to discussion before voting.

Performance and memory

The primary benefit is amortizing dictionary preparation across multiple subjects.

The prototype uses an alphabet-reduced Aho-Corasick automaton. The replacement implementation uses a reversed automaton to select the longest keyword at each starting position.

Benchmarks compare the proposed API with existing PHP functions and PCRE across different dictionary sizes and workloads, both with and without preparation costs.

The results show substantial benefits for prepared dictionaries in the measured workloads, while existing functions often remain faster for individual operations that include preparation.

The prototype also has memory costs associated with its transition table. A more compact representation may be considered during implementation.

The complete benchmarks, methodology, input corpora, correctness checks, and raw measurements are available at:

https://github.com/ArtUkrainskiy/php-src-bench/tree/main/reports/multi-pattern-search

Examples

Overlapping matches

<?php
 
$matcher = new Str\Matcher(['a', 'aa', 'aaa']);
 
echo $matcher->countMatches('aaaa');
// 9
 
foreach ($matcher->matchAll('aaa') as $match) {
    echo "{$match->offset}:{$match->keyword}\n";
}
 
// 0:a
// 0:aa
// 0:aaa
// 1:a
// 1:aa
// 2:a

Longest-match replacement

<?php
 
$replacer = new Str\Replacer([
    'ab' => 'X',
    'abc' => 'Y',
    'bc' => 'Z',
]);
 
$count = 0;
$result = $replacer->replace('abcab', $count);
 
var_dump($result); // string(2) "YX"
var_dump($count);  // int(2)

Backward Incompatible Changes

The proposal introduces three new names:

Existing functions and language syntax are unchanged.

Applications defining classes with these fully qualified names may encounter naming conflicts when the new extension is enabled.

The extent of existing userland use of these names has not yet been investigated.

Proposed PHP Version(s)

The next suitable PHP minor release following acceptance of the RFC, subject to the release schedule.

RFC Impact

To the Ecosystem

The proposal introduces new internal classes and methods that can be recognized by IDEs, language servers, static analyzers, and documentation tools.

Existing userland APIs and libraries are not modified.

To Existing Extensions

No changes to existing extensions are proposed.

The reference implementation adds a new ext/strmatch extension.

To SAPIs

The API is intended to work across PHP SAPIs, including CLI and PHP-FPM.

Prepared objects have ordinary PHP object lifetimes. They do not automatically persist between independent requests.

Long-running PHP processes may retain and reuse prepared objects across multiple operations.

The proposed feature does not change the execution model, request lifecycle, or threading model of PHP.

Open Issues

The following questions remain open for discussion:

Future Scope

The following features are outside the scope of this RFC but may be considered separately:

These features are not required by the proposed API.

Voting Choices

A primary vote requiring a two-thirds majority is proposed:

Accept the addition of the Str\Matcher, Str\Replacer, and Str\MatchResult APIs as described in this RFC?

The voting widget and voting dates will be added when the RFC enters the voting phase.

Patches and Tests

Reference implementation:

https://github.com/php/php-src/pull/25122

The draft PR includes the C implementation, build configuration for Unix and Windows, and PHPT tests.

Benchmark report:

https://github.com/ArtUkrainskiy/php-src-bench/tree/main/reports/multi-pattern-search

Implementation

Not yet implemented in a PHP release.

This section will be updated after acceptance and merging.

References

Rejected Features

None at this stage.

Changelog