mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +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:
@@ -292,6 +292,10 @@ everything-claude-code/
|
|||||||
| |-- django-security/ # Django security best practices (NEW)
|
| |-- django-security/ # Django security best practices (NEW)
|
||||||
| |-- django-tdd/ # Django TDD workflow (NEW)
|
| |-- django-tdd/ # Django TDD workflow (NEW)
|
||||||
| |-- django-verification/ # Django verification loops (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-patterns/ # Python idioms and best practices (NEW)
|
||||||
| |-- python-testing/ # Python testing with pytest (NEW)
|
| |-- python-testing/ # Python testing with pytest (NEW)
|
||||||
| |-- springboot-patterns/ # Java Spring Boot patterns (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)
|
| |-- saas-nextjs-CLAUDE.md # Real-world SaaS (Next.js + Supabase + Stripe)
|
||||||
| |-- go-microservice-CLAUDE.md # Real-world Go microservice (gRPC + PostgreSQL)
|
| |-- go-microservice-CLAUDE.md # Real-world Go microservice (gRPC + PostgreSQL)
|
||||||
| |-- django-api-CLAUDE.md # Real-world Django REST API (DRF + Celery)
|
| |-- 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)
|
| |-- rust-api-CLAUDE.md # Real-world Rust API (Axum + SQLx + PostgreSQL) (NEW)
|
||||||
|
|
|
|
||||||
|-- mcp-configs/ # MCP server configurations
|
|-- 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/
|
cp -r everything-claude-code/skills/search-first ~/.claude/skills/
|
||||||
|
|
||||||
# Optional: add niche/framework-specific skills only when needed
|
# 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/
|
# cp -r everything-claude-code/skills/$s ~/.claude/skills/
|
||||||
# done
|
# done
|
||||||
```
|
```
|
||||||
@@ -869,7 +874,7 @@ Please contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|||||||
### Ideas for Contributions
|
### Ideas for Contributions
|
||||||
|
|
||||||
- Language-specific skills (Rust, C#, Kotlin, Java) — Go, Python, Perl, Swift, and TypeScript already included
|
- 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)
|
- DevOps agents (Kubernetes, Terraform, AWS, Docker)
|
||||||
- Testing strategies (different frameworks, visual regression)
|
- Testing strategies (different frameworks, visual regression)
|
||||||
- Domain-specific knowledge (ML, data engineering, mobile)
|
- Domain-specific knowledge (ML, data engineering, mobile)
|
||||||
|
|||||||
311
examples/laravel-api-CLAUDE.md
Normal file
311
examples/laravel-api-CLAUDE.md
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -25,6 +25,11 @@ paths:
|
|||||||
- Use **PHPStan** or **Psalm** for static analysis.
|
- Use **PHPStan** or **Psalm** for static analysis.
|
||||||
- Keep Composer scripts checked in so the same commands run locally and in CI.
|
- 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
|
## Error Handling
|
||||||
|
|
||||||
- Throw exceptions for exceptional states; avoid returning `false`/`null` as hidden error channels in new code.
|
- Throw exceptions for exceptional states; avoid returning `false`/`null` as hidden error channels in new code.
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ paths:
|
|||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
See skill: `api-design` for endpoint conventions and response-shape guidance.
|
See skill: `api-design` for endpoint conventions and response-shape guidance.
|
||||||
|
See skill: `laravel-patterns` for Laravel-specific architecture guidance.
|
||||||
|
|||||||
@@ -31,3 +31,7 @@ paths:
|
|||||||
- Use `password_hash()` / `password_verify()` for password storage.
|
- Use `password_hash()` / `password_verify()` for password storage.
|
||||||
- Regenerate session identifiers after authentication and privilege changes.
|
- Regenerate session identifiers after authentication and privilege changes.
|
||||||
- Enforce CSRF protection on state-changing web requests.
|
- Enforce CSRF protection on state-changing web requests.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
See skill: `laravel-security` for Laravel-specific security guidance.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ paths:
|
|||||||
|
|
||||||
## Framework
|
## 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
|
## 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.
|
- 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.
|
- 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
|
## Reference
|
||||||
|
|
||||||
See skill: `tdd-workflow` for the repo-wide RED -> GREEN -> REFACTOR loop.
|
See skill: `tdd-workflow` for the repo-wide RED -> GREEN -> REFACTOR loop.
|
||||||
|
See skill: `laravel-tdd` for Laravel-specific testing patterns (PHPUnit and Pest).
|
||||||
|
|||||||
@@ -82,12 +82,12 @@ If the user chooses niche or core + niche, continue to category selection below
|
|||||||
|
|
||||||
### 2b: Choose Skill Categories
|
### 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?"
|
Question: "Which skill categories do you want to install?"
|
||||||
Options:
|
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"
|
- "Database" — "PostgreSQL, ClickHouse, JPA/Hibernate patterns"
|
||||||
- "Workflow & Quality" — "TDD, verification, learning, security review, compaction"
|
- "Workflow & Quality" — "TDD, verification, learning, security review, compaction"
|
||||||
- "Research & APIs" — "Deep research, Exa search, Claude API patterns"
|
- "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.
|
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 |
|
| 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-security` | Django security: auth, CSRF, SQL injection, XSS prevention |
|
||||||
| `django-tdd` | Django testing with pytest-django, factory_boy, mocking, coverage |
|
| `django-tdd` | Django testing with pytest-django, factory_boy, mocking, coverage |
|
||||||
| `django-verification` | Django verification loop: migrations, linting, tests, security scans |
|
| `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-patterns` | React, Next.js, state management, performance, UI patterns |
|
||||||
| `frontend-slides` | Zero-dependency HTML presentations, style previews, and PPTX-to-web conversion |
|
| `frontend-slides` | Zero-dependency HTML presentations, style previews, and PPTX-to-web conversion |
|
||||||
| `golang-patterns` | Idiomatic Go patterns, conventions for robust Go applications |
|
| `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:
|
Some skills reference others. Verify these dependencies:
|
||||||
- `django-tdd` may reference `django-patterns`
|
- `django-tdd` may reference `django-patterns`
|
||||||
|
- `laravel-tdd` may reference `laravel-patterns`
|
||||||
- `springboot-tdd` may reference `springboot-patterns`
|
- `springboot-tdd` may reference `springboot-patterns`
|
||||||
- `continuous-learning-v2` references `~/.claude/homunculus/` directory
|
- `continuous-learning-v2` references `~/.claude/homunculus/` directory
|
||||||
- `python-testing` may reference `python-patterns`
|
- `python-testing` may reference `python-patterns`
|
||||||
|
|||||||
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
|
||||||
285
skills/laravel-security/SKILL.md
Normal file
285
skills/laravel-security/SKILL.md
Normal 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
283
skills/laravel-tdd/SKILL.md
Normal 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.
|
||||||
179
skills/laravel-verification/SKILL.md
Normal file
179
skills/laravel-verification/SKILL.md
Normal 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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user