From 113119dc6f35ced0fdbc4f4dd4184541bf0cc22f Mon Sep 17 00:00:00 2001 From: Sam Rook Date: Mon, 16 Mar 2026 20:35:23 +0000 Subject: [PATCH] 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 --- README.md | 9 +- examples/laravel-api-CLAUDE.md | 311 ++++++++++++++++++++ rules/php/coding-style.md | 5 + rules/php/patterns.md | 1 + rules/php/security.md | 4 + rules/php/testing.md | 7 +- skills/configure-ecc/SKILL.md | 11 +- skills/laravel-patterns/SKILL.md | 415 +++++++++++++++++++++++++++ skills/laravel-security/SKILL.md | 285 ++++++++++++++++++ skills/laravel-tdd/SKILL.md | 283 ++++++++++++++++++ skills/laravel-verification/SKILL.md | 179 ++++++++++++ 11 files changed, 1504 insertions(+), 6 deletions(-) create mode 100644 examples/laravel-api-CLAUDE.md create mode 100644 skills/laravel-patterns/SKILL.md create mode 100644 skills/laravel-security/SKILL.md create mode 100644 skills/laravel-tdd/SKILL.md create mode 100644 skills/laravel-verification/SKILL.md diff --git a/README.md b/README.md index 84f39f6a..5d0b2605 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,10 @@ everything-claude-code/ | |-- django-security/ # Django security best practices (NEW) | |-- django-tdd/ # Django TDD workflow (NEW) | |-- django-verification/ # Django verification loops (NEW) +| |-- laravel-patterns/ # Laravel architecture patterns (NEW) +| |-- laravel-security/ # Laravel security best practices (NEW) +| |-- laravel-tdd/ # Laravel TDD workflow (NEW) +| |-- laravel-verification/ # Laravel verification loops (NEW) | |-- python-patterns/ # Python idioms and best practices (NEW) | |-- python-testing/ # Python testing with pytest (NEW) | |-- springboot-patterns/ # Java Spring Boot patterns (NEW) @@ -411,6 +415,7 @@ everything-claude-code/ | |-- saas-nextjs-CLAUDE.md # Real-world SaaS (Next.js + Supabase + Stripe) | |-- go-microservice-CLAUDE.md # Real-world Go microservice (gRPC + PostgreSQL) | |-- django-api-CLAUDE.md # Real-world Django REST API (DRF + Celery) +| |-- laravel-api-CLAUDE.md # Real-world Laravel API (PostgreSQL + Redis) (NEW) | |-- rust-api-CLAUDE.md # Real-world Rust API (Axum + SQLx + PostgreSQL) (NEW) | |-- mcp-configs/ # MCP server configurations @@ -615,7 +620,7 @@ cp -r everything-claude-code/.agents/skills/* ~/.claude/skills/ cp -r everything-claude-code/skills/search-first ~/.claude/skills/ # Optional: add niche/framework-specific skills only when needed -# for s in django-patterns django-tdd springboot-patterns; do +# for s in django-patterns django-tdd laravel-patterns springboot-patterns; do # cp -r everything-claude-code/skills/$s ~/.claude/skills/ # done ``` @@ -869,7 +874,7 @@ Please contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. ### Ideas for Contributions - Language-specific skills (Rust, C#, Kotlin, Java) — Go, Python, Perl, Swift, and TypeScript already included -- Framework-specific configs (Rails, Laravel, FastAPI, NestJS) — Django, Spring Boot already included +- Framework-specific configs (Rails, FastAPI, NestJS) — Django, Spring Boot, Laravel already included - DevOps agents (Kubernetes, Terraform, AWS, Docker) - Testing strategies (different frameworks, visual regression) - Domain-specific knowledge (ML, data engineering, mobile) diff --git a/examples/laravel-api-CLAUDE.md b/examples/laravel-api-CLAUDE.md new file mode 100644 index 00000000..3a8c7174 --- /dev/null +++ b/examples/laravel-api-CLAUDE.md @@ -0,0 +1,311 @@ +# Laravel API — Project CLAUDE.md + +> Real-world example for a Laravel API with PostgreSQL, Redis, and queues. +> Copy this to your project root and customize for your service. + +## Project Overview + +**Stack:** PHP 8.2+, Laravel 11.x, PostgreSQL, Redis, Horizon, PHPUnit/Pest, Docker Compose + +**Architecture:** Modular Laravel app with controllers -> services -> actions, Eloquent ORM, queues for async work, Form Requests for validation, and API Resources for consistent JSON responses. + +## Critical Rules + +### PHP Conventions + +- `declare(strict_types=1)` in all PHP files +- Use typed properties and return types everywhere +- Prefer `final` classes for services and actions +- No `dd()` or `dump()` in committed code +- Formatting via Laravel Pint (PSR-12) + +### API Response Envelope + +All API responses use a consistent envelope: + +```json +{ + "success": true, + "data": {"...": "..."}, + "error": null, + "meta": {"page": 1, "per_page": 25, "total": 120} +} +``` + +### Database + +- Migrations committed to git +- Use Eloquent or query builder (no raw SQL unless parameterized) +- Index any column used in `where` or `orderBy` +- Avoid mutating model instances in services; prefer create/update through repositories or query builders + +### Authentication + +- API auth via Sanctum +- Use policies for model-level authorization +- Enforce auth in controllers and services + +### Validation + +- Use Form Requests for validation +- Transform input to DTOs for business logic +- Never trust request payloads for derived fields + +### Error Handling + +- Throw domain exceptions in services +- Map exceptions to HTTP responses in `bootstrap/app.php` via `withExceptions` +- Never expose internal errors to clients + +### Code Style + +- No emojis in code or comments +- Max line length: 120 characters +- Controllers are thin; services and actions hold business logic + +## File Structure + +``` +app/ + Actions/ + Console/ + Events/ + Exceptions/ + Http/ + Controllers/ + Middleware/ + Requests/ + Resources/ + Jobs/ + Models/ + Policies/ + Providers/ + Services/ + Support/ +config/ +database/ + factories/ + migrations/ + seeders/ +routes/ + api.php + web.php +``` + +## Key Patterns + +### Service Layer + +```php +orders->create($data); + } +} + +final class OrderService +{ + public function __construct(private CreateOrderAction $createOrder) {} + + public function placeOrder(CreateOrderData $data): Order + { + return $this->createOrder->handle($data); + } +} +``` + +### Controller Pattern + +```php +service->placeOrder($request->toDto()); + + return response()->json([ + 'success' => true, + 'data' => OrderResource::make($order), + 'error' => null, + 'meta' => null, + ], 201); + } +} +``` + +### Policy Pattern + +```php +user_id === $user->id; + } +} +``` + +### Form Request + DTO + +```php +user(); + } + + public function rules(): array + { + return [ + 'items' => ['required', 'array', 'min:1'], + 'items.*.sku' => ['required', 'string'], + 'items.*.quantity' => ['required', 'integer', 'min:1'], + ]; + } + + public function toDto(): CreateOrderData + { + return new CreateOrderData( + userId: (int) $this->user()->id, + items: $this->validated('items'), + ); + } +} +``` + +### API Resource + +```php + $this->id, + 'status' => $this->status, + 'total' => $this->total, + 'created_at' => $this->created_at?->toIso8601String(), + ]; + } +} +``` + +### Queue Job + +```php +findOrFail($this->orderId); + $mailer->sendOrderConfirmation($order); + } +} +``` + +### Test Pattern (Pest) + +```php +create(); + + actingAs($user); + + $response = postJson('/api/orders', [ + 'items' => [['sku' => 'sku-1', 'quantity' => 2]], + ]); + + $response->assertCreated(); + assertDatabaseHas('orders', ['user_id' => $user->id]); +}); +``` + +### Test Pattern (PHPUnit) + +```php +create(); + + $response = $this->actingAs($user)->postJson('/api/orders', [ + 'items' => [['sku' => 'sku-1', 'quantity' => 2]], + ]); + + $response->assertCreated(); + $this->assertDatabaseHas('orders', ['user_id' => $user->id]); + } +} +``` diff --git a/rules/php/coding-style.md b/rules/php/coding-style.md index 44d5f907..382ea9b4 100644 --- a/rules/php/coding-style.md +++ b/rules/php/coding-style.md @@ -25,6 +25,11 @@ paths: - Use **PHPStan** or **Psalm** for static analysis. - Keep Composer scripts checked in so the same commands run locally and in CI. +## Imports + +- Add `use` statements for all referenced classes, interfaces, and traits. +- Avoid relying on the global namespace unless the project explicitly prefers fully qualified names. + ## Error Handling - Throw exceptions for exceptional states; avoid returning `false`/`null` as hidden error channels in new code. diff --git a/rules/php/patterns.md b/rules/php/patterns.md index 6af81105..b9144746 100644 --- a/rules/php/patterns.md +++ b/rules/php/patterns.md @@ -30,3 +30,4 @@ paths: ## Reference See skill: `api-design` for endpoint conventions and response-shape guidance. +See skill: `laravel-patterns` for Laravel-specific architecture guidance. diff --git a/rules/php/security.md b/rules/php/security.md index b745fa17..d559b8a6 100644 --- a/rules/php/security.md +++ b/rules/php/security.md @@ -31,3 +31,7 @@ paths: - Use `password_hash()` / `password_verify()` for password storage. - Regenerate session identifiers after authentication and privilege changes. - Enforce CSRF protection on state-changing web requests. + +## Reference + +See skill: `laravel-security` for Laravel-specific security guidance. diff --git a/rules/php/testing.md b/rules/php/testing.md index 0adf9d6f..b069901c 100644 --- a/rules/php/testing.md +++ b/rules/php/testing.md @@ -11,7 +11,7 @@ paths: ## Framework -Use **PHPUnit** as the default test framework. **Pest** is also acceptable when the project already uses it. +Use **PHPUnit** as the default test framework. If **Pest** is configured in the project, prefer Pest for new tests and avoid mixing frameworks. ## Coverage @@ -29,6 +29,11 @@ Prefer **pcov** or **Xdebug** in CI, and keep coverage thresholds in CI rather t - Use factory/builders for fixtures instead of large hand-written arrays. - Keep HTTP/controller tests focused on transport and validation; move business rules into service-level tests. +## Inertia + +If the project uses Inertia.js, prefer `assertInertia` with `AssertableInertia` to verify component names and props instead of raw JSON assertions. + ## Reference See skill: `tdd-workflow` for the repo-wide RED -> GREEN -> REFACTOR loop. +See skill: `laravel-tdd` for Laravel-specific testing patterns (PHPUnit and Pest). diff --git a/skills/configure-ecc/SKILL.md b/skills/configure-ecc/SKILL.md index 53fe74a9..07109a30 100644 --- a/skills/configure-ecc/SKILL.md +++ b/skills/configure-ecc/SKILL.md @@ -82,12 +82,12 @@ If the user chooses niche or core + niche, continue to category selection below ### 2b: Choose Skill Categories -There are 7 selectable category groups below. The detailed confirmation lists that follow cover 41 skills across 8 categories, plus 1 standalone template. Use `AskUserQuestion` with `multiSelect: true`: +There are 7 selectable category groups below. The detailed confirmation lists that follow cover 45 skills across 8 categories, plus 1 standalone template. Use `AskUserQuestion` with `multiSelect: true`: ``` Question: "Which skill categories do you want to install?" Options: - - "Framework & Language" — "Django, Spring Boot, Go, Python, Java, Frontend, Backend patterns" + - "Framework & Language" — "Django, Laravel, Spring Boot, Go, Python, Java, Frontend, Backend patterns" - "Database" — "PostgreSQL, ClickHouse, JPA/Hibernate patterns" - "Workflow & Quality" — "TDD, verification, learning, security review, compaction" - "Research & APIs" — "Deep research, Exa search, Claude API patterns" @@ -101,7 +101,7 @@ Options: For each selected category, print the full list of skills below and ask the user to confirm or deselect specific ones. If the list exceeds 4 items, print the list as text and use `AskUserQuestion` with an "Install all listed" option plus "Other" for the user to paste specific names. -**Category: Framework & Language (17 skills)** +**Category: Framework & Language (21 skills)** | Skill | Description | |-------|-------------| @@ -111,6 +111,10 @@ For each selected category, print the full list of skills below and ask the user | `django-security` | Django security: auth, CSRF, SQL injection, XSS prevention | | `django-tdd` | Django testing with pytest-django, factory_boy, mocking, coverage | | `django-verification` | Django verification loop: migrations, linting, tests, security scans | +| `laravel-patterns` | Laravel architecture patterns: routing, controllers, Eloquent, queues, caching | +| `laravel-security` | Laravel security: auth, policies, CSRF, mass assignment, rate limiting | +| `laravel-tdd` | Laravel testing with PHPUnit and Pest, factories, fakes, coverage | +| `laravel-verification` | Laravel verification: linting, static analysis, tests, security scans | | `frontend-patterns` | React, Next.js, state management, performance, UI patterns | | `frontend-slides` | Zero-dependency HTML presentations, style previews, and PPTX-to-web conversion | | `golang-patterns` | Idiomatic Go patterns, conventions for robust Go applications | @@ -258,6 +262,7 @@ grep -rn "skills/" $TARGET/skills/ Some skills reference others. Verify these dependencies: - `django-tdd` may reference `django-patterns` +- `laravel-tdd` may reference `laravel-patterns` - `springboot-tdd` may reference `springboot-patterns` - `continuous-learning-v2` references `~/.claude/homunculus/` directory - `python-testing` may reference `python-patterns` diff --git a/skills/laravel-patterns/SKILL.md b/skills/laravel-patterns/SKILL.md new file mode 100644 index 00000000..ec59113d --- /dev/null +++ b/skills/laravel-patterns/SKILL.md @@ -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 diff --git a/skills/laravel-security/SKILL.md b/skills/laravel-security/SKILL.md new file mode 100644 index 00000000..653773c3 --- /dev/null +++ b/skills/laravel-security/SKILL.md @@ -0,0 +1,285 @@ +--- +name: laravel-security +description: Laravel security best practices for authn/authz, validation, CSRF, mass assignment, file uploads, secrets, rate limiting, and secure deployment. +origin: ECC +--- + +# Laravel Security Best Practices + +Comprehensive security guidance for Laravel applications to protect against common vulnerabilities. + +## When to Activate + +- Adding authentication or authorization +- Handling user input and file uploads +- Building new API endpoints +- Managing secrets and environment settings +- Hardening production deployments + +## How It Works + +- Middleware provides baseline protections (CSRF via `VerifyCsrfToken`, security headers via `SecurityHeaders`). +- Guards and policies enforce access control (`auth:sanctum`, `$this->authorize`, policy middleware). +- Form Requests validate and shape input (`UploadInvoiceRequest`) before it reaches services. +- Rate limiting adds abuse protection (`RateLimiter::for('login')`) alongside auth controls. +- Data safety comes from encrypted casts, mass-assignment guards, and signed routes (`URL::temporarySignedRoute` + `signed` middleware). + +## Core Security Settings + +- `APP_DEBUG=false` in production +- `APP_KEY` must be set and rotated on compromise +- Set `SESSION_SECURE_COOKIE=true` and `SESSION_SAME_SITE=lax` (or `strict` for sensitive apps) +- Configure trusted proxies for correct HTTPS detection + +## Session and Cookie Hardening + +- Set `SESSION_HTTP_ONLY=true` to prevent JavaScript access +- Use `SESSION_SAME_SITE=strict` for high-risk flows +- Regenerate sessions on login and privilege changes + +## Authentication and Tokens + +- Use Laravel Sanctum or Passport for API auth +- Prefer short-lived tokens with refresh flows for sensitive data +- Revoke tokens on logout and compromised accounts + +Example route protection: + +```php +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Route; + +Route::middleware('auth:sanctum')->get('/me', function (Request $request) { + return $request->user(); +}); +``` + +## Password Security + +- Hash passwords with `Hash::make()` and never store plaintext +- Use Laravel's password broker for reset flows + +```php +use Illuminate\Support\Facades\Hash; +use Illuminate\Validation\Rules\Password; + +$validated = $request->validate([ + 'password' => ['required', 'string', Password::min(12)->letters()->mixedCase()->numbers()->symbols()], +]); + +$user->update(['password' => Hash::make($validated['password'])]); +``` + +## Authorization: Policies and Gates + +- Use policies for model-level authorization +- Enforce authorization in controllers and services + +```php +$this->authorize('update', $project); +``` + +Use policy middleware for route-level enforcement: + +```php +use Illuminate\Support\Facades\Route; + +Route::put('/projects/{project}', [ProjectController::class, 'update']) + ->middleware(['auth:sanctum', 'can:update,project']); +``` + +## Validation and Data Sanitization + +- Always validate inputs with Form Requests +- Use strict validation rules and type checks +- Never trust request payloads for derived fields + +## Mass Assignment Protection + +- Use `$fillable` or `$guarded` and avoid `Model::unguard()` +- Prefer DTOs or explicit attribute mapping + +## SQL Injection Prevention + +- Use Eloquent or query builder parameter binding +- Avoid raw SQL unless strictly necessary + +```php +DB::select('select * from users where email = ?', [$email]); +``` + +## XSS Prevention + +- Blade escapes output by default (`{{ }}`) +- Use `{!! !!}` only for trusted, sanitized HTML +- Sanitize rich text with a dedicated library + +## CSRF Protection + +- Keep `VerifyCsrfToken` middleware enabled +- Include `@csrf` in forms and send XSRF tokens for SPA requests + +For SPA authentication with Sanctum, ensure stateful requests are configured: + +```php +// config/sanctum.php +'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost')), +``` + +## File Upload Safety + +- Validate file size, MIME type, and extension +- Store uploads outside the public path when possible +- Scan files for malware if required + +```php +final class UploadInvoiceRequest extends FormRequest +{ + public function authorize(): bool + { + return (bool) $this->user()?->can('upload-invoice'); + } + + public function rules(): array + { + return [ + 'invoice' => ['required', 'file', 'mimes:pdf', 'max:5120'], + ]; + } +} +``` + +```php +$path = $request->file('invoice')->store( + 'invoices', + config('filesystems.private_disk', 'local') // set this to a non-public disk +); +``` + +## Rate Limiting + +- Apply `throttle` middleware on auth and write endpoints +- Use stricter limits for login, password reset, and OTP + +```php +use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\RateLimiter; + +RateLimiter::for('login', function (Request $request) { + return [ + Limit::perMinute(5)->by($request->ip()), + Limit::perMinute(5)->by(strtolower((string) $request->input('email'))), + ]; +}); +``` + +## Secrets and Credentials + +- Never commit secrets to source control +- Use environment variables and secret managers +- Rotate keys after exposure and invalidate sessions + +## Encrypted Attributes + +Use encrypted casts for sensitive columns at rest. + +```php +protected $casts = [ + 'api_token' => 'encrypted', +]; +``` + +## Security Headers + +- Add CSP, HSTS, and frame protection where appropriate +- Use trusted proxy configuration to enforce HTTPS redirects + +Example middleware to set headers: + +```php +use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\Response; + +final class SecurityHeaders +{ + public function handle(Request $request, \Closure $next): Response + { + $response = $next($request); + + $response->headers->add([ + 'Content-Security-Policy' => "default-src 'self'", + 'Strict-Transport-Security' => 'max-age=31536000', // add includeSubDomains/preload only when all subdomains are HTTPS + 'X-Frame-Options' => 'DENY', + 'X-Content-Type-Options' => 'nosniff', + 'Referrer-Policy' => 'no-referrer', + ]); + + return $response; + } +} +``` + +## CORS and API Exposure + +- Restrict origins in `config/cors.php` +- Avoid wildcard origins for authenticated routes + +```php +// config/cors.php +return [ + 'paths' => ['api/*', 'sanctum/csrf-cookie'], + 'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], + 'allowed_origins' => ['https://app.example.com'], + 'allowed_headers' => [ + 'Content-Type', + 'Authorization', + 'X-Requested-With', + 'X-XSRF-TOKEN', + 'X-CSRF-TOKEN', + ], + 'supports_credentials' => true, +]; +``` + +## Logging and PII + +- Never log passwords, tokens, or full card data +- Redact sensitive fields in structured logs + +```php +use Illuminate\Support\Facades\Log; + +Log::info('User updated profile', [ + 'user_id' => $user->id, + 'email' => '[REDACTED]', + 'token' => '[REDACTED]', +]); +``` + +## Dependency Security + +- Run `composer audit` regularly +- Pin dependencies with care and update promptly on CVEs + +## Signed URLs + +Use signed routes for temporary, tamper-proof links. + +```php +use Illuminate\Support\Facades\URL; + +$url = URL::temporarySignedRoute( + 'downloads.invoice', + now()->addMinutes(15), + ['invoice' => $invoice->id] +); +``` + +```php +use Illuminate\Support\Facades\Route; + +Route::get('/invoices/{invoice}/download', [InvoiceController::class, 'download']) + ->name('downloads.invoice') + ->middleware('signed'); +``` diff --git a/skills/laravel-tdd/SKILL.md b/skills/laravel-tdd/SKILL.md new file mode 100644 index 00000000..e80fa4d9 --- /dev/null +++ b/skills/laravel-tdd/SKILL.md @@ -0,0 +1,283 @@ +--- +name: laravel-tdd +description: Test-driven development for Laravel with PHPUnit and Pest, factories, database testing, fakes, and coverage targets. +origin: ECC +--- + +# Laravel TDD Workflow + +Test-driven development for Laravel applications using PHPUnit and Pest with 80%+ coverage (unit + feature). + +## When to Use + +- New features or endpoints in Laravel +- Bug fixes or refactors +- Testing Eloquent models, policies, jobs, and notifications +- Prefer Pest for new tests unless the project already standardizes on PHPUnit + +## How It Works + +### Red-Green-Refactor Cycle + +1) Write a failing test +2) Implement the minimal change to pass +3) Refactor while keeping tests green + +### Test Layers + +- **Unit**: pure PHP classes, value objects, services +- **Feature**: HTTP endpoints, auth, validation, policies +- **Integration**: database + queue + external boundaries + +Choose layers based on scope: + +- Use **Unit** tests for pure business logic and services. +- Use **Feature** tests for HTTP, auth, validation, and response shape. +- Use **Integration** tests when validating DB/queues/external services together. + +### Database Strategy + +- `RefreshDatabase` for most feature/integration tests (runs migrations once per test run, then wraps each test in a transaction when supported; in-memory databases may re-migrate per test) +- `DatabaseTransactions` when the schema is already migrated and you only need per-test rollback +- `DatabaseMigrations` when you need a full migrate/fresh for every test and can afford the cost + +Use `RefreshDatabase` as the default for tests that touch the database: for databases with transaction support, it runs migrations once per test run (via a static flag) and wraps each test in a transaction; for `:memory:` SQLite or connections without transactions, it migrates before each test. Use `DatabaseTransactions` when the schema is already migrated and you only need per-test rollbacks. + +### Testing Framework Choice + +- Default to **Pest** for new tests when available. +- Use **PHPUnit** only if the project already standardizes on it or requires PHPUnit-specific tooling. + +## Examples + +### PHPUnit Example + +```php +use App\Models\User; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; + +final class ProjectControllerTest extends TestCase +{ + use RefreshDatabase; + + public function test_owner_can_create_project(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->postJson('/api/projects', [ + 'name' => 'New Project', + ]); + + $response->assertCreated(); + $this->assertDatabaseHas('projects', ['name' => 'New Project']); + } +} +``` + +### Feature Test Example (HTTP Layer) + +```php +use App\Models\Project; +use App\Models\User; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; + +final class ProjectIndexTest extends TestCase +{ + use RefreshDatabase; + + public function test_projects_index_returns_paginated_results(): void + { + $user = User::factory()->create(); + Project::factory()->count(3)->for($user)->create(); + + $response = $this->actingAs($user)->getJson('/api/projects'); + + $response->assertOk(); + $response->assertJsonStructure(['success', 'data', 'error', 'meta']); + } +} +``` + +### Pest Example + +```php +use App\Models\User; +use Illuminate\Foundation\Testing\RefreshDatabase; + +use function Pest\Laravel\actingAs; +use function Pest\Laravel\assertDatabaseHas; + +uses(RefreshDatabase::class); + +test('owner can create project', function () { + $user = User::factory()->create(); + + $response = actingAs($user)->postJson('/api/projects', [ + 'name' => 'New Project', + ]); + + $response->assertCreated(); + assertDatabaseHas('projects', ['name' => 'New Project']); +}); +``` + +### Feature Test Pest Example (HTTP Layer) + +```php +use App\Models\Project; +use App\Models\User; +use Illuminate\Foundation\Testing\RefreshDatabase; + +use function Pest\Laravel\actingAs; + +uses(RefreshDatabase::class); + +test('projects index returns paginated results', function () { + $user = User::factory()->create(); + Project::factory()->count(3)->for($user)->create(); + + $response = actingAs($user)->getJson('/api/projects'); + + $response->assertOk(); + $response->assertJsonStructure(['success', 'data', 'error', 'meta']); +}); +``` + +### Factories and States + +- Use factories for test data +- Define states for edge cases (archived, admin, trial) + +```php +$user = User::factory()->state(['role' => 'admin'])->create(); +``` + +### Database Testing + +- Use `RefreshDatabase` for clean state +- Keep tests isolated and deterministic +- Prefer `assertDatabaseHas` over manual queries + +### Persistence Test Example + +```php +use App\Models\Project; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; + +final class ProjectRepositoryTest extends TestCase +{ + use RefreshDatabase; + + public function test_project_can_be_retrieved_by_slug(): void + { + $project = Project::factory()->create(['slug' => 'alpha']); + + $found = Project::query()->where('slug', 'alpha')->firstOrFail(); + + $this->assertSame($project->id, $found->id); + } +} +``` + +### Fakes for Side Effects + +- `Bus::fake()` for jobs +- `Queue::fake()` for queued work +- `Mail::fake()` and `Notification::fake()` for notifications +- `Event::fake()` for domain events + +```php +use Illuminate\Support\Facades\Queue; + +Queue::fake(); + +dispatch(new SendOrderConfirmation($order->id)); + +Queue::assertPushed(SendOrderConfirmation::class); +``` + +```php +use Illuminate\Support\Facades\Notification; + +Notification::fake(); + +$user->notify(new InvoiceReady($invoice)); + +Notification::assertSentTo($user, InvoiceReady::class); +``` + +### Auth Testing (Sanctum) + +```php +use Laravel\Sanctum\Sanctum; + +Sanctum::actingAs($user); + +$response = $this->getJson('/api/projects'); +$response->assertOk(); +``` + +### HTTP and External Services + +- Use `Http::fake()` to isolate external APIs +- Assert outbound payloads with `Http::assertSent()` + +### Coverage Targets + +- Enforce 80%+ coverage for unit + feature tests +- Use `pcov` or `XDEBUG_MODE=coverage` in CI + +### Test Commands + +- `php artisan test` +- `vendor/bin/phpunit` +- `vendor/bin/pest` + +### Test Configuration + +- Use `phpunit.xml` to set `DB_CONNECTION=sqlite` and `DB_DATABASE=:memory:` for fast tests +- Keep separate env for tests to avoid touching dev/prod data + +### Authorization Tests + +```php +use Illuminate\Support\Facades\Gate; + +$this->assertTrue(Gate::forUser($user)->allows('update', $project)); +$this->assertFalse(Gate::forUser($otherUser)->allows('update', $project)); +``` + +### Inertia Feature Tests + +When using Inertia.js, assert on the component name and props with the Inertia testing helpers. + +```php +use App\Models\User; +use Inertia\Testing\AssertableInertia; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; + +final class DashboardInertiaTest extends TestCase +{ + use RefreshDatabase; + + public function test_dashboard_inertia_props(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/dashboard'); + + $response->assertOk(); + $response->assertInertia(fn (AssertableInertia $page) => $page + ->component('Dashboard') + ->where('user.id', $user->id) + ->has('projects') + ); + } +} +``` + +Prefer `assertInertia` over raw JSON assertions to keep tests aligned with Inertia responses. diff --git a/skills/laravel-verification/SKILL.md b/skills/laravel-verification/SKILL.md new file mode 100644 index 00000000..138b00b2 --- /dev/null +++ b/skills/laravel-verification/SKILL.md @@ -0,0 +1,179 @@ +--- +name: laravel-verification +description: Verification loop for Laravel projects: env checks, linting, static analysis, tests with coverage, security scans, and deployment readiness. +origin: ECC +--- + +# Laravel Verification Loop + +Run before PRs, after major changes, and pre-deploy. + +## When to Use + +- Before opening a pull request for a Laravel project +- After major refactors or dependency upgrades +- Pre-deployment verification for staging or production +- Running full lint -> test -> security -> deploy readiness pipeline + +## How It Works + +- Run phases sequentially from environment checks through deployment readiness so each layer builds on the last. +- Environment and Composer checks gate everything else; stop immediately if they fail. +- Linting/static analysis should be clean before running full tests and coverage. +- Security and migration reviews happen after tests so you verify behavior before data or release steps. +- Build/deploy readiness and queue/scheduler checks are final gates; any failure blocks release. + +## Phase 1: Environment Checks + +```bash +php -v +composer --version +php artisan --version +``` + +- Verify `.env` is present and required keys exist +- Confirm `APP_DEBUG=false` for production environments +- Confirm `APP_ENV` matches the target deployment (`production`, `staging`) + +If using Laravel Sail locally: + +```bash +./vendor/bin/sail php -v +./vendor/bin/sail artisan --version +``` + +## Phase 1.5: Composer and Autoload + +```bash +composer validate +composer dump-autoload -o +``` + +## Phase 2: Linting and Static Analysis + +```bash +vendor/bin/pint --test +vendor/bin/phpstan analyse +``` + +If your project uses Psalm instead of PHPStan: + +```bash +vendor/bin/psalm +``` + +## Phase 3: Tests and Coverage + +```bash +php artisan test +``` + +Coverage (CI): + +```bash +XDEBUG_MODE=coverage php artisan test --coverage +``` + +CI example (format -> static analysis -> tests): + +```bash +vendor/bin/pint --test +vendor/bin/phpstan analyse +XDEBUG_MODE=coverage php artisan test --coverage +``` + +## Phase 4: Security and Dependency Checks + +```bash +composer audit +``` + +## Phase 5: Database and Migrations + +```bash +php artisan migrate --pretend +php artisan migrate:status +``` + +- Review destructive migrations carefully +- Ensure migration filenames follow `Y_m_d_His_*` (e.g., `2025_03_14_154210_create_orders_table.php`) and describe the change clearly +- Ensure rollbacks are possible +- Verify `down()` methods and avoid irreversible data loss without explicit backups + +## Phase 6: Build and Deployment Readiness + +```bash +php artisan optimize:clear +php artisan config:cache +php artisan route:cache +php artisan view:cache +``` + +- Ensure cache warmups succeed in production configuration +- Verify queue workers and scheduler are configured +- Confirm `storage/` and `bootstrap/cache/` are writable in the target environment + +## Phase 7: Queue and Scheduler Checks + +```bash +php artisan schedule:list +php artisan queue:failed +``` + +If Horizon is used: + +```bash +php artisan horizon:status +``` + +If `queue:monitor` is available, use it to check backlog without processing jobs: + +```bash +php artisan queue:monitor default --max=100 +``` + +Active verification (staging only): dispatch a no-op job to a dedicated queue and run a single worker to process it (ensure a non-`sync` queue connection is configured). + +```bash +php artisan tinker --execute="dispatch((new App\\Jobs\\QueueHealthcheck())->onQueue('healthcheck'))" +php artisan queue:work --once --queue=healthcheck +``` + +Verify the job produced the expected side effect (log entry, healthcheck table row, or metric). + +Only run this on non-production environments where processing a test job is safe. + +## Examples + +Minimal flow: + +```bash +php -v +composer --version +php artisan --version +composer validate +vendor/bin/pint --test +vendor/bin/phpstan analyse +php artisan test +composer audit +php artisan migrate --pretend +php artisan config:cache +php artisan queue:failed +``` + +CI-style pipeline: + +```bash +composer validate +composer dump-autoload -o +vendor/bin/pint --test +vendor/bin/phpstan analyse +XDEBUG_MODE=coverage php artisan test --coverage +composer audit +php artisan migrate --pretend +php artisan optimize:clear +php artisan config:cache +php artisan route:cache +php artisan view:cache +php artisan schedule:list +```