====== PHP RFC: Prefix and Suffix Functions ======
* Version: 0.1
* Date: 2026-01-21
* Author: Carlos Granados, barel.barelon@gmail.com
* Status: Draft
* Implementation: https://github.com/php/php-src/pull/20953
===== Introduction =====
In PHP applications, it’s extremely common to normalize strings by conditionally adding, removing, or replacing a prefix or suffix. Today, developers typically implement these patterns by combining ''str_starts_with()'' / ''str_ends_with()'' with ''substr()'', or by reaching for regular expressions. While these approaches are workable, they tend to be verbose, repetitive, and easy to get subtly wrong (off-by-one lengths, duplicated separators, or inconsistent normalization). This RFC proposes six small, focused functions that express these operations directly and perform them in a single step, resulting in code that is shorter, clearer, and more intention-revealing.
Typical use cases include: normalizing hostnames ( ''www.'' removal), ensuring URL schemes (''http:''''/''''/''), enforcing trailing slashes on base URLs, rewriting legacy schemes (''http:''''/''''/'' → ''https:''''/''''/''), standardizing file extensions (''.jpeg'' → ''.jpg'', ''.tar.gz'' handling), stripping known prefixes/suffixes from IDs (''user:'' / '':v2''), normalizing storage keys (''uploads/'' prefix), building cache keys consistently (''app:'' prefix). In all of these cases, the core need is the same: perform a well-defined prefix/suffix transformation only when applicable, without duplicating logic throughout codebases.
===== Proposal =====
This RFC proposes adding six small, orthogonal string functions to ext/standard for conditionally ensuring, removing, or replacing a prefix or suffix in a string, in a single step.
These operations appear across PHP codebases whenever developers normalize identifiers, cache keys, headers, routing segments, file-ish tokens, and other “structured-enough” strings where the presence of a known prefix/suffix must be checked before transforming. Today, this is typically implemented via ''str_starts_with()'' / ''str_ends_with()'' plus ''substr()''/concatenation (or preg_replace()), resulting in repeated boilerplate and inconsistent edge-case handling. This proposal standardizes the behavior in the core, making intent explicit (“ensure prefix”, “remove suffix”, “replace prefix”), and enabling a fast, allocation-minimal implementation in C.
Why this brings substantial value: PHP is one of the world’s most widely used languages, and string normalization is daily work in PHP applications, frameworks, and libraries. These helpers provide:
* Readability / intent: “what” is expressed directly, not reconstructed from ''if'' + ''str_starts_with()'' / ''str_ends_with()'' + ''substr''.
* Consistency: everyone gets the same semantics for edge cases (empty prefix/suffix, longer-than-subject checks, binary strings).
* Less error-prone code: avoids off-by-one length mistakes and duplicated conditional logic.
* Performance: single ''memcmp()'' check and at most one allocation when a change is required; returns the original string unchanged otherwise.
(Important note: these are string helpers, not URL/filepath parsers. They intentionally do not validate or interpret structured formats; they just do prefix/suffix handling.)
==== Features and examples ====
1) Ensure prefix/suffix (idempotent “add if missing”)
Use case examples
* Ensure a cache key prefix (''app:'')
* Ensure a routing prefix (''/api'')
* Ensure a trailing slash in paths (''/'')
$key = str_prefix_ensure('app:', $key);
$url = str_prefix_ensure('/api', $url);
$path = str_suffix_ensure('path', $path);
2) Remove prefix/suffix (strip if present)
Use case examples
* Remove ''www.'' from host-like strings
* Remove a known namespace prefix (''user:'')
* Remove a known extension token (''.json'') when you treat it as a suffix token
$host = str_prefix_remove('www.', $host);
$id = str_prefix_remove('user:', $id);
$name = str_suffix_remove('.json', $name);
3) Replace prefix/suffix (rewrite if present)
Use case examples
* Rewrite ''http:''''/''''/'' → ''https:''''/''''/'' (string-level)
* Normalize extensions: ''.jpeg'' → ''.jpg''
* Rewrite version tokens: '':v1'' → '':v2''
$url = str_prefix_replace('http://', 'https://', $url);
$file = str_suffix_replace('.jpeg', '.jpg', $file);
$key = str_suffix_replace(':v1', ':v2', $key);
==== Desired syntax and semantics ====
Function list (global namespace)
As implemented in the PR, the proposed names and parameter order are:
str_prefix_ensure(string $prefix, string $subject): string
str_suffix_ensure(string $suffix, string $subject): string
str_prefix_remove(string $prefix, string $subject): string
str_suffix_remove(string $suffix, string $subject): string
str_prefix_replace(string $prefix, string $replace, string $subject): string
str_suffix_replace(string $suffix, string $replace, string $subject): string
==== Common behavior (all functions) ====
Binary-safe, byte-wise comparison: uses memcmp() on raw string bytes; comparisons are case-sensitive and do not perform Unicode normalization. Works with embedded \0 bytes.
No warnings/notices for non-match: a non-matching prefix/suffix simply results in returning the original subject.
Return type is always string: never false.
These are pure functions: no modification of input. Returns either a copy of the original or a newly allocated string containing the transformed value.
Time complexity: O(m) where m is the prefix/suffix length for the comparison, plus O(n) copying when a new string is created.
==== Exact semantics (per function) ====
str_prefix_ensure($prefix, $subject)
* If ''$prefix'' is empty: return ''$subject'' unchanged.
* If ''$subject'' already starts with ''$prefix'': return ''$subject'' unchanged.
* Otherwise: return ''$prefix . $subject''.
str_suffix_ensure($suffix, $subject)
* If ''$suffix'' is empty: return ''$subject'' unchanged.
* If ''$subject'' already ends with ''$suffix'': return $subject unchanged.
* Otherwise: return ''$subject . $suffix''.
str_prefix_remove($prefix, $subject)
* If ''$prefix'' is longer than ''$subject:'' return ''$subject'' unchanged.
* If ''$subject'' starts with ''$prefix'': return ''$subject'' without that leading segment.
* Otherwise: return ''$subject'' unchanged.
* If ''$prefix'' is empty: it is considered “present” at the start, but removing it removes zero bytes, so the result is unchanged.
str_suffix_remove($suffix, $subject)
* If ''$suffix'' is longer than ''$subject'': return ''$subject'' unchanged.
* If ''$subject'' ends with ''$suffix'': return ''$subject'' without that trailing segment.
* Otherwise: return ''$subject'' unchanged.
* If ''$suffix'' is empty: unchanged (removes zero bytes).
str_prefix_replace($prefix, $replace, $subject)
* If ''$prefix'' is longer than ''$subject'': return ''$subject'' unchanged.
* If ''$subject'' starts with ''$prefix'': return ''$replace . substr($subject, strlen($prefix))''.
* Otherwise: return ''$subject'' unchanged.
* If ''$prefix'' is empty, it is always considered to match at the start, so this function becomes an unconditional prepend: return ''$replace . $subject''.
str_suffix_replace($suffix, $replace, $subject)
* If ''$suffix'' is longer than ''$subject'': return ''$subject'' unchanged.
* If ''$subject'' ends with ''$suffix'': return ''substr($subject, 0, -strlen($suffix)) . $replace''.
* Otherwise: return ''$subject'' unchanged.
* If ''$suffix'' is empty, it is always considered to match at the end, so this function becomes an unconditional append: return ''$subject . $replace''.
==== Interactions with existing PHP functionality ====
These functions are essentially standard-library shorthands for common patterns built from:
* ''str_starts_with()'' / ''str_ends_with()''
* concatenation (''.'')
* ''substr()'' with computed offsets
They do not overlap with locale-aware or multibyte string APIs; they mirror ''str_starts_with'' / ''str_ends_with'' semantics (byte-wise, case-sensitive).
They do not replace structured parsing (e.g. ''parse_url()'', filesystem path handling).
==== Edge cases and potential gotchas ====
* **Empty prefix/suffix**
* ''_ensure("", $subject)'' returns ''$subject'' unchanged.
* ''_remove("", $subject)'' returns ''$subject'' unchanged (removes zero bytes).
* ''_replace("", $replace, $subject)'' always matches and thus always prepends/appends ''$replace''. This is consistent and useful, but might surprise if not documented.
* **Prefix/suffix longer than subject**
* ''_remove'' and ''_replace'' return ''$subject'' unchanged if the token is longer.
* ''_ensure'' will still add the token when it’s missing; if the token is longer, it cannot “already be present”, so it will be added.
* **Binary strings and null bytes**
* Functions operate on bytes; embedded \0 is supported and tested (no C-string truncation).
* **Case sensitivity**
* Comparisons are case-sensitive.
* **Multiple occurrences**
* These functions operate only on the start or end of the string. They do not operate on repeated or internal occurrences.
* **No regex support**
*Patterns are treated as literal strings. This avoids complexity, performance costs, and ambiguity. Developers requiring pattern-based behaviour should continue using regular expressions and functions like ''preg_replace()''.
==== Examples ====
Simple examples:
Empty prefix/suffix:
Exact-match only:
===== Backward Incompatible Changes =====
This proposal introduces six new global functions. It does not modify the behavior of any existing functions, classes, language constructs, or extensions.
As with any new global function, there is a theoretical risk of name collisions with user-defined functions of the same name. However, the proposed names follow the established str_* naming convention used by existing PHP string helpers (such as str_contains(), str_starts_with(), and str_ends_with()), and are sufficiently specific to minimize the likelihood of collisions in real-world codebases.
===== Proposed PHP Version(s) =====
PHP 8.6
===== RFC Impact =====
==== To the Ecosystem ====
What effect will the RFC have on IDEs, Language Servers (LSPs), Static Analyzers, Auto-Formatters, Linters and commonly used userland PHP libraries?
==== To Existing Extensions ====
Will existing extensions be affected?
==== To SAPIs ====
Describe the impact to CLI, Development web server, embedded PHP etc.
===== Open Issues =====
Make sure there are no open issues when the vote starts!
===== Future Scope =====
This section should outline areas that you are not planning to work on in the scope of this RFC, but that might be iterated upon in the future by yourself or another contributor.
This helps with long-term planning and ensuring this RFC does not prevent future work.
===== Voting Choices =====
Pick a title that reflects the concrete choice people will vote on.
Please consult [[https://github.com/php/policies/blob/main/feature-proposals.rst#voting-phase|the php/policies repository]] for the current voting guidelines.
----
Primary Vote requiring a 2/3 majority to accept the RFC:
* Yes
* No
* Abstain
===== Patches and Tests =====
Links to proof of concept PR.
If there is no patch, make it clear who will create a patch, or whether a volunteer to help with implementation is needed.
===== Implementation =====
After the RFC is implemented, this section should contain:
- the version(s) it was merged into
- a link to the git commit(s)
- a link to the PHP manual entry for the feature
===== References =====
Links to external references, discussions, or RFCs.
===== Rejected Features =====
Keep this updated with features that were discussed on the mail lists.
===== Changelog =====
If there are major changes to the initial proposal, please include a short summary with a date or a link to the mailing list announcement here, as not everyone has access to the wikis' version history.