Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Laravel architecture patterns, routing/controllers, Eloquent ORM, service layers, queues, events, caching, and API resources for production apps.
.claude/skills/loulanyue-laravel-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 76% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 77% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 64% | 0% |
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.phpKeep controllers thin. Put orchestration in services and single-purpose logic in actions.
phpfinal 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.
phpuse Illuminate\Support\Facades\Route; Route::middleware('auth:sanctum')->group(function () { Route::apiResource('projects', ProjectController::class); });
Use scoped bindings to prevent cross-tenant access.
phpRoute::scopeBindings()->group(function () { Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']); });
conversation vs conversations).{conversation} for Conversation).phpuse 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.
phpuse 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.
phpuse 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); } }
phpfinal 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.
phpuse Illuminate\Database\Eloquent\Casts\Attribute; protected $casts = [ 'status' => ProjectStatus::class, ];
phpprotected function budgetCents(): Attribute { return Attribute::make( get: fn (int $value) => Money::fromCents($value), set: fn (Money $money) => $money->toCents(), ); }
php$orders = Order::query() ->with(['customer', 'items.product']) ->latest() ->paginate(25);
phpfinal 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.
phpuse 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'); }); } }
phpuse 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();
phpuse 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 defaultphpuse 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.
phpuse 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.
php$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 production| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 15,406 | 11,389 | -26% | 1 | 1 | 0% | 2,879 | 5,055 | +76% | 0 | 0 | — |
case-02 | pass→pass | 14,154 | 9,216 | -35% | 1 | 1 | 0% | 2,636 | 4,325 | +64% | 0 | 0 | — |
case-03 | fail→pass | 13,884 | 7,660 | -45% | 1 | 1 | 0% | 2,596 | 4,082 | +57% | 0 | 0 | — |
case-04 | fail→fail | 17,384 | 16,379 | -6% | 1 | 1 | 0% | 3,184 | 5,748 | +81% | 0 | 0 | — |
case-05 | pass→pass | 11,276 | 7,022 | -38% | 1 | 1 | 0% | 2,084 | 3,874 | +86% | 0 | 0 | — |
case-06 | fail→pass | 12,707 | 7,306 | -43% | 1 | 1 | 0% | 2,310 | 3,971 | +72% | 0 | 0 | — |
case-07 | pass→pass | 9,709 | 6,243 | -36% | 1 | 1 | 0% | 1,816 | 3,753 | +107% | 0 | 0 | — |
case-08 | pass→pass | 12,677 | 11,117 | -12% | 1 | 1 | 0% | 2,374 | 4,813 | +103% | 0 | 0 | — |
case-09 | pass→pass | 9,376 | 7,205 | -23% | 1 | 1 | 0% | 1,879 | 4,096 | +118% | 0 | 0 | — |
case-10 | pass→pass | 11,067 | 7,099 | -36% | 1 | 1 | 0% | 1,922 | 3,930 | +104% | 0 | 0 | — |
case-11 | fail→pass | 13,744 | 8,482 | -38% | 1 | 1 | 0% | 2,368 | 4,190 | +77% | 0 | 0 | — |
case-12 | pass→pass | 16,324 | 7,017 | -57% | 1 | 1 | 0% | 3,188 | 3,927 | +23% | 0 | 0 | — |
case-13 | pass→pass | 13,053 | 6,663 | -49% | 1 | 1 | 0% | 2,242 | 3,824 | +71% | 0 | 0 | — |
case-14 | pass→pass | 11,932 | 10,684 | -10% | 1 | 1 | 0% | 2,041 | 4,551 | +123% | 0 | 0 | — |
case-15 | pass→pass | 13,628 | 10,941 | -20% | 1 | 1 | 0% | 2,479 | 4,712 | +90% | 0 | 0 | — |
case-16 | pass→pass | 17,527 | 12,607 | -28% | 1 | 1 | 0% | 3,000 | 4,881 | +63% | 0 | 0 | — |
case-17 | pass→pass | 14,956 | 10,150 | -32% | 1 | 1 | 0% | 2,430 | 4,350 | +79% | 0 | 0 | — |
case-18 | fail→pass | 15,940 | 11,413 | -28% | 1 | 1 | 0% | 2,875 | 4,725 | +64% | 0 | 0 | — |
case-19 | pass→pass | 24,020 | 8,499 | -65% | 1 | 1 | 0% | 2,206 | 4,053 | +84% | 0 | 0 | — |
case-20 | pass→pass | 9,875 | 7,677 | -22% | 1 | 1 | 0% | 1,639 | 3,928 | +140% | 0 | 0 | — |
case-21 | fail→fail | 9,015 | 5,575 | -38% | 1 | 1 | 0% | 1,825 | 3,758 | +106% | 0 | 0 | — |
case-22 | pass→pass | 6,476 | 3,665 | -43% | 1 | 1 | 0% | 1,036 | 3,190 | +208% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.