laravel-maestro
npx skills add https://github.com/lpmatrix/skills --skill laravel-maestro
Agent 安装分布
Skill 文档
Production Laravel Architecture
Transform business requirements into maintainable, production-ready Laravel applications. This skill enforces strict separation between HTTP, domain logic, and infrastructureâmaking business rules testable, configurable, and independent of framework details.
When to Apply
- Complex Workflows: Processes spanning multiple models or external APIs (checkouts, approvals, provisioning, imports)
- State Machines: Entities with strict lifecycle rules and guarded transitions (orders, documents, jobs, tasks)
- Policy Engines: Features with configurable rules, tiers, or permissions that change based on business strategy
- Financial Logic: Money handling, calculations, fee structures, or multi-party payments
- Notification Systems: Multi-channel, scheduled, or conditional user communications
- Refactoring: Moving from fat controllers or fat models to explicit service layers
Scope check: DDD overhead pays off at ~30+ use cases. For simpler apps, a well-structured MVC monolith is the right call. Don’t architect past your complexity.
Requirements: PHP 8.0+ (uses
matchexpressions and constructor property promotion). For PHP 7.4, replacematchwithswitchand drop constructor promotion.
Suggested Structure
app/
âââ Models/ # Eloquent only â relationships, casts, scopes
âââ Services/ # Domain operations
â âââ Orders/
â â âââ OrderProcessingService.php
â â âââ OrderCancellationService.php
â âââ Policies/
â âââ PricingPolicy.php
â âââ FulfillmentPolicy.php
âââ ValueObjects/ # Money, Address, DateRange
âââ Http/
âââ Controllers/ # Thin â delegate to Services
âââ Requests/ # FormRequest validation
Core Principles
1. Model the Domain, Not the Database
Eloquent models are active-record objects â they know about the database by design. Don’t fight that. Instead, keep business behavior out of them.
- Models (
app/Models/): Relationships, casts, scopes, accessors only. No business logic, no external API calls. - Services (
app/Services/{Domain}/): Coordinate models and external systems. - Value Objects: Immutable PHP classes for complex concepts (
Money,Address,DateRange). Never raw primitives for these.
AI Instruction: When the user describes a business entity or workflow, STOP. Create the Service class defining operations BEFORE creating migrations or models.
2. Services Orchestrate, Controllers Delegate
Controllers handle HTTP concerns. Services handle business concerns. The line is: if it’s not about a request/response, it doesn’t belong in a controller.
final class OrderProcessingService
{
public function __construct(
private InventoryService $inventory,
private PaymentGateway $payments,
private NotificationDispatcher $notifications,
private AuditLogger $logger,
) {}
public function fulfill(Order $order): Order
{
// 1. Validate state (throw before transactions)
// 2. Execute in DB transaction
// 3. Trigger side effects (events, notifications)
// 4. Return mutated entity
}
}
Rules:
- Prefer
finalon service classes to prevent inheritance drift, but use judgment âfinalbreaks Mockery’s default proxy strategy, so weigh that against your testing approach. - Constructor injection only â no
app(),resolve(), or Facade calls inside methods. - Method names describe business events (
submitForReview,approve,processRefund), not CRUD verbs. - Throw
ValidationExceptionfor invalid business states before opening database transactions.
3. Configuration Over Code
Business rules change. Code should not have to.
// config/orders.php (or any domain-appropriate name)
return [
'orders' => [
'auto_cancel_hours' => env('ORDER_AUTO_CANCEL_HOURS', 48),
'max_revision_count' => 3,
],
'commissions' => [
'standard_rate' => 0.15,
'vip_rate' => 0.10,
],
'notifications' => [
'reminder_hours_before' => [24, 2],
'escalation_delay_minutes' => 30,
],
];
AI Instruction: When the user mentions “policy”, “rules”, or “fees”, extract values to a domain-appropriate config file with sensible defaults immediately. Reference config keys in implementation â never hardcode business numbers.
4. Explicit State Machines
Entities with status fields must have guarded transitions. Status as a free-form string field with no transition logic is a bug waiting to happen.
public function transitionStatus(Entity $entity, string $newStatus, User $actor): void
{
$allowed = match($entity->status) {
'draft' => ['pending', 'cancelled'],
'pending' => ['approved', 'rejected'],
'approved' => ['published', 'archived'],
default => [],
};
throw_if(
!in_array($newStatus, $allowed),
ValidationException::withMessages([
'status' => "Cannot transition from {$entity->status} to {$newStatus}"
])
);
DB::transaction(function () use ($entity, $newStatus, $actor) {
$entity->update(['status' => $newStatus]);
$this->logger->recordTransition($entity, $newStatus, $actor);
$this->notifications->statusChanged($entity);
});
}
Pattern:
- Define allowed transitions explicitly
- Validate current state before the transaction
- Log all transitions with context (who, when, fromâto)
- Side effects (notifications, events) happen inside the same transaction
5. Money as Value Objects
Floating-point arithmetic is non-deterministic for currency. 0.1 + 0.2 !== 0.3 in PHP as in any IEEE 754 language.
Rules:
- Store money as integer cents (or smallest currency unit) in the database
- Never use
floatordoublecolumn types for money - Create a
Moneyvalue object withadd(),subtract(),multiply(),allocate() - Fees and commissions live in dedicated service/policy classes, not inline arithmetic
// Schema
$table->integer('amount_cents');
$table->integer('platform_fee_cents');
$table->integer('net_amount_cents');
AI Instruction: When handling prices, fees, or calculations, STOP. Create a Money Value Object and use integer columns. Reject any suggestion of float or double for monetary values.
6. Policy Services for Dynamic Rules
if/else chains based on user attributes scattered across controllers are a maintenance trap. Encapsulate conditional logic that varies by context.
final class PricingPolicy
{
public function calculateFee(Order $order, User $user): Money
{
$rate = match($user->tier) {
'enterprise' => config('business.commissions.vip_rate'),
default => config('business.commissions.standard_rate'),
};
return $order->amount->multiply($rate);
}
public function canBypassApproval(User $user): bool
{
return $user->trust_score > 90 && $user->isVerified();
}
}
Policy services handle conditional business rules. Laravel’s built-in Policy classes handle authorization. Keep these distinct â conflating them creates confusion.
7. Notifications as Structured Events
Don’t call Mail::send() or Notification::send() inline in services. Schedule notifications as domain events that a queue worker processes asynchronously.
Schema:
notifiable_id,notifiable_type(polymorphic)channel(sms, email, push),template_keydata(json payload)scheduled_at,sent_at,failed_at
Service API:
$dispatcher->sendNow($user, new WelcomeMessage($context));
$dispatcher->schedule($user, new Reminder($order), $order->due_date->subDay());
$dispatcher->delay($user, new FollowUp($survey), now()->addWeek());
AI Instruction: When implementing notifications, create a database-backed queue with scheduling. Never dispatch immediately inside a business service.
8. Thin Controllers, Rich Requests
A controller method should do exactly three things: validate input (via FormRequest), call a service, return a response.
class OrderController extends Controller
{
public function __construct(
private OrderProcessingService $processor,
) {}
/** Create a new order and return it with its items. */
public function store(CreateOrderRequest $request): JsonResponse
{
$order = $this->processor->create(
user: $request->user(),
items: $request->validated('items'),
shippingAddress: new Address($request->validated('shipping')),
);
return response()->json($order->load('items'), 201);
}
/** Transition an order to the approved state. */
public function approve(Order $order): JsonResponse
{
$this->authorize('approve', $order);
return response()->json(
$this->processor->approve($order, auth()->user())
);
}
}
Rules:
- Every controller method except the constructor must have a one-line docblock describing its intent
- FormRequest classes for all input validation â never
$request->validate()inline - Never
$request->all()or$request->except()â security risk, bypasses validation - Return API Resources or DTOs, not raw Eloquent models
- No config values, mailers, or third-party API clients directly in controllers
9. Feature Testing Over Unit Testing
Test business outcomes and policy enforcement, not implementation internals.
Required coverage:
- Happy paths: complete workflows end-to-end
- Policy enforcement: fees calculated correctly, permissions respected
- State guards: invalid transitions return 422
- Side effects: notifications queued, payments processed, logs written
/** @test */
public function it_applies_late_fee_when_cancelling_within_24_hours(): void
{
$user = User::factory()->create(['tier' => 'basic']);
$order = Order::factory()->forUser($user)->create([
'status' => 'confirmed',
'scheduled_at' => now()->addHours(12),
]);
$this->actingAs($user)
->postJson("/orders/{$order->id}/cancel")
->assertOk();
$this->assertDatabaseHas('orders', [
'id' => $order->id,
'status' => 'cancelled',
'cancellation_fee_cents' => 500,
]);
}
10. Structured Logging
Every significant business event needs an audit trail. Use structured log data â not narrative strings.
Log::info('order_fulfilled', [
'order_id' => $order->id,
'user_id' => $order->user_id,
'amount_cents' => $order->amount->toCents(),
'processor_fee_cents' => $fee->toCents(),
'duration_seconds' => $timer->elapsed(),
]);
Log: entity lifecycle transitions, payment authorizations, permission checks, external API calls, notification attempts.
Implementation Checklist
Before marking a feature complete:
- Configuration: All business numbers in
config/with env overrides - Service Layer: Prefer
finalclass, constructor injection, no facades in methods - State Safety: Invalid transitions blocked before DB transactions open
- Money Safety: Integer cents columns,
Moneyvalue object, no floats - Audit Trail: Structured logs for every state change and financial event
- Async Processing: Heavy operations (notifications, reports, exports) on queues
- Authorization: Laravel
Policyclasses for permissions; Service classes for business rules - Validation:
FormRequestfor input; Service layer for business rule validation - Testing: Feature tests proving policy outcomes and state machine correctness
Anti-Patterns
-
DB::raw()orDB::table()->update()in service classes. These bypass Eloquent model events entirely â observers, casts, and event listeners won’t fire. Always go through the model. -
$request->all()or$request->except()in controllers. Mass-assignment with unvalidated input is a security hole. Use$request->validated()exclusively, which only returns fields your FormRequest declared. -
Business logic in Eloquent accessors, mutators, or
booted()hooks. These fire implicitly and are nearly impossible to test in isolation. If it’s a business rule, it belongs in a service. -
env()calls outside of config files.env()returnsnullafter config is cached in production (php artisan config:cache). All environment variables must be read inconfig/files and referenced viaconfig()everywhere else. -
Floats for monetary values. IEEE 754 makes
0.1 + 0.2 !== 0.3in any language. Store cents as integers; compute with integers. -
Role checks scattered in controllers.
if ($user->role === 'admin')repeated across controllers means business rules live in HTTP layer. Centralize in Policy classes so the rule has one home. -
Synchronous mail or notification dispatch in services.
Mail::send()andNotification::send()block the request and couple delivery to the transaction. Use a queued dispatcher. -
Third-party API clients instantiated directly in controllers. Wrap external clients in a service adapter. Controllers should never know which HTTP client, SDK, or vendor you’re using.
-
Repository pattern wrapping Eloquent. Eloquent is already an active record implementation. A Repository layer on top adds indirection without benefit unless you’re swapping database drivers (rare) or doing full Event Sourcing with domain aggregates. Neither of those is this skill’s scope.