mcpField
P
package-development

package-development

github.com/bagisto開発13318 回インストール2 週間前に更新
リポジトリ

package-development について

Bagisto の機能パッケージを開発、保守する開発者向けのスキルです。パッケージ構成、データアクセス、ルーティング、画面、コーディング規約を扱います。

主な機能

  1. 01パッケージのディレクトリ構成
  2. 02マイグレーション、モデル、リポジトリの実装
  3. 03管理画面とショップのルート作成
  4. 04コントローラー、ビュー、DataGrid の構築
  5. 05多言語、メニュー、権限の設定
  6. 06PHP のドキュメントと書式規約

活用シーン

  1. 01Bagisto の新規パッケージ作成
  2. 02パッケージのマイグレーションとデータアクセス追加
  3. 03管理画面やショップ画面の開発
  4. 04設定、権限、ナビゲーションの実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/bagisto/agent-skills --skill package-development

インストール後、Claude Code で/package-developmentを入力して読み込まれたか確認します。

詳細

Bagisto でのパッケージ開発

概要

パッケージは、Bagisto の特定の機能をカプセル化した、自己完結型のモジュールです。この包括的なスキルでは、構成から高度な機能まで、パッケージ開発のあらゆる側面を扱います。

適用するタイミング

以下の場合に、このスキルを有効にしてください。

  • Bagisto 用の新しいパッケージを作成するとき
  • パッケージのディレクトリー構成を設定するとき
  • データベースのマイグレーションを作成するとき
  • コントラクトとプロキシを備えた Eloquent モデルを構築するとき
  • データアクセス用のリポジトリーを実装するとき
  • 管理画面/ショップ用のルートを作成するとき
  • 依存性注入を使用してコントローラーを構築するとき
  • Bagisto のレイアウトを使用して Blade ビューを作成するとき
  • 多言語対応を追加するとき
  • 管理画面の DataGrid テーブルを作成するとき
  • 管理画面のナビゲーションメニューを設定するとき
  • 権限ベースのアクセス制御を実装するとき
  • 管理画面で設定可能な項目を作成するとき

PHP コードスタイル

vendor/bin/pint は書式の大部分を修正するため、実行して任せてください。以下のルールは Pint が適用しないもので、手動で記述する必要があります。また、パッケージ内のすべての PHP ファイル(コントローラー、リポジトリー、モデル、DataGrid、列挙型、リスナー、ジョブ)に適用されます。

これらのルールは .php ファイルに適用されます。また、Blade ファイル内の @php … @endphp ブロックに記述された PHP にも同様に適用されます。Pint は .blade.php を処理できないため、そこでは手動で適用してください。Blade 独自のレイヤー(@props、ディレクティブの引数、マークアップ)には独自の規約があります。blade-conventions スキルを参照してください。

複数節からなる条件式は複数行にする

複数の節(&&/|| で結ばれたオペランドが2つ以上)からなる条件式は、複数行に分けます。( のみの行から始め、各節を同じインデントで個別の行に置き、ブール演算子は結合する行の先頭に置き、) { は文と同じインデントで閉じます。

// Good — each clause is one scannable line, and the operator is the first thing you read.
if (
    $product->is_owner
    && $product->is_approved
    && ! $product->is_draft
) {
    app(SellerProductIndexer::class)->reindex($product->product);
}

// Bad — clauses run together, and the operators hide at the end of the line.
if ($product->is_owner && $product->is_approved && ! $product->is_draft) {
    app(SellerProductIndexer::class)->reindex($product->product);
}

単一節の条件式はインラインのままにします。複数行にすると、ノイズが増え、見通しもよくなりません。

if (! $product) {
    return;
}

if ($request->ajax()) {
    return datagrid(ProductDataGrid::class)->process();
}

このルールは行の長さではなく、節の数に基づきます。短い節が2つであっても、複数行にします。

if (
    $product->is_approved
    && $product->seller->is_approved
) {
    // ...
}

elseif、while、複合ブール式を返す return にも同様に適用されます。

return is_null($value)
    || $value === '';

1つの呼び出しを否定するだけのガード節(if (! $this->cart) {)は単一節なので、インラインのままにします。単一節の return、三項演算子、アロー関数も同様です。

すべてのメソッドに DocBlock を付ける

例外はありません。可視性(public、protected、private)も問いません。メソッド名から内容が明らかに思えても、DocBlock のないメソッドは未完成です。

説明は文にします。先頭を大文字にし、ピリオドで終わらせてください。通常は1行で記述し、1行では表現できない場合に限り、2行目を追加します。

/**
 * Reindex all products.
 */
public function reIndexAll()

/**
 * Source documents from the core product index for the given ids, keyed by product id.
 */
protected function fetchSourceDocuments($channel, $locale, array $productIds): array
/**
 * reindex all products      ← no capital, no full stop
 */

/**
 * Reindexes all of the products
 */                          ← no full stop

型情報はシグネチャに記述します。ネイティブ型では表現できない情報(配列の形状や、戻り値が mixed の場合など)に限り、@param/@return を追加します。

/**
 * Products grouped by seller id.
 *
 * @return array<int, list<Product>>
 */
protected function groupBySeller(array $products): array

すべてのプロパティにも DocBlock を付ける

同じルールはクラス定数とプロパティにも適用されます。可視性にかかわらず、const、static、型付き、型なしのすべてが対象です。Bagisto のモデルでは $table、$fillable、$casts に DocBlock が付いているため、新しいプロパティに付いていないと目立ちます。各プロパティにはそれぞれ DocBlock を付けてください。隣接する複数のプロパティを1つの DocBlock で説明すると、2つ目のプロパティにはドキュメントがない状態になります。

// Wrong — the second property has no docblock of its own.
/**
 * The table and columns the index covers.
 */
protected string $table = 'product_inventories';

protected array $columns = ['vendor_id', 'product_id', 'qty'];
/**
 * The table associated with the model.
 *
 * @var string
 */
protected $table = 'marketplace_pickups';

/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'scheduled_from' => 'datetime',
];

/**
 * The courier is booked and has not yet called.
 */
public const STATUS_SCHEDULED = 'scheduled';

型なしのプロパティでは、唯一の型情報となるため @var を残してください。宣言に型がすでに指定されているプロパティでは、重複しても意味がないため @var を省略します。

/**
 * Unique carrier code, matching the key under `marketplace_carriers.carriers`.
 */
protected string $code;

コンストラクターでプロモートされたプロパティは例外です。 これらはパラメーターであり、コンストラクター自体の DocBlock で説明されるため、個別にドキュメントを付けないでください。

/**
 * Create a new repository instance.
 */
public function __construct(
    protected PickupRepository $pickupRepository,
    protected ShippingLabelRepository $labelRepository
) {}

クラスの docblock は定義を記述し、経緯は語りません

クラスが何であるかを、1、2文で説明します。どのように生まれたか、何に取って代わったか、過去のアプローチがなぜ誤っていたかは書きません。そうした説明はコミットメッセージに記載します。

/**
 * Drives a carrier for a saved shipment: buys the label, books the collection, and returns the
 * values to write back onto the shipment.
 */
class FulfilmentBooker
/**
 * Turns a saved shipment into a real, carried parcel.
 *
 * This is the step the package was missing. A marketplace seller does not invent a tracking
 * number — they pick a carrier and a collection slot, and the carrier hands back the tracking
 * number, the label and the booking. So the carrier is driven here, straight after the shipment
 * row exists, and whatever it returns is written back onto that row.
 */                              ← history and justification, not a definition
class FulfilmentBooker

実際の制約には、引き続きコメントを付ける価値があります。ただし、「コードが表現できないことだけをコメントする」にあるように、適用される場所、つまりメソッド内に記述します。再配信されたジョブに関するルールを確認したい読者には、読み飛ばしてしまう前置きではなく、ガードのすぐそばにある説明が必要です。

単なる Class ProductRepository は宣言を言い換えているだけで、何もないより悪いものです。

可視性の順にメンバーを並べる

Bagisto 自身のクラスで採用されている順序でクラスを構成します。新しいメンバーを、差分を作りやすい場所ではなく、読者が予想する場所に配置できます。

  1. 定数
  2. プロパティ
  3. コンストラクター
  4. 抽象メソッドの宣言(トレイトまたは基底クラスが必要とする契約)
  5. パブリックメソッド
  6. プロテクトメソッド
  7. プライベートメソッド

最も重要なのは可視性の順序で、双方向に守る必要があります。protected または private のヘルパーをパブリックメソッドより上やその間に置かず、public メソッドをプロテクトメソッドの中に埋もれさせないでください。それぞれの可視性は、連続したひとまとまりにします。

既存のパブリックメソッドから呼び出されるヘルパーを追加するときは、呼び出し元のすぐ後に配置しないようにします。そこに置くと、パブリックメソッドの間にプロテクトメソッドが入り込んでしまいます。一番下のプロテクトブロックに配置してください。

class Reporting
{
    public function countLowStock($seller): int
    {
        return $this->lowStockQuery($seller)->count();   // caller stays up top…
    }

    public function getTopProducts($seller) { /* … */ }

    // …every other public method…

    protected function lowStockQuery($seller)            // …the helper lives down here
    {
        // …
    }
}

自分が変更した行だけでなく、ファイル全体を確認してください。 クラスを編集するときは、メンバー全体の順序を確認し、位置が不適切なメンバーがあれば修正します。プロテクトメソッドの間に入り込んだパブリックヘルパー、docblock のないプロパティ、パブリックブロックに置かれたプライベートメソッドなどが該当します。編集したファイルに既存の違反を残すことは、新たに違反を持ち込むのと同じ欠陥です。

同じ可視性のグループ内では、関連するメソッド(ラップするクエリの隣に置く getter など)をまとめます。ただし、そのために既存のメンバーの順序を変えてはいけません。このグループ化は、順序が同等の場合の判断基準であり、ファイルを大幅に変更する理由ではありません。

コードが表現できないことだけをコメントする

上記のルールはdocblockに関するものです。こちらはメソッド本体内の説明に関するルールで、コメントが増えすぎるのは主にここです。

Bagisto 自身のパッケージにはコメントがほとんどなく、そうでない生成コードはすぐに目立ちます。メソッド内では、コメントなしが基本です。コメントを付けるだけの理由がある場合に限り、記述してください。

コードを理解している読者であっても、その説明がなければ誤った判断をしてしまう場合に、コメントが必要です。ほとんどの場合、局所的には見えない制約をコードが表現しているためです。

  • 削除または簡略化できそうに見えて、実際には重要な役割を果たしている行(ベースクエリから意図的に外している結合、理由があって否定形で記述されたフィルターなど)。
  • 自明でない順序、互換性、または移行に関する懸念。
  • コア、Laravel、またはサードパーティーサービスの動作に対する回避策。
/**
 * Joined only for these filters. Kept out of the base query because the `or` in its condition is
 * not indexable — MySQL scans the whole products table per candidate row.
 */
$qb->leftJoin('products as variants', function ($join) {
    $join->on('variants.parent_id', '=', 'products.id')
        ->orOn('variants.id', '=', 'products.id');
});

すでに明快なコードを説明してはいけません。以下はすべて不要です。

/**
 * Loop through the products.        ← narrates the obvious
 */
foreach ($products as $product) { ... }

/**
 * Set the total.                    ← labels an assignment
 */
$total = $invoice->sub_total - $invoice->discount_amount;

/**
 * Dispatch the event.               ← restates the call
 */
Event::dispatch('marketplace.product.update.after', $sellerProduct);

コメントを付ける場合は簡潔に、2、3行に収めてください。症状を詳しく説明する段落は、文の上に書くのではなく、コミットメッセージまたは PR に記載します。経緯ではなく、制約を記述してください。


データアクセス

リポジトリを経由し、クエリビルダーを直接使わない

読み取りと書き込みはすべてリポジトリを経由します。コントローラー、リスナー、ジョブ、サービスから DB::table(...) やモデルのクエリビルダーを使うと、コードベース全体の基盤となるレイヤーを迂回することになり、処理を再利用したりオーバーライドしたりできなくなります。

// Good — the operation lives on the repository, named for what it does.
$this->pickupRepository->attachShipment($pickup, $shipment->id);
// Bad — a service reaching past the repository into the table.
DB::table('marketplace_pickups')
    ->where('id', $pickup->id)
    ->update(['package_count' => $pickup->shipments()->count()]);

必要なメソッドがリポジトリにない場合は、追加してください。そこが拡張ポイントです。

/**
 * Attach a shipment to a collection and refresh its package count.
 */
public function attachShipment(Pickup $pickup, int $shipmentId): void
{
    $pickup->shipments()->syncWithoutDetaching([$shipmentId]);

    $this->update(['package_count' => $pickup->shipments()->count()], $pickup->id);
}

DB の使用が想定されている唯一の場所は、DataGrid の prepareQueryBuilder() です。これは設計上、クエリビルダーを基盤としており、グリッドでページネーションするための Builder を返します。リポジトリ内での DB::transaction() と DB::raw() も問題ありません。問題としているのはファサードそのものではなく、データレイヤーの外部から テーブル に対してクエリを実行することです。

リポジトリ内の販売者向けクエリはすべてスコープを設定する

マーケットプレイスでは、販売者が所有するデータを扱うリポジトリのメソッドは、最初の引数に販売者 ID を取り、それで絞り込みます。これにより、他の販売者の行にアクセスできる呼び出し方がなくなります。

/**
 * One of a seller's collections, or null when it is not theirs.
 */
public function findForSeller(int $sellerId, int $pickupId): ?Pickup
{
    return $this->model
        ->where('marketplace_seller_id', $sellerId)
        ->where('id', $pickupId)
        ->first();
}

@core: パッケージ開発 - コア

パッケージ構成

標準ディレクトリ構成
packages/Webkul/{PackageName}/
├── src/
│   ├── Config/
│   │   ├── admin-menu.php
│   │   ├── acl.php
│   │   └── system.php
│   ├── Database/
│   │   ├── Migrations/
│   │   ├── Seeders/
│   │   └── Factories/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── Admin/
│   │   │   └── Shop/
│   │   ├── Middleware/
│   │   └── Requests/
│   ├── Models/
│   │   └── {Package}Proxy.php
│   ├── Repositories/
│   │   └── {Package}Repository.php
│   ├── Resources/
│   │   ├── views/
│   │   └── lang/
│   ├── Providers/
│   │   ├── {Package}ServiceProvider.php
│   │   └── ModuleServiceProvider.php
│   ├── DataGrids/
│   │   └── Admin/
│   └── manifest.php
└── composer.json

パッケージジェネレーターの使用

ジェネレーターは必須ではなく、あくまで利便性のためのものです。以下の「手動セットアップ」で説明するファイルをひな形として作成するだけです。自分でファイルを作成する場合や、プロジェクトへの開発依存関係の追加に事前承認が必要な場合は、このセクションを丸ごとスキップしてください。

インストール
composer require --dev bagisto/bagisto-package-generator
パッケージの作成
# If package directory doesn't exist
php artisan package:make Webkul/RMA

# If package directory already exists
php artisan package:make Webkul/RMA --force
モデルの作成
php artisan package:make-model ReturnRequest Webkul/RMA
リポジトリの作成
php artisan package:make-repository ReturnRequestRepository Webkul/RMA
マイグレーションの作成
php artisan package:make-migration CreateRmaRequestsTable Webkul/RMA

手動セットアップ

パッケージディレクトリの作成
mkdir -p packages/Webkul/RMA/src/Providers
サービスプロバイダーの作成

ファイル: packages/Webkul/RMA/src/Providers/RMAServiceProvider.php

<?php

namespace Webkul\RMA\Providers;

use Illuminate\Support\ServiceProvider;

class RMAServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        //
    }

    public function boot(): void
    {
        //
    }
}

パッケージの登録

Composer オートローダーの更新

ルートの composer.json で:

{
    "autoload": {
        "psr-4": {
            "Webkul\\RMA\\": "packages/Webkul/RMA/src"
        }
    }
}

次に、以下を実行します。

composer dump-autoload
サービスプロバイダーの登録

bootstrap/providers.php で:

<?php

return [
    App\Providers\AppServiceProvider::class,
    
    // ... other providers ...
    
    Webkul\RMA\Providers\RMAServiceProvider::class,
];
キャッシュのクリア
php artisan optimize:clear

サービスプロバイダーのメソッド

マイグレーションの読み込み
public function boot(): void
{
    $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
}
ルートの読み込み
public function boot(): void
{
    $this->loadRoutesFrom(__DIR__ . '/../Routes/admin-routes.php');
    $this->loadRoutesFrom(__DIR__ . '/../Routes/shop-routes.php');
}
ビューの読み込み
public function boot(): void
{
    $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'rma');
}
翻訳の読み込み
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'rma');
}
設定のマージ
public function register(): void
{
    $this->mergeConfigFrom(
        dirname(__DIR__) . '/Config/admin-menu.php',
        'menu.admin'
    );

    $this->mergeConfigFrom(
        dirname(__DIR__) . '/Config/acl.php',
        'acl'
    );

    $this->mergeConfigFrom(
        dirname(__DIR__) . '/Config/system.php',
        'core'
    );
}

Concord モデルの登録

ModuleServiceProvider の作成

ファイル: packages/Webkul/RMA/src/Providers/ModuleServiceProvider.php

<?php

namespace Webkul\RMA\Providers;

use Konekt\Concord\BaseModuleServiceProvider;

class ModuleServiceProvider extends BaseModuleServiceProvider
{
    protected $models = [
        \Webkul\RMA\Models\ReturnRequest::class,
    ];
}
concord.php への登録

config/concord.php で:

<?php

return [
    'modules' => [
        // Other service providers...
        \Webkul\RMA\Providers\ModuleServiceProvider::class,
    ],
];

@data: パッケージ開発 - データレイヤー

マイグレーション

マイグレーションの作成
# Using Bagisto generator
php artisan package:make-migration CreateRmaRequestsTable Webkul/RMA

# Using Laravel artisan
php artisan make:migration CreateRmaRequestsTable --path=packages/Webkul/RMA/src/Database/Migrations
基本的なマイグレーションの構成
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('rma_requests', function (Blueprint $table) {
            $table->id();
            $table->unsignedInteger('customer_id');
            $table->unsignedInteger('order_id');
            $table->string('product_sku');
            $table->string('product_name');
            $table->integer('product_quantity');
            $table->string('status')->default('pending');
            $table->string('reason')->nullable();
            $table->text('admin_notes')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('rma_requests');
    }
};
マイグレーションの実行

マイグレーションはデータベースを変更するため、ローカルまたはステージングのデータベースに対して実行し、本番環境に直接実行しないでください。また、実行待ちのマイグレーションがほかにあるか不明な場合は、対象パッケージのマイグレーションだけが保留中であることを確認してください(php artisan migrate:status で最初に一覧を確認できます)。

# Run all migrations
php artisan migrate

# Run specific package migrations
php artisan migrate --path=packages/Webkul/RMA/src/Database/Migrations

# Check migration status
php artisan migrate:status

モデル

Bagisto のモデルアーキテクチャ

Bagisto は、次の 3 要素からなるモデルシステムを使用します。

  1. コントラクト - パブリック API を定義するインターフェース
  2. モデル - Eloquent モデルの実装
  3. プロキシ - Concord による実行時のモデル解決
モデルコンポーネントの作成
# Using Bagisto generator (creates all three)
php artisan package:make-model ReturnRequest Webkul/RMA
コントラクト

ファイル: packages/Webkul/RMA/src/Contracts/ReturnRequest.php

<?php

namespace Webkul\RMA\Contracts;

interface ReturnRequest
{
}
モデルプロキシ

ファイル: packages/Webkul/RMA/src/Models/ReturnRequestProxy.php

<?php

namespace Webkul\RMA\Models;

use Konekt\Concord\Proxies\ModelProxy;

class ReturnRequestProxy extends ModelProxy
{
}
ベースモデル

ファイル: packages/Webkul/RMA/src/Models/ReturnRequest.php

<?php

namespace Webkul\RMA\Models;

use Illuminate\Database\Eloquent\Model;
use Webkul\RMA\Contracts\ReturnRequest as ReturnRequestContract;

class ReturnRequest extends Model implements ReturnRequestContract
{
    protected $table = 'rma_requests';

    protected $fillable = [
        'customer_id',
        'order_id',
        'product_sku',
        'product_name',
        'product_quantity',
        'status',
        'reason',
        'admin_notes',
    ];
}
モデルのプロパティ
プロパティ用途
$tableデータベースのテーブル名(パッケージのプレフィックスを使用)
$fillable一括代入可能なフィールド
$guarded一括代入できないフィールド
$dates日付カラム
$casts型キャスト
$withEager ロードするリレーション

リポジトリ

リポジトリパターン

Bagisto は、データアクセスを抽象化するために Prettus L5 Repository パッケージを使用します。

リポジトリの作成
php artisan package:make-repository ReturnRequestRepository Webkul/RMA
基本的なリポジトリ構造

ファイル: packages/Webkul/RMA/src/Repositories/ReturnRequestRepository.php

<?php

namespace Webkul\RMA\Repositories;

use Webkul\Core\Eloquent\Repository;

class ReturnRequestRepository extends Repository
{
    public function model(): string
    {
        return 'Webkul\RMA\Contracts\ReturnRequest';
    }
}
利用可能なリポジトリメソッド
基本的な CRUD
// Create
$returnRequest = $repository->create([
    'customer_id' => 1,
    'order_id' => 123,
    'product_sku' => 'SAMPLE-001',
    'status' => 'pending',
]);

// Read
$all = $repository->all();
$find = $repository->find($id);
$findOrFail = $repository->findOrFail($id);
$first = $repository->findWhere(['status' => 'pending'])->first();

// Update
$repository->update(['status' => 'approved'], $id);

// Delete
$repository->delete($id);
高度なクエリ
// Where conditions
$results = $repository->findWhere([
    'status' => 'pending',
    'customer_id' => 456,
]);

// Where in
$results = $repository->findWhereIn('id', [1, 2, 3]);

// Where between
$results = $repository->findWhereBetween('created_at', ['2024-01-01', '2024-12-31']);

// Pagination
$paginator = $repository->paginate(15);

// Eager loading
$withRelations = $repository->with(['customer', 'order'])->find($id);
カスタムリポジトリメソッド
<?php

namespace Webkul\RMA\Repositories;

use Webkul\Core\Eloquent\Repository;

class ReturnRequestRepository extends Repository
{
    public function model(): string
    {
        return 'Webkul\RMA\Contracts\ReturnRequest';
    }

    public function getPendingForCustomer(int $customerId)
    {
        return $this->findWhere([
            'customer_id' => $customerId,
            'status' => 'pending'
        ]);
    }

    public function getStats(): array
    {
        return [
            'total' => $this->count(),
            'pending' => $this->findWhere(['status' => 'pending'])->count(),
            'approved' => $this->findWhere(['status' => 'approved'])->count(),
        ];
    }

    public function getRecent(int $limit = 10)
    {
        return $this->orderBy('created_at', 'desc')
            ->limit($limit)
            ->get();
    }
}

@ui: パッケージ開発 - UI レイヤー

ルート

管理者ルート

ファイル: packages/Webkul/RMA/src/Routes/admin-routes.php

<?php

use Illuminate\Support\Facades\Route;
use Webkul\RMA\Http\Controllers\Admin\ReturnRequestController;

Route::group([
    'middleware' => ['web', 'admin'],
    'prefix' => config('app.admin_url')
], function () {
    Route::prefix('rma/return-requests')->group(function () {
        Route::get('', [ReturnRequestController::class, 'index'])
            ->name('admin.rma.return-requests.index');

        Route::get('{id}', [ReturnRequestController::class, 'show'])
            ->name('admin.rma.return-requests.show');

        Route::post('', [ReturnRequestController::class, 'store'])
            ->name('admin.rma.return-requests.store');

        Route::put('{id}', [ReturnRequestController::class, 'update'])
            ->name('admin.rma.return-requests.update');

        Route::delete('{id}', [ReturnRequestController::class, 'destroy'])
            ->name('admin.rma.return-requests.destroy');

        Route::post('mass-delete', [ReturnRequestController::class, 'massDestroy'])
            ->name('admin.rma.return-requests.mass-delete');
    });
});
ショップルート

ファイル: packages/Webkul/RMA/src/Routes/shop-routes.php

<?php

use Illuminate\Support\Facades\Route;
use Webkul\RMA\Http\Controllers\Shop\ReturnRequestController;

Route::group([
    'middleware' => ['web', 'locale', 'theme', 'currency']
], function () {
    Route::prefix('rma/return-requests')->group(function () {
        Route::get('', [ReturnRequestController::class, 'index'])
            ->name('shop.rma.return-requests.index');

        Route::post('', [ReturnRequestController::class, 'store'])
            ->name('shop.rma.return-requests.store');
    });
});
ルートミドルウェア
ミドルウェア目的
webセッション管理、CSRF 保護
admin管理者認証
locale言語処理
themeテーマ解決
currency通貨処理

コントローラー

ベースコントローラー

ファイル: packages/Webkul/RMA/src/Http/Controllers/Controller.php

<?php

namespace Webkul\RMA\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
管理者コントローラー

ファイル: packages/Webkul/RMA/src/Http/Controllers/Admin/ReturnRequestController.php

<?php

namespace Webkul\RMA\Http\Controllers\Admin;

use Webkul\RMA\Http\Controllers\Controller;
use Webkul\RMA\Repositories\ReturnRequestRepository;
use Webkul\RMA\DataGrids\Admin\ReturnRequestDataGrid;

class ReturnRequestController extends Controller
{
    public function __construct(
        protected ReturnRequestRepository $returnRequestRepository
    ) {}

    public function index()
    {
        if (request()->ajax()) {
            return datagrid(ReturnRequestDataGrid::class)->process();
        }

        return view('rma::admin.return-requests.index');
    }

    public function show(int $id)
    {
        $returnRequest = $this->returnRequestRepository->findOrFail($id);

        return view('rma::admin.return-requests.show', compact('returnRequest'));
    }

    public function store(Request $request)
    {
        $data = $request->validate([
            'customer_id' => 'required|integer',
            'order_id' => 'required|integer',
            'product_sku' => 'required|string',
            'product_name' => 'required|string',
            'product_quantity' => 'required|integer|min:1',
            'reason' => 'nullable|string',
        ]);

        $this->returnRequestRepository->create($data);

        return redirect()->route('admin.rma.return-requests.index')
            ->with('success', 'Return request created successfully.');
    }

    public function update(Request $request, int $id)
    {
        $data = $request->validate([
            'status' => 'required|string|in:pending,approved,rejected',
            'admin_notes' => 'nullable|string',
        ]);

        $this->returnRequestRepository->update($data, $id);

        return redirect()->back()->with('success', 'Return request updated.');
    }

    public function destroy(int $id)
    {
        $this->returnRequestRepository->delete($id);

        return redirect()->back()->with('success', 'Return request deleted.');
    }

    public function massDestroy()
    {
        $indices = request()->input('indices');

        foreach ($indices as $index) {
            $this->returnRequestRepository->delete($index);
        }

        return response()->json(['message' => 'Selected records deleted.']);
    }
}
ショップコントローラー

ファイル: packages/Webkul/RMA/src/Http/Controllers/Shop/ReturnRequestController.php

<?php

namespace Webkul\RMA\Http\Controllers\Shop;

use Webkul\RMA\Http\Controllers\Controller;
use Webkul\RMA\Repositories\ReturnRequestRepository;

class ReturnRequestController extends Controller
{
    public function __construct(
        protected ReturnRequestRepository $returnRequestRepository
    ) {}

    public function index()
    {
        $returnRequests = $this->returnRequestRepository->findWhere([
            'customer_id' => auth()->id()
        ]);

        return view('rma::shop.return-requests.index', compact('returnRequests'));
    }

    public function store(Request $request)
    {
        $data = $request->validate([
            'order_id' => 'required|integer',
            'product_sku' => 'required|string',
            'product_name' => 'required|string',
            'product_quantity' => 'required|integer|min:1',
            'reason' => 'required|string',
        ]);

        $data['customer_id'] = auth()->id();
        $data['status'] = 'pending';

        $this->returnRequestRepository->create($data);

        return redirect()->back()->with('success', 'Return request submitted.');
    }
}

ビュー

Blade 自体を作成していますか? マークアップのルールについては、blade-conventions スキルを参照してください。 属性バインディングの : と ::、匿名コンポーネントと Vue ベースのコンポーネント、インデント、コメントの スタイル、翻訳と view_render_event フックを配置する場所について説明しています。

管理者レイアウト
<x-admin::layouts>
    <x-slot:title>
        @lang('rma::app.admin.return-requests.title')
    </x-slot:title>

    
</x-admin::layouts>
ショップレイアウト
<x-shop::layouts>
    <x-slot:title>
        @lang('rma::app.shop.return-requests.title')
    </x-slot:title>

    
</x-shop::layouts>
管理者一覧ビュー

ファイル: packages/Webkul/RMA/src/Resources/views/admin/return-requests/index.blade.php

<x-admin::layouts>
    <x-slot:title>
        @lang('rma::app.admin.return-requests.title')
    </x-slot:title>

    <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
        <p class="text-xl text-gray-800 dark:text-white font-bold">
            @lang('rma::app.admin.return-requests.title')
        </p>
    </div>

    <x-admin::datagrid :src="route('admin.rma.return-requests.index')" />
</x-admin::layouts>
管理者詳細ビュー

ファイル: packages/Webkul/RMA/src/Resources/views/admin/return-requests/show.blade.php

<x-admin::layouts>
    <x-slot:title>
        @lang('rma::app.admin.return-requests.show.title')
    </x-slot:title>

    <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
        <p class="text-xl text-gray-800 dark:text-white font-bold">
            @lang('rma::app.admin.return-requests.show.title') #{{ $returnRequest->id }}
        </p>
    </div>

    <div class="flex gap-2.5 mt-3.5 max-xl:flex-wrap">
        <div class="flex flex-col gap-2 flex-1 max-xl:flex-auto">
            <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
                <p class="text-base text-gray-800 dark:text-white font-semibold mb-4">
                    @lang('rma::app.admin.return-requests.show.general-info')
                </p>

                <div class="grid grid-cols-2 gap-4">
                    <div>
                        <p class="text-gray-600 dark:text-gray-300 font-semibold">
                            @lang('rma::app.admin.return-requests.show.product-name'):
                        </p>
                        <p class="text-gray-800 dark:text-white">
                            {{ $returnRequest->product_name }}
                        </p>
                    </div>

                    <div>
                        <p class="text-gray-600 dark:text-gray-300 font-semibold">
                            @lang('rma::app.admin.return-requests.show.status'):
                        </p>
                        <span class="badge label-info">
                            {{ ucfirst($returnRequest->status) }}
                        </span>
                    </div>
                </div>
            </div>
        </div>
    </div>
</x-admin::layouts>

@features: パッケージ開発 - 機能

ローカライズ

翻訳ファイルの作成

ファイル: packages/Webkul/RMA/src/Resources/lang/en/app.php

<?php

return [
    'admin' => [
        'return-requests' => [
            'title' => 'RMA Listing',
            'datagrid' => [
                'id' => 'ID',
                'product-name' => 'Product Name',
                'status' => 'Status',
                'view' => 'View',
            ],
        ],
    ],
];
翻訳の読み込み

サービスプロバイダーの boot() メソッド内で:

$this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'rma');
翻訳の使用

@lang('rma::app.admin.return-requests.title')
// In controllers/code
trans('rma::app.admin.return-requests.title')
__('rma::app.admin.return-requests.title')
翻訳の公開(任意)
public function boot(): void
{
    $this->publishes([
        __DIR__ . '/../Resources/lang' => resource_path('lang/vendor/rma'),
    ], 'rma-translations');
}

ユーザーは次のコマンドを実行できます:

php artisan vendor:publish --tag=rma-translations

DataGrid

DataGrid の作成

ファイル: packages/Webkul/RMA/src/DataGrids/Admin/ReturnRequestDataGrid.php

<?php

namespace Webkul\RMA\DataGrids\Admin;

use Illuminate\Support\Facades\DB;
use Webkul\DataGrid\DataGrid;

class ReturnRequestDataGrid extends DataGrid
{
    public function prepareQueryBuilder()
    {
        $queryBuilder = DB::table('rma_requests')
            ->select('id', 'product_name', 'status', 'created_at');

        return $queryBuilder;
    }

    public function prepareColumns()
    {
        $this->addColumn([
            'index' => 'id',
            'label' => trans('rma::app.admin.return-requests.datagrid.id'),
            'type' => 'integer',
            'sortable' => true,
            'filterable' => false,
        ]);

        $this->addColumn([
            'index' => 'product_name',
            'label' => trans('rma::app.admin.return-requests.datagrid.product-name'),
            'type' => 'string',
            'sortable' => true,
            'filterable' => true,
        ]);

        $this->addColumn([
            'index' => 'status',
            'label' => trans('rma::app.admin.return-requests.datagrid.status'),
            'type' => 'string',
            'sortable' => true,
            'filterable' => true,
            'filterable_type' => 'dropdown',
            'filterable_options' => [
                ['label' => 'Pending', 'value' => 'pending'],
                ['label' => 'Approved', 'value' => 'approved'],
                ['label' => 'Rejected', 'value' => 'rejected'],
            ],
            'closure' => function ($row) {
                return "<span class='badge label-info'>" . ucfirst($row->status) . "</span>";
            },
        ]);
    }

    public function prepareActions()
    {
        $this->addAction([
            'icon' => 'icon-view',
            'title' => trans('rma::app.admin.return-requests.datagrid.view'),
            'method' => 'GET',
            'url' => function ($row) {
                return route('admin.rma.return-requests.show', $row->id);
            },
        ]);
    }

    public function prepareMassActions()
    {
        $this->addMassAction([
            'icon' => 'icon-delete',
            'title' => trans('rma::app.admin.return-requests.datagrid.mass-delete'),
            'method' => 'POST',
            'url' => route('admin.rma.return-requests.mass-delete'),
        ]);
    }
}
列のオプション
オプション目的
indexデータベースの列名
label列ヘッダーのテキスト
typeデータ型(文字列、整数、日付など)
sortable並べ替えを有効化
filterableフィルタリングを有効化
filterable_typeフィルターの種類(ドロップダウン、日付範囲)
closureカスタム書式設定関数
コントローラーでの DataGrid の使用
public function index()
{
    if (request()->ajax()) {
        return datagrid(ReturnRequestDataGrid::class)->process();
    }

    return view('rma::admin.return-requests.index');
}
ビューでの DataGrid の表示
<x-admin::datagrid :src="route('admin.rma.return-requests.index')" />

管理者メニュー

メニュー設定の作成

ファイル: packages/Webkul/RMA/src/Config/admin-menu.php

<?php

return [
    [
        'key' => 'rma',
        'name' => 'rma::app.admin.menu.rma',
        'route' => 'admin.rma.return-requests.index',
        'sort' => 100,
        'icon' => 'icon-rma',
    ],
    [
        'key' => 'rma.return-requests',
        'name' => 'rma::app.admin.menu.return-requests',
        'route' => 'admin.rma.return-requests.index',
        'sort' => 1,
    ],
];
メニューの登録

サービスプロバイダーの register() メソッド内で:

$this->mergeConfigFrom(
    dirname(__DIR__) . '/Config/admin-menu.php',
    'menu.admin'
);

アクセス制御リスト(ACL)

ACL 設定の作成

ファイル: packages/Webkul/RMA/src/Config/acl.php

<?php

return [
    [
        'key' => 'rma',
        'name' => 'rma::app.admin.acl.rma',
        'route' => 'admin.rma.return-requests.index',
        'sort' => 1,
    ],
    [
        'key' => 'rma.return-requests',
        'name' => 'rma::app.admin.acl.return-requests',
        'route' => 'admin.rma.return-requests.index',
        'sort' => 1,
    ],
    [
        'key' => 'rma.return-requests.view',
        'name' => 'rma::app.admin.acl.view',
        'route' => 'admin.rma.return-requests.show',
        'sort' => 1,
    ],
];
ACL の登録

サービスプロバイダーの register() メソッド内で:

$this->mergeConfigFrom(
    dirname(__DIR__) . '/Config/acl.php',
    'acl'
);
権限の確認
// In controller
if (! bouncer()->hasPermission('rma')) {
    abort(401, 'Unauthorized access.');
}

@if (bouncer()->hasPermission('rma'))
    
@endif

システム設定

設定の作成

ファイル: packages/Webkul/RMA/src/Config/system.php

<?php

return [
    [
        'key' => 'rma',
        'name' => 'rma::app.admin.system.rma',
        'info' => 'rma::app.admin.system.rma-info',
        'sort' => 1,
    ],
    [
        'key' => 'rma.settings',
        'name' => 'rma::app.admin.system.general-settings',
        'info' => 'rma::app.admin.system.general-settings-info',
        'icon' => 'settings/settings.svg',
        'sort' => 1,
    ],
    [
        'key' => 'rma.settings.general',
        'name' => 'rma::app.admin.system.rma-configuration',
        'info' => 'rma::app.admin.system.rma-configuration-info',
        'sort' => 1,
        'fields' => [
            [
                'name' => 'enable',
                'title' => 'rma::app.admin.system.enable-rma',
                'type' => 'boolean',
            ],
            [
                'name' => 'allow_partial_returns',
                'title' => 'rma::app.admin.system.allow-partial-returns',
                'type' => 'boolean',
            ],
            [
                'name' => 'max_return_days',
                'title' => 'rma::app.admin.system.max-return-days',
                'type' => 'number',
                'validation' => 'numeric|min:1',
            ],
            [
                'name' => 'default_status',
                'title' => 'rma::app.admin.system.default-status',
                'type' => 'select',
                'options' => [
                    ['title' => 'Pending', 'value' => 'pending'],
                    ['title' => 'Approved', 'value' => 'approved'],
                ],
            ],
        ],
    ],
];
設定の登録

サービスプロバイダーの register() メソッド内で:

$this->mergeConfigFrom(
    dirname(__DIR__) . '/Config/system.php',
    'core'
);
フィールドの種類
種類目的
textテキスト入力
passwordパスワード入力
number数値入力
boolean有効/無効の切り替え
selectドロップダウン選択
multiselect複数選択ドロップダウン
textareaテキストエリア
editorリッチテキストエディター(TinyMCE)
image画像のアップロード
fileファイルのアップロード
country国のドロップダウン
state州のドロップダウン(国に依存)
colorカラーピッカー
依存フィールド
[
    'name' => 'enable_policy',
    'title' => 'Enable Return Policy',
    'type' => 'boolean',
], [
    'name' => 'max_return_days',
    'title' => 'Maximum Return Days',
    'type' => 'number',
    'depends' => 'enable_policy:1',  // Show only when enable_policy is 1
],
設定値の使用
// In controller
$isEnabled = core()->getConfigData('rma.settings.general.enable');
$maxDays = core()->getConfigData('rma.settings.general.max_return_days');

@if (core()->getConfigData('rma.settings.general.enable'))
    
@endif

主要ファイルの参照

ファイル用途
src/Providers/ServiceProvider.phpメインのサービスプロバイダー
src/Providers/ModuleServiceProvider.phpConcord モデルの登録
src/manifest.phpパッケージのメタデータ
src/Database/Migrations/マイグレーションファイル
src/Contracts/モデルのコントラクトインターフェース
src/Models/Eloquent モデル
src/Models/*Proxy.phpConcord モデルのプロキシ
src/Repositories/リポジトリークラス
src/Routes/admin-routes.php管理画面のルート
src/Routes/shop-routes.phpショップのルート
src/Http/Controllers/コントローラー
src/Resources/views/Blade テンプレート
src/Resources/lang/翻訳ファイル
src/DataGrids/Admin/DataGrid クラス
src/Config/admin-menu.phpメニュー設定
src/Config/acl.phpACL 権限
src/Config/system.phpシステム設定

よくある落とし穴

  • パッケージを追加した後に composer dump-autoload を実行し忘れる
  • bootstrap/providers.php にサービスプロバイダーを登録しない
  • 変更後にキャッシュをクリアしない
  • PSR-4 オートローディングで名前空間を誤る
  • テーブル名にパッケージのプレフィックスを使用しない
  • ModuleServiceProvider にモデルを登録しない
  • サービスプロバイダーで設定をマージしない
  • 翻訳キーではなく、ハードコードされたテキストを使用する
  • コントローラー/ビューで権限を確認しない

コメント