LaravelのModelをパッケージ化してCMSとフロントのアプリケーション実装を楽にする

この記事は、オールアバウト 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を定義する

良い感じに定義しましょう！

```vendor/all-about/model-package/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に定義する

良い感じに定義しましょう！
業務で使うならプライベートリポジトリだと思うので、下記のような感じになりそうですね！

```laravel/composer.json
{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://example.com/all-about/model-package"
        }
    ],
    "require": {
        "all-about/model-package": "dev-master"
    }
}
```

## 【CMS or フロント】Modelを呼び出す

呼び出しましょう！

```laravel/app/Http/HogeController.php
use AllAbout\ModelPackage\Models\Eloquent\User;
```

以上で、Modelのパッケージ化が完了しました。
ついでなので、テーブル定義もパッケージ内で管理してみましょう！

# テーブル定義
artisanコマンドによる[マイグレーション](https://laravel.com/docs/5.3/migrations#generating-migrations)では、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の設定

よしなに設定しましょう！

``` vendor/all-about/model-package/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](https://github.com/orchestral/testbench)」をインストールすることにより、Laravelに依存した機能が担保されます。
testsディレクトリにorchestra/testbenchベースのTestCase.phpを作成しましょう！

必要最低限なら、継承元のクラスで設定済みなので、下記のようなクラス定義のファイルだけを作ればOK！

```vendor/all-about/model-package/tests/TestCase.php
<?php

class TestCase extends \Orchestra\Testbench\TestCase
{
    //
}
```

# 【パッケージ】テストを書く

下記testsディレクトリで、orchestra/testbenchで作成したベースクラスを継承してテストを書きまくろう！

```vendor/all-about/model-package/tests/HogeTest.php
<?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の[モデルファクトリー](https://laravel.com/docs/5.3/seeding#using-model-factories)を使いましょう！

下記ディレクトリにModelFactory.phpを配置しましょう。

``` vendor/all-about/model-package/database/factories/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データを作成しましょう。

```vendor/all-about/model-package/database/seeds/HogeSeeder.php
<?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に書いても良いと思います。

```laravel/tests/ModelPackageTestCase.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定義を追加

別クラスにした場合、定義しないと読み込めないので定義しましょう！

```laravel/composer.json
"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用に書き直した内容で動作確認をしていないので、もし動かない場合は、コメントしてください。