Miles' Blog

天涯何處無幹話,何必要講實務話

Faker 提供的 Provider 非常多,除了預設之外,還有不同語系實作。

不過我們先來解決昨天的疑惑:這些 Provider 到底是如何使用 Generator 的?

Magic 的中介層設計

搜尋了一下,會發現 Provider 大部分會使用 Generatorparse() 方法,而 Day 7 有提到,它的本質是 format()。換句話說,Provider 會經由 Generator 來存取其他 Provider。

這個設計有點類似 Mediator Pattern,它們的關係如下:

@startuml
Class Client
Class Base {
# generator: Generator
}
Class Provider1
Class Provider2
Class Generator
Client -> Generator
Base <|-- Provider1
Base <|-- Provider2
Generator <- Base : Midiator
Generator -> Provider1
Generator ---> Provider2
@enduml

Provider 要使用 Generator 當 Mediator 時,必須小心循環呼叫的問題,比方說 A Provider 呼叫 B Provider,而 B Provider 又要呼叫 A Provider。

基礎是非常重要的

Day 8 提到 Provider\Base 類別提供非常多基本亂數取樣方法,今天就派得上用場了!

Provider 最常用到的肯定是 randomElement(),不同領域的 Provider 通常都會有自己的口袋名單,要從口袋名單裡隨便選一個,當然就是用它。numberBetween() 也是個常用到方法,因為不同領域的 Provider 值域都不大一樣。

如果有仔細觀察,會發現 Provider 非常多方法都有用到 Provider\Base 的亂數取樣方法。

語系擴充

Provider 語系擴充的設計是,繼承的時候覆寫對應的口袋名單、樣版或是產生的方法即可。

比方說 Provider\zh_TW\Person 類別,它繼承自 Provider\Person,覆寫 $maleNameFormats$femaleNameFormats 樣版,因為台灣名字的顯示慣例先姓後名:

protected static $maleNameFormats = array(
'{{lastName}}{{firstNameMale}}',
);

protected static $femaleNameFormats = array(
'{{lastName}}{{firstNameFemale}}',
);

口袋名單 $lastName$characterMale$characterFemale 等,當然也會覆寫:

protected static $lastName = array(
// ...
);

protected static $characterMale = array(
// ...
);

protected static $characterFemale = array(
// ...
);

產生名字的方法也覆寫了,因為台灣大部分是一個姓配兩個名:

public static function firstNameMale()
{
return static::randomName(static::$characterMale, mt_rand(1, 2));
}

public static function firstNameFemale()
{
return static::randomName(static::$characterFemale, mt_rand(1, 2));
}

Provider]\Personname() 方法並沒有被覆寫,原始碼如下:

public function name($gender = null)
{
if ($gender === static::GENDER_MALE) {
$format = static::randomElement(static::$maleNameFormats);
} elseif ($gender === static::GENDER_FEMALE) {
$format = static::randomElement(static::$femaleNameFormats);
} else {
$format = static::randomElement(array_merge(static::$maleNameFormats, static::$femaleNameFormats));
}

return $this->generator->parse($format);
}

因此,當我們使用 zh_TW 語系,在呼叫 $generator->name 的時候,事實上它會先跑 Provider\Personname() 方法,然後使用 $generator->parse() 把「覆寫的樣板」放進去,接著再把「覆寫的口袋名單」與呼叫「覆寫產生名的方法」,最後組合出回傳結果。

這種覆寫的方法也是 Laravel 常見擴充寫法。

了解它的設計後,後面要擴充自己的假資料清單就會非常容易了。


Faker 主框架差不多介紹完了,明天來試試自定義 Provider。

參考資料

重構的過程中,最重要卻也最麻煩的流程,就是驗證。我們必須確保重構的過程不會把原本的功能改壞,只能靠不斷的測試,驗證功能沒壞,才能繼續下一步。

閱讀全文 »

如果有認真看前兩天的文章,應該會發現一個很奇怪的事:

  • Day 7 提到:Factory 產生 Generator 物件時,會使用 addProvider()Provider 加入 Generator
  • Day 8 提到:Provider\Base 建構子的依賴是 Generator

這造成了一個循環引用的關係

@startuml
Class Provider\Base
Class Generator
Generator <- Provider\Base
Generator -> Provider\Base
@enduml

通常這樣的關係是不利於維護的,容易修改一個地方,而讓很多地方同時受影響。不過我們還是先一起來看看它到底在做什麼吧!

關於 Generator 使用 Provider 的部分,Day 7 已經有說明,不再贅述。

Provider\Base 如何使用 Generator

打開 IDE 搜尋一下,會發現 Provider\Base 有 3 個方法會用到 Generator

  • optional()
  • unique()
  • valid()

與其他產生假資料的方法不同的是:這三個方法會回傳 Generator 物件或是另外三種 Generator 物件:

先來了解這三個方法在做什麼:

optional()

optional() 方法需要給一個權重值(百分比)與一個預設值(default),它會依權重值來隨機決定要回傳 DefaultGenerator 還是正常的 Generator

先來看個簡單的範例:

$generator = Faker\Factory::create('en_US');

echo $generator->optional(0, '預設值')->name . PHP_EOL;
echo $generator->optional(50, '預設值')->name . PHP_EOL;
echo $generator->optional(100, '預設值')->name . PHP_EOL;

輸出效果:

預設值
預設值
Prof. Raquel Stokes III

其中權重 0% 指的是一定會回傳預設值,而 100% 則會回傳 Generator,因此有產生隨機的名字,中間的機率則各是 50%。

DefaultGenerator 的設計是:先把預設值存下來之後,在由 __get()__call() 回傳回去。這樣使用回傳的 Generator 時,我們也不會發現它是 Generator 還是 DefaultGenerator,蠻有趣的。

unique()

連續產生一百組假資料,有可能會出現一樣的資料。有時會不允許資料裡有重覆的值,比方說資料庫欄位被設定成 UNIQUE,如果 insert 遇到重覆的值就會發生錯誤。

unique() 能解決這個問題,它會回傳 UniqueGenerator。同一個 UniqueGenerator 產生的假資料,可以確保每次都會不一樣。

它的設計是 Generator 任務不變,而 UniqueGenerator 的任務是記錄已產生過的資料,如有重覆,會再重新跟 Generator 要資料。

UniqueGenerator 呼叫 Generator方式如下:

public function __get($attribute)
{
return $this->__call($attribute, array());
}

public function __call($name, $arguments)
{
// ... 略

$res = call_user_func_array(array($this->generator, $name), $arguments);

// ... 略
}

一樣舉 name 屬性與 name() 方法為例,從上面 UniqueGenerator 的程式可以得知,最後呼叫 Generator 的結果如下:

$generator = Faker\Factory::create('en_US');
$uniqueGenerator = new \Faker\UniqueGenerator($generator);

echo $uniqueGenerator->name . PHP_EOL;
// call_user_func_array(array($this->generator, 'name'), []);

echo $uniqueGenerator->name('male') . PHP_EOL;
// call_user_func_array(array($this->generator, 'name'), ['male']);

這裡先提示一下:$generator->name$generator->name() 的結果是一樣的。Day 7 介紹 Generator 曾有翻過原始碼,知道 __get() 最終跟 __call() 一樣,是呼叫 format()

因此這個結果可以得知:雖然包裝有做處理,但 UniqueGenerator 的介面接到 Generator 的介面是一模一樣的。這正是標準的 Proxy Pattern

@startuml
Class Client
Interface MagicMethod {
+ __get()
+ __call()
}
Class Generator
Class UniqueGenerator
Client .> MagicMethod
MagicMethod <|-- Generator
MagicMethod <|-- UniqueGenerator
UniqueGenerator -> Generator
@enduml

如果昨天有翻程式碼的話應該也會發現另一件事:Provider 提供的方法如果有參數,幾乎都會有預設值,即使是看起來沒用處的 [a, b, c],目前也只有發現,還不清楚目的。

valid()

如果需要產出名字為 M 開頭的假資料的話,那麼使用 valid() 會是最好的選擇!

UniqueGenerator 在驗證假資料不重覆很類似,而 valid() 的驗證方法使用 Closure 自定義方法,做法可以更為廣泛。

也因為很類似,所以設計也很像 UniqueGenerator,一樣透過 __get()__call() 轉接到 Generator,所以也是使用 Proxy Pattern


Provider\Base 本身並沒有使用到 Generator 的功能,只是單純的把它包裝後再回傳,這樣比較不容易有改 A 壞 B 的情況發生。

事實上,用最頻繁的是其他的 Provider。我們明天再來看看這些 Provider 的細節吧!

參考資料

文字清單如果都寫死在程式裡的話,擴充性就太差了,預期它應該要可以從檔案抓出文字清單。

閱讀全文 »

昨天有提到 GeneratoraddProvider() 方法,可以把各式各樣的 Provider 加入 Generator。而也有提到 Generator 的屬性或方法,會轉接到 Provider 提供的方法。

雖然沒有硬性規定,不過它的 Provider 都有繼承自 Provider\Base 的基礎類別,這個類別定義了很多產生假資料的基本方法,讓我們一起來看看。

Random

基本隨機取樣的方法

$generator = new Faker\Generator();
$base = new Base($generator);

echo $base->randomDigit() . "\n";
echo $base->randomNumber() . "\n";
echo $base->randomFloat() . "\n";
echo $base->randomLetter() . "\n";
echo $base->randomElement() . "\n"; // 預設 array 為 [a, b, c]

輸出效果如下:

2
49902
14820.4454
d
c

這些是最基本的隨機取樣方法,後面提到的其他 Provider 會依賴這些方法來取得隨機樣本。

Shuffle

基本的洗亂方法:

$generator = new Faker\Generator();
$base = new Base($generator);

$shuffledArray = $base->shuffleArray(['s', 't', 'r', 'i', 'n', 'g', ]);
$shuffledString = $base->shuffleString('string');

var_export($shuffledArray);
echo PHP_EOL;
var_export($shuffledString);

輸出效果如下:

array (
0 => 't',
1 => 'n',
2 => 'g',
3 => 'r',
4 => 's',
5 => 'i',
)
'rinstg'

特殊符號轉化成亂數

特別的是,它有一些方法,可以把字串樣版裡的 wildcard 使用亂數取代,如:

$generator = new Faker\Generator();
$base = new Base($generator);

$template = "取代範例:### %%% ??? ***\n";

echo $base->numerify($template);
echo $base->lexify($template);
echo $base->asciify($template);
echo $base->bothify($template);

輸出效果如下:

取代範例:833 554 ??? ***
取代範例:### %%% rgv ***
取代範例:### %%% ??? p@u
取代範例:420 641 xju 26z

規則是:

  • # 會用 randomDigit() 的結果取代
  • % 會用 randomDigitNot() 的結果取代
  • ? 會用 randomLetter() 的結果取代
  • * 會用 randomAscii() 的結果取代

不同的方法會取代不同的 wildcard,這是為了保留在不同場合使用適合方法的彈性。

  • numerify() 只會取代 #%
  • lexify() 只會取代 ?
  • asciify() 只會取代 *
  • bothify() 會全取代

正則亂數產生器

這個產生器很有趣,通常我們會使用正則來檢驗 Email 是否是正常的,如:

preg_match('/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}/', '[email protected]')

regexify() 這個方法是反過來,用正則來產生符合規則的假資料:

$regex = '/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}/';

echo $base->regexify($regex) . PHP_EOL;
echo $base->regexify($regex) . PHP_EOL;
echo $base->regexify($regex) . PHP_EOL;

輸出效果:

d%363@m.ncqs
92@q.rjg
[email protected]

注意:官方 doc block 有提醒,這個方法會「非常地」慢。


今天先介紹 Provider 基本產假資料的方法,未來介紹其他 Provider 都會使用到這些方法來產生更多樣化的假資料。

0%