PHP版本不兼容导致搜索功能失效怎么办
前言网站升级了 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或mysqliereg()/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_types1); // 从 GBK 页面表单收到的搜索词转成 UTF-8 再进数据库 $keyword mb_convert_encoding($rawKeyword, UTF-8, GBK); // 判断转换是否真的成功非法字节会被替换成 UFFFD 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 和部分生僻汉字根本存不进去会被截断或替换成?。中文搜索要用utf8mb4ALTER 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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_types1); /** * 搜索服务优先走全文索引失败则降级为 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:host127.0.0.1;dbnametest;charsetutf8mb4, 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_errorsOn弃用警告被打印在{之前前端JSON.parse直接失败。✅ 正确写法接口入口先把警告导向日志不要让它进响应体?php ini_set(display_errors, 0); ini_set(log_errors, 1); error_reporting(E_ALL); header(Content-Type: application/json; charsetUTF-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等已移除 APIPDO 行为层突然抛PDOException显式设置ATTR_ERRMODELIMIT绑定PARAM_INT编码层中文搜不到、乱码统一utf8mb4用mb_convert_encoding而非utf8_encode数据库层英文能搜、中文不能FULLTEXT ... WITH PARSER ngram注意ngram_token_size输出层接口 JSON 解析失败弃用警告写日志不进响应体搜索失效从来不是单一原因而是PHP 层 数据库层 编码层叠在一起的结果。排查时按先确认错误看得见再分层验证的顺序走先用诊断脚本确认哪些函数不存在、扩展没加载再单独用 SQL 客户端跑一遍查询语句把 PHP 从那口锅里摘出去。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →