記一次 Laravel 專案遷移之後 Model 報錯問題

Yusure發表於2018-03-30

  之前遷移過一個 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/feedba...

    /**
     * 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

探究到這裡,我想這個問題真的明白了。

按照慣例得總結一下結尾:

  1. Model 裡面儘量指定一個 $table,有可能把握不準單詞複數的形式。
  2. composer update 之後要透過 composer.lock 檢查有版本變化的包。
  3. 英文真的很重要。
  4. 原始碼面前,了無秘密。
  5. 祝閱讀到最後的人技術再上一個 level。

附帶部落格連結

http://yusure.cn/php/221.html

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章