☰
PHP版本不兼容导致搜索功能失效怎么办
2026/10/2 12:52:43 网站建设 项目流程

前言

"网站升级了 PHP,别的页面都好,就搜索不行了"——这是升级后最典型的报障之一,症状很有辨识度:结果永远是空的,但数据库里明明有数据;或者页面直接 500,日志里写着Call to undefined function;或者搜英文正常、搜中文什么都搜不到;或者前端报"接口返回格式错误",因为 JSON 前面多了一行Deprecated警告。

搜索之所以特别容易在升级时阵亡,是因为它同时踩在三条线上:老代码里用了被移除的函数、旧版 PDO/MySQL 的行为变了、中文全文检索本来就依赖编码和分词配置。三条线里任何一条断了,表现都是"搜不到"。本文按"先分清是哪一层的锅,再逐个修"的顺序讲清排查路径,并给出一个可直接跑的诊断脚本和一份可切换的搜索实现。

一、先分清是 PHP 的锅、MySQL 的锅,还是编码的锅

搜索不工作的时候,不要一上来就读代码,先按症状对号入座。

症状最可能的原因一句话验证方法
日志里Call to undefined function mysql_connect()mysql_*扩展在 PHP 7.0 已被移除`php -m \
日志里Uncaught PDOException但代码里没写 try/catchPDO 默认错误模式在 PHP 8.0 改成了抛异常看PDO::ATTR_ERRMODE是否显式设置
搜英文有结果,搜中文没结果字符集/排序规则不是utf8mb4,或全文索引分词不匹配SHOW FULL COLUMNS FROM 表名看 Collation
分页一加LIMIT就报 SQL 语法错误关掉模拟预处理后,绑定的参数被当成字符串检查bindValue的第三个参数
接口报 JSON 解析失败弃用警告被打印进了响应体看响应前几个字节是不是Deprecated
搜索词里的特殊字符导致报错老代码用手工拼接 SQL,新版本转义行为变了搜'单引号试试

这里有个几乎人人都会踩的点:PHP 8.0 把 PDO 的默认错误模式从"静默"改成了"抛异常"。在老代码里,下面这种写法是"安全的"——因为它根本不检查返回值:

<?php // 老代码常见写法,PHP 7.x 下静默失败,PHP 8.0 起抛 PDOException $stmt = $pdo->query($sql); $rows = $stmt->fetchAll();

如果 SQL 写错了,7.x 下query()返回false,然后false->fetchAll()报一个"在 bool 上调用方法"的错;8.0 下直接在query()这一步就抛异常。表面上看是"升级后搜索坏了",本质是升级把一个早就存在的 SQL 错误暴露了出来。

二、搜索相关的 API 存废清单

搜索代码里高频出现、且在版本升级中出问题的 API 如下。

API / 写法状态变化替代方案
mysql_query()/mysql_fetch_array()PHP 7.0 移除PDO或mysqli
ereg()/eregi()PHP 7.0 移除preg_match()
split()/spliti()PHP 7.0 移除preg_split()
preg_replace()的/e修饰符PHP 7.0 移除preg_replace_callback()
mb_ereg_replace()的e选项PHP 7.0 移除mb_ereg_replace_callback()
create_function()PHP 8.0 移除匿名函数
utf8_encode()/utf8_decode()PHP 8.2 弃用mb_convert_encoding()
iconv()的//TRANSLIT行为各版本对非法字符处理不同显式判断返回值
FILTER_SANITIZE_STRINGPHP 8.1 弃用按用途选htmlspecialchars()
strpos()判断前缀一直可用但易踩0 == falsePHP 8.0 起用str_contains()

很多老项目的搜索高亮功能用的是mb_ereg_replace()的e选项,它从 PHP 8.0 起彻底没了,必须改用mb_ereg_replace_callback()。

utf8_encode()/utf8_decode()是最容易误用的一对函数:它们不是"UTF-8 编解码",而是"ISO-8859-1 与 UTF-8 之间的转换"。老代码经常拿它处理 GBK 数据,结果本来能搜到的词变成乱码。正确做法是明确声明源编码:

<?php declare(strict_types=1); // 从 GBK 页面表单收到的搜索词,转成 UTF-8 再进数据库 $keyword = mb_convert_encoding($rawKeyword, 'UTF-8', 'GBK'); // 判断转换是否真的成功(非法字节会被替换成 U+FFFD) if (str_contains($keyword, "\u{FFFD}")) { throw new InvalidArgumentException('搜索词编码无法识别'); }

mb_convert_encoding($str, $toEncoding, $fromEncoding)的参数顺序是"目标在前、来源在后",和iconv($from, $to, $str)正好相反。这个顺序搞反了不会报错,只会安安静静地输出乱码,非常难查。

三、MySQL 侧:中文搜索为什么搜不到

把 PHP 那层修好之后,如果中文还是搜不到,问题通常在数据库这一侧,而且是两个独立的坑。

第一个坑:字符集和排序规则。表用的如果是utf8(实际上是"最多 3 字节的 UTF-8"),那么 emoji 和部分生僻汉字根本存不进去,会被截断或替换成?。中文搜索要用utf8mb4:

ALTER TABLE articles CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

第二个坑:全文索引的分词。MySQL 内置的全文解析器是按"空格和标点切词"的,中文句子中间没有空格,整句会被当成一个巨大的"词",所以MATCH ... AGAINST永远匹配不到。解决办法是使用ngram解析器(MySQL 5.7.6 及以上支持),它按固定长度切分汉字:

-- 建表时指定:按 2 个字符一组切分 CREATE TABLE articles ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL, body TEXT NOT NULL, PRIMARY KEY (id), FULLTEXT KEY ft_article (title, body) WITH PARSER ngram ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 已有表补索引 ALTER TABLE articles ADD FULLTEXT INDEX ft_article (title, body) WITH PARSER ngram;

对应的查询用布尔模式:

SELECT id, title FROM articles WHERE MATCH (title, body) AGAINST ('数据库优化' IN BOOLEAN MODE) ORDER BY id DESC LIMIT 20;

这里有一个必须知道的限制:ngram解析器的切分长度由系统变量ngram_token_size决定(默认值为 2),而MATCH ... AGAINST里小于这个长度的搜索词会被直接忽略。默认配置下搜单个汉字什么都搜不到——这不是 bug,是分词规则;需要单字搜索可以调这个变量,但改完必须重建索引才生效。

不想动全文索引也可以退回LIKE '%关键词%',但它以通配符开头时用不上索引,会退化成全表扫描,万行级以内可以接受,十万行以上就该上FULLTEXT或外部搜索引擎了。

四、代码实战:一个跨版本可用的搜索封装

下面这份代码同时演示三件事:显式设置 PDO 错误模式(避免 8.0 的行为突变)、正确绑定LIMIT参数、以及在全文索引不可用时降级到LIKE。最低要求 PHP 8.1。

<?php declare(strict_types=1); /** * 搜索服务:优先走全文索引,失败则降级为 LIKE * 最低版本:PHP 8.2(使用了枚举和 readonly 类;枚举本身是 8.1 引入的) */ enum SearchMode: string { case Fulltext = 'fulltext'; case Like = 'like'; } final readonly class SearchResult { public function __construct( public array $rows, public int $total, public SearchMode $mode, ) {} } final class ArticleSearcher { private const MAX_LIMIT = 100; public function __construct(private PDO $pdo) {} public function search(string $keyword, int $page = 1, int $perPage = 20): SearchResult { $keyword = trim($keyword); if ($keyword === '') { throw new InvalidArgumentException('搜索词不能为空'); } // 关键:不让用户输入影响 LIMIT 上限 $perPage = max(1, min($perPage, self::MAX_LIMIT)); $offset = max(0, ($page - 1) * $perPage); try { return $this->fulltext($keyword, $perPage, $offset); } catch (PDOException $e) { // 1191 = 全文索引不存在,1214 = 搜索词语法错误 if (in_array((int) $e->errorInfo[1], [1191, 1214], true)) { return $this->like($keyword, $perPage, $offset); } throw $e; } } private function fulltext(string $keyword, int $limit, int $offset): SearchResult { // 布尔模式下把每个词用引号包起来,避免用户写的 + - * 被当成运算符 $boolean = implode(' ', array_map( static fn(string $w): string => '"' . str_replace('"', '', $w) . '"', preg_split('/\s+/u', $keyword) ?: [$keyword] )); $sql = 'SELECT id, title FROM articles WHERE MATCH (title, body) AGAINST (:kw IN BOOLEAN MODE) ORDER BY id DESC LIMIT :limit OFFSET :offset'; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':kw', $boolean, PDO::PARAM_STR); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); // 必须显式指定 INT $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); $rows = $stmt->fetchAll(); return new SearchResult($rows, count($rows), SearchMode::Fulltext); } private function like(string $keyword, int $limit, int $offset): SearchResult { // LIKE 的通配符必须转义,否则用户输入 % 会匹配全部 $escaped = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $keyword); $pattern = '%' . $escaped . '%'; $sql = 'SELECT id, title FROM articles WHERE title LIKE :kw OR body LIKE :kw ORDER BY id DESC LIMIT :limit OFFSET :offset'; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':kw', $pattern, PDO::PARAM_STR); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); $rows = $stmt->fetchAll(); return new SearchResult($rows, count($rows), SearchMode::Like); } } // ---------- 使用示例 ---------- $pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 显式声明,不依赖默认值 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, // 使用真正的预处理语句 ]); $searcher = new ArticleSearcher($pdo); $result = $searcher->search('数据库 优化', 1, 10); printf("命中 %d 条,模式: %s\n", $result->total, $result->mode->value); foreach ($result->rows as $row) { printf(" #%d %s\n", $row['id'], $row['title']); }

bindValue(':limit', $limit, PDO::PARAM_INT)里的第三个参数是必须的。当PDO::ATTR_EMULATE_PREPARES设为false(使用服务端真正的预处理)时,如果把LIMIT的值当字符串绑定,MySQL 会直接报语法错误,因为LIMIT只接受字面整数,不接受字符串参数。这个坑在升级时特别常见:老代码在模拟预处理下能凑合跑,一旦有人为了安全关掉模拟预处理,搜索分页立刻炸。

再配一段三行的诊断代码,把"版本相关事实"直接打出来:

<?php foreach (['mysql_connect', 'mb_ereg', 'utf8_encode', 'create_function', 'split', 'str_contains'] as $fn) { printf("%-18s %s\n", $fn, function_exists($fn) ? '有' : '无'); } printf("pdo_mysql: %s, 默认字符集: %s\n", extension_loaded('pdo_mysql') ? '已加载' : '未加载', ini_get('default_charset'));

在目标版本上跑一遍,输出的"无"就是升级后会炸的点。注意extension_loaded()和function_exists()查的是不同层面:扩展可能加载了,但某个函数被单独移除了,所以两个都要查。

常见坑点

1.strpos()判断"是否包含"时把位置 0 当成 false

❌ 错误写法:

<?php if (strpos($title, $keyword)) { // 关键词在开头时返回 0,条件为假,结果被漏掉 highlight($title); }

✅ 正确写法:

<?php if (str_contains($title, $keyword)) { // PHP 8.0 起可用 highlight($title); }

这个 bug 在搜索里表现为"永远搜不到以关键词开头的那条记录",属于最典型的"结果少了但没人发现"。

2. 用LIKE时没转义通配符

❌ 错误写法:

<?php $stmt = $pdo->prepare('SELECT * FROM articles WHERE title LIKE ?'); $stmt->execute(['%' . $keyword . '%']); // 用户输入 % 就搜出全表

✅ 正确写法:

<?php $escaped = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $keyword); $stmt = $pdo->prepare('SELECT * FROM articles WHERE title LIKE ?'); $stmt->execute(['%' . $escaped . '%']);

用户输入一个%,全表数据就返回了;输入_则变成"任意单字符"。这不只是性能问题,也是信息泄露。

3.LIMIT参数当字符串绑定

❌ 错误写法:

<?php $stmt = $pdo->prepare($sql); $stmt->execute([$keyword, $limit, $offset]); // 全部按字符串处理

✅ 正确写法:

<?php $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute();

在PDO::ATTR_EMULATE_PREPARES => false下,字符串形式的LIMIT会直接触发 SQL 语法错误。

4. 用utf8_encode()处理 GBK 数据

❌ 错误写法:

<?php $keyword = utf8_encode($_GET['q']); // 它做的是 ISO-8859-1 → UTF-8

✅ 正确写法:

<?php $keyword = mb_convert_encoding($_GET['q'], 'UTF-8', 'GBK');

utf8_encode()只认识 ISO-8859-1,把 GBK 字节喂进去会得到一堆问号,然后你搜什么都是空的,而且日志里一条错误都没有。这个函数在 PHP 8.2 起还会额外发一条弃用警告。

5. 弃用警告污染 JSON 响应

❌ 错误写法:接口里直接输出 JSON,但display_errors=On,弃用警告被打印在{之前,前端JSON.parse直接失败。

✅ 正确写法:接口入口先把警告导向日志,不要让它进响应体:

<?php ini_set('display_errors', '0'); ini_set('log_errors', '1'); error_reporting(E_ALL); header('Content-Type: application/json; charset=UTF-8'); echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);

JSON_THROW_ON_ERROR是 PHP 7.3 引入的,配合try/catch (JsonException)能确保编码失败时你不会往外吐一个空字符串。

6. 直接把用户输入拼进 SQL

❌ 错误写法:$sql = "SELECT * FROM articles WHERE title LIKE '%{$_GET['q']}%'";

✅ 正确写法:一律用预处理语句加参数绑定。升级到新版 PHP 后,老的mysql_real_escape_string()已经没有了(mysql_*整族在 7.0 就被移除),靠手工转义维持安全的写法在新环境里根本无处落脚。

7. 改了ngram_token_size却没重建索引

❌ 错误写法:调整系统变量后继续用旧索引,搜索行为没有任何变化,还以为是 PHP 的问题。

✅ 正确写法:修改分词长度后必须重建全文索引:

ALTER TABLE articles DROP INDEX ft_article; ALTER TABLE articles ADD FULLTEXT INDEX ft_article (title, body) WITH PARSER ngram;

总结

排查层次典型症状处理方向
PHP 语言层Call to undefined function替换mysql_*、ereg、create_function等已移除 API
PDO 行为层突然抛PDOException显式设置ATTR_ERRMODE,LIMIT绑定PARAM_INT
编码层中文搜不到、乱码统一utf8mb4,用mb_convert_encoding而非utf8_encode
数据库层英文能搜、中文不能FULLTEXT ... WITH PARSER ngram,注意ngram_token_size
输出层接口 JSON 解析失败弃用警告写日志,不进响应体


搜索失效从来不是单一原因,而是"PHP 层 + 数据库层 + 编码层"叠在一起的结果。排查时按"先确认错误看得见,再分层验证"的顺序走:先用诊断脚本确认哪些函数不存在、扩展没加载,再单独用 SQL 客户端跑一遍查询语句,把 PHP 从那口锅里摘出去。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询