记一次 Laravel 项目迁移之后 Model 报错问题
之前迁移过一个 Laravel 5.3 的网站,发布完代码,composer update 之后,能正常访问,随便点了点就再没去管它,后来在后台点击反馈模块就报错,当时在 laravel.log 看到 sql 语句是表名后面没有 s,那肯定报错啊,于是徒手在那个 Model 里面指定上 $table,解决了之后,也就没去深究,后来感觉心里越来越不安,虽然不是我写的,但没去深究,就感觉有罪恶感,于是决定重现这个问题来深入研究一下。
问题现象
数据库有数据表 feedbacks
, 对应的 Model 为 Feedback.php
内部没有指定 $table.
在我本地是没有问题的,可以正确指向到 feedbacks 表,于是我从服务器上把代码打了个包,download 到本地重放,果然在本地也报错,可以断言是代码的问题。
代码是这个样子
Feedback.php
<?php
namespace App\Http\Models;
use Illuminate\Database\Eloquent\Model;
class Feedback extends Model {
protected $fillable = [];
protected $dates = [];
public static $rules = [
];
}
报错是这个样子
[2018-03-28 19:59:40] production.ERROR: PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'xxx.feedback' doesn't exist in /www/....../vendor/laravel/framework/src/Illuminate/Database/Connection.php:333
开始排查代码
Step 1. 打印表名
在调用 Feedback 模型之前打印表名出来看看,结果是 feedback
没有 s,报错是肯定的!
$feedbackObj = new Feedback();
$table = $feedbackObj->getTable();
dump( $table );
Step 2. 进入 Model.php 排查
文件路径:/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
, 跳转到 getTable
方法
从源码很容易看出,如果我在模型里面指定了 $table,会走 if 这块代码直接返回自己设置的表名,如果我没有设置 table,肯定走的下面的自动获取表名逻辑,既然锁定了问题出在自动获取表名这里,就在 return 之前依次打印结果观察。
源码如下:
/**
* Get the table associated with the model.
*
* @return string
*/
public function getTable()
{
if (isset($this->table)) {
return $this->table;
}
return str_replace('\\', '', Str::snake(Str::plural(class_basename($this))));
}
打印代码及打印结果如下:
dump( class_basename($this) ); // Feedback
dump( Str::plural( class_basename($this) ) ); // Feedback
dump( Str::snake( Str::plural( class_basename($this) ) ) ); // feedback
从打印结果来看 Str::plural( class_basename($this) )
这一行已经出现问题了
Step 3. 继续进入 Str.php
排查
文件路径:/vendor/laravel/framework/src/Illuminate/Support/Str.php
, 跳转到 plural
方法
代码很简单,获取英文单词的复数形式,用 Pluralizer
类去调用 plural
静态方法
/**
* Get the plural form of an English word.
*
* @param string $value
* @param int $count
* @return string
*/
public static function plural($value, $count = 2)
{
return Pluralizer::plural($value, $count);
}
Step 4. 继续进入 Pluralizer.php
排查
文件路径:/vendor/laravel/framework/src/Illuminate/Support/Pluralizer.php
, 跳转到 plural
方法
if 这段代码不会走,因为 $count 默认是2,feedback 这个单词没有在 $uncountable 这个数组里面出现,两个条件没有一个成立的。
继续打印 $plural
,打印结果 Feedback,这里就有问题了。
源码如下:
/**
* Get the plural form of an English word.
*
* @param string $value
* @param int $count
* @return string
*/
public static function plural($value, $count = 2)
{
if ((int) $count === 1 || static::uncountable($value)) {
return $value;
}
$plural = Inflector::pluralize($value);
return static::matchCase($plural, $value);
}
Step 5. 继续进入 Inflector.php
排查
文件路径:/vendor/doctrine/inflector/lib/Doctrine/Common/Inflector/Inflector.php
, 跳转到 pluralize
方法。
/**
* Returns a word in plural form.
*
* @param string $word The word in singular form.
*
* @return string The word in plural form.
*/
public static function pluralize(string $word) : string
{
if (isset(self::$cache['pluralize'][$word])) {
return self::$cache['pluralize'][$word];
}
if (!isset(self::$plural['merged']['irregular'])) {
self::$plural['merged']['irregular'] = self::$plural['irregular'];
}
if (!isset(self::$plural['merged']['uninflected'])) {
self::$plural['merged']['uninflected'] = array_merge(self::$plural['uninflected'], self::$uninflected);
}
if (!isset(self::$plural['cacheUninflected']) || !isset(self::$plural['cacheIrregular'])) {
self::$plural['cacheUninflected'] = '(?:' . implode('|', self::$plural['merged']['uninflected']) . ')';
self::$plural['cacheIrregular'] = '(?:' . implode('|', array_keys(self::$plural['merged']['irregular'])) . ')';
}
if (preg_match('/(.*)\\b(' . self::$plural['cacheIrregular'] . ')$/i', $word, $regs)) {
self::$cache['pluralize'][$word] = $regs[1] . $word[0] . substr(self::$plural['merged']['irregular'][strtolower($regs[2])], 1);
return self::$cache['pluralize'][$word];
}
if (preg_match('/^(' . self::$plural['cacheUninflected'] . ')$/i', $word, $regs)) {
self::$cache['pluralize'][$word] = $word;
return $word;
}
foreach (self::$plural['rules'] as $rule => $replacement) {
if (preg_match($rule, $word)) {
self::$cache['pluralize'][$word] = preg_replace($rule, $replacement, $word);
return self::$cache['pluralize'][$word];
}
}
}
这个 function 里面 if 判断很多,通过打印锁定在这一行
if (preg_match('/^(' . self::$plural['cacheUninflected'] . ')$/i', $word, $regs))
self::$plural['cacheUninflected']
打印结果里面发现了 feedback 这个单词,原因就是在这里了,但这个单词是怎么来的呢?
"(?:.*[nrlm]ese|.*deer|.*fish|.*measles|.*ois|.*pox|.*sheep|people|cookie|police|.*?media|Amoyese|audio|bison|Borghese|bream|breeches|britches|buffalo|cantus|carp|chassis|clippers|cod|coitus|compensation|Congoese|contretemps|coreopsis|corps|data|debris|deer|diabetes|djinn|education|eland|elk|emoji|equipment|evidence|Faroese|feedback|fish|flounder|Foochowese|Furniture|furniture|gallows|Genevese|Genoese|Gilbertese|gold|headquarters|herpes|hijinks|Hottentotese|information|innings|jackanapes|jedi|Kiplingese|knowledge|Kongoese|love|Lucchese|Luggage|mackerel|Maltese|metadata|mews|moose|mumps|Nankingese|news|nexus|Niasese|nutrition|offspring|Pekingese|Piedmontese|pincers|Pistoiese|plankton|pliers|pokemon|police|Portuguese|proceedings|rabies|rain|rhinoceros|rice|salmon|Sarawakese|scissors|sea[- ]bass|series|Shavese|shears|sheep|siemens|species|staff|swine|traffic|trousers|trout|tuna|us|Vermontese|Wenchowese|wheat|whiting|wildebeest|Yengeese)"
顺着往上找发现在 第三个 if 判断的时候执行了这一行代码,self::$uninflected
这个是关键,马上查找这个变量。
if (!isset(self::$plural['merged']['uninflected'])) {
self::$plural['merged']['uninflected'] = array_merge(self::$plural['uninflected'], self::$uninflected);
}
在 :223 行找到了这个变量的所有值,这个变量的意思是复数是单词原形,不受影响,What?feedback 复数不加 s?顺手查了一下,百度词典,金山词霸很明确的说复数加s,有道词典没有说明,只显示 feedbacks 是名词回馈的意思,通过查了一些资料还是推荐 feedback 为复数形式。
参考资料链接:
http://www.learnenglishwithwill.com/feedback-vs-feedbacks-plural-form/
/**
* Words that should not be inflected.
*
* @var array
*/
private static $uninflected = array(
'.*?media', 'Amoyese', 'audio', 'bison', 'Borghese', 'bream', 'breeches',
'britches', 'buffalo', 'cantus', 'carp', 'chassis', 'clippers', 'cod', 'coitus', 'compensation', 'Congoese',
'contretemps', 'coreopsis', 'corps', 'data', 'debris', 'deer', 'diabetes', 'djinn', 'education', 'eland',
'elk', 'emoji', 'equipment', 'evidence', 'Faroese', 'feedback', 'fish', 'flounder', 'Foochowese',
'Furniture', 'furniture', 'gallows', 'Genevese', 'Genoese', 'Gilbertese', 'gold',
'headquarters', 'herpes', 'hijinks', 'Hottentotese', 'information', 'innings', 'jackanapes', 'jedi',
'Kiplingese', 'knowledge', 'Kongoese', 'love', 'Lucchese', 'Luggage', 'mackerel', 'Maltese', 'metadata',
'mews', 'moose', 'mumps', 'Nankingese', 'news', 'nexus', 'Niasese', 'nutrition', 'offspring',
'Pekingese', 'Piedmontese', 'pincers', 'Pistoiese', 'plankton', 'pliers', 'pokemon', 'police', 'Portuguese',
'proceedings', 'rabies', 'rain', 'rhinoceros', 'rice', 'salmon', 'Sarawakese', 'scissors', 'sea[- ]bass',
'series', 'Shavese', 'shears', 'sheep', 'siemens', 'species', 'staff', 'swine', 'traffic',
'trousers', 'trout', 'tuna', 'us', 'Vermontese', 'Wenchowese', 'wheat', 'whiting', 'wildebeest', 'Yengeese'
);
代码找到这里,这个问题就已经明白了,是因为 update 了 doctrine/inflector
这个包导致的。
Step 6. 继续深究
于是重开一个目录,pull 一下这几个版本发现 1.3.0 开始发生了变化,加入了 feedback 没有复数形式。
composer require doctrine/inflector 1.2.0
composer require doctrine/inflector 1.3.0
继续开新目录 Clone 源代码分析:
git clone https://github.com/doctrine/inflector.git
查看 git log 看到了这个注释信息 Added more uninflected words
探究到这里,我想这个问题真的明白了。
按照惯例得总结一下结尾:
- Model 里面尽量指定一个 $table,有可能把握不准单词复数的形式。
- composer update 之后要通过 composer.lock 检查有版本变化的包。
- 英文真的很重要。
- 源码面前,了无秘密。
- 祝阅读到最后的人技术再上一个 level。