mcpField
P
package-development

package-development

github.com/bagisto开发13318 次安装2 周前更新
代码仓库

关于 package-development

此技能指导 Bagisto 扩展包的开发,供构建和维护 Bagisto 功能模块的开发者参考。内容涉及包结构、数据访问、路由、界面与代码规范。

主要功能

  1. 01规划包目录与结构
  2. 02实现迁移、模型和仓库
  3. 03创建管理端与商店路由
  4. 04构建控制器、视图和 DataGrid
  5. 05配置多语言、菜单与权限
  6. 06遵循 PHP 文档和格式规范

适用场景

  1. 01为 Bagisto 创建新扩展包
  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 技能。

多子句条件应换行

包含多个子句(两个或更多操作数通过 && / || 连接)的条件必须拆分为多行:( 单独占一行,每个子句各占一行且缩进一级,布尔运算符放在其连接的那一行开头,) { 回到语句的缩进位置闭合。

// 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();
}

此规则取决于子句数量,而非行长:即使两个子句都很短,也仍须换行。

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

elseif、while 以及返回复合布尔值的 return 同样适用此规则:

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

仅对一次调用取反的守卫子句(if (! $this->cart) {)属于单子句,应保持在同一行;单子句的 return、三元表达式和箭头函数也一样。

每个方法都要有文档块

没有例外,不论可见性如何——public、protected 和 private 都一样。没有文档块的方法都不完整,即使方法名似乎已经说明了一切。

描述应为完整句子:首字母大写,以句号结尾。通常写一行;只有在第一行无法完整表达时才写第二行。

/**
 * 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

类型信息应写在签名中。只有原生类型无法表达的信息才添加 @param / @return,例如数组结构或混合返回值:

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

每个属性也要有文档块

同样的规则也适用于类常量和属性——无论是 const、static、有类型还是无类型,也无论可见性如何。Bagisto 自带的模型会为 $table、$fillable 和 $casts 添加文档,新属性如果漏掉文档就会显得格外突兀。每个属性都要有自己的文档块,即使多个属性相邻声明也一样——描述两个相邻属性的一个文档块会导致第二个属性没有文档:

// 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;

**构造函数提升属性是例外。**它们属于参数,由构造函数自身的文档块涵盖——不要逐一为它们编写文档:

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

类文档块用于定义,而非叙述

用一两句话说明这个类是什么。不要描述它是如何出现的、取代了什么,或过去的方法为何不对——这些内容应放在提交说明中。

/**
 * 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. 抽象方法声明(trait 或基类所要求的契约)
  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
    {
        // …
    }
}

检查整个文件,而不只是你修改的行。 每次编辑一个类时,都要检查其完整的成员顺序,并修正所有已经放错位置的成员——比如夹在受保护方法中的公共辅助方法、没有文档块的属性、放在公共区块中的私有方法。在刚刚修改的文件中保留原有违规,与引入违规是同样的问题。

在同一可见性分组内,将相关方法放在一起(例如,将 getter 放在它封装的查询旁边),但不要为了实现这一点而重新排列现有成员——分组只用于打破平局,不是强制要求,也不值得为此大幅改动文件。

只注释代码无法表达的内容

上面的规则针对的是文档块。这条规则针对的是方法体内的说明性注释,过度注释往往就出现在这里。

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);

注释要简短——两到三行即可。详细描述症状的段落应放在提交说明或 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() 也没有问题——反对的是在数据层外查询表,而不是反对 facade 本身。

在仓储中限定所有面向卖家的查询范围

在 marketplace 中,访问卖家所有数据的仓储方法应将卖家 ID 作为第一个参数,并按该 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 使用由三个组件构成的模型系统:

  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类型转换
$with预加载关联关系

仓储

仓储模式

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

数据网格

创建数据网格

文件: 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自定义格式化函数
在控制器中使用数据网格
public function index()
{
    if (request()->ajax()) {
        return datagrid(ReturnRequestDataGrid::class)->process();
    }

    return view('rma::admin.return-requests.index');
}
在视图中显示数据网格
<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 中注册模型
  • 未在服务提供者中合并配置
  • 使用硬编码文本而非翻译键
  • 未在控制器/视图中检查权限

评论

支持 Markdown