mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-09 10:53:34 +08:00
feat: add laravel skills (#420)
* feat: add laravel skills * docs: fix laravel patterns example * docs: add laravel api example * docs: update readme and configure-ecc for laravel skills * docs: reference laravel skills in php rules * docs: add php import guidance * docs: expand laravel skills with more pattern, security, testing, and verification examples * docs: add laravel routing, security, testing, and sail guidance * docs: fix laravel example issues from code review * docs: fix laravel examples and skills per review findings * docs: resolve remaining laravel review fixes * docs: refine laravel patterns and tdd guidance * docs: clarify laravel queue healthcheck guidance * docs: fix laravel examples and test guidance * docs: correct laravel tdd and api example details * docs: align laravel form request auth semantics * docs: fix laravel coverage, imports, and scope guidance * docs: align laravel tdd and security examples with guidance * docs: tighten laravel form request authorization examples * docs: fix laravel tdd and queue job examples * docs: harden laravel rate limiting and policy examples * docs: fix laravel pagination, validation, and verification examples * docs: align laravel controller response with envelope * docs: strengthen laravel password validation example * docs: address feedback regarding examples * docs: improve guidance and examples for pest usage * docs: clarify laravel upload storage and authorization notes * docs: tighten up examples
This commit is contained in:
415
skills/laravel-patterns/SKILL.md
Normal file
415
skills/laravel-patterns/SKILL.md
Normal file
@@ -0,0 +1,415 @@
|
||||
---
|
||||
name: laravel-patterns
|
||||
description: Laravel architecture patterns, routing/controllers, Eloquent ORM, service layers, queues, events, caching, and API resources for production apps.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Laravel Development Patterns
|
||||
|
||||
Production-grade Laravel architecture patterns for scalable, maintainable applications.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Building Laravel web applications or APIs
|
||||
- Structuring controllers, services, and domain logic
|
||||
- Working with Eloquent models and relationships
|
||||
- Designing APIs with resources and pagination
|
||||
- Adding queues, events, caching, and background jobs
|
||||
|
||||
## How It Works
|
||||
|
||||
- Structure the app around clear boundaries (controllers -> services/actions -> models).
|
||||
- Use explicit bindings and scoped bindings to keep routing predictable; still enforce authorization for access control.
|
||||
- Favor typed models, casts, and scopes to keep domain logic consistent.
|
||||
- Keep IO-heavy work in queues and cache expensive reads.
|
||||
- Centralize config in `config/*` and keep environments explicit.
|
||||
|
||||
## Examples
|
||||
|
||||
### Project Structure
|
||||
|
||||
Use a conventional Laravel layout with clear layer boundaries (HTTP, services/actions, models).
|
||||
|
||||
### Recommended Layout
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
### Controllers -> Services -> Actions
|
||||
|
||||
Keep controllers thin. Put orchestration in services and single-purpose logic in actions.
|
||||
|
||||
```php
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Routing and Controllers
|
||||
|
||||
Prefer route-model binding and resource controllers for clarity.
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::apiResource('projects', ProjectController::class);
|
||||
});
|
||||
```
|
||||
|
||||
### Route Model Binding (Scoped)
|
||||
|
||||
Use scoped bindings to prevent cross-tenant access.
|
||||
|
||||
```php
|
||||
Route::scopeBindings()->group(function () {
|
||||
Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']);
|
||||
});
|
||||
```
|
||||
|
||||
### Nested Routes and Binding Names
|
||||
|
||||
- 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.
|
||||
|
||||
```php
|
||||
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.
|
||||
|
||||
```php
|
||||
use App\Models\AiConversation;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::model('conversation', AiConversation::class);
|
||||
```
|
||||
|
||||
### Service Container Bindings
|
||||
|
||||
Bind interfaces to implementations in a service provider for clear dependency wiring.
|
||||
|
||||
```php
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Eloquent Model Patterns
|
||||
|
||||
### Model Configuration
|
||||
|
||||
```php
|
||||
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');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Casts and Value Objects
|
||||
|
||||
Use enums or value objects for strict typing.
|
||||
|
||||
```php
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
||||
protected $casts = [
|
||||
'status' => ProjectStatus::class,
|
||||
];
|
||||
```
|
||||
|
||||
```php
|
||||
protected function budgetCents(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn (int $value) => Money::fromCents($value),
|
||||
set: fn (Money $money) => $money->toCents(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Eager Loading to Avoid N+1
|
||||
|
||||
```php
|
||||
$orders = Order::query()
|
||||
->with(['customer', 'items.product'])
|
||||
->latest()
|
||||
->paginate(25);
|
||||
```
|
||||
|
||||
### Query Objects for Complex Filters
|
||||
|
||||
```php
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Global Scopes and Soft Deletes
|
||||
|
||||
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.
|
||||
|
||||
```php
|
||||
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');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Query Scopes for Reusable Filters
|
||||
|
||||
```php
|
||||
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();
|
||||
```
|
||||
|
||||
### Transactions for Multi-Step Updates
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
DB::transaction(function (): void {
|
||||
$order->update(['status' => 'paid']);
|
||||
$order->items()->update(['paid_at' => now()]);
|
||||
});
|
||||
```
|
||||
|
||||
### Migrations
|
||||
|
||||
### Naming Convention
|
||||
|
||||
- File names use timestamps: `YYYY_MM_DD_HHMMSS_create_users_table.php`
|
||||
- Migrations use anonymous classes (no named class); the filename communicates intent
|
||||
- Table names are `snake_case` and plural by default
|
||||
|
||||
### Example Migration
|
||||
|
||||
```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('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');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Form Requests and Validation
|
||||
|
||||
Keep validation in form requests and transform inputs to DTOs.
|
||||
|
||||
```php
|
||||
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 Resources
|
||||
|
||||
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(),
|
||||
],
|
||||
]);
|
||||
```
|
||||
|
||||
### Events, Jobs, and Queues
|
||||
|
||||
- Emit domain events for side effects (emails, analytics)
|
||||
- Use queued jobs for slow work (reports, exports, webhooks)
|
||||
- Prefer idempotent handlers with retries and backoff
|
||||
|
||||
### Caching
|
||||
|
||||
- Cache read-heavy endpoints and expensive queries
|
||||
- Invalidate caches on model events (created/updated/deleted)
|
||||
- Use tags when caching related data for easy invalidation
|
||||
|
||||
### Configuration and Environments
|
||||
|
||||
- Keep secrets in `.env` and config in `config/*.php`
|
||||
- Use per-environment config overrides and `config:cache` in production
|
||||
Reference in New Issue
Block a user