关于 package-development
此技能指导 Bagisto 扩展包的开发,供构建和维护 Bagisto 功能模块的开发者参考。内容涉及包结构、数据访问、路由、界面与代码规范。
主要功能
- 01规划包目录与结构
- 02实现迁移、模型和仓库
- 03创建管理端与商店路由
- 04构建控制器、视图和 DataGrid
- 05配置多语言、菜单与权限
- 06遵循 PHP 文档和格式规范
适用场景
- 01为 Bagisto 创建新扩展包
- 02添加包内数据库迁移和数据访问
- 03开发管理端或商店页面
- 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 自身类使用的顺序排列类成员,让新成员出现在读者预期的位置,而不是因为修改差异最容易而随意放置:
- 常量
- 属性
- 构造函数
- 抽象方法声明(trait 或基类所要求的契约)
- 公共方法
- 受保护方法
- 私有方法
可见性顺序最重要,而且它双向适用——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 使用由三个组件构成的模型系统:
- 契约 - 定义公共 API 的接口
- 模型 - Eloquent 模型实现
- 代理 - 通过 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.php | Concord 模型注册 |
src/manifest.php | 软件包元数据 |
src/Database/Migrations/ | 迁移文件 |
src/Contracts/ | 模型契约接口 |
src/Models/ | Eloquent 模型 |
src/Models/*Proxy.php | Concord 模型代理 |
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.php | ACL 权限 |
src/Config/system.php | 系统配置 |
常见问题
- 添加软件包后忘记运行
composer dump-autoload - 未在
bootstrap/providers.php中注册服务提供者 - 更改后未清除缓存
- PSR-4 自动加载中的命名空间不正确
- 未在表名中使用软件包前缀
- 未在 ModuleServiceProvider 中注册模型
- 未在服务提供者中合并配置
- 使用硬编码文本而非翻译键
- 未在控制器/视图中检查权限