rfc:prefix_suffix_functions

PHP RFC: Prefix and Suffix Functions

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.

<?php
 
$host = "www.example.com";
$host = str_prefix_remove($host, "www.");          // "example.com"
 
$filename = "photo.jpeg";
$filename = str_suffix_replace(".jpeg", ".jpg", $filename); // "photo.jpg"
 
$key = "user:123";
$key = str_prefix_ensure($key, "app:");               // "app:user:123"
 
?>

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($key, 'app:');
$url = str_prefix_ensure($url, '/api');
$path = str_suffix_ensure($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($host, 'www.');
$id   = str_prefix_remove($id, 'user:');
$name = str_suffix_remove($name, '.json');

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 $subject, string $prefix): string
 
str_suffix_ensure(string $subject, string $suffix): string
 
str_prefix_remove(string $subject, string $prefix): string
 
str_suffix_remove(string $subject, string $suffix): string
 
str_prefix_replace(string $prefix, string $replace, string $subject): string
 
str_suffix_replace(string $suffix, string $replace, string $subject): string

Parameter order aims to follow existing conventions in already existing php functions which are the closest relatives to the new functions

  • The “ensure” and “remove” functions follow the parameter order of functions like str_starts_with() or str_contains() ($haystack, $needle)
  • The “replace” functions follow the parameter order of functions like str_replace or preg_replace() ($search, replace, $subject)

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)

function str_prefix_ensure(string $subject, string $prefix): string {
    if ($prefix === '' || str_starts_with($subject, $prefix)) {
        return $subject;
    }
    return $prefix . $subject;
}
  • If $prefix is empty: return $subject unchanged.
  • If $subject already starts with $prefix: return $subject unchanged.
  • Otherwise: return $prefix . $subject.
function str_suffix_ensure(string $subject, string $suffix): string {
    if ($suffix === '' || str_ends_with($subject, $suffix)) {
        return $subject;
    }
    return $subject . $suffix;
}
  • If $suffix is empty: return $subject unchanged.
  • If $subject already ends with $suffix: return $subject unchanged.
  • Otherwise: return $subject . $suffix.
function str_prefix_remove(string $subject, string $prefix): string {
    if ($prefix !== '' && str_starts_with($subject, $prefix)) {
        return substr($subject, strlen($prefix));
    }
    return $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.
function str_suffix_remove(string $subject, string $suffix): string {
    if ($suffix !== '' && str_ends_with($subject, $suffix)) {
        return substr($subject, 0, -strlen($suffix));
    }
    return $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).
function str_prefix_replace(string $prefix, string $replace, string $subject): string {
    if ($prefix !== '' && str_starts_with($subject, $prefix)) {
        return $replace . substr($subject, strlen($prefix));
    }
    return $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.
function str_suffix_replace(string $suffix, string $replace, string $subject): string {
    if ($suffix !== '' && str_ends_with($subject, $suffix)) {
        return substr($subject, 0, -strlen($suffix)) . $replace;
    }
    return $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.

Parameter Order for _replace Functions

There appears to be some disagreement regarding the expected parameter order for the _replace functions. Some users believe they should follow the pattern used by the str_replace and preg_replace functions, where $subject is the last parameter. Others argue that they should follow the pattern used by the other prefix/suffix functions, where $subject is the first parameter.

Given this discrepancy, a secondary vote has been introduced to determine the appropriate parameter order for these functions.

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:

<?php
 
$host = str_prefix_remove("www.example.com", "www.");
// "example.com"
 
$url2 = str_prefix_replace("http://", "https://", "http://example.org");
// "https://example.org"
 
$base = str_suffix_ensure("https://example.org/api", "/");
// "https://example.org/api/"
 
?>

Empty prefix/suffix:

<?php
 
str_prefix_ensure("abc", "");             // "abc"
 
str_prefix_remove("abc", "");             // "abc"
 
// An empty old suffix always matches at the end
str_suffix_replace("", "X", "abc");       // "abcX" 
?>

Exact-match only:

<?php
 
// Case-sensitive: does not match if case differs
str_suffix_replace(".jpeg", ".jpg", "IMAGE.JPEG");     // "IMAGE.JPEG"
 
// Only removes once per call
str_suffix_remove("path///", "/");                     // "path//"
 
// These functions do NOT parse URLs or filenames
str_suffix_replace(".jpeg", ".jpg", "image.jpeg?x=1"); // "image.jpeg?x=1"
 
?>

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. A search in GitHub did not reveal any public PHP repositories using these function names.

Proposed PHP Version(s)

PHP 8.6

RFC Impact

To the Ecosystem

The impact on the ecosystem is limited to tooling updates to recognize the newly introduced functions; no changes in behavior, syntax, or analysis rules are required.

To Existing Extensions

None.

To SAPIs

None.

Open Issues

None currently.

Future Scope

A possible future extension of this proposal would be to allow the prefix or suffix parameter to also accept an array of strings. This would enable removing, ensuring, or replacing any one of multiple known prefixes or suffixes in a single operation, a pattern that frequently appears in real-world code (for example, handling multiple URL schemes, legacy prefixes, or alternative file extensions). While this RFC intentionally limits the API to a single prefix or suffix to keep the semantics simple and predictable, an array-based variant could build upon the same mental model and semantics without introducing ambiguity.

Such an extension could allow code like the following:

 
// Remove any known scheme
$url = str_prefix_remove(
    "https://example.com",
    ["http://", "https://", "ftp://"],
);
// "example.com"
 
// Replace any legacy scheme with https
$url = str_prefix_replace(
    ["http://", "ftp://"],
    "https://",
    "http://example.com"
);
// "https://example.com"
 
// Normalize multiple image extensions
$file = str_suffix_replace(
    [".jpeg", ".jpe", ".jfif"],
    ".jpg",
    "photo.jfif"
);
// "photo.jpg"

Voting Choices

Primary Vote requiring a 2/3 majority to accept the RFC:

Implement prefix and suffix functions as outlined in the RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Secondary vote requiring simple majority:

Should the $subject parameter be the first or the last parameter in the _replace functions?
Real name First Last Abstain
Final result: 0 0 0
This poll has been closed.

In case of a tie in this secondary vote, the option to make $subject the last parameter will be chosen.

Patches and Tests

Implementation

TODO: After acceptance.

References

Rejected Features

None currently.

Changelog

  • 2026-01-22: Initial RFC published
  • 2026-01-22: Discussion started on internals
  • 2026-01-22: Invert the order of parameters for remove and ensure functions
  • 2026-01-23: Added userland implementation of functions and comment about GitHub usages found
  • 2026-02-06: Start of voting period announced
  • 2026-02-07: Voting cancelled, added paragraph about parameter order and secondary vote
  • 2026-02-08: Adds a note to explain the tie breaker for the secondary vote
rfc/prefix_suffix_functions.txt · Last modified: by barel