laravel-patterns by affaan-m/everything-claude-code
npx skills add https://github.com/affaan-m/everything-claude-code --skill laravel-patterns适用于可扩展、可维护应用程序的生产级 Laravel 架构模式。
config/* 中,并保持环境配置明确。使用具有清晰层级边界(HTTP、服务/操作、模型)的常规 Laravel 布局。
app/
├── Actions/ # 单一用途的用例
├── Console/
├── Events/
├── Exceptions/
├── Http/
│ ├── Controllers/
│ ├── Middleware/
│ ├── Requests/ # 表单请求验证
│ └── Resources/ # API 资源
├── Jobs/
├── Models/
├── Policies/
├── Providers/
├── Services/ # 协调领域服务
└── Support/
config/
database/
├── factories/
├── migrations/
└── seeders/
resources/
├── views/
└── lang/
routes/
├── api.php
├── web.php
└── console.php
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
保持控制器精简。将编排逻辑放在服务中,将单一用途的逻辑放在操作中。
final class CreateOrderAction
{
public function __construct(private OrderRepository $orders) {}
public function handle(CreateOrderData $data): Order
{
return $this->orders->create($data);
}
}
final class OrdersController extends Controller
{
public function __construct(private CreateOrderAction $createOrder) {}
public function store(StoreOrderRequest $request): JsonResponse
{
$order = $this->createOrder->handle($request->toDto());
return response()->json([
'success' => true,
'data' => OrderResource::make($order),
'error' => null,
'meta' => null,
], 201);
}
}
为了清晰起见,优先使用路由模型绑定和资源控制器。
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('projects', ProjectController::class);
});
使用范围绑定来防止跨租户访问。
Route::scopeBindings()->group(function () {
Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']);
});
保持前缀和路径一致,避免双重嵌套(例如 conversation 与 conversations)。
使用与绑定模型匹配的单个参数名称(例如,Conversation 模型使用 {conversation})。
嵌套时优先使用范围绑定以强制执行父子关系。
use App\Http\Controllers\Api\ConversationController; use App\Http\Controllers\Api\MessageController; use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->prefix('conversations')->group(function () { Route::post('/', [ConversationController::class, 'store'])->name('conversations.store');
Route::scopeBindings()->group(function () {
Route::get('/{conversation}', [ConversationController::class, 'show'])
->name('conversations.show');
Route::post('/{conversation}/messages', [MessageController::class, 'store'])
->name('conversation-messages.store');
Route::get('/{conversation}/messages/{message}', [MessageController::class, 'show'])
->name('conversation-messages.show');
});
});
如果希望参数解析为不同的模型类,请定义显式绑定。对于自定义绑定逻辑,请使用 Route::bind() 或在模型上实现 resolveRouteBinding()。
use App\Models\AiConversation;
use Illuminate\Support\Facades\Route;
Route::model('conversation', AiConversation::class);
在服务提供者中将接口绑定到实现,以实现清晰的依赖关系连接。
use App\Repositories\EloquentOrderRepository;
use App\Repositories\OrderRepository;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(OrderRepository::class, EloquentOrderRepository::class);
}
}
final class Project extends Model
{
use HasFactory;
protected $fillable = ['name', 'owner_id', 'status'];
protected $casts = [
'status' => ProjectStatus::class,
'archived_at' => 'datetime',
];
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_id');
}
public function scopeActive(Builder $query): Builder
{
return $query->whereNull('archived_at');
}
}
使用枚举或值对象进行严格类型化。
use Illuminate\Database\Eloquent\Casts\Attribute;
protected $casts = [
'status' => ProjectStatus::class,
];
protected function budgetCents(): Attribute
{
return Attribute::make(
get: fn (int $value) => Money::fromCents($value),
set: fn (Money $money) => $money->toCents(),
);
}
$orders = Order::query()
->with(['customer', 'items.product'])
->latest()
->paginate(25);
final class ProjectQuery
{
public function __construct(private Builder $query) {}
public function ownedBy(int $userId): self
{
$query = clone $this->query;
return new self($query->where('owner_id', $userId));
}
public function active(): self
{
$query = clone $this->query;
return new self($query->whereNull('archived_at'));
}
public function builder(): Builder
{
return $this->query;
}
}
使用全局作用域进行默认筛选,使用 SoftDeletes 处理可恢复的记录。对于相同的筛选条件,使用全局作用域或命名作用域中的一种,除非您打算实现分层行为。
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;
final class Project extends Model
{
use SoftDeletes;
protected static function booted(): void
{
static::addGlobalScope('active', function (Builder $builder): void {
$builder->whereNull('archived_at');
});
}
}
use Illuminate\Database\Eloquent\Builder;
final class Project extends Model
{
public function scopeOwnedBy(Builder $query, int $userId): Builder
{
return $query->where('owner_id', $userId);
}
}
// 在服务、仓库等中使用
$projects = Project::ownedBy($user->id)->get();
use Illuminate\Support\Facades\DB;
DB::transaction(function (): void {
$order->update(['status' => 'paid']);
$order->items()->update(['paid_at' => now()]);
});
YYYY_MM_DD_HHMMSS_create_users_table.phpsnake_case 和复数形式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('orders', function (Blueprint $table): void {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->string('status', 32)->index();
$table->unsignedInteger('total_cents');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};
将验证逻辑放在表单请求中,并将输入转换为 DTO。
use App\Models\Order;
final class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can('create', Order::class) ?? false;
}
public function rules(): array
{
return [
'customer_id' => ['required', 'integer', 'exists:customers,id'],
'items' => ['required', 'array', 'min:1'],
'items.*.sku' => ['required', 'string'],
'items.*.quantity' => ['required', 'integer', 'min:1'],
];
}
public function toDto(): CreateOrderData
{
return new CreateOrderData(
customerId: (int) $this->validated('customer_id'),
items: $this->validated('items'),
);
}
}
使用资源和分页保持 API 响应的一致性。
$projects = Project::query()->active()->paginate(25);
return response()->json([
'success' => true,
'data' => ProjectResource::collection($projects->items()),
'error' => null,
'meta' => [
'page' => $projects->currentPage(),
'per_page' => $projects->perPage(),
'total' => $projects->total(),
],
]);
.env 中,配置保存在 config/*.php 中config:cache每周安装量
305
代码仓库
GitHub 星标数
102.1K
首次出现
8 天前
安全审计
已安装于
codex290
cursor257
amp254
gemini-cli254
opencode254
github-copilot254
Production-grade Laravel architecture patterns for scalable, maintainable applications.
config/* and keep environments explicit.Use a conventional Laravel layout with clear layer boundaries (HTTP, services/actions, models).
app/
├── Actions/ # Single-purpose use cases
├── Console/
├── Events/
├── Exceptions/
├── Http/
│ ├── Controllers/
│ ├── Middleware/
│ ├── Requests/ # Form request validation
│ └── Resources/ # API resources
├── Jobs/
├── Models/
├── Policies/
├── Providers/
├── Services/ # Coordinating domain services
└── Support/
config/
database/
├── factories/
├── migrations/
└── seeders/
resources/
├── views/
└── lang/
routes/
├── api.php
├── web.php
└── console.php
Keep controllers thin. Put orchestration in services and single-purpose logic in actions.
final class CreateOrderAction
{
public function __construct(private OrderRepository $orders) {}
public function handle(CreateOrderData $data): Order
{
return $this->orders->create($data);
}
}
final class OrdersController extends Controller
{
public function __construct(private CreateOrderAction $createOrder) {}
public function store(StoreOrderRequest $request): JsonResponse
{
$order = $this->createOrder->handle($request->toDto());
return response()->json([
'success' => true,
'data' => OrderResource::make($order),
'error' => null,
'meta' => null,
], 201);
}
}
Prefer route-model binding and resource controllers for clarity.
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('projects', ProjectController::class);
});
Use scoped bindings to prevent cross-tenant access.
Route::scopeBindings()->group(function () {
Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']);
});
Keep prefixes and paths consistent to avoid double nesting (e.g., conversation vs conversations).
Use a single parameter name that matches the bound model (e.g., {conversation} for Conversation).
Prefer scoped bindings when nesting to enforce parent-child relationships.
use App\Http\Controllers\Api\ConversationController; use App\Http\Controllers\Api\MessageController; use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->prefix('conversations')->group(function () { Route::post('/', [ConversationController::class, 'store'])->name('conversations.store');
Route::scopeBindings()->group(function () {
Route::get('/{conversation}', [ConversationController::class, 'show'])
->name('conversations.show');
Route::post('/{conversation}/messages', [MessageController::class, 'store'])
->name('conversation-messages.store');
Route::get('/{conversation}/messages/{message}', [MessageController::class, 'show'])
->name('conversation-messages.show');
});
If you want a parameter to resolve to a different model class, define explicit binding. For custom binding logic, use Route::bind() or implement resolveRouteBinding() on the model.
use App\Models\AiConversation;
use Illuminate\Support\Facades\Route;
Route::model('conversation', AiConversation::class);
Bind interfaces to implementations in a service provider for clear dependency wiring.
use App\Repositories\EloquentOrderRepository;
use App\Repositories\OrderRepository;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(OrderRepository::class, EloquentOrderRepository::class);
}
}
final class Project extends Model
{
use HasFactory;
protected $fillable = ['name', 'owner_id', 'status'];
protected $casts = [
'status' => ProjectStatus::class,
'archived_at' => 'datetime',
];
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_id');
}
public function scopeActive(Builder $query): Builder
{
return $query->whereNull('archived_at');
}
}
Use enums or value objects for strict typing.
use Illuminate\Database\Eloquent\Casts\Attribute;
protected $casts = [
'status' => ProjectStatus::class,
];
protected function budgetCents(): Attribute
{
return Attribute::make(
get: fn (int $value) => Money::fromCents($value),
set: fn (Money $money) => $money->toCents(),
);
}
$orders = Order::query()
->with(['customer', 'items.product'])
->latest()
->paginate(25);
final class ProjectQuery
{
public function __construct(private Builder $query) {}
public function ownedBy(int $userId): self
{
$query = clone $this->query;
return new self($query->where('owner_id', $userId));
}
public function active(): self
{
$query = clone $this->query;
return new self($query->whereNull('archived_at'));
}
public function builder(): Builder
{
return $this->query;
}
}
Use global scopes for default filtering and SoftDeletes for recoverable records. Use either a global scope or a named scope for the same filter, not both, unless you intend layered behavior.
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;
final class Project extends Model
{
use SoftDeletes;
protected static function booted(): void
{
static::addGlobalScope('active', function (Builder $builder): void {
$builder->whereNull('archived_at');
});
}
}
use Illuminate\Database\Eloquent\Builder;
final class Project extends Model
{
public function scopeOwnedBy(Builder $query, int $userId): Builder
{
return $query->where('owner_id', $userId);
}
}
// In service, repository etc.
$projects = Project::ownedBy($user->id)->get();
use Illuminate\Support\Facades\DB;
DB::transaction(function (): void {
$order->update(['status' => 'paid']);
$order->items()->update(['paid_at' => now()]);
});
YYYY_MM_DD_HHMMSS_create_users_table.phpsnake_case and plural by defaultuse 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('orders', function (Blueprint $table): void {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->string('status', 32)->index();
$table->unsignedInteger('total_cents');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};
Keep validation in form requests and transform inputs to DTOs.
use App\Models\Order;
final class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can('create', Order::class) ?? false;
}
public function rules(): array
{
return [
'customer_id' => ['required', 'integer', 'exists:customers,id'],
'items' => ['required', 'array', 'min:1'],
'items.*.sku' => ['required', 'string'],
'items.*.quantity' => ['required', 'integer', 'min:1'],
];
}
public function toDto(): CreateOrderData
{
return new CreateOrderData(
customerId: (int) $this->validated('customer_id'),
items: $this->validated('items'),
);
}
}
Keep API responses consistent with resources and pagination.
$projects = Project::query()->active()->paginate(25);
return response()->json([
'success' => true,
'data' => ProjectResource::collection($projects->items()),
'error' => null,
'meta' => [
'page' => $projects->currentPage(),
'per_page' => $projects->perPage(),
'total' => $projects->total(),
],
]);
.env and config in config/*.phpconfig:cache in productionWeekly Installs
305
Repository
GitHub Stars
102.1K
First Seen
8 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex290
cursor257
amp254
gemini-cli254
opencode254
github-copilot254
WordPress插件开发指南:架构、安全、设置API与生命周期管理
989 周安装
});