この記事は、オールアバウト Advent Calendar 2016の21日目の記事です。
オールアバウトでエンジニアをしている@pakkunです。
LaravelでCMSとフロントでアプリケーションを分ける際に、Modelをパッケージ化したいなんていう悩みを持ったことはありませんか?
というわけで、オールアバウトでは、Modelをパッケージ化して運用している場合があるので、どんな感じで運用しているのかざっくりまとめます。
※Laravel5.3前提で書きますので、バージョン違いでできないこともあるかも?
Modelのパッケージ化
Gitにパッケージ用のリポジトリを作る
GitHubでもBitbucketでも、なんでも良いのでリポジトリを作りましょう!
【パッケージ】パッケージの構造を作る
良い感じに作りましょう!
vendor
└ all-about/
└ model-package
├ config
├ database
│ ├ factories
│ ├ migrations
│ └ seeds
├ src
│ ├ Models
│ │ └ Eloquent // ここらへんにModelを配置すると良さげな気がする
│ │ ├ User
│ │ └ Content
│ └ Providers
├ tests
├ composer.json
├ gulpfile.js
├ package.json
├ phpcs.xml
├ phpunit.xml
└ readme.md
【パッケージ】Composer.jsonを定義する
良い感じに定義しましょう!
{
"name": "all-about/model-package",
"version": "0.0.1",
"description": "",
"keywords": [],
"license": "",
"type": "package",
"require": {
"php": ">=5.6.4",
"laravel/framework": "5.3.*"
},
"autoload": {
"psr-4": {
"AllAbout\\ModelPackage\\": "src/",
"AllAbout\\ModelPackage\\Database\\": "database/"
}
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
},
"config": {
"preferred-install": "dist"
},
"minimum-stability": "stable"
}
【CMS or フロント】Composer.jsonに定義する
良い感じに定義しましょう!
業務で使うならプライベートリポジトリだと思うので、下記のような感じになりそうですね!
{
"repositories": [
{
"type": "vcs",
"url": "https://example.com/all-about/model-package"
}
],
"require": {
"all-about/model-package": "dev-master"
}
}
【CMS or フロント】Modelを呼び出す
呼び出しましょう!
use AllAbout\ModelPackage\Models\Eloquent\User;
以上で、Modelのパッケージ化が完了しました。
ついでなので、テーブル定義もパッケージ内で管理してみましょう!
テーブル定義
artisanコマンドによるマイグレーションでは、make:migration、migrate、migrate:refreshで、パス指定ができます。
【CMS・フロント】テーブル定義
CMS・フロントのアプリケーションのルートディレクトリで下記artisanコマンドを実行して、パッケージ内にマイグレーションファイルを作成しましょう!
php artisan make:migration create_contents_table --path=vendor/all-about/model-package/database/migrations/
【CMS・フロント】マイグレーション実行
作成されたファイルにテーブル定義を行ったら、パスを指定してマイグレーションを実行しましょう!
初期開発だとテーブル構造が何回も変わったりするので、潔くリフレッシュ(削除&実行)しましょう。
開発で本番DBに向けるなんていう人はいないよね??
php artisan migrate:refresh --path=vendor/all-about/model-package/database/migrations/
以上で、データベースマイグレーションが完了です。
CMS・フロントのアプリケーションからマイグレーションし放題ですね!
artisanコマンドは、readme.mdに書いておくと使いやすいです。
ただ、これだけだとパッケージ内のテストができないので、設定していきます。
パッケージのテスト定義
【パッケージ】composer.jsonにテスト関連のパッケージを追加する
テストを書くなら、とりあえず、下記パッケージをインストールしましょう!
"require-dev": {
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~5.0",
"orchestra/testbench": "~3.3"
}
【パッケージ】phpunit.xmlの設定
よしなに設定しましょう!
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
</php>
</phpunit>
【パッケージ】orchestra/testbenchでテストのベースクラスの作成
Laravelパッケージ用のテストサポートパッケージ「orchestra/testbench」をインストールすることにより、Laravelに依存した機能が担保されます。
testsディレクトリにorchestra/testbenchベースのTestCase.phpを作成しましょう!
必要最低限なら、継承元のクラスで設定済みなので、下記のようなクラス定義のファイルだけを作ればOK!
<?php
class TestCase extends \Orchestra\Testbench\TestCase
{
//
}
【パッケージ】テストを書く
下記testsディレクトリで、orchestra/testbenchで作成したベースクラスを継承してテストを書きまくろう!
<?php
class HogeTest extends TestCase
{
public function testHoge()
{
$this->assertTrue(true);
}
}
【パッケージ】テストを実行
下記コマンドを実行して、動けばOK!
cd vendor/all-about/model-package
./vendor/bin/phpunit
以上で、パッケージ内のテストの設定が完了です。
でも、手動でテストを回すのはつらいですよね?自動化しましょう!
【パッケージ】gulpfile.jsを設定する。
nodejs、gulp、gulp-phpunitは事前にインストールしておきましょう。
var gulp = require('gulp');
var phpunit = require('gulp-phpunit');
var targets = [
'./config/**/*.php',
'./database/**/*.php',
'./src/**/*.php',
'./tests/**/*.php',
'!node_modules/', '!vendor/**/*'
];
gulp.task('default', [
'phpunit',
]);
gulp.task("watch", function () {
var gaze_option = {
debounceDelay: 1000 // wait 1 sec after the last run
};
gulp.watch(targets, [
'phpunit',
], gaze_option);
});
// shellだと色がつかないので・・・。
gulp.task('phpunit', function () {
var options = {debug: false};
gulp.src('phpunit.xml')
.pipe(phpunit('./vendor/bin/phpunit', options)).on('error', function (err) {});
});
【パッケージ】gulpでファイル監視しながらテストを書く
nodejs、gulp、gulp-phpunitがインストールできていれば、あとは下記コマンドをEnterキーでターンッ!
gulp watch
ファイル監視されているので、ファイル更新のたびにテストが実行されるようになりました!
以上でパッケージの自動テストが完了です。
でも、CMS・フロントのアプリケーションで、それぞれのSeederデータ、テストクラスで使うデータを作るなんてめんどくさいですよね?
なんとかしましょう!
モデルファクトリー
【パッケージ】モデルファクトリーを定義
CMS・フロントで同じSeederデータの定義・テストデータの定義をまとめて使えるように、Laravelのモデルファクトリーを使いましょう!
下記ディレクトリにModelFactory.phpを配置しましょう。
<?php
use Faker\Factory as FakerFactory;
$factory->define(\AllAbout\ModelPackage\Models\Eloquent\Content::class, function (Faker\Generator $faker) {
// Fakerを使うとダミーデータの生成が楽。
$faker = FakerFactory::create('ja_JP');
$data = [
'title' => $faker->word,
'description' => $faker->sentence(),
'body' => $faker->sentence(),
];
// DB用格納時に生成されているデータをテスト時には、ダミーで作成
if (app()->environment() === 'testing') {
$data['id'] = crc32(uniqid());
$data['created_at'] = $faker->dateTime();
$data['updated_at'] = $faker->dateTime();
}
return $data;
});
これだけで、Seeder、テストのデータ作成が簡単にできるよう仕組みができました。
Seederから設定していきましょう!
Seeder
【パッケージ】Seederクラスを定義
作成したモデルファクトリーを読み込んで、Seederデータを作成しましょう。
<?php
namespace AllAbout\ModelPackage\Database\Seeds;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Factory as EloquentFactory;
use Illuminate\Database\Seeder;
class HogeSeeder extends Seeder
{
protected $factory_path = 'vendor/all-about/model-package/database/factories';
public function setContainer(Container $container)
{
// モデルファクトリー読み込み
$container->make(EloquentFactory::class)->load($this->factory_path);
return parent::setContainer($container);
}
public function run()
{
// DBにContentデータが3件生成される処理。なんて便利なんだ、モデルファクトリー。
factory(\AllAbout\ModelPackage\Models\Eloquent\Content::class, 3)->create();
}
}
【CMS・フロント】Seederを実行
CMS・フロントのルートディレクトリに移動して下記artisanコマンドを実行!
php artisan db:seed --class="AllAbout\ModelPackage\Database\Seeds\HogeSeeder"
これ一つでCMS・フロントで作るダミーデータをパッケージで一元管理することが可能になりました!
Seederデータの設定は完了しましたが、CMS・フロントでのテストを書く際にモデルファクトリーが呼べないので、設定をしましょう。
CMS・フロントのテスト
【CMS・フロント】 tests配下にModelPackageTestCase.phpを作成する。
CMS・フロントで、モデルファクトリーを呼ぶために、継承クラス「ModelPackageTestCase」を作成しましょう。
私は別クラスで定義したい派なので継承クラス「ModelPackageTestCase」を作成しましたが、TestCase.phpに書いても良いと思います。
<?php
use Illuminate\Database\Eloquent\Factory as EloquentFactory;
abstract class ModelPackageTestCase extends TestCase
{
// パッケージのfacitoriesのディレクトリまでを定義
protected $factory_path = 'vendor/all-about/model-package/database/factories';
/**
* Setup the test environment.
*
* @return void
*/
public function setUp()
{
parent::setUp();
$this->app->make(EloquentFactory::class)->load(base_path($this->factory_path));
}
}
【CMS・フロント】composer.jsonにModelPackageTestCase.phpのautolaod定義を追加
別クラスにした場合、定義しないと読み込めないので定義しましょう!
"autoload-dev": {
"classmap": [
"tests/ModelPackageTestCase.php", // 追加
"tests/TestCase.php"
]
},
【CMS・フロント】 ModelPackageTestCase.phpを継承してテストを作成する。
モデルファクトリーによって簡単に定義できるようになったテストデータを使用して、モックしまくろう!
<?php
class ContentControllerTest extends ModelPackageTestCase {
public function testIndex()
{
$content_model_mock = Mockery::mock(Content::class);
// テスト時に近しいデータを、モデルファクトリーによって、生成
// make()を使用しているのでDBアクセスしていない
$content = factory(\AllAbout\ModelPackage\Models\Eloquent\Content::class)->make();
$content_model_mock->shouldReceive('find')->andReturn($content);
$this->app->instance(\AllAbout\ModelPackage\Models\Eloquent\Content::class, $content_model_mock);
// 省略
}
}
これでテストが簡単にかけるようになりました!
CMS・フロントのアプリケーションでのphpunit、phpcsの設定・実行・自動化は、パッケージのところで書いたので、省略します・・・。
以上が、オールアバウトで行っているModelのパッケージ化でした。
案外簡単にできるのでぜひお試しをー!
※今回、Qiita用に書き直した内容で動作確認をしていないので、もし動かない場合は、コメントしてください。