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:
Sam Rook
2026-03-16 20:35:23 +00:00
committed by GitHub
parent 17a6ef4edb
commit 113119dc6f
11 changed files with 1504 additions and 6 deletions

View File

@@ -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)

View File

@@ -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
<?php
declare(strict_types=1);
final class CreateOrderAction
{
public function __construct(private OrderRepository $orders) {}
public function handle(CreateOrderData $data): Order
{
return $this->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
<?php
declare(strict_types=1);
final class OrdersController extends Controller
{
public function __construct(private OrderService $service) {}
public function store(StoreOrderRequest $request): JsonResponse
{
$order = $this->service->placeOrder($request->toDto());
return response()->json([
'success' => true,
'data' => OrderResource::make($order),
'error' => null,
'meta' => null,
], 201);
}
}
```
### Policy Pattern
```php
<?php
declare(strict_types=1);
use App\Models\Order;
use App\Models\User;
final class OrderPolicy
{
public function view(User $user, Order $order): bool
{
return $order->user_id === $user->id;
}
}
```
### Form Request + DTO
```php
<?php
declare(strict_types=1);
final class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return (bool) $this->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
<?php
declare(strict_types=1);
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'status' => $this->status,
'total' => $this->total,
'created_at' => $this->created_at?->toIso8601String(),
];
}
}
```
### Queue Job
```php
<?php
declare(strict_types=1);
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Repositories\OrderRepository;
use App\Services\OrderMailer;
final class SendOrderConfirmation implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(private int $orderId) {}
public function handle(OrderRepository $orders, OrderMailer $mailer): void
{
$order = $orders->findOrFail($this->orderId);
$mailer->sendOrderConfirmation($order);
}
}
```
### Test Pattern (Pest)
```php
<?php
declare(strict_types=1);
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\postJson;
uses(RefreshDatabase::class);
test('user can place order', function () {
$user = User::factory()->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
<?php
declare(strict_types=1);
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class OrdersControllerTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_place_order(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->postJson('/api/orders', [
'items' => [['sku' => 'sku-1', 'quantity' => 2]],
]);
$response->assertCreated();
$this->assertDatabaseHas('orders', ['user_id' => $user->id]);
}
}
```

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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).

View File

@@ -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`

View 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

View File

@@ -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');
```

283
skills/laravel-tdd/SKILL.md Normal file
View File

@@ -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.

View File

@@ -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
```