この記事は その1 の続きです。
単体としても読めなくは無いと思います。
Spatie パッケージの開発ガイド
公式のトレーニングが確実だが、それを見ていない私の Tips を以下に記録しておく。
main と sub を行ったり来たりするので、shell の先頭に必ず cd
を入れてある。
ファイル構成
ざっとこんな感じ。
エントリポイントは、パッケージ名と同じファイル。
こいつをライブラリみたいにしていく。
main への登録は ServiceProvider が全てやってくれる。
何か公開したいものは、ここで登録する。
ほか route や model が必要であれば、好きなように増やしてOK。
(artisan コマンドが使用できないので、その点は注意)
(いくつかのコマンドは composer script に用意されている)
namespace
は、 src/
が Crest\SubModule
に紐づいている。
ServiceProvicer の設定
パッケージの設定、紐づけ処理は、この ServiceProvider が全て受け持っている。
package/sub-module/src/SubModuleServiceProvider.php
初期のものだけ紹介する。
{
$package
->name('sub-module') // パッケージの名前
->hasConfigFile() // config ファイルを利用するか
->hasViews() // view ファイルを利用するか
->hasMigration('create_sub_module_table') // migration ファイルを利用するか
->hasCommand(SubModuleCommand::class); // command を利用するか
}
設定次第で、自動 route 紐づけ、自動 publish などもできる。(が今回は紹介しない)
詳しくはこちらへ。
基本的に何かファイルを増やしたら、ここに紐づける。
機能の追加方法
消費税計算の機能を追加してみる。
テストも合わせて追加する。
機能追加
<?php
namespace Crest\SubModule;
class SubModule {
/**
* 消費税額(VAT) を算出する
*
* @param integer $price
* @return integer 四捨五入した整数
*/
public function calcVat(int $price): int
{
return round($price * 0.1, 0);
}
}
テスト
<?php
use Crest\SubModule\SubModule;
it('calc VAT', function () {
expect((new SubModule)->calcVat(205))
->toBe(21);
expect((new SubModule)->calcVat(204))
->toBe(20);
});
テストを実行する。
$ cd package/sub-module
$ composer test
> vendor/bin/pest
PASS Tests\ArchTest
✓ it will not use debugging functions
PASS Tests\ExampleTest
✓ it calc VAT
Tests: 2 passed (5 assertions)
Duration: 0.10s
main から呼び出す
簡略化のため、コマンドから呼び出す。
$ cd ../../
$ php artisan make:command AppVat
Facade も自動的に公開されているので、好きなほうを使う。
<?php
namespace App\Console\Commands;
use Crest\SubModule\SubModule;
use Illuminate\Console\Command;
class AppVat extends Command
{
protected $signature = 'app:vat {price}';
protected $description = 'calc vat';
public function handle()
{
$price = $this->argument('price');
$vat = (new SubModule)->calcVat($price);
$this->info('VAT: '.$vat);
return self::SUCCESS;
}
}
$ cd ../../
$ php artisan app:vat 105
VAT: 11
かんぺき!
モデル + マイグレーション の追加方法
一言メモを保存する機能を追加してみる。
機能追加
migration ファイルは以下のように、スタブ形式で保存されている。
package/sub-module/database/migrations/create_sub_module_table.php.stub
これは Laravel のライブラリを使ったことがあると分かりやすいが、vendor:publish
で、main 側の migration フォルダにコピーして配置するためなのです。
テーブル名は他と被らないような名前にしよう。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('crest_microblogs', function (Blueprint $table) {
$table->id();
$table->text('message');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('crest_microblogs');
}
};
テーブル名を変えたら、ファイル名と ServiceProvider への紐づけも変えておこう。
- ->hasMigration('create_sub_module_table')
+ ->hasMigration('create_crest_microblogs_table')
Model は初期ディレクトリが無いので、src/Models/*
に作成する。
<?php
namespace Crest\SubModule\Models;
use Illuminate\Database\Eloquent\Model;
class Microblog extends Model
{
protected $table = 'crest_microblogs';
protected $fillable = ['message'];
}
必要であれば Factory ファイルを作成する。
最後に機能を追加する。
<?php
namespace Crest\SubModule;
class SubModule {
/**
* 一言メモを追加する.
*
* @param string $message メモ
* @return Microblog
*/
public function addBlog(string $message): Microblog
{
return Microblog::create([
'message' => $message,
]);
}
/**
* 一言メモを取得する.
*
* @param integer $id メモID
* @return Microblog
*/
public function getBlog(int $id): Microblog|null
{
return Microblog::where('id', $id)->first();
}
}
テスト
マイグレーションが必要なテストは、ちょっとコツが必要。
TestCase.php
の中の Setup
で利用する migration を手動登録しなければならない。
(Laravel 起動してないからね、仕方ないね)
public function getEnvironmentSetUp($app)
{
config()->set('database.default', 'testing');
- /*
- $migration = include __DIR__.'/../database/migrations/create_sub-module_table.php.stub';
- $migration->up();
- */
+ $migration = include __DIR__.'/../database/migrations/create_sub-create_crest_microblogs_table.php.stub';
+ $migration->up();
}
<?php
use Crest\SubModule\SubModule;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
beforeEach(function () {
uses(RefreshDatabase::class);
Carbon::setTestNowAndTimezone(
Carbon::parse('2025-01-23 01:23:45'),
);
});
it('add microblog', function () {
$message = '朝ごはんは目玉焼きでした。';
$blog = (new SubModule)->addBlog($message);
expect($blog->toArray())
->toMatchArray([
'id' => 1,
'message' => $message,
'updated_at' => '2025-01-23T01:23:45.000000Z',
'created_at' => '2025-01-23T01:23:45.000000Z',
]);
expect((new SubModule)->getBlog($blog->id))
->toMatchArray($blog->toArray());
});
テストを実行する。
$ cd package/sub-module
$ composer test
> vendor/bin/pest
PASS Tests\ArchTest
✓ it will not use debugging functions
PASS Tests\ExampleTest
✓ it add microblogs
✓ it calc VAT
Tests: 3 passed (21 assertions)
Duration: 0.12s
main から呼び出す
migration
を main で呼び出す場合、vendor:publish
を行う必要がある。
詳しくは readme に書いてある。
package/sub-module/README.md
$ cd ../../
$ php artisan vendor:publish --tag="sub-module-migrations"
$ php artisan migrate
そして、コマンドから呼び出す。
$ cd ../../
$ php artisan make:command AppMicroblog
<?php
namespace App\Console\Commands;
use Crest\SubModule\SubModule;
use Illuminate\Console\Command;
class AppVat extends Command
{
protected $signature = 'app:vat {price}';
protected $description = 'calc vat';
public function handle()
{
$price = $this->argument('price');
$vat = (new SubModule)->calcVat($price);
$this->info('VAT: '.$vat);
return self::SUCCESS;
}
}
$ cd ../../
$ php artisan app:microblog "早起きした"
{"message":"早起きした","updated_at":"2025-01-09T04:13:06.000000Z","created_at":"2025-01-09T04:13:06.000000Z","id":3}
かんぺき!
migration の更新時の対応
vendor:publish
を使用する場合、ファイルをコピーする都合、変更がリアルタイムには反映されなくなる。
基本的には、別のマイグレーションファイルを作って、そこに変更を書き込む。
元のファイルに書き込む場合は、publish したファイルを手動で削除する必要がある。
実例は次の章へ
モデルに「ユーザー」とのリレーションを作成する
パッケージにした時点で他のデータと関連することはそこまで無い。
ただ「ユーザー」を使いたい場面は多いのではないか。
先ほどの一言機能に、書いた人を追加する。
機能追加
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('crest_microblogs', function (Blueprint $table) {
$table->bigInteger('author_by')->nullable();
});
}
public function down()
{
Schema::table('crest_microblogs', function (Blueprint $table) {
$table->dropColumn('author_by');
});
}
};
ServiceProvider への紐づけを増やしておく。
->hasMigration('create_crest_microblogs_table')
+ ->hasMigration('append_author_by_crest_microblogs_table')
Model も書き換えておく。
この時、User
テーブルと紐づけるには、config('auth.providers.users.model')
を使うと良い。
main の config を見ると分かるが、認証時に利用する User Model が登録されている。
紐づけるカラム名など、柔軟に変更したい場合は、後述の config
と併用し、main から書き換えられるようにすると良い。
class Microblog extends Model
{
protected $table = 'crest_microblogs';
- protected $fillable = ['message'];
+ protected $fillable = ['message', 'author_by'];
+ public function author(): BelongsTo
+ {
+ return $this->belongsTo(config('auth.providers.users.model'), 'author_by');
+ }
}
最後に機能を書き換える。
+ use Illuminate\Support\Facades\Auth;
public function addBlog(string $message): Microblog
{
- return Microblog::create([
- 'message' => $message,
- 'author_by' => Auth::id(),
- ]);
+ $blog = Microblog::create([
+ 'message' => $message,
+ 'author_by' => Auth::id(),
+ ]);
+ return $blog->load(['author']);
}
public function getBlog(int $id): Microblog|null
{
- return Microblog::where('id', $id)->first();
+ return Microblog::with(['author'])->where('id', $id)->first();
}
テスト
TestCase に migration を登録する。
public function getEnvironmentSetUp($app)
{
config()->set('database.default', 'testing');
$migration = include __DIR__.'/../database/migrations/create_sub-create_crest_microblogs_table.php.stub';
$migration->up();
+ $migration = include __DIR__.'/../database/migrations/append_author_by_crest_microblogs_table.php.stub';
+ $migration->up();
}
テストは User ロジックをモックする必要があるのでちょっと大変。
<?php
use Crest\SubModule\SubModule;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Schema;
beforeEach(function () {
uses(RefreshDatabase::class);
Carbon::setTestNowAndTimezone(
Carbon::parse('2025-01-23 01:23:45'),
);
});
it('add microblog with author', function () {
class User extends Illuminate\Foundation\Auth\User {
protected $table = 'users';
protected $fillable = ['name'];
}
Config::set('auth.providers.users.model', User::class);
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
$author = User::create(['name' => '太宰治']);
$this->actingAs($author);
///
$message = '朝ごはんは目玉焼きでした。';
$blog = (new SubModule)->addBlog($message);
expect($blog->toArray())
->toMatchArray([
'id' => 1,
'message' => $message,
'author_by' => $author->id,
'updated_at' => '2025-01-23T01:23:45.000000Z',
'created_at' => '2025-01-23T01:23:45.000000Z',
]);
expect((new SubModule)->getBlog($blog->id))
->toMatchArray($blog->toArray());
});
テストを実行する。
$ cd package/sub-module
$ composer test
> vendor/bin/pest
PASS Tests\ArchTest
✓ it will not use debugging functions
PASS Tests\ExampleTest
✓ it add microblog with author
✓ it calc VAT
Tests: 3 passed (25 assertions)
Duration: 0.12s
main から呼び出す
上記と同じように vendor:publish
を行う。
そうすると、新しいファイルのみ出力される。
$ cd ../../
$ php artisan vendor:publish --tag="sub-module-migrations"
$ php artisan migrate
そして、上記で作成したコマンドを編集して呼び出す。
+ use App\Models\User;
+ use Illuminate\Support\Facades\Auth;
public function handle()
{
$message = $this->argument('message');
+ $user = User::find(1) ?? User::factory()->create(['name' => 'たろう']);
+ Auth::login($user);
$blog = (new SubModule)->addBlog($message);
$this->info($blog->toJson(JSON_UNESCAPED_UNICODE));
return self::SUCCESS;
}
$ cd ../../
$ php artisan app:microblog "早寝した。"
{"message":"早寝した。","author_by":1,"updated_at":"2025-01-09T05:04:32.000000Z","created_at":"2025-01-09T05:04:32.000000Z","id":8,"author":{"id":1,"name":"たろう","email":"willard95@example.com","email_verified_at":"2025-01-09T04:59:35.000000Z","created_at":"2025-01-09T04:59:35.000000Z","updated_at":"2025-01-09T04:59:35.000000Z"}}
かんぺき!
config の作り方
最後に config についてふわっと触れておく。
こちらも vendor:publish
対象のものである。
package/sub-module/config/sub-module.php
にデフォルトの設定を書いておく。
publish しない状態であれば、デフォルトの設定が読み込まれる。
ローカルパッケージの場合は、必要最低限の設定で大丈夫。
$ cd ../../
$ php artisan vendor:publish --tag="sub-module-config"
おわりに
一通りパッケージ開発に必要な機能に触れた。
view や route は触れない。
このプロジェクトはテンプレートなので、使わないものは消しておこう。
パッケージ開発、そんなに難しくなくてビックリ。
ローカルパッケージ戦略、試してみてください。
参考
- Composer Local Package
- Laravel のパッケージドキュメント
- Spatie のパッケージドキュメント