說明:本文主要聊一聊Laravel測試數據填充器Seeder的小技巧,同時介紹下Laravel開發插件三件套
,這三個插件挺好用哦。同時,會將開發過程當中的一些截圖和代碼黏上去,提升閱讀效率。php
備註:在設計我的博客軟件時,總會碰到有分類Category、博客Post、給博客貼的標籤Tag、博客內容的評論Comment。
並且,Category與Post是一對多關係One-Many
:一個分類下有不少Post,一個Post只能歸屬於一個Category;Post與Comment是一對多關係One-Many
:一篇博客Post下有不少Comment,一條Comment只能歸屬於一篇Post;Post與Tag是多對多關係Many-Many
:一篇Post有不少Tag,一個Tag下有不少Post。
開發環境:Laravel5.2 + MAMP + PHP7 + MySQL5.5
laravel
在先聊測試數據填充器seeder以前,先裝上開發插件三件套,開發神器。先無論這能幹些啥,裝上再說。
一、barryvdh/laravel-debugbar
數據庫
composer require barryvdh/laravel-debugbar --dev
二、barryvdh/laravel-ide-helper
app
composer require barryvdh/laravel-ide-helper --dev
三、mpociot/laravel-test-factory-helper
composer
composer require mpociot/laravel-test-factory-helper --dev
而後在config/app.php文件中填上:dom
/** *Develop Plugin */ Barryvdh\Debugbar\ServiceProvider::class, Mpociot\LaravelTestFactoryHelper\TestFactoryHelperServiceProvider::class, Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
按照上文提到的Category、Post、Comment和Tag之間的關係建立遷移Migration和模型Model,在項目根目錄輸入:ide
php artisan make:model Category -m php artisan make:model Post -m php artisan make:model Comment -m php artisan make:model Tag -m
在各個表的遷移migrations文件中根據表的功能設計字段:post
//Category表 class CreateCategoriesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('categories', function (Blueprint $table) { $table->increments('id'); $table->string('name')->comment('分類名稱'); $table->integer('hot')->comment('分類熱度'); $table->string('image')->comment('分類圖片'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('categories'); } } //Post表 class CreatePostsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->integer('category_id')->unsigned()->comment('外鍵'); $table->string('title')->comment('標題'); $table->string('slug')->unique()->index()->comment('錨點'); $table->string('summary')->comment('概要'); $table->text('content')->comment('內容'); $table->text('origin')->comment('文章來源'); $table->integer('comment_count')->unsigned()->comment('評論次數'); $table->integer('view_count')->unsigned()->comment('瀏覽次數'); $table->integer('favorite_count')->unsigned()->comment('點贊次數'); $table->boolean('published')->comment('文章是否發佈'); $table->timestamps(); //Post表中category_id字段做爲外鍵,與Category一對多關係 $table->foreign('category_id') ->references('id') ->on('categories') ->onUpdate('cascade') ->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { //刪除表時要刪除外鍵約束,參數爲外鍵名稱 Schema::table('posts', function(Blueprint $tabel){ $tabel->dropForeign('posts_category_id_foreign'); }); Schema::drop('posts'); } } //Comment表 class CreateCommentsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('comments', function (Blueprint $table) { $table->increments('id'); $table->integer('post_id')->unsigned()->comment('外鍵'); $table->integer('parent_id')->comment('父評論id'); $table->string('parent_name')->comment('父評論標題'); $table->string('username')->comment('評論者用戶名'); $table->string('email')->comment('評論者郵箱'); $table->string('blog')->comment('評論者博客地址'); $table->text('content')->comment('評論內容'); $table->timestamps(); //Comment表中post_id字段做爲外鍵,與Post一對多關係 $table->foreign('post_id') ->references('id') ->on('posts') ->onUpdate('cascade') ->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { //刪除表時要刪除外鍵約束,參數爲外鍵名稱 Schema::table('comments', function(Blueprint $tabel){ $tabel->dropForeign('comments_post_id_foreign'); }); Schema::drop('comments'); } } //Tag表 class CreateTagsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('tags', function (Blueprint $table) { $table->increments('id'); $table->string('name')->comment('標籤名稱'); $table->integer('hot')->unsigned()->comment('標籤熱度'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('tags'); } }
因爲Post表與Tag表是多對多關係,還須要一張存放二者關係的表:測試
//多對多關係,中間表的命名laravel默認按照兩張表字母排序來的,寫成tag_post會找不到中間表 php artisan make:migration create_post_tag_table --create=post_tag
而後填上中間表的字段:ui
class CreatePostTagTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('post_tag', function (Blueprint $table) { $table->increments('id'); $table->integer('post_id')->unsigned(); $table->integer('tag_id')->unsigned(); $table->timestamps(); //post_id字段做爲外鍵 $table->foreign('post_id') ->references('id') ->on('posts') ->onUpdate('cascade') ->onDelete('cascade'); //tag_id字段做爲外鍵 $table->foreign('tag_id') ->references('id') ->on('tags') ->onUpdate('cascade') ->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('post_tag', function(Blueprint $tabel){ $tabel->dropForeign('post_tag_post_id_foreign'); $tabel->dropForeign('post_tag_tag_id_foreign'); }); Schema::drop('post_tag'); } }
寫上Migration後,還得在Model裏寫上關聯:
class Category extends Model { //Category-Post:One-Many public function posts() { return $this->hasMany(Post::class); } } class Post extends Model { //Post-Category:Many-One public function category() { return $this->belongsTo(Category::class); } //Post-Comment:One-Many public function comments() { return $this->hasMany(Comment::class); } //Post-Tag:Many-Many public function tags() { return $this->belongsToMany(Tag::class)->withTimestamps(); } } class Comment extends Model { //Comment-Post:Many-One public function post() { return $this->belongsTo(Post::class); } } class Tag extends Model { //Tag-Post:Many-Many public function posts() { return $this->belongsToMany(Post::class)->withTimestamps(); } }
而後執行遷移:
php artisan migrate
數據庫中會生成新建表,表的關係以下:
好,在聊到seeder測試數據填充以前,看下開發插件三件套
能幹些啥,下文中命令可在項目根目錄輸入php artisan
指令列表中查看。
一、barryvdh/laravel-ide-helper
執行php artisan ide-helper:generate
指令前:
執行php artisan ide-helper:generate
指令後:
不只Facade模式的Route由以前的反白了變爲能夠定位到源碼了,並且輸入Config Facade時還方法自動補全auto complete,這個很方便啊。
輸入指令php artisan ide-helper:models
後,看看各個Model,如Post這個Model:
<?php namespace App; use Illuminate\Database\Eloquent\Model; /** * App\Post * * @property integer $id * @property integer $category_id 外鍵 * @property string $title 標題 * @property string $slug 錨點 * @property string $summary 概要 * @property string $content 內容 * @property string $origin 文章來源 * @property integer $comment_count 評論次數 * @property integer $view_count 瀏覽次數 * @property integer $favorite_count 點贊次數 * @property boolean $published 文章是否發佈 * @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $updated_at * @property-read \App\Category $category * @property-read \Illuminate\Database\Eloquent\Collection|\App\Comment[] $comments * @property-read \Illuminate\Database\Eloquent\Collection|\App\Tag[] $tags * @method static \Illuminate\Database\Query\Builder|\App\Post whereId($value) * @method static \Illuminate\Database\Query\Builder|\App\Post whereCategoryId($value) * @method static \Illuminate\Database\Query\Builder|\App\Post whereTitle($value) * @method static \Illuminate\Database\Query\Builder|\App\Post whereSlug($value) * @method static \Illuminate\Database\Query\Builder|\App\Post whereSummary($value) * @method static \Illuminate\Database\Query\Builder|\App\Post whereContent($value) * @method static \Illuminate\Database\Query\Builder|\App\Post whereOrigin($value) * @method static \Illuminate\Database\Query\Builder|\App\Post whereCommentCount($value) * @method static \Illuminate\Database\Query\Builder|\App\Post whereViewCount($value) * @method static \Illuminate\Database\Query\Builder|\App\Post whereFavoriteCount($value) * @method static \Illuminate\Database\Query\Builder|\App\Post wherePublished($value) * @method static \Illuminate\Database\Query\Builder|\App\Post whereCreatedAt($value) * @method static \Illuminate\Database\Query\Builder|\App\Post whereUpdatedAt($value) * @mixin \Eloquent */ class Post extends Model { //Post-Category:Many-One public function category() { return $this->belongsTo(Category::class); } //Post-Comment:One-Many public function comments() { return $this->hasMany(Comment::class); } //Post-Tag:Many-Many public function tags() { return $this->belongsToMany(Tag::class)->withTimestamps(); } }
根據遷移到庫裏的表生成字段屬性和對應的方法提示,在控制器裏輸入方法時會自動補全auto complete字段屬性的方法:
二、mpociot/laravel-test-factory-helper
輸入指令php artisan test-factory-helper:generate
後,database/factory/ModelFactory.php模型工廠文件會自動生成各個模型對應字段數據。Faker是一個好用的生成假數據的第三方庫,而這個開發插件會自動幫你生成這些屬性,不用本身寫了。
<?php /* |-------------------------------------------------------------------------- | Model Factories |-------------------------------------------------------------------------- | | Here you may define all of your model factories. Model factories give | you a convenient way to create models for testing and seeding your | database. Just tell the factory how a default model should look. | */ $factory->define(App\User::class, function (Faker\Generator $faker) { return [ 'name' => $faker->name, 'email' => $faker->safeEmail, 'password' => bcrypt(str_random(10)), 'remember_token' => str_random(10), ]; }); $factory->define(App\Category::class, function (Faker\Generator $faker) { return [ 'name' => $faker->name , 'hot' => $faker->randomNumber() , 'image' => $faker->word , ]; }); $factory->define(App\Comment::class, function (Faker\Generator $faker) { return [ 'post_id' => function () { return factory(App\Post::class)->create()->id; } , 'parent_id' => $faker->randomNumber() , 'parent_name' => $faker->word , 'username' => $faker->userName , 'email' => $faker->safeEmail , 'blog' => $faker->word , 'content' => $faker->text , ]; }); $factory->define(App\Post::class, function (Faker\Generator $faker) { return [ 'category_id' => function () { return factory(App\Category::class)->create()->id; } , 'title' => $faker->word , 'slug' => $faker->slug ,//修改成slug 'summary' => $faker->word , 'content' => $faker->text , 'origin' => $faker->text , 'comment_count' => $faker->randomNumber() , 'view_count' => $faker->randomNumber() , 'favorite_count' => $faker->randomNumber() , 'published' => $faker->boolean , ]; }); $factory->define(App\Tag::class, function (Faker\Generator $faker) { return [ 'name' => $faker->name , 'hot' => $faker->randomNumber() , ]; });
在聊第三個debugbar插件前先聊下seeder小技巧,用debugbar來幫助查看。Laravel官方推薦使用模型工廠自動生成測試數據,推薦這麼寫的:
//先輸入指令生成database/seeds/CategoryTableSeeder.php文件: php artisan make:seeder CategoryTableSeeder <?php use Illuminate\Database\Seeder; class CategoryTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { factory(\App\Category::class, 5)->create()->each(function($category){ $category->posts()->save(factory(\App\Post::class)->make()); }); } } //而後php artisan db:seed執行數據填充
可是這種方式效率並不高,由於每一次create()都是一次query,並且每生成一個Category也就對應生成一個Post,固然能夠在each()裏每一次Category繼續foreach()生成幾個Post,但每一次foreach也是一次query,效率更差。能夠用debugbar小能手看看。先在DatabaseSeeder.php文件中填上此次要填充的Seeder:
public function run() { // $this->call(UsersTableSeeder::class); $this->call(CategoryTableSeeder::class); }
在路由文件中寫上:
Route::get('/artisan', function () { $exitCode = Artisan::call('db:seed'); return $exitCode; });
輸入路由/artisan
後用debugbar查看執行了15次query,耗時7.11ms:
實際上纔剛剛輸入幾個數據呢,Category插入了10個,Post插入了5個。
能夠用DB::table()->insert()批量插入,拷貝ModelFactory.php中表的字段定義放入每個表對應Seeder,固然能夠有些字段爲便利也適當修改對應假數據。
class CategoryTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { // factory(\App\Category::class, 20)->create()->each(function($category){ // $category->posts()->save(factory(\App\Post::class)->make()); // }); $faker = Faker\Factory::create(); $datas = []; foreach (range(1, 10) as $key => $value) { $datas[] = [ 'name' => 'category'.$faker->randomNumber() , 'hot' => $faker->randomNumber() , 'image' => $faker->url , 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() ]; } DB::table('categories')->insert($datas); } } class PostTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $faker = Faker\Factory::create(); $category_ids = \App\Category::lists('id')->toArray(); $datas = []; foreach (range(1, 10) as $key => $value) { $datas[] = [ 'category_id' => $faker->randomElement($category_ids), 'title' => $faker->word , 'slug' => $faker->slug , 'summary' => $faker->word , 'content' => $faker->text , 'origin' => $faker->text , 'comment_count' => $faker->randomNumber() , 'view_count' => $faker->randomNumber() , 'favorite_count' => $faker->randomNumber() , 'published' => $faker->boolean , 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() ]; } DB::table('posts')->insert($datas); } } class CommentTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $faker = Faker\Factory::create(); $post_ids = \App\Post::lists('id')->toArray(); $datas = []; foreach (range(1, 50) as $key => $value) { $datas[] = [ 'post_id' => $faker->randomElement($post_ids), 'parent_id' => $faker->randomNumber() , 'parent_name' => $faker->word , 'username' => $faker->userName , 'email' => $faker->safeEmail , 'blog' => $faker->word , 'content' => $faker->text , 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() ]; } DB::table('comments')->insert($datas); } } class TagTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $faker = Faker\Factory::create(); $datas = []; foreach (range(1, 10) as $key => $value) { $datas[] = [ 'name' => 'tag'.$faker->randomNumber() , 'hot' => $faker->randomNumber() , 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() ]; } DB::table('tags')->insert($datas); } } class PostTagTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $faker = Faker\Factory::create(); $post_ids = \App\Post::lists('id')->toArray(); $tag_ids = \App\Tag::lists('id')->toArray(); $datas = []; foreach (range(1, 20) as $key => $value) { $datas[] = [ 'post_id' => $faker->randomElement($post_ids) , 'tag_id' => $faker->randomElement($tag_ids) , 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() ]; } DB::table('post_tag')->insert($datas); } }
在DatabaseSeeder.php中按照順序依次填上Seeder,順序不能顛倒,尤爲有關聯關係的表:
class DatabaseSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { // $this->call(UsersTableSeeder::class); $this->call(CategoryTableSeeder::class); $this->call(PostTableSeeder::class); $this->call(CommentTableSeeder::class); $this->call(TagTableSeeder::class); $this->call(PostTagTableSeeder::class); } }
輸入路由/artisan
後,生成了10個Category、10個Post、50個Comments、10個Tag和PostTag表中多對多關係,共有9個Query耗時13.52ms:
It is working!!!
表的遷移Migration和關聯Relationship都已設計好,測試數據也已經Seeder好了,就能夠根據Repository模式來設計一些數據庫邏輯了。準備趁着端午節研究下Repository模式的測試,PHPUnit結合Mockery包來TDD測試也是一種不錯的玩法。M(Model)-V(View)-C(Controller)模式去組織代碼,不少時候也未必指導性很強,給Model加一個Repository,給Controller加一個Service,給View加一個Presenter,或許代碼結構更清晰。具體可看下面分享的一篇文章。
最近一直在給本身充電,研究MySQL,PHPUnit,Laravel,上班並按時打卡,看博客文章,天天喝紅牛。不少不會,有些以前沒咋學過,哎,頭疼。後悔之前讀書太少,書到用時方恨少,人醜還需多讀書。
分享下最近發現的一張好圖和一篇極讚的文章:
文章連接:Laravel的中大型專案架構