0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Laravel】ローカルパッケージを利用した開発戦略 その2:実践編

Last updated at Posted at 2025-02-02

この記事は その1 の続きです。
単体としても読めなくは無いと思います。

Spatie パッケージの開発ガイド

公式のトレーニングが確実だが、それを見ていない私の Tips を以下に記録しておく。
main と sub を行ったり来たりするので、shell の先頭に必ず cd を入れてある。

ファイル構成

drawio (24).png

ざっとこんな感じ。
エントリポイントは、パッケージ名と同じファイル。
こいつをライブラリみたいにしていく。

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 などもできる。(が今回は紹介しない)
詳しくはこちらへ。

基本的に何かファイルを増やしたら、ここに紐づける。

機能の追加方法

消費税計算の機能を追加してみる。
テストも合わせて追加する。

機能追加

package/sub-module/src/SubModule.php
<?php

namespace Crest\SubModule;

class SubModule {
    /**
     * 消費税額(VAT) を算出する
     *
     * @param integer $price
     * @return integer 四捨五入した整数
     */
    public function calcVat(int $price): int
    {
        return round($price * 0.1, 0);
    }
}

テスト

package/sub-module/tests/ExampleTest.php
<?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 も自動的に公開されているので、好きなほうを使う。

app/Console/Commands/AppVat.php
<?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

かんぺき! :raised_hands: :raised_hands: :raised_hands:

モデル + マイグレーション の追加方法

一言メモを保存する機能を追加してみる。

機能追加

migration ファイルは以下のように、スタブ形式で保存されている。
package/sub-module/database/migrations/create_sub_module_table.php.stub

これは Laravel のライブラリを使ったことがあると分かりやすいが、vendor:publish で、main 側の migration フォルダにコピーして配置するためなのです。

テーブル名は他と被らないような名前にしよう。

package/sub-module/database/migrations/create_sub_module_table.php.stub
<?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 への紐づけも変えておこう。

package/sub-module/src/SubModuleServiceProvider.php
- ->hasMigration('create_sub_module_table')
+ ->hasMigration('create_crest_microblogs_table')

Model は初期ディレクトリが無いので、src/Models/* に作成する。

package/sub-module/src/Models/Microblog.php
<?php

namespace Crest\SubModule\Models;

use Illuminate\Database\Eloquent\Model;

class Microblog extends Model
{
    protected $table = 'crest_microblogs';

    protected $fillable = ['message'];
}

必要であれば Factory ファイルを作成する。

最後に機能を追加する。

package/sub-module/src/SubModule.php
<?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 起動してないからね、仕方ないね)

package/sub-module/tests/TestCase.php
  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();
  }
package/sub-module/tests/ExampleTest.php
<?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
app/Console/Commands/AppMicroblog.php
<?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}

かんぺき! :raised_hands: :raised_hands: :raised_hands:

migration の更新時の対応

vendor:publish を使用する場合、ファイルをコピーする都合、変更がリアルタイムには反映されなくなる。
基本的には、別のマイグレーションファイルを作って、そこに変更を書き込む。
元のファイルに書き込む場合は、publish したファイルを手動で削除する必要がある。

実例は次の章へ

モデルに「ユーザー」とのリレーションを作成する

パッケージにした時点で他のデータと関連することはそこまで無い。
ただ「ユーザー」を使いたい場面は多いのではないか。

先ほどの一言機能に、書いた人を追加する。

機能追加

package/sub-module/database/migrations/append_author_by_crest_microblogs_table.php.stub
<?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 への紐づけを増やしておく。

package/sub-module/src/SubModuleServiceProvider.php
  ->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 から書き換えられるようにすると良い。

package/sub-module/src/Models/Microblog.php
  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');
+     }
  }

最後に機能を書き換える。

package/sub-module/src/SubModule.php
+ 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 を登録する。

package/sub-module/tests/TestCase.php
  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 ロジックをモックする必要があるのでちょっと大変。

package/sub-module/tests/ExampleTest.php
<?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

そして、上記で作成したコマンドを編集して呼び出す。

app/Console/Commands/AppMicroblog.php
+ 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"}}

かんぺき! :raised_hands: :raised_hands: :raised_hands:

config の作り方

最後に config についてふわっと触れておく。
こちらも vendor:publish 対象のものである。

package/sub-module/config/sub-module.php にデフォルトの設定を書いておく。
publish しない状態であれば、デフォルトの設定が読み込まれる。

ローカルパッケージの場合は、必要最低限の設定で大丈夫。

$ cd ../../

$ php artisan vendor:publish --tag="sub-module-config"

おわりに

一通りパッケージ開発に必要な機能に触れた。
view や route は触れない。
このプロジェクトはテンプレートなので、使わないものは消しておこう。

パッケージ開発、そんなに難しくなくてビックリ。
ローカルパッケージ戦略、試してみてください。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?