之前遷移過一個 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。