str_contains / str_starts_with / str_ends_with
debt(d3/e1/b1/t3)
Closest to 'default linter catches the common case' (d3), Rector and phpcs (listed in detection_hints.tools) automatically flag strpos() !== false patterns and can rewrite them.
Closest to 'one-line patch or single-call swap' (e1), quick_fix says replace strpos($h,$n) !== false with str_contains($h,$n) — a direct call swap, Rector can automate it.
Closest to 'minimal commitment' (b1), this is a localised readability improvement at each call site with no architectural reach despite applying across web/cli/queue contexts.
Closest to 'minor surprise' (t3), misconception notes case-sensitivity assumption — a single documented edge case devs may guess wrong about, but the function otherwise behaves as named.
TL;DR
Explanation
Before PHP 8.0: if (strpos($str, $needle) !== false) was the idiom — prone to off-by-one with === 0 vs !== false. PHP 8.0 added: str_contains(string $haystack, string $needle): bool, str_starts_with(), str_ends_with(). All return bool, handle empty strings predictably (str_contains('foo', '') === true), and are case-sensitive. For case-insensitive search still use stripos() !== false or strtolower() + str_contains(). These functions are simple, readable, and Rector can auto-migrate from strpos patterns.
Common Misconception
Why It Matters
Common Mistakes
- Using strpos() === false instead of !== false — logic inversion bug.
- Not knowing str_contains()/str_starts_with() are in PHP 8.0+ only — use Rector to polyfill for 7.x.
- Assuming case-insensitive behaviour.
Code Examples
// Fragile: easy to confuse === and !==
if (strpos($path, '/admin') === 0) { /* starts with */ }
if (strpos($email, '@') !== false) { /* contains */ }
if (str_starts_with($path, '/admin')) { /* clear intent */ }
if (str_contains($email, '@')) { /* readable */ }
if (str_ends_with($file, '.php')) { /* obvious */ }
// Case-insensitive:
if (str_contains(strtolower($input), 'error')) { }