laravel-maestro

📁 lpmatrix/skills 📅 9 days ago
3
总安装量
3
周安装量
#58911
全站排名
安装命令
npx skills add https://github.com/lpmatrix/skills --skill laravel-maestro

Agent 安装分布

amp 3
gemini-cli 3
github-copilot 3
codex 3
kimi-cli 3
opencode 3

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 match expressions and constructor property promotion). For PHP 7.4, replace match with switch and 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 final on service classes to prevent inheritance drift, but use judgment — final breaks 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 ValidationException for 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 float or double column types for money
  • Create a Money value object with add(), 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_key
  • data (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:

  1. Configuration: All business numbers in config/ with env overrides
  2. Service Layer: Prefer final class, constructor injection, no facades in methods
  3. State Safety: Invalid transitions blocked before DB transactions open
  4. Money Safety: Integer cents columns, Money value object, no floats
  5. Audit Trail: Structured logs for every state change and financial event
  6. Async Processing: Heavy operations (notifications, reports, exports) on queues
  7. Authorization: Laravel Policy classes for permissions; Service classes for business rules
  8. Validation: FormRequest for input; Service layer for business rule validation
  9. Testing: Feature tests proving policy outcomes and state machine correctness

Anti-Patterns

  • DB::raw() or DB::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() returns null after config is cached in production (php artisan config:cache). All environment variables must be read in config/ files and referenced via config() everywhere else.

  • Floats for monetary values. IEEE 754 makes 0.1 + 0.2 !== 0.3 in 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() and Notification::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.