PHP 8.0+ 新字符串函数
概述
PHP 8.0 引入了三个新字符串函数:str_contains()、str_starts_with() 和 str_ends_with()。这些函数解决了长期以来使用 strpos 进行子串检查时的可读性和安全性问题,已经成为现代 PHP 开发中字符串操作的首选。
基础概念
新旧写法对比
| 操作 | 旧写法(PHP < 8.0) | 新写法(PHP 8.0+) |
|---|---|---|
| 包含检查 | strpos($str, $needle) !== false | str_contains($str, $needle) |
| 前缀检查 | strpos($str, $needle) === 0 | str_starts_with($str, $needle) |
| 后缀检查 | substr($str, -strlen($needle)) === $needle | str_ends_with($str, $needle) |
为什么需要新函数
旧的 strpos 检查方式有两个问题:(1) 可读性差,需要理解 !== false 的含义;(2) 容易出错,新手常写成 == 或忘记处理返回值 0。新函数返回布尔值,语义清晰,不会出错。
语法与代码
str_contains — 包含检查
php
<?php
declare(strict_types=1);
// str_contains(string $haystack, string $needle): bool
// 检查 $haystack 是否包含 $needle,区分大小写
$email = 'alice@example.com';
echo str_contains($email, '@'); // true
echo str_contains($email, 'example'); // true
echo str_contains($email, 'EXAMPLE'); // false(区分大小写)
// 空字符串始终返回 true
echo str_contains('Hello', ''); // true
echo str_contains('', ''); // true
// 替代旧的 strpos 写法
$old = strpos($email, '@') !== false; // 旧写法
$new = str_contains($email, '@'); // 新写法,等价且更清晰str_starts_with — 前缀检查
php
<?php
declare(strict_types=1);
// str_starts_with(string $haystack, string $needle): bool
// 检查 $haystack 是否以 $needle 开头
$url = 'https://example.com/path';
echo str_starts_with($url, 'https://'); // true
echo str_starts_with($url, 'http://'); // false
// 常见应用场景:协议检查
function isHttpsUrl(string $url): bool
{
return str_starts_with($url, 'https://');
}
// 命名空间前缀检查
$class = 'App\\Services\\UserService';
echo str_starts_with($class, 'App\\'); // true
echo str_starts_with($class, 'App\\Services\\'); // true
// 替代旧的 strpos 写法
$old = strpos($url, 'https://') === 0; // 旧写法
$new = str_starts_with($url, 'https://'); // 新写法str_ends_with — 后缀检查
php
<?php
declare(strict_types=1);
// str_ends_with(string $haystack, string $needle): bool
// 检查 $haystack 是否以 $needle 结尾
$filename = 'photo.jpg';
echo str_ends_with($filename, '.jpg'); // true
echo str_ends_with($filename, '.png'); // false
// 常见应用场景:文件扩展名检查
function isImageFile(string $filename): bool
{
return str_ends_with(strtolower($filename), '.jpg')
|| str_ends_with(strtolower($filename), '.png')
|| str_ends_with(strtolower($filename), '.gif')
|| str_ends_with(strtolower($filename), '.webp');
}
// 命名空间后缀检查
$class = 'Controller\\AdminController';
echo str_ends_with($class, 'Controller'); // true
// 替代旧的 substr 写法
$needle = '.jpg';
$old = substr($filename, -strlen($needle)) === $needle; // 旧写法
$new = str_ends_with($filename, $needle); // 新写法大小写不敏感的版本
PHP 核心没有直接提供大小写不敏感的版本,但可以通过辅助函数实现。
php
<?php
declare(strict_types=1);
// 大小写不敏感的包含检查
function str_icontains(string $haystack, string $needle): bool
{
return str_contains(strtolower($haystack), strtolower($needle));
}
// 大小写不敏感的前缀检查
function str_istarts_with(string $haystack, string $needle): bool
{
return str_starts_with(strtolower($haystack), strtolower($needle));
}
// 大小写不敏感的后缀检查
function str_iends_with(string $haystack, string $needle): bool
{
return str_ends_with(strtolower($haystack), strtolower($needle));
}
// 使用示例
echo str_istarts_with('HTTPS://example.com', 'https://'); // true
echo str_iends_with('photo.JPG', '.jpg'); // true详细说明
与 strpos 的性能对比
str_contains 等新函数在内部实现上与 strpos 类似,性能差异可以忽略不计。新函数的主要优势在于可读性和安全性。
php
<?php
declare(strict_types=1);
// 旧写法容易出错的各种场景
$str = 'Hello World';
// 错误 1:使用 == 比较(0 == false 为 true,但 0 !== false)
if (strpos($str, 'Hello') == false) {
// 这里不会执行(strpos 返回 0,0 != false)
}
// 错误 2:忘记 ===
if (strpos($str, 'World')) {
// 正确执行(strpos 返回 6,truthy)
}
if (strpos($str, 'Hello')) {
// 不执行!(strpos 返回 0,falsy)
}
// 新写法永远正确
if (str_contains($str, 'Hello')) {
// 正确执行
}
if (str_contains($str, 'World')) {
// 正确执行
}函数签名
php
<?php
// PHP 8.0+ 函数签名
str_contains(string $haystack, string $needle): bool
str_starts_with(string $haystack, string $needle): bool
str_ends_with(string $haystack, string $needle): bool
// 注意:
// - 两个参数都是 string 类型(PHP 8.0+ 严格类型)
// - 返回值始终是 bool
// - 空的 $needle 始终返回 true
// - PHP 8.0 之前传入非字符串参数会产生 TypeError实战示例
路由匹配系统
php
<?php
declare(strict_types=1);
class Router
{
private array $routes = [];
public function get(string $pattern, callable $handler): void
{
$this->routes['GET'][] = ['pattern' => $pattern, 'handler' => $handler];
}
public function dispatch(string $method, string $uri): mixed
{
foreach ($this->routes[$method] ?? [] as $route) {
if (str_starts_with($uri, $route['pattern'])) {
return ($route['handler'])($uri);
}
}
return null;
}
}
$router = new Router();
$router->get('/api/users', fn(string $uri) => ['handler' => 'userList', 'uri' => $uri]);
$router->get('/api/posts', fn(string $uri) => ['handler' => 'postList', 'uri' => $uri]);
$result = $router->dispatch('GET', '/api/users/123');
print_r($result);CI/CD 分支保护(str_ends_with 实战)
php
<?php
declare(strict_types=1);
class BranchValidator
{
private const PROTECTED_SUFFIXES = ['-main', '-production', '-staging'];
private const ALLOWED_PREFIXES = ['feature/', 'bugfix/', 'hotfix/', 'release/'];
public static function isProtectedBranch(string $branchName): bool
{
foreach (self::PROTECTED_SUFFIXES as $suffix) {
if (str_ends_with($branchName, $suffix)) {
return true;
}
}
return false;
}
public static function isValidFeatureBranch(string $branchName): bool
{
foreach (self::ALLOWED_PREFIXES as $prefix) {
if (str_starts_with($branchName, $prefix)) {
return true;
}
}
return false;
}
public static function validate(string $branchName): array
{
$errors = [];
if (self::isProtectedBranch($branchName)) {
$errors[] = "分支 {$branchName} 受保护,不能直接推送";
}
if (!self::isValidFeatureBranch($branchName) && !self::isProtectedBranch($branchName)) {
$errors[] = "分支名必须以以下前缀之一开头: " . implode(', ', self::ALLOWED_PREFIXES);
}
return $errors;
}
}
print_r(BranchValidator::isProtectedBranch('project-main')); // true
print_r(BranchValidator::isValidFeatureBranch('feature/login')); // true
print_r(BranchValidator::validate('random-branch')); // ['错误信息...']文件类型过滤器
php
<?php
declare(strict_types=1);
class FileFilter
{
/**
* @param string[] $allowedExtensions 允许的扩展名(不含点号)
*/
public static function filterByExtension(array $files, array $allowedExtensions): array
{
return array_filter($files, function (string $file) use ($allowedExtensions): bool {
$lower = strtolower($file);
foreach ($allowedExtensions as $ext) {
if (str_ends_with($lower, '.' . strtolower($ext))) {
return true;
}
}
return false;
});
}
public static function filterByPrefix(array $files, string $prefix): array
{
return array_filter(
$files,
fn(string $file): bool => str_starts_with(basename($file), $prefix)
);
}
}
$files = ['photo.jpg', 'document.pdf', 'image.png', 'data.csv', 'picture.jpeg'];
$images = FileFilter::filterByExtension($files, ['jpg', 'png', 'jpeg', 'gif', 'webp']);
print_r(array_values($images));
// ['photo.jpg', 'image.png', 'picture.jpeg']输入验证与清洗
php
<?php
declare(strict_types=1);
class InputValidator
{
public static function isValidEmail(string $email): bool
{
return str_contains($email, '@') && str_contains($email, '.');
}
public static function isInternalUrl(string $url): bool
{
return str_starts_with($url, '/internal/')
|| str_starts_with($url, '/admin/');
}
public static function hasDangerousContent(string $input): bool
{
$patterns = ['<script', 'javascript:', 'data:', 'onerror=', 'onload='];
foreach ($patterns as $pattern) {
if (str_icontains($input, $pattern)) {
return true;
}
}
return false;
}
}
// 辅助函数:大小写不敏感的包含检查
function str_icontains(string $haystack, string $needle): bool
{
return str_contains(strtolower($haystack), strtolower($needle));
}str_contains 的内部实现
php
<?php
declare(strict_types=1);
// str_contains 的 polyfill(PHP < 8.0)
if (!function_exists('str_contains')) {
function str_contains(string $haystack, string $needle): bool
{
return strpos($haystack, $needle) !== false;
}
}
if (!function_exists('str_starts_with')) {
function str_starts_with(string $haystack, string $needle): bool
{
return strncmp($haystack, $needle, strlen($needle)) === 0;
}
}
if (!function_exists('str_ends_with')) {
function str_ends_with(string $haystack, string $needle): bool
{
$length = strlen($needle);
if ($length === 0) {
return true;
}
return substr($haystack, -$length) === $needle;
}
}更多的前缀/后缀检查场景
php
<?php
declare(strict_types=1);
// 命名空间检测
function isInNamespace(string $class, string $namespace): bool
{
return str_starts_with($class, $namespace . '\\');
}
// MIME 类型检测
function isImageMimeType(string $mime): bool
{
return str_starts_with($mime, 'image/');
}
// 版本号比较前缀
function isStableVersion(string $version): bool
{
return !str_contains($version, '-') && !str_contains($version, '+');
}
// 路径规范化检测
function isAbsolutePath(string $path): bool
{
return str_starts_with($path, '/') || str_starts_with($path, 'C:\\');
}
// API 路由前缀匹配
class RoutePrefix
{
private const PREFIXES = [
'api' => '/api/',
'admin' => '/admin/',
'internal' => '/internal/',
];
public static function match(string $uri): ?string
{
foreach (self::PREFIXES as $name => $prefix) {
if (str_starts_with($uri, $prefix)) {
return $name;
}
}
return null;
}
}
echo RoutePrefix::match('/api/users/123'); // api
echo RoutePrefix::match('/admin/dashboard'); // admin
echo RoutePrefix::match('/public/index.html'); // null输入验证实战
php
<?php
declare(strict_types=1);
class StringValidator
{
// 验证密码策略
public static function checkPasswordPolicy(string $password): array
{
$errors = [];
if (strlen($password) < 8) {
$errors[] = '密码至少 8 个字符';
}
if (!str_contains($password, ' ')) {
// 不允许空格
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = '需要至少一个大写字母';
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = '需要至少一个数字';
}
return $errors;
}
// 验证域名
public static function isDomain(string $value): bool
{
if (!str_contains($value, '.')) {
return false;
}
$parts = explode('.', $value);
$tld = strtolower(end($parts));
return str_ends_with($value, '.' . $tld);
}
// 检查是否为内部 API
public static function isInternalApi(string $url): bool
{
$internalHosts = ['localhost', '127.0.0.1', 'internal.example.com'];
foreach ($internalHosts as $host) {
if (str_starts_with($url, "http://{$host}") ||
str_starts_with($url, "https://{$host}")) {
return true;
}
}
return false;
}
}性能说明
str_contains、str_starts_with、str_ends_with 在内部使用了优化的实现。对于简单的字符串检查,它们的性能与 strpos 几乎相同,但代码更安全、更可读。
注意事项
空字符串行为
三个新函数中,当 $needle 为空字符串时,始终返回 true。这与 strpos 的行为一致(strpos($str, '') 返回 0),但可能在逻辑判断中产生意外结果。建议在调用前检查 $needle 是否为空。
PHP 版本要求
str_contains、str_starts_with、str_ends_with 需要 PHP 8.0+。如果需要在旧版本中使用,可以通过 polyfill 实现,或继续使用 strpos / substr 模式。
大小写敏感
这三个函数都是大小写敏感的。如果需要大小写不敏感的比较,需要先将两个参数都转换为小写,或使用自定义辅助函数。
最佳实践
- PHP 8.0+ 项目优先使用新函数:
str_contains/str_starts_with/str_ends_with是字符串检查的首选 - 避免旧模式:不再使用
strpos($str, $needle) !== false、strpos($str, $needle) === 0、substr(...) === $needle - 封装大小写不敏感版本:在项目中创建
str_icontains等辅助函数复用 - 检查空 needle:在可能传入空字符串的场景中,提前检查
- 结合类型声明使用:配合
strict_types=1确保参数类型正确