Merge remote-tracking branch 'origin/next' into feat/database-service-logs-endpoint
This commit is contained in:
commit
adc4b3091f
2139 changed files with 224986 additions and 44105 deletions
404
.agents/skills/configure-nightwatch/SKILL.md
Normal file
404
.agents/skills/configure-nightwatch/SKILL.md
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
---
|
||||
name: configure-nightwatch
|
||||
description: Configures Laravel Nightwatch data collection, sampling rates, filtering rules, and redaction policies. Use when setting up Nightwatch, managing data volume, protecting sensitive data (PII), or optimizing event collection for production workloads.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Nightwatch Configuration Guide
|
||||
|
||||
This skill helps configure Laravel Nightwatch data collection to balance observability, performance, and privacy. Covers sampling strategies, filtering rules, and redaction methods across all event types.
|
||||
|
||||
## Documentation Reference
|
||||
|
||||
The [Nightwatch Documentation](https://nightwatch.laravel.com/docs) is the definitive and up-to-date source of information for all Nightwatch configuration options. This skill provides practical guidance and common patterns, but always consult the official documentation as the primary source of truth for specific details, environment variables, and API behavior. The documentation includes comprehensive coverage of:
|
||||
|
||||
- [Filtering and Configuration](https://nightwatch.laravel.com/docs/filtering) - Core concepts for sampling, filtering, and redaction
|
||||
- Individual event type pages with specific configuration options:
|
||||
- [Requests](https://nightwatch.laravel.com/docs/requests) - Request sampling, header handling, payload capture
|
||||
- [Commands](https://nightwatch.laravel.com/docs/commands) - Command sampling and redaction
|
||||
- [Queries](https://nightwatch.laravel.com/docs/queries) - Query filtering and redaction
|
||||
- [Cache](https://nightwatch.laravel.com/docs/cache) - Cache event filtering by key or pattern
|
||||
- [Jobs](https://nightwatch.laravel.com/docs/jobs) - Job filtering and sampling decoupling
|
||||
- [Mail](https://nightwatch.laravel.com/docs/mail) - Mail event filtering
|
||||
- [Notifications](https://nightwatch.laravel.com/docs/notifications) - Notification filtering by channel
|
||||
- [Exceptions](https://nightwatch.laravel.com/docs/exceptions) - Exception sampling and throttling
|
||||
- [Outgoing Requests](https://nightwatch.laravel.com/docs/outgoing-requests) - HTTP request filtering
|
||||
- [reference.md](reference.md) - Quick lookup table by event type, production presets, and verification checklist
|
||||
|
||||
## Data Collection Flow
|
||||
|
||||
Nightwatch processes events through three stages:
|
||||
|
||||
1. **Sampling** - Controls which entry points are captured (requests, commands, scheduled tasks)
|
||||
2. **Filtering** - Excludes specific events after sampling (queries, cache, mail, etc.)
|
||||
3. **Redaction** - Modifies captured data to remove/obfuscate sensitive information
|
||||
|
||||
```
|
||||
Request/Command/Scheduled Task
|
||||
|
|
||||
v
|
||||
[Sampling?] ----NO----> Drop entire trace
|
||||
| YES
|
||||
v
|
||||
Events generated
|
||||
|
|
||||
v
|
||||
[Filtering?] ----YES---> Drop specific event
|
||||
| NO
|
||||
v
|
||||
[Redaction] ----------> Store modified data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sampling Configuration
|
||||
|
||||
Sampling determines which entry points (requests, commands, scheduled tasks) trigger full trace collection. When an entry point is sampled, all related events are captured.
|
||||
|
||||
### Global Sample Rates
|
||||
|
||||
Configure via environment variables:
|
||||
|
||||
```bash
|
||||
|
||||
# Default: 100% sampling (all requests/commands captured)
|
||||
|
||||
NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1 # Recommended: 10% of requests
|
||||
|
||||
NIGHTWATCH_COMMAND_SAMPLE_RATE=1.0 # Capture all commands
|
||||
|
||||
NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0 # Always capture exceptions
|
||||
|
||||
```
|
||||
|
||||
**Recommendation**: Start with `0.1` (10%) for requests in production, adjust based on volume and needs.
|
||||
|
||||
### Route-Based Sampling
|
||||
|
||||
Apply different rates to specific routes using the `Sample` middleware:
|
||||
|
||||
```php routes/web.php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Laravel\Nightwatch\Http\Middleware\Sample;
|
||||
|
||||
// Sample admin routes at 100%
|
||||
Route::middleware(Sample::rate(1.0))->prefix('admin')->group(function () {
|
||||
// All admin routes sampled fully
|
||||
});
|
||||
|
||||
// Sample API routes at 5%
|
||||
Route::middleware(Sample::rate(0.05))->prefix('api')->group(function () {
|
||||
// API routes sampled sparingly
|
||||
});
|
||||
|
||||
// Always sample critical endpoints
|
||||
Route::post('/checkout', [CheckoutController::class, 'process'])
|
||||
->middleware(Sample::always());
|
||||
|
||||
// Never sample health checks
|
||||
Route::get('/health', [HealthController::class, 'check'])
|
||||
->middleware(Sample::never());
|
||||
```
|
||||
|
||||
### Unmatched Route Sampling
|
||||
|
||||
Handle 404/bot traffic with reduced sampling:
|
||||
|
||||
```php routes/web.php
|
||||
Route::fallback(fn () => abort(404))
|
||||
->middleware(Sample::rate(0.01)); // 1% sampling for unmatched routes
|
||||
```
|
||||
|
||||
### Dynamic Sampling
|
||||
|
||||
Sample based on runtime conditions (user role, request attributes):
|
||||
|
||||
```php app/Http/Middleware/SampleAdminRequests.php
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Nightwatch\Facades\Nightwatch;
|
||||
|
||||
class SampleAdminRequests
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($request->user()?->isAdmin()) {
|
||||
Nightwatch::sample(); // Always sample admin requests
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Command Sampling
|
||||
|
||||
Exclude specific commands from sampling:
|
||||
|
||||
```php AppServiceProvider.php
|
||||
use Illuminate\Console\Events\CommandStarting;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Laravel\Nightwatch\Facades\Nightwatch;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
Event::listen(function (CommandStarting $event) {
|
||||
if (in_array($event->command, ['schedule:finish', 'horizon:snapshot'])) {
|
||||
Nightwatch::dontSample();
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Vendor Commands
|
||||
|
||||
Nightwatch automatically ignores framework/internal commands. Opt-in to capture them:
|
||||
|
||||
```php
|
||||
Nightwatch::captureDefaultVendorCommands();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Filtering Configuration
|
||||
|
||||
Filtering excludes specific events from collection after sampling. Use filtering to reduce noise and quota usage.
|
||||
|
||||
### Database Queries
|
||||
|
||||
**Filter all queries** (disable query collection):
|
||||
|
||||
```bash
|
||||
NIGHTWATCH_IGNORE_QUERIES=true
|
||||
```
|
||||
|
||||
**Filter specific queries** by SQL pattern:
|
||||
|
||||
```php AppServiceProvider.php
|
||||
use Laravel\Nightwatch\Facades\Nightwatch;
|
||||
use Laravel\Nightwatch\Records\Query;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
// Filter job table queries (PostgreSQL)
|
||||
Nightwatch::rejectQueries(function (Query $query) {
|
||||
return str_contains($query->sql, 'into "jobs"');
|
||||
});
|
||||
|
||||
// Filter cache table queries (MySQL)
|
||||
Nightwatch::rejectQueries(function (Query $query) {
|
||||
return str_contains($query->sql, 'from `cache`')
|
||||
|| str_contains($query->sql, 'into `cache`');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Events
|
||||
|
||||
**Filter all cache events**:
|
||||
|
||||
```bash
|
||||
NIGHTWATCH_IGNORE_CACHE_EVENTS=true
|
||||
```
|
||||
|
||||
**Filter by cache key patterns**:
|
||||
|
||||
```php
|
||||
Nightwatch::rejectCacheKeys([
|
||||
'my-app:users', // Exact match
|
||||
'/^my-app:posts:/', // Regex: starts with my-app:posts:
|
||||
'/^[a-zA-Z0-9]{40}$/', // Regex: session IDs
|
||||
]);
|
||||
```
|
||||
|
||||
**Filter with callback**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\CacheEvent;
|
||||
|
||||
Nightwatch::rejectCacheEvents(function (CacheEvent $cacheEvent) {
|
||||
return str_starts_with($cacheEvent->key, 'temp:');
|
||||
});
|
||||
```
|
||||
|
||||
### Mail Events
|
||||
|
||||
**Filter all mail**:
|
||||
|
||||
```bash
|
||||
NIGHTWATCH_IGNORE_MAIL=true
|
||||
```
|
||||
|
||||
**Filter specific mail**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Mail;
|
||||
|
||||
Nightwatch::rejectMail(function (Mail $mail) {
|
||||
return str_contains($mail->subject, 'Newsletter');
|
||||
});
|
||||
```
|
||||
|
||||
### Notification Events
|
||||
|
||||
**Filter all notifications**:
|
||||
|
||||
```bash
|
||||
NIGHTWATCH_IGNORE_NOTIFICATIONS=true
|
||||
```
|
||||
|
||||
**Filter by channel**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Notification;
|
||||
|
||||
Nightwatch::rejectNotifications(function (Notification $notification) {
|
||||
return $notification->channel === 'database';
|
||||
});
|
||||
```
|
||||
|
||||
### Outgoing HTTP Requests
|
||||
|
||||
**Filter all outgoing requests**:
|
||||
|
||||
```bash
|
||||
NIGHTWATCH_IGNORE_OUTGOING_REQUESTS=true
|
||||
```
|
||||
|
||||
**Filter by URL**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\OutgoingRequest;
|
||||
|
||||
Nightwatch::rejectOutgoingRequests(function (OutgoingRequest $request) {
|
||||
return str_contains($request->url, 'analytics.example.com');
|
||||
});
|
||||
```
|
||||
|
||||
### Queued Jobs
|
||||
|
||||
**Filter specific jobs**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\QueuedJob;
|
||||
|
||||
Nightwatch::rejectQueuedJobs(function (QueuedJob $job) {
|
||||
return $job->name === 'App\Jobs\LowPriorityJob';
|
||||
});
|
||||
```
|
||||
|
||||
### Decoupling Job Sampling
|
||||
|
||||
Sample jobs independently from parent contexts:
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
Queue::before(fn () => Nightwatch::sample(rate: 0.5));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Redaction Configuration
|
||||
|
||||
Redaction modifies captured data to remove or obfuscate sensitive information. Unlike filtering, redaction keeps the event but sanitizes its content.
|
||||
|
||||
### Request Redaction
|
||||
|
||||
**Redact sensitive headers** (automatically redacts: Authorization, Cookie, X-XSRF-TOKEN):
|
||||
|
||||
```bash
|
||||
|
||||
# Customize redacted headers
|
||||
|
||||
NIGHTWATCH_REDACT_HEADERS=Authorization,Cookie,Proxy-Authorization,X-API-Key
|
||||
```
|
||||
|
||||
**Redact request payloads** (disabled by default):
|
||||
|
||||
```bash
|
||||
|
||||
# Enable payload capture
|
||||
|
||||
NIGHTWATCH_CAPTURE_REQUEST_PAYLOAD=true
|
||||
|
||||
# Customize redacted fields
|
||||
|
||||
NIGHTWATCH_REDACT_PAYLOAD_FIELDS=password,password_confirmation,ssn,credit_card
|
||||
```
|
||||
|
||||
**Programmatic redaction**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Facades\Nightwatch;
|
||||
use Laravel\Nightwatch\Records\Request;
|
||||
|
||||
Nightwatch::redactRequests(function (Request $request) {
|
||||
$request->url = str_replace('secret', '***', $request->url);
|
||||
$request->ip = preg_replace('/\d+$/', '***', $request->ip);
|
||||
});
|
||||
```
|
||||
|
||||
### Query Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Query;
|
||||
|
||||
Nightwatch::redactQueries(function (Query $query) {
|
||||
$query->sql = str_replace('secret_token', '***', $query->sql);
|
||||
});
|
||||
```
|
||||
|
||||
### Cache Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\CacheEvent;
|
||||
|
||||
Nightwatch::redactCacheEvents(function (CacheEvent $cacheEvent) {
|
||||
$cacheEvent->key = str_replace('user:', 'user:***:', $cacheEvent->key);
|
||||
});
|
||||
```
|
||||
|
||||
### Command Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Command;
|
||||
|
||||
Nightwatch::redactCommands(function (Command $command) {
|
||||
$command->command = preg_replace('/--password=\S+/', '--password=***', $command->command);
|
||||
});
|
||||
```
|
||||
|
||||
### Exception Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Exception;
|
||||
|
||||
Nightwatch::redactExceptions(function (Exception $exception) {
|
||||
$exception->message = str_replace('secret', '***', $exception->message);
|
||||
});
|
||||
```
|
||||
|
||||
### Mail Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Mail;
|
||||
|
||||
Nightwatch::redactMail(function (Mail $mail) {
|
||||
$mail->subject = str_replace('Invoice #', 'Invoice ***', $mail->subject);
|
||||
});
|
||||
```
|
||||
|
||||
### Outgoing Request Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\OutgoingRequest;
|
||||
|
||||
Nightwatch::redactOutgoingRequests(function (OutgoingRequest $outgoingRequest) {
|
||||
$outgoingRequest->url = preg_replace('/api_key=\w+/', 'api_key=***', $outgoingRequest->url);
|
||||
});
|
||||
```
|
||||
108
.agents/skills/configure-nightwatch/reference.md
Normal file
108
.agents/skills/configure-nightwatch/reference.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Nightwatch Configuration Reference
|
||||
|
||||
## Configuration Summary by Event Type
|
||||
|
||||
| Event Type | Sampling | Filtering | Redaction |
|
||||
| --------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------- |
|
||||
| **Requests** | `NIGHTWATCH_REQUEST_SAMPLE_RATE`, Route middleware | Not applicable | Headers, payload, URL, IP |
|
||||
| **Commands** | `NIGHTWATCH_COMMAND_SAMPLE_RATE`, Event listener | Not applicable | Command arguments |
|
||||
| **Queries** | Parent context | `rejectQueries()`, `NIGHTWATCH_IGNORE_QUERIES` | SQL statement |
|
||||
| **Cache** | Parent context | `rejectCacheKeys()`, `rejectCacheEvents()`, `NIGHTWATCH_IGNORE_CACHE_EVENTS` | Cache key |
|
||||
| **Jobs** | Parent context, Queue::before | `rejectQueuedJobs()` | Not applicable |
|
||||
| **Mail** | Parent context | `rejectMail()`, `NIGHTWATCH_IGNORE_MAIL` | Subject |
|
||||
| **Notifications** | Parent context | `rejectNotifications()`, `NIGHTWATCH_IGNORE_NOTIFICATIONS` | Not applicable |
|
||||
| **Outgoing Requests** | Parent context | `rejectOutgoingRequests()`, `NIGHTWATCH_IGNORE_OUTGOING_REQUESTS` | URL |
|
||||
| **Exceptions** | `NIGHTWATCH_EXCEPTION_SAMPLE_RATE` | Not applicable | Exception message |
|
||||
|
||||
---
|
||||
|
||||
## Production Recommendations
|
||||
|
||||
### High-Traffic Applications
|
||||
|
||||
```bash
|
||||
|
||||
# Conservative sampling
|
||||
|
||||
NIGHTWATCH_REQUEST_SAMPLE_RATE=0.01 # 1% of requests
|
||||
|
||||
NIGHTWATCH_COMMAND_SAMPLE_RATE=0.1 # 10% of commands
|
||||
|
||||
NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0 # Always capture exceptions
|
||||
|
||||
# Filter noisy events
|
||||
|
||||
NIGHTWATCH_IGNORE_CACHE_EVENTS=true
|
||||
NIGHTWATCH_IGNORE_QUERIES=true # Or filter specific queries programmatically
|
||||
|
||||
```
|
||||
|
||||
### Privacy-Conscious Applications
|
||||
|
||||
```bash
|
||||
|
||||
# Disable sensitive data collection
|
||||
|
||||
NIGHTWATCH_CAPTURE_REQUEST_PAYLOAD=false
|
||||
NIGHTWATCH_REDACT_HEADERS=Authorization,Cookie,Proxy-Authorization,X-XSRF-TOKEN
|
||||
|
||||
# Or use redaction in AppServiceProvider
|
||||
|
||||
```
|
||||
|
||||
### Balanced Configuration (Recommended Start)
|
||||
|
||||
```bash
|
||||
|
||||
# Sample rates
|
||||
|
||||
NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1
|
||||
NIGHTWATCH_COMMAND_SAMPLE_RATE=1.0
|
||||
NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0
|
||||
|
||||
# Filter obvious noise programmatically
|
||||
|
||||
# Redact PII as needed
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After configuration:
|
||||
|
||||
- [ ] Sampling rates appropriate for traffic volume
|
||||
- [ ] Noisy events filtered (cache, certain queries)
|
||||
- [ ] Sensitive data redacted (PII, tokens, credentials)
|
||||
- [ ] Exceptions always captured for debugging
|
||||
- [ ] Test in development with `NIGHTWATCH_REQUEST_SAMPLE_RATE=1.0`
|
||||
- [ ] Monitor event quota usage in Nightwatch dashboard
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Filter Health Checks + Reduce Sampling
|
||||
|
||||
```php
|
||||
Route::get('/health', fn() => ['status' => 'ok'])
|
||||
->middleware(Sample::never());
|
||||
```
|
||||
|
||||
### Exclude Internal/Vendor Queries
|
||||
|
||||
```php
|
||||
Nightwatch::rejectQueries(fn($q) =>
|
||||
str_contains($q->sql, 'telescope') ||
|
||||
str_contains($q->sql, 'pulse')
|
||||
);
|
||||
```
|
||||
|
||||
### Protect User Data in Cache Keys
|
||||
|
||||
```php
|
||||
Nightwatch::redactCacheEvents(fn($e) =>
|
||||
$e->key = preg_replace('/user:\d+/', 'user:***', $e->key)
|
||||
);
|
||||
```
|
||||
85
.agents/skills/configuring-horizon/SKILL.md
Normal file
85
.agents/skills/configuring-horizon/SKILL.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
name: configuring-horizon
|
||||
description: "Use this skill whenever the user mentions Horizon by name in a Laravel context. Covers the full Horizon lifecycle: installing Horizon (horizon:install, Sail setup), configuring config/horizon.php (supervisor blocks, queue assignments, balancing strategies, minProcesses/maxProcesses), fixing the dashboard (authorization via Gate::define viewHorizon, blank metrics, horizon:snapshot scheduling), and troubleshooting production issues (worker crashes, timeout chain ordering, LongWaitDetected notifications, waits config). Also covers job tagging and silencing. Do not use for generic Laravel queues without Horizon, SQS or database drivers, standalone Redis setup, Linux supervisord, Telescope, or job batching."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Horizon Configuration
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Horizon patterns and documentation covering configuration, supervisors, balancing, dashboard authorization, tags, notifications, metrics, and deployment.
|
||||
|
||||
For deeper guidance on specific topics, read the relevant reference file before implementing:
|
||||
|
||||
- `references/supervisors.md` covers supervisor blocks, balancing strategies, multi-queue setups, and auto-scaling
|
||||
- `references/notifications.md` covers LongWaitDetected alerts, notification routing, and the `waits` config
|
||||
- `references/tags.md` covers job tagging, dashboard filtering, and silencing noisy jobs
|
||||
- `references/metrics.md` covers the blank metrics dashboard, snapshot scheduling, and retention config
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
php artisan horizon:install
|
||||
```
|
||||
|
||||
### Supervisor Configuration
|
||||
|
||||
Define supervisors in `config/horizon.php`. The `environments` array merges into `defaults` and does not replace the whole supervisor block:
|
||||
|
||||
<!-- Supervisor Config -->
|
||||
```php
|
||||
'defaults' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['default'],
|
||||
'balance' => 'auto',
|
||||
'minProcesses' => 1,
|
||||
'maxProcesses' => 10,
|
||||
'tries' => 3,
|
||||
],
|
||||
],
|
||||
|
||||
'environments' => [
|
||||
'production' => [
|
||||
'supervisor-1' => ['maxProcesses' => 20, 'balanceCooldown' => 3],
|
||||
],
|
||||
'local' => [
|
||||
'supervisor-1' => ['maxProcesses' => 2],
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
### Dashboard Authorization
|
||||
|
||||
Restrict access in `App\Providers\HorizonServiceProvider`:
|
||||
|
||||
<!-- Dashboard Gate -->
|
||||
```php
|
||||
protected function gate(): void
|
||||
{
|
||||
Gate::define('viewHorizon', function (User $user) {
|
||||
return $user->is_admin;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Run `php artisan horizon` and visit `/horizon`
|
||||
2. Confirm dashboard access is restricted as expected
|
||||
3. Check that metrics populate after scheduling `horizon:snapshot`
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Horizon only works with the Redis queue driver. Other drivers such as database and SQS are not supported.
|
||||
- Redis Cluster is not supported. Horizon requires a standalone Redis connection.
|
||||
- Always check `config/horizon.php` before making changes to understand the current supervisor and environment configuration.
|
||||
- The `environments` array overrides only the keys you specify. It merges into `defaults` and does not replace it.
|
||||
- The timeout chain must be ordered: job `timeout` less than supervisor `timeout` less than `retry_after`. The wrong order can cause jobs to be retried before Horizon finishes timing them out.
|
||||
- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `php artisan horizon` alone does not populate metrics.
|
||||
- Always use `search-docs` for the latest Horizon documentation rather than relying on this skill alone.
|
||||
21
.agents/skills/configuring-horizon/references/metrics.md
Normal file
21
.agents/skills/configuring-horizon/references/metrics.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Metrics & Snapshots
|
||||
|
||||
## Where to Find It
|
||||
|
||||
Search with `search-docs`:
|
||||
- `"horizon metrics snapshot"` for the snapshot command and scheduling
|
||||
- `"horizon trim snapshots"` for retention configuration
|
||||
|
||||
## What to Watch For
|
||||
|
||||
### Metrics dashboard stays blank until `horizon:snapshot` is scheduled
|
||||
|
||||
Running `horizon` artisan command does not populate metrics automatically. The metrics graph is built from snapshots, so `horizon:snapshot` must be scheduled to run every 5 minutes via Laravel's scheduler.
|
||||
|
||||
### Register the snapshot in the scheduler rather than running it manually
|
||||
|
||||
A single manual run populates the dashboard momentarily but will not keep it updated. Search `"horizon metrics snapshot"` for the exact scheduler registration syntax, which differs between Laravel 10 and 11+.
|
||||
|
||||
### `metrics.trim_snapshots` is a snapshot count, not a time duration
|
||||
|
||||
The `trim_snapshots.job` and `trim_snapshots.queue` values in `config/horizon.php` are counts of snapshots to keep, not minutes or hours. With the default of 24 snapshots at 5-minute intervals, that provides 2 hours of history. Increase the value to retain more history at the cost of Redis memory usage.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Notifications & Alerts
|
||||
|
||||
## Where to Find It
|
||||
|
||||
Search with `search-docs`:
|
||||
- `"horizon notifications"` for Horizon's built-in notification routing helpers
|
||||
- `"horizon long wait detected"` for LongWaitDetected event details
|
||||
|
||||
## What to Watch For
|
||||
|
||||
### `waits` in `config/horizon.php` controls the LongWaitDetected threshold
|
||||
|
||||
The `waits` array (e.g., `'redis:default' => 60`) defines how many seconds a job can wait in a queue before Horizon fires a `LongWaitDetected` event. This value is set in the config file, not in Horizon's notification routing. If alerts are firing too often or too late, adjust `waits` rather than the routing configuration.
|
||||
|
||||
### Use Horizon's built-in notification routing in `HorizonServiceProvider`
|
||||
|
||||
Configure notifications in the `boot()` method of `App\Providers\HorizonServiceProvider` using `Horizon::routeMailNotificationsTo()`, `Horizon::routeSlackNotificationsTo()`, or `Horizon::routeSmsNotificationsTo()`. Horizon already wires `LongWaitDetected` to its notification sender, so the documented setup is notification routing rather than manual listener registration.
|
||||
|
||||
### Failed job alerts are separate from Horizon's documented notification routing
|
||||
|
||||
Horizon's 12.x documentation covers built-in long-wait notifications. Do not assume the docs provide a `JobFailed` listener example in `HorizonServiceProvider`. If a user needs failed job alerts, treat that as custom queue event handling and consult the queue documentation instead of Horizon's notification-routing API.
|
||||
27
.agents/skills/configuring-horizon/references/supervisors.md
Normal file
27
.agents/skills/configuring-horizon/references/supervisors.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Supervisor & Balancing Configuration
|
||||
|
||||
## Where to Find It
|
||||
|
||||
Search with `search-docs` before writing any supervisor config, as option names and defaults change between Horizon versions:
|
||||
- `"horizon supervisor configuration"` for the full options list
|
||||
- `"horizon balancing strategies"` for auto, simple, and false modes
|
||||
- `"horizon autoscaling workers"` for autoScalingStrategy details
|
||||
- `"horizon environment configuration"` for the defaults and environments merge
|
||||
|
||||
## What to Watch For
|
||||
|
||||
### The `environments` array merges into `defaults` rather than replacing it
|
||||
|
||||
The `defaults` array defines the complete base supervisor config. The `environments` array patches it per environment, overriding only the keys listed. There is no need to repeat every key in each environment block. A common pattern is to define `connection`, `queue`, `balance`, `autoScalingStrategy`, `tries`, and `timeout` in `defaults`, then override only `maxProcesses`, `balanceMaxShift`, and `balanceCooldown` in `production`.
|
||||
|
||||
### Use separate named supervisors to enforce queue priority
|
||||
|
||||
Horizon does not enforce queue order when using `balance: auto` on a single supervisor. The `queue` array order is ignored for load balancing. To process `notifications` before `default`, use two separately named supervisors: one for the high-priority queue with a higher `maxProcesses`, and one for the low-priority queue with a lower cap. The docs include an explicit note about this.
|
||||
|
||||
### Use `balance: false` to keep a fixed number of workers on a dedicated queue
|
||||
|
||||
Auto-balancing suits variable load, but if a queue should always have exactly N workers such as a video-processing queue limited to 2, set `balance: false` and `maxProcesses: 2`. Auto-balancing would scale it up during bursts, which may be undesirable.
|
||||
|
||||
### Set `balanceCooldown` to prevent rapid worker scaling under bursty load
|
||||
|
||||
When using `balance: auto`, the supervisor can scale up and down rapidly under bursty load. Set `balanceCooldown` to the number of seconds between scaling decisions, typically 3 to 5, to smooth this out. `balanceMaxShift` limits how many processes are added or removed per cycle.
|
||||
21
.agents/skills/configuring-horizon/references/tags.md
Normal file
21
.agents/skills/configuring-horizon/references/tags.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Tags & Silencing
|
||||
|
||||
## Where to Find It
|
||||
|
||||
Search with `search-docs`:
|
||||
- `"horizon tags"` for the tagging API and auto-tagging behaviour
|
||||
- `"horizon silenced jobs"` for the `silenced` and `silenced_tags` config options
|
||||
|
||||
## What to Watch For
|
||||
|
||||
### Eloquent model jobs are tagged automatically without any extra code
|
||||
|
||||
If a job's constructor accepts Eloquent model instances, Horizon automatically tags the job with `ModelClass:id` such as `App\Models\User:42`. These tags are filterable in the dashboard without any changes to the job class. Only add a `tags()` method when custom tags beyond auto-tagging are needed.
|
||||
|
||||
### `silenced` hides jobs from the dashboard completed list but does not stop them from running
|
||||
|
||||
Adding a job class to the `silenced` array in `config/horizon.php` removes it from the completed jobs view. The job still runs normally. This is a dashboard noise-reduction tool, not a way to disable jobs.
|
||||
|
||||
### `silenced_tags` hides all jobs carrying a matching tag from the completed list
|
||||
|
||||
Any job carrying a matching tag string is hidden from the completed jobs view. This is useful for silencing a category of jobs such as all jobs tagged `notifications`, rather than silencing specific classes.
|
||||
|
|
@ -0,0 +1,414 @@
|
|||
---
|
||||
name: debugging-output-and-previewing-html-using-ray
|
||||
description: Use when user says "send to Ray," "show in Ray," "debug in Ray," "log to Ray," "display in Ray," or wants to visualize data, debug output, or show diagrams in the Ray desktop application.
|
||||
metadata:
|
||||
author: Spatie
|
||||
tags:
|
||||
- debugging
|
||||
- logging
|
||||
- visualization
|
||||
- ray
|
||||
---
|
||||
|
||||
# Ray Skill
|
||||
|
||||
## Overview
|
||||
|
||||
Ray is Spatie's desktop debugging application for developers. Send data directly to Ray by making HTTP requests to its local server.
|
||||
|
||||
This can be useful for debugging applications, or to preview design, logos, or other visual content.
|
||||
|
||||
This is what the `ray()` PHP function does under the hood.
|
||||
|
||||
## Connection Details
|
||||
|
||||
| Setting | Default | Environment Variable |
|
||||
|---------|---------|---------------------|
|
||||
| Host | `localhost` | `RAY_HOST` |
|
||||
| Port | `23517` | `RAY_PORT` |
|
||||
| URL | `http://localhost:23517/` | - |
|
||||
|
||||
## Request Format
|
||||
|
||||
**Method:** POST
|
||||
**Content-Type:** `application/json`
|
||||
**User-Agent:** `Ray 1.0`
|
||||
|
||||
### Basic Request Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "unique-identifier-for-this-ray-instance",
|
||||
"payloads": [
|
||||
{
|
||||
"type": "log",
|
||||
"content": { },
|
||||
"origin": {
|
||||
"file": "/path/to/file.php",
|
||||
"line_number": 42,
|
||||
"hostname": "my-machine"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"ray_package_version": "1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `uuid` | string | Unique identifier for this Ray instance. Reuse the same UUID to update an existing entry. |
|
||||
| `payloads` | array | Array of payload objects to send |
|
||||
| `meta` | object | Optional metadata (ray_package_version, project_name, php_version) |
|
||||
|
||||
### Origin Object
|
||||
|
||||
Every payload includes origin information:
|
||||
|
||||
```json
|
||||
{
|
||||
"file": "/Users/dev/project/app/Controller.php",
|
||||
"line_number": 42,
|
||||
"hostname": "dev-machine"
|
||||
}
|
||||
```
|
||||
|
||||
## Payload Types
|
||||
|
||||
### Log (Send Values)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "log",
|
||||
"content": {
|
||||
"values": ["Hello World", 42, {"key": "value"}]
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Custom (HTML/Text Content)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "custom",
|
||||
"content": {
|
||||
"content": "<h1>HTML Content</h1><p>With formatting</p>",
|
||||
"label": "My Label"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Table
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "table",
|
||||
"content": {
|
||||
"values": {"name": "John", "email": "john@example.com", "age": 30},
|
||||
"label": "User Data"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Color
|
||||
|
||||
Set the color of the preceding log entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "color",
|
||||
"content": {
|
||||
"color": "green"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
**Available colors:** `green`, `orange`, `red`, `purple`, `blue`, `gray`
|
||||
|
||||
### Screen Color
|
||||
|
||||
Set the background color of the screen:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "screen_color",
|
||||
"content": {
|
||||
"color": "green"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Label
|
||||
|
||||
Add a label to the entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "label",
|
||||
"content": {
|
||||
"label": "Important"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Size
|
||||
|
||||
Set the size of the entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "size",
|
||||
"content": {
|
||||
"size": "lg"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
**Available sizes:** `sm`, `lg`
|
||||
|
||||
### Notify (Desktop Notification)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "notify",
|
||||
"content": {
|
||||
"value": "Task completed!"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### New Screen
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "new_screen",
|
||||
"content": {
|
||||
"name": "Debug Session"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Measure (Timing)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "measure",
|
||||
"content": {
|
||||
"name": "my-timer",
|
||||
"is_new_timer": true,
|
||||
"total_time": 0,
|
||||
"time_since_last_call": 0,
|
||||
"max_memory_usage_during_total_time": 0,
|
||||
"max_memory_usage_since_last_call": 0
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
For subsequent measurements, set `is_new_timer: false` and provide actual timing values.
|
||||
|
||||
### Simple Payloads (No Content)
|
||||
|
||||
These payloads only need a `type` and empty `content`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "separator",
|
||||
"content": {},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
| Type | Purpose |
|
||||
|------|---------|
|
||||
| `separator` | Add visual divider |
|
||||
| `clear_all` | Clear all entries |
|
||||
| `hide` | Hide this entry |
|
||||
| `remove` | Remove this entry |
|
||||
| `confetti` | Show confetti animation |
|
||||
| `show_app` | Bring Ray to foreground |
|
||||
| `hide_app` | Hide Ray window |
|
||||
|
||||
## Combining Multiple Payloads
|
||||
|
||||
Send multiple payloads in one request. Use the same `uuid` to apply modifiers (color, label, size) to a log entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "abc-123",
|
||||
"payloads": [
|
||||
{
|
||||
"type": "log",
|
||||
"content": { "values": ["Important message"] },
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"content": { "color": "red" },
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"content": { "label": "ERROR" },
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
},
|
||||
{
|
||||
"type": "size",
|
||||
"content": { "size": "lg" },
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
],
|
||||
"meta": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Complete Request
|
||||
|
||||
Send a green, labeled log message:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:23517/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "User-Agent: Ray 1.0" \
|
||||
-d '{
|
||||
"uuid": "my-unique-id-123",
|
||||
"payloads": [
|
||||
{
|
||||
"type": "log",
|
||||
"content": {
|
||||
"values": ["User logged in", {"user_id": 42, "name": "John"}]
|
||||
},
|
||||
"origin": {
|
||||
"file": "/app/AuthController.php",
|
||||
"line_number": 55,
|
||||
"hostname": "dev-server"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"content": { "color": "green" },
|
||||
"origin": { "file": "/app/AuthController.php", "line_number": 55, "hostname": "dev-server" }
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"content": { "label": "Auth" },
|
||||
"origin": { "file": "/app/AuthController.php", "line_number": 55, "hostname": "dev-server" }
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"project_name": "my-app"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Availability Check
|
||||
|
||||
Before sending data, you can check if Ray is running:
|
||||
|
||||
```
|
||||
GET http://localhost:23517/_availability_check
|
||||
```
|
||||
|
||||
Ray responds with HTTP 404 when available (the endpoint doesn't exist, but the server is running).
|
||||
|
||||
## Getting Ray Information
|
||||
|
||||
### Get Windows
|
||||
|
||||
Retrieve information about all open Ray windows:
|
||||
|
||||
```
|
||||
GET http://localhost:23517/windows
|
||||
```
|
||||
|
||||
Returns an array of window objects with their IDs and names:
|
||||
|
||||
```json
|
||||
[
|
||||
{"id": 1, "name": "Window 1"},
|
||||
{"id": 2, "name": "Debug Session"}
|
||||
]
|
||||
```
|
||||
|
||||
### Get Theme Colors
|
||||
|
||||
Retrieve the current theme colors being used by Ray:
|
||||
|
||||
```
|
||||
GET http://localhost:23517/theme
|
||||
```
|
||||
|
||||
Returns the theme information including color palette:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Dark",
|
||||
"colors": {
|
||||
"primary": "#000000",
|
||||
"secondary": "#1a1a1a",
|
||||
"accent": "#3b82f6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case:** When sending custom HTML content to Ray, use these theme colors to ensure your content matches Ray's current theme and looks visually integrated.
|
||||
|
||||
**Example:** Send HTML with matching colors:
|
||||
|
||||
```bash
|
||||
|
||||
# First, get the theme
|
||||
|
||||
THEME=$(curl -s http://localhost:23517/theme)
|
||||
PRIMARY_COLOR=$(echo $THEME | jq -r '.colors.primary')
|
||||
|
||||
# Then send HTML using those colors
|
||||
|
||||
curl -X POST http://localhost:23517/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"uuid": "theme-matched-html",
|
||||
"payloads": [{
|
||||
"type": "custom",
|
||||
"content": {
|
||||
"content": "<div style=\"background: '"$PRIMARY_COLOR"'; padding: 20px;\"><h1>Themed Content</h1></div>",
|
||||
"label": "Themed HTML"
|
||||
},
|
||||
"origin": {"file": "script.sh", "line_number": 1, "hostname": "localhost"}
|
||||
}]
|
||||
}'
|
||||
```
|
||||
|
||||
## Payload Type Reference
|
||||
|
||||
| Type | Content Fields | Purpose |
|
||||
|------|----------------|---------|
|
||||
| `log` | `values` (array) | Send values to Ray |
|
||||
| `custom` | `content`, `label` | HTML or text content |
|
||||
| `table` | `values`, `label` | Display as table |
|
||||
| `color` | `color` | Set entry color |
|
||||
| `screen_color` | `color` | Set screen background |
|
||||
| `label` | `label` | Add label to entry |
|
||||
| `size` | `size` | Set entry size (sm/lg) |
|
||||
| `notify` | `value` | Desktop notification |
|
||||
| `new_screen` | `name` | Create new screen |
|
||||
| `measure` | `name`, `is_new_timer`, timing fields | Performance timing |
|
||||
| `separator` | (empty) | Visual divider |
|
||||
| `clear_all` | (empty) | Clear all entries |
|
||||
| `hide` | (empty) | Hide entry |
|
||||
| `remove` | (empty) | Remove entry |
|
||||
| `confetti` | (empty) | Confetti animation |
|
||||
| `show_app` | (empty) | Show Ray window |
|
||||
| `hide_app` | (empty) | Hide Ray window |
|
||||
151
.agents/skills/fortify-development/SKILL.md
Normal file
151
.agents/skills/fortify-development/SKILL.md
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
---
|
||||
name: fortify-development
|
||||
description: 'ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), passkeys, profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, passkeys, WebAuthn, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.'
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Laravel Fortify Development
|
||||
|
||||
Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Laravel Fortify patterns and documentation.
|
||||
|
||||
## Usage
|
||||
|
||||
- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints
|
||||
- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.)
|
||||
- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field
|
||||
- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.)
|
||||
- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc.
|
||||
|
||||
## Available Features
|
||||
|
||||
Enable in `config/fortify.php` features array:
|
||||
|
||||
- `Features::registration()` - User registration
|
||||
- `Features::resetPasswords()` - Password reset via email
|
||||
- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail`
|
||||
- `Features::updateProfileInformation()` - Profile updates
|
||||
- `Features::updatePasswords()` - Password changes
|
||||
- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes
|
||||
- `Features::passkeys()` - Passwordless authentication with WebAuthn passkeys
|
||||
|
||||
> Use `search-docs` for feature configuration options and customization patterns.
|
||||
|
||||
## Setup Workflows
|
||||
|
||||
### Two-Factor Authentication Setup
|
||||
|
||||
```
|
||||
- [ ] Add TwoFactorAuthenticatable trait to User model
|
||||
- [ ] Enable feature in config/fortify.php
|
||||
- [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
|
||||
- [ ] Set up view callbacks in FortifyServiceProvider
|
||||
- [ ] Create 2FA management UI
|
||||
- [ ] Test QR code and recovery codes
|
||||
```
|
||||
|
||||
> Use `search-docs` for TOTP implementation and recovery code handling patterns.
|
||||
|
||||
### Passkeys Setup
|
||||
|
||||
```
|
||||
- [ ] Add PasskeyAuthenticatable trait to User model and implement PasskeyUser
|
||||
- [ ] Enable passkeys feature in config/fortify.php
|
||||
- [ ] If the passkeys table migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
|
||||
- [ ] Configure passkeys relying_party_id, allowed_origins, user_handle_secret, and timeout if defaults are not suitable
|
||||
- [ ] Build UI with @laravel/passkeys for registration, login, confirmation, and deletion
|
||||
```
|
||||
|
||||
> Use `search-docs` for passkey configuration options. For `@laravel/passkeys` frontend usage, refer to the package's README on npm.
|
||||
|
||||
### Email Verification Setup
|
||||
|
||||
```
|
||||
- [ ] Enable emailVerification feature in config
|
||||
- [ ] Implement MustVerifyEmail interface on User model
|
||||
- [ ] Set up verifyEmailView callback
|
||||
- [ ] Add verified middleware to protected routes
|
||||
- [ ] Test verification email flow
|
||||
```
|
||||
|
||||
> Use `search-docs` for MustVerifyEmail implementation patterns.
|
||||
|
||||
### Password Reset Setup
|
||||
|
||||
```
|
||||
- [ ] Enable resetPasswords feature in config
|
||||
- [ ] Set up requestPasswordResetLinkView callback
|
||||
- [ ] Set up resetPasswordView callback
|
||||
- [ ] Define password.reset named route (if views disabled)
|
||||
- [ ] Test reset email and link flow
|
||||
```
|
||||
|
||||
> Use `search-docs` for custom password reset flow patterns.
|
||||
|
||||
### SPA Authentication Setup
|
||||
|
||||
```
|
||||
- [ ] Set 'views' => false in config/fortify.php
|
||||
- [ ] Install and configure Laravel Sanctum for session-based SPA authentication
|
||||
- [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication)
|
||||
- [ ] Set up CSRF token handling
|
||||
- [ ] Test XHR authentication flows
|
||||
```
|
||||
|
||||
> Use `search-docs` for integration and SPA authentication patterns.
|
||||
|
||||
#### Two-Factor Authentication in SPA Mode
|
||||
|
||||
When `views` is set to `false`, Fortify returns JSON responses instead of redirects.
|
||||
|
||||
If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required:
|
||||
|
||||
```json
|
||||
{
|
||||
"two_factor": true
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Custom Authentication Logic
|
||||
|
||||
Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects.
|
||||
|
||||
### Registration Customization
|
||||
|
||||
Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields.
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination.
|
||||
|
||||
## Key Endpoints
|
||||
|
||||
| Feature | Method | Endpoint |
|
||||
|------------------------|----------|---------------------------------------------|
|
||||
| Login | POST | `/login` |
|
||||
| Logout | POST | `/logout` |
|
||||
| Register | POST | `/register` |
|
||||
| Password Reset Request | POST | `/forgot-password` |
|
||||
| Password Reset | POST | `/reset-password` |
|
||||
| Email Verify Notice | GET | `/email/verify` |
|
||||
| Resend Verification | POST | `/email/verification-notification` |
|
||||
| Password Confirm | POST | `/user/confirm-password` |
|
||||
| Enable 2FA | POST | `/user/two-factor-authentication` |
|
||||
| Confirm 2FA | POST | `/user/confirmed-two-factor-authentication` |
|
||||
| 2FA Challenge | POST | `/two-factor-challenge` |
|
||||
| Get QR Code | GET | `/user/two-factor-qr-code` |
|
||||
| Recovery Codes | GET/POST | `/user/two-factor-recovery-codes` |
|
||||
| Passkey Login Options | GET | `/passkeys/login/options` |
|
||||
| Passkey Login | POST | `/passkeys/login` |
|
||||
| Passkey Confirm Options| GET | `/passkeys/confirm/options` |
|
||||
| Passkey Confirm | POST | `/passkeys/confirm` |
|
||||
| Passkey Options | GET | `/user/passkeys/options` |
|
||||
| Register Passkey | POST | `/user/passkeys` |
|
||||
| Delete Passkey | DELETE | `/user/passkeys/{passkey}` |
|
||||
302
.agents/skills/laravel-actions/SKILL.md
Normal file
302
.agents/skills/laravel-actions/SKILL.md
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
---
|
||||
name: laravel-actions
|
||||
description: Build, refactor, and troubleshoot Laravel Actions using lorisleiva/laravel-actions. Use when implementing reusable action classes (object/controller/job/listener/command), converting service classes/controllers/jobs into actions, orchestrating workflows via faked actions, or debugging action entrypoints and wiring.
|
||||
---
|
||||
|
||||
# Laravel Actions or `lorisleiva/laravel-actions`
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill to implement or update actions based on `lorisleiva/laravel-actions` with consistent structure and predictable testing patterns.
|
||||
|
||||
## Quick Workflow
|
||||
|
||||
1. Confirm the package is installed with `composer show lorisleiva/laravel-actions`.
|
||||
2. Create or edit an action class that uses `Lorisleiva\Actions\Concerns\AsAction`.
|
||||
3. Implement `handle(...)` with the core business logic first.
|
||||
4. Add adapter methods only when needed for the requested entrypoint:
|
||||
- `asController` (+ route/invokable controller usage)
|
||||
- `asJob` (+ dispatch)
|
||||
- `asListener` (+ event listener wiring)
|
||||
- `asCommand` (+ command signature/description)
|
||||
5. Add or update tests for the chosen entrypoint.
|
||||
6. When tests need isolation, use action fakes (`MyAction::fake()`) and assertions (`MyAction::assertDispatched()`).
|
||||
|
||||
## Base Action Pattern
|
||||
|
||||
Use this minimal skeleton and expand only what is needed.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class PublishArticle
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(int $articleId): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Project Conventions
|
||||
|
||||
- Place action classes in `App\Actions` unless an existing domain sub-namespace is already used.
|
||||
- Use descriptive `VerbNoun` naming (e.g. `PublishArticle`, `SyncVehicleTaxStatus`).
|
||||
- Keep domain/business logic in `handle(...)`; keep transport and framework concerns in adapter methods (`asController`, `asJob`, `asListener`, `asCommand`).
|
||||
- Prefer explicit parameter and return types in all action methods.
|
||||
- Prefer PHPDoc for complex data contracts (e.g. array shapes), not inline comments.
|
||||
|
||||
### When to Use an Action
|
||||
|
||||
- Use an Action when the same use-case needs multiple entrypoints (HTTP, queue, event, CLI) or benefits from first-class orchestration/faking.
|
||||
- Keep a plain service class when logic is local, single-entrypoint, and unlikely to be reused as an Action.
|
||||
|
||||
## Entrypoint Patterns
|
||||
|
||||
### Run as Object
|
||||
|
||||
- (prefer method) Use static helper from the trait: `PublishArticle::run($id)`.
|
||||
- Use make and call handle: `PublishArticle::make()->handle($id)`.
|
||||
- Call with dependency injection: `app(PublishArticle::class)->handle($id)`.
|
||||
|
||||
### Run as Controller
|
||||
|
||||
- Use route to class (invokable style), e.g. `Route::post('/articles/{id}/publish', PublishArticle::class)`.
|
||||
- Add `asController(...)` for HTTP-specific adaptation and return a response.
|
||||
- Add request validation (`rules()` or custom validator hooks) when input comes from HTTP.
|
||||
|
||||
### Run as Job
|
||||
|
||||
- Dispatch with `PublishArticle::dispatch($id)`.
|
||||
- Use `asJob(...)` only for queue-specific behavior; keep domain logic in `handle(...)`.
|
||||
- In this project, job Actions often define additional queue lifecycle methods and job properties for retries, uniqueness, and timing control.
|
||||
|
||||
#### Project Pattern: Job Action with Extra Methods
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Demo;
|
||||
|
||||
use App\Models\Demo;
|
||||
use DateTime;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Lorisleiva\Actions\Decorators\JobDecorator;
|
||||
|
||||
class GetDemoData
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public int $jobTries = 3;
|
||||
|
||||
public int $jobMaxExceptions = 3;
|
||||
|
||||
public function getJobRetryUntil(): DateTime
|
||||
{
|
||||
return now()->addMinutes(30);
|
||||
}
|
||||
|
||||
public function getJobBackoff(): array
|
||||
{
|
||||
return [60, 120];
|
||||
}
|
||||
|
||||
public function getJobUniqueId(Demo $demo): string
|
||||
{
|
||||
return $demo->id;
|
||||
}
|
||||
|
||||
public function handle(Demo $demo): void
|
||||
{
|
||||
// Core business logic.
|
||||
}
|
||||
|
||||
public function asJob(JobDecorator $job, Demo $demo): void
|
||||
{
|
||||
// Queue-specific orchestration and retry behavior.
|
||||
$this->handle($demo);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use these members only when needed:
|
||||
|
||||
- `$jobTries`: max attempts for the queued execution.
|
||||
- `$jobMaxExceptions`: max unhandled exceptions before failing.
|
||||
- `getJobRetryUntil()`: absolute retry deadline.
|
||||
- `getJobBackoff()`: retry delay strategy per attempt.
|
||||
- `getJobUniqueId(...)`: deduplication key for unique jobs.
|
||||
- `asJob(JobDecorator $job, ...)`: access attempt metadata and queue-only branching.
|
||||
|
||||
### Run as Listener
|
||||
|
||||
- Register the action class as listener in `EventServiceProvider`.
|
||||
- Use `asListener(EventName $event)` and delegate to `handle(...)`.
|
||||
|
||||
### Run as Command
|
||||
|
||||
- Define `$commandSignature` and `$commandDescription` properties.
|
||||
- Implement `asCommand(Command $command)` and keep console IO in this method only.
|
||||
- Import `Command` with `use Illuminate\Console\Command;`.
|
||||
|
||||
## Testing Guidance
|
||||
|
||||
Use a two-layer strategy:
|
||||
|
||||
1. `handle(...)` tests for business correctness.
|
||||
2. entrypoint tests (`asController`, `asJob`, `asListener`, `asCommand`) for wiring/orchestration.
|
||||
|
||||
### Deep Dive: `AsFake` methods (2.x)
|
||||
|
||||
Reference: https://www.laravelactions.com/2.x/as-fake.html
|
||||
|
||||
Use these methods intentionally based on what you want to prove.
|
||||
|
||||
#### `mock()`
|
||||
|
||||
- Replaces the action with a full mock.
|
||||
- Best when you need strict expectations and argument assertions.
|
||||
|
||||
```php
|
||||
PublishArticle::mock()
|
||||
->shouldReceive('handle')
|
||||
->once()
|
||||
->with(42)
|
||||
->andReturnTrue();
|
||||
```
|
||||
|
||||
#### `partialMock()`
|
||||
|
||||
- Replaces the action with a partial mock.
|
||||
- Best when you want to keep most real behavior but stub one expensive/internal method.
|
||||
|
||||
```php
|
||||
PublishArticle::partialMock()
|
||||
->shouldReceive('fetchRemoteData')
|
||||
->once()
|
||||
->andReturn(['ok' => true]);
|
||||
```
|
||||
|
||||
#### `spy()`
|
||||
|
||||
- Replaces the action with a spy.
|
||||
- Best for post-execution verification ("was called with X") without predefining all expectations.
|
||||
|
||||
```php
|
||||
$spy = PublishArticle::spy()->allows('handle')->andReturnTrue();
|
||||
|
||||
// execute code that triggers the action...
|
||||
|
||||
$spy->shouldHaveReceived('handle')->with(42);
|
||||
```
|
||||
|
||||
#### `shouldRun()`
|
||||
|
||||
- Shortcut for `mock()->shouldReceive('handle')`.
|
||||
- Best for compact orchestration assertions.
|
||||
|
||||
```php
|
||||
PublishArticle::shouldRun()->once()->with(42)->andReturnTrue();
|
||||
```
|
||||
|
||||
#### `shouldNotRun()`
|
||||
|
||||
- Shortcut for `mock()->shouldNotReceive('handle')`.
|
||||
- Best for guard-clause tests and branch coverage.
|
||||
|
||||
```php
|
||||
PublishArticle::shouldNotRun();
|
||||
```
|
||||
|
||||
#### `allowToRun()`
|
||||
|
||||
- Shortcut for spy + allowing `handle`.
|
||||
- Best when you want execution to proceed but still assert interaction.
|
||||
|
||||
```php
|
||||
$spy = PublishArticle::allowToRun()->andReturnTrue();
|
||||
// ...
|
||||
$spy->shouldHaveReceived('handle')->once();
|
||||
```
|
||||
|
||||
#### `isFake()` and `clearFake()`
|
||||
|
||||
- `isFake()` checks whether the class is currently swapped.
|
||||
- `clearFake()` resets the fake and prevents cross-test leakage.
|
||||
|
||||
```php
|
||||
expect(PublishArticle::isFake())->toBeFalse();
|
||||
PublishArticle::mock();
|
||||
expect(PublishArticle::isFake())->toBeTrue();
|
||||
PublishArticle::clearFake();
|
||||
expect(PublishArticle::isFake())->toBeFalse();
|
||||
```
|
||||
|
||||
### Recommended test matrix for Actions
|
||||
|
||||
- Business rule test: call `handle(...)` directly with real dependencies/factories.
|
||||
- HTTP wiring test: hit route/controller, fake downstream actions with `shouldRun` or `shouldNotRun`.
|
||||
- Job wiring test: dispatch action as job, assert expected downstream action calls.
|
||||
- Event listener test: dispatch event, assert action interaction via fake/spy.
|
||||
- Console test: run artisan command, assert action invocation and output.
|
||||
|
||||
### Practical defaults
|
||||
|
||||
- Prefer `shouldRun()` and `shouldNotRun()` for readability in branch tests.
|
||||
- Prefer `spy()`/`allowToRun()` when behavior is mostly real and you only need call verification.
|
||||
- Prefer `mock()` when interaction contracts are strict and should fail fast.
|
||||
- Use `clearFake()` in cleanup when a fake might leak into another test.
|
||||
- Keep side effects isolated: fake only the action under test boundary, not everything.
|
||||
|
||||
### Pest style examples
|
||||
|
||||
```php
|
||||
it('dispatches the downstream action', function () {
|
||||
SendInvoiceEmail::shouldRun()->once()->withArgs(fn (int $invoiceId) => $invoiceId > 0);
|
||||
|
||||
FinalizeInvoice::run(123);
|
||||
});
|
||||
|
||||
it('does not dispatch when invoice is already sent', function () {
|
||||
SendInvoiceEmail::shouldNotRun();
|
||||
|
||||
FinalizeInvoice::run(123, alreadySent: true);
|
||||
});
|
||||
```
|
||||
|
||||
Run the minimum relevant suite first, e.g. `php artisan test --compact --filter=PublishArticle` or by specific test file.
|
||||
|
||||
## Troubleshooting Checklist
|
||||
|
||||
- Ensure the class uses `AsAction` and namespace matches autoload.
|
||||
- Check route registration when used as controller.
|
||||
- Check queue config when using `dispatch`.
|
||||
- Verify event-to-listener mapping in `EventServiceProvider`.
|
||||
- Keep transport concerns in adapter methods (`asController`, `asCommand`, etc.), not in `handle(...)`.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Putting HTTP response/redirect logic inside `handle(...)` instead of `asController(...)`.
|
||||
- Duplicating business rules across `as*` methods rather than delegating to `handle(...)`.
|
||||
- Assuming listener wiring works without explicit registration where required.
|
||||
- Testing only entrypoints and skipping direct `handle(...)` behavior tests.
|
||||
- Overusing Actions for one-off, single-context logic with no reuse pressure.
|
||||
|
||||
## Topic References
|
||||
|
||||
Use these references for deep dives by entrypoint/topic. Keep `SKILL.md` focused on workflow and decision rules.
|
||||
|
||||
- Object entrypoint: `references/object.md`
|
||||
- Controller entrypoint: `references/controller.md`
|
||||
- Job entrypoint: `references/job.md`
|
||||
- Listener entrypoint: `references/listener.md`
|
||||
- Command entrypoint: `references/command.md`
|
||||
- With attributes: `references/with-attributes.md`
|
||||
- Testing and fakes: `references/testing-fakes.md`
|
||||
- Troubleshooting: `references/troubleshooting.md`
|
||||
160
.agents/skills/laravel-actions/references/command.md
Normal file
160
.agents/skills/laravel-actions/references/command.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# Command Entrypoint (`asCommand`)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when exposing actions as Artisan commands.
|
||||
|
||||
## Recap
|
||||
|
||||
- Documents command execution via `asCommand(...)` and fallback to `handle(...)`.
|
||||
- Covers command metadata via methods/properties (signature, description, help, hidden).
|
||||
- Includes registration example and focused artisan test pattern.
|
||||
- Reinforces separation between console I/O and domain logic.
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Define `$commandSignature` and `$commandDescription`.
|
||||
- Implement `asCommand(Command $command)` for console I/O.
|
||||
- Keep business logic in `handle(...)`.
|
||||
|
||||
## Methods used (`CommandDecorator`)
|
||||
|
||||
### `asCommand`
|
||||
|
||||
Called when executed as a command. If missing, it falls back to `handle(...)`.
|
||||
|
||||
```php
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UpdateUserRole
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $commandSignature = 'users:update-role {user_id} {role}';
|
||||
|
||||
public function handle(User $user, string $newRole): void
|
||||
{
|
||||
$user->update(['role' => $newRole]);
|
||||
}
|
||||
|
||||
public function asCommand(Command $command): void
|
||||
{
|
||||
$this->handle(
|
||||
User::findOrFail($command->argument('user_id')),
|
||||
$command->argument('role')
|
||||
);
|
||||
|
||||
$command->info('Done!');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `getCommandSignature`
|
||||
|
||||
Defines the command signature. Required when registering an action as a command if no `$commandSignature` property is set.
|
||||
|
||||
```php
|
||||
public function getCommandSignature(): string
|
||||
{
|
||||
return 'users:update-role {user_id} {role}';
|
||||
}
|
||||
```
|
||||
|
||||
### `$commandSignature`
|
||||
|
||||
Property alternative to `getCommandSignature`.
|
||||
|
||||
```php
|
||||
public string $commandSignature = 'users:update-role {user_id} {role}';
|
||||
```
|
||||
|
||||
### `getCommandDescription`
|
||||
|
||||
Provides command description.
|
||||
|
||||
```php
|
||||
public function getCommandDescription(): string
|
||||
{
|
||||
return 'Updates the role of a given user.';
|
||||
}
|
||||
```
|
||||
|
||||
### `$commandDescription`
|
||||
|
||||
Property alternative to `getCommandDescription`.
|
||||
|
||||
```php
|
||||
public string $commandDescription = 'Updates the role of a given user.';
|
||||
```
|
||||
|
||||
### `getCommandHelp`
|
||||
|
||||
Provides additional help text shown with `--help`.
|
||||
|
||||
```php
|
||||
public function getCommandHelp(): string
|
||||
{
|
||||
return 'My help message.';
|
||||
}
|
||||
```
|
||||
|
||||
### `$commandHelp`
|
||||
|
||||
Property alternative to `getCommandHelp`.
|
||||
|
||||
```php
|
||||
public string $commandHelp = 'My help message.';
|
||||
```
|
||||
|
||||
### `isCommandHidden`
|
||||
|
||||
Defines whether command should be hidden from artisan list. Default is `false`.
|
||||
|
||||
```php
|
||||
public function isCommandHidden(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### `$commandHidden`
|
||||
|
||||
Property alternative to `isCommandHidden`.
|
||||
|
||||
```php
|
||||
public bool $commandHidden = true;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Register in console kernel
|
||||
|
||||
```php
|
||||
// app/Console/Kernel.php
|
||||
protected $commands = [
|
||||
UpdateUserRole::class,
|
||||
];
|
||||
```
|
||||
|
||||
### Focused command test
|
||||
|
||||
```php
|
||||
$this->artisan('users:update-role 1 admin')
|
||||
->expectsOutput('Done!')
|
||||
->assertSuccessful();
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- `use Illuminate\Console\Command;` is imported.
|
||||
- Signature/options/arguments are documented.
|
||||
- Command test verifies invocation and output.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Mixing command I/O with domain logic in `handle(...)`.
|
||||
- Missing/ambiguous command signature.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-command.html
|
||||
339
.agents/skills/laravel-actions/references/controller.md
Normal file
339
.agents/skills/laravel-actions/references/controller.md
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
# Controller Entrypoint (`asController`)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when exposing an action through HTTP routes.
|
||||
|
||||
## Recap
|
||||
|
||||
- Documents controller lifecycle around `asController(...)` and response adapters.
|
||||
- Covers routing patterns, middleware, and optional in-action `routes()` registration.
|
||||
- Summarizes validation/authorization hooks used by `ActionRequest`.
|
||||
- Provides extension points for JSON/HTML responses and failure customization.
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Route directly to action class when appropriate.
|
||||
- Keep HTTP adaptation in controller methods (`asController`, `jsonResponse`, `htmlResponse`).
|
||||
- Keep domain logic in `handle(...)`.
|
||||
|
||||
## Methods provided (`AsController` trait)
|
||||
|
||||
### `__invoke`
|
||||
|
||||
Required so Laravel can register the action class as an invokable controller.
|
||||
|
||||
```php
|
||||
$action($someArguments);
|
||||
|
||||
// Equivalent to:
|
||||
$action->handle($someArguments);
|
||||
```
|
||||
|
||||
If the method does not exist, Laravel route registration fails for invokable controllers.
|
||||
|
||||
```php
|
||||
// Illuminate\Routing\RouteAction
|
||||
protected static function makeInvokable($action)
|
||||
{
|
||||
if (! method_exists($action, '__invoke')) {
|
||||
throw new UnexpectedValueException("Invalid route action: [{$action}].");
|
||||
}
|
||||
|
||||
return $action.'@__invoke';
|
||||
}
|
||||
```
|
||||
|
||||
If you need your own `__invoke`, alias the trait implementation:
|
||||
|
||||
```php
|
||||
class MyAction
|
||||
{
|
||||
use AsAction {
|
||||
__invoke as protected invokeFromLaravelActions;
|
||||
}
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
// Custom behavior...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Methods used (`ControllerDecorator` + `ActionRequest`)
|
||||
|
||||
### `asController`
|
||||
|
||||
Called when used as invokable controller. If missing, it falls back to `handle(...)`.
|
||||
|
||||
```php
|
||||
public function asController(User $user, Request $request): Response
|
||||
{
|
||||
$article = $this->handle(
|
||||
$user,
|
||||
$request->get('title'),
|
||||
$request->get('body')
|
||||
);
|
||||
|
||||
return redirect()->route('articles.show', [$article]);
|
||||
}
|
||||
```
|
||||
|
||||
### `jsonResponse`
|
||||
|
||||
Called after `asController` when request expects JSON.
|
||||
|
||||
```php
|
||||
public function jsonResponse(Article $article, Request $request): ArticleResource
|
||||
{
|
||||
return new ArticleResource($article);
|
||||
}
|
||||
```
|
||||
|
||||
### `htmlResponse`
|
||||
|
||||
Called after `asController` when request expects HTML.
|
||||
|
||||
```php
|
||||
public function htmlResponse(Article $article, Request $request): Response
|
||||
{
|
||||
return redirect()->route('articles.show', [$article]);
|
||||
}
|
||||
```
|
||||
|
||||
### `getControllerMiddleware`
|
||||
|
||||
Adds middleware directly on the action controller.
|
||||
|
||||
```php
|
||||
public function getControllerMiddleware(): array
|
||||
{
|
||||
return ['auth', MyCustomMiddleware::class];
|
||||
}
|
||||
```
|
||||
|
||||
### `routes`
|
||||
|
||||
Defines routes directly in the action.
|
||||
|
||||
```php
|
||||
public static function routes(Router $router)
|
||||
{
|
||||
$router->get('author/{author}/articles', static::class);
|
||||
}
|
||||
```
|
||||
|
||||
To enable this, register routes from actions in a service provider:
|
||||
|
||||
```php
|
||||
use Lorisleiva\Actions\Facades\Actions;
|
||||
|
||||
Actions::registerRoutes();
|
||||
Actions::registerRoutes('app/MyCustomActionsFolder');
|
||||
Actions::registerRoutes([
|
||||
'app/Authentication',
|
||||
'app/Billing',
|
||||
'app/TeamManagement',
|
||||
]);
|
||||
```
|
||||
|
||||
### `prepareForValidation`
|
||||
|
||||
Called before authorization and validation are resolved.
|
||||
|
||||
```php
|
||||
public function prepareForValidation(ActionRequest $request): void
|
||||
{
|
||||
$request->merge(['some' => 'additional data']);
|
||||
}
|
||||
```
|
||||
|
||||
### `authorize`
|
||||
|
||||
Defines authorization logic.
|
||||
|
||||
```php
|
||||
public function authorize(ActionRequest $request): bool
|
||||
{
|
||||
return $request->user()->role === 'author';
|
||||
}
|
||||
```
|
||||
|
||||
You can also return gate responses:
|
||||
|
||||
```php
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
public function authorize(ActionRequest $request): Response
|
||||
{
|
||||
if ($request->user()->role !== 'author') {
|
||||
return Response::deny('You must be an author to create a new article.');
|
||||
}
|
||||
|
||||
return Response::allow();
|
||||
}
|
||||
```
|
||||
|
||||
### `rules`
|
||||
|
||||
Defines validation rules.
|
||||
|
||||
```php
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'min:8'],
|
||||
'body' => ['required', IsValidMarkdown::class],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### `withValidator`
|
||||
|
||||
Adds custom validation logic with an after hook.
|
||||
|
||||
```php
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
public function withValidator(Validator $validator, ActionRequest $request): void
|
||||
{
|
||||
$validator->after(function (Validator $validator) use ($request) {
|
||||
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
|
||||
$validator->errors()->add('current_password', 'Wrong password.');
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### `afterValidator`
|
||||
|
||||
Alternative to add post-validation checks.
|
||||
|
||||
```php
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
public function afterValidator(Validator $validator, ActionRequest $request): void
|
||||
{
|
||||
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
|
||||
$validator->errors()->add('current_password', 'Wrong password.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidator`
|
||||
|
||||
Provides a custom validator instead of default rules pipeline.
|
||||
|
||||
```php
|
||||
use Illuminate\Validation\Factory;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
public function getValidator(Factory $factory, ActionRequest $request): Validator
|
||||
{
|
||||
return $factory->make($request->only('title', 'body'), [
|
||||
'title' => ['required', 'min:8'],
|
||||
'body' => ['required', IsValidMarkdown::class],
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationData`
|
||||
|
||||
Defines which data is validated (default: `$request->all()`).
|
||||
|
||||
```php
|
||||
public function getValidationData(ActionRequest $request): array
|
||||
{
|
||||
return $request->all();
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationMessages`
|
||||
|
||||
Custom validation error messages.
|
||||
|
||||
```php
|
||||
public function getValidationMessages(): array
|
||||
{
|
||||
return [
|
||||
'title.required' => 'Looks like you forgot the title.',
|
||||
'body.required' => 'Is that really all you have to say?',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationAttributes`
|
||||
|
||||
Human-friendly names for request attributes.
|
||||
|
||||
```php
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'headline',
|
||||
'body' => 'content',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationRedirect`
|
||||
|
||||
Custom redirect URL on validation failure.
|
||||
|
||||
```php
|
||||
public function getValidationRedirect(UrlGenerator $url): string
|
||||
{
|
||||
return $url->to('/my-custom-redirect-url');
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationErrorBag`
|
||||
|
||||
Custom error bag name on validation failure (default: `default`).
|
||||
|
||||
```php
|
||||
public function getValidationErrorBag(): string
|
||||
{
|
||||
return 'my_custom_error_bag';
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationFailure`
|
||||
|
||||
Override validation failure behavior.
|
||||
|
||||
```php
|
||||
public function getValidationFailure(): void
|
||||
{
|
||||
throw new MyCustomValidationException();
|
||||
}
|
||||
```
|
||||
|
||||
### `getAuthorizationFailure`
|
||||
|
||||
Override authorization failure behavior.
|
||||
|
||||
```php
|
||||
public function getAuthorizationFailure(): void
|
||||
{
|
||||
throw new MyCustomAuthorizationException();
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Route wiring points to the action class.
|
||||
- `asController(...)` delegates to `handle(...)`.
|
||||
- Validation/authorization methods are explicit where needed.
|
||||
- Response mapping is split by channel (`jsonResponse`, `htmlResponse`) when useful.
|
||||
- HTTP tests cover both success and validation/authorization failure branches.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Putting response/redirect logic in `handle(...)`.
|
||||
- Duplicating business rules in `asController(...)` instead of delegating.
|
||||
- Assuming action route discovery works without `Actions::registerRoutes(...)` when using in-action `routes()`.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-controller.html
|
||||
425
.agents/skills/laravel-actions/references/job.md
Normal file
425
.agents/skills/laravel-actions/references/job.md
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
# Job Entrypoint (`dispatch`, `asJob`)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when running an action through queues.
|
||||
|
||||
## Recap
|
||||
|
||||
- Lists async/sync dispatch helpers and conditional dispatch variants.
|
||||
- Covers job wrapping/chaining with `makeJob`, `makeUniqueJob`, and `withChain`.
|
||||
- Documents queue assertion helpers for tests (`assertPushed*`).
|
||||
- Summarizes `JobDecorator` hooks/properties for retries, uniqueness, timeout, and failure handling.
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Dispatch with `Action::dispatch(...)` for async execution.
|
||||
- Keep queue-specific orchestration in `asJob(...)`.
|
||||
- Keep reusable business logic in `handle(...)`.
|
||||
|
||||
## Methods provided (`AsJob` trait)
|
||||
|
||||
### `dispatch`
|
||||
|
||||
Dispatches the action asynchronously.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatch($team);
|
||||
```
|
||||
|
||||
### `dispatchIf`
|
||||
|
||||
Dispatches asynchronously only if condition is met.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatchIf($team->plan === 'premium', $team);
|
||||
```
|
||||
|
||||
### `dispatchUnless`
|
||||
|
||||
Dispatches asynchronously unless condition is met.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatchUnless($team->plan === 'free', $team);
|
||||
```
|
||||
|
||||
### `dispatchSync`
|
||||
|
||||
Dispatches synchronously.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatchSync($team);
|
||||
```
|
||||
|
||||
### `dispatchNow`
|
||||
|
||||
Alias of `dispatchSync`.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatchNow($team);
|
||||
```
|
||||
|
||||
### `dispatchAfterResponse`
|
||||
|
||||
Dispatches synchronously after the HTTP response is sent.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatchAfterResponse($team);
|
||||
```
|
||||
|
||||
### `makeJob`
|
||||
|
||||
Creates a `JobDecorator` wrapper. Useful with `dispatch(...)` helper or chains.
|
||||
|
||||
```php
|
||||
dispatch(SendTeamReportEmail::makeJob($team));
|
||||
```
|
||||
|
||||
### `makeUniqueJob`
|
||||
|
||||
Creates a `UniqueJobDecorator` wrapper. Usually automatic with `ShouldBeUnique`, but can be forced.
|
||||
|
||||
```php
|
||||
dispatch(SendTeamReportEmail::makeUniqueJob($team));
|
||||
```
|
||||
|
||||
### `withChain`
|
||||
|
||||
Attaches jobs to run after successful processing.
|
||||
|
||||
```php
|
||||
$chain = [
|
||||
OptimizeTeamReport::makeJob($team),
|
||||
SendTeamReportEmail::makeJob($team),
|
||||
];
|
||||
|
||||
CreateNewTeamReport::withChain($chain)->dispatch($team);
|
||||
```
|
||||
|
||||
Equivalent using `Bus::chain(...)`:
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
Bus::chain([
|
||||
CreateNewTeamReport::makeJob($team),
|
||||
OptimizeTeamReport::makeJob($team),
|
||||
SendTeamReportEmail::makeJob($team),
|
||||
])->dispatch();
|
||||
```
|
||||
|
||||
Chain assertion example:
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
Bus::fake();
|
||||
|
||||
Bus::assertChained([
|
||||
CreateNewTeamReport::makeJob($team),
|
||||
OptimizeTeamReport::makeJob($team),
|
||||
SendTeamReportEmail::makeJob($team),
|
||||
]);
|
||||
```
|
||||
|
||||
### `assertPushed`
|
||||
|
||||
Asserts the action was queued.
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
Queue::fake();
|
||||
|
||||
SendTeamReportEmail::assertPushed();
|
||||
SendTeamReportEmail::assertPushed(3);
|
||||
SendTeamReportEmail::assertPushed($callback);
|
||||
SendTeamReportEmail::assertPushed(3, $callback);
|
||||
```
|
||||
|
||||
`$callback` receives:
|
||||
- Action instance.
|
||||
- Dispatched arguments.
|
||||
- `JobDecorator` instance.
|
||||
- Queue name.
|
||||
|
||||
### `assertNotPushed`
|
||||
|
||||
Asserts the action was not queued.
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
Queue::fake();
|
||||
|
||||
SendTeamReportEmail::assertNotPushed();
|
||||
SendTeamReportEmail::assertNotPushed($callback);
|
||||
```
|
||||
|
||||
### `assertPushedOn`
|
||||
|
||||
Asserts the action was queued on a specific queue.
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
Queue::fake();
|
||||
|
||||
SendTeamReportEmail::assertPushedOn('reports');
|
||||
SendTeamReportEmail::assertPushedOn('reports', 3);
|
||||
SendTeamReportEmail::assertPushedOn('reports', $callback);
|
||||
SendTeamReportEmail::assertPushedOn('reports', 3, $callback);
|
||||
```
|
||||
|
||||
## Methods used (`JobDecorator`)
|
||||
|
||||
### `asJob`
|
||||
|
||||
Called when dispatched as a job. Falls back to `handle(...)` if missing.
|
||||
|
||||
```php
|
||||
class SendTeamReportEmail
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Team $team, bool $fullReport = false): void
|
||||
{
|
||||
// Prepare report and send it to all $team->users.
|
||||
}
|
||||
|
||||
public function asJob(Team $team): void
|
||||
{
|
||||
$this->handle($team, true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `getJobMiddleware`
|
||||
|
||||
Adds middleware to the queued action.
|
||||
|
||||
```php
|
||||
public function getJobMiddleware(array $parameters): array
|
||||
{
|
||||
return [new RateLimited('reports')];
|
||||
}
|
||||
```
|
||||
|
||||
### `configureJob`
|
||||
|
||||
Configures `JobDecorator` options.
|
||||
|
||||
```php
|
||||
use Lorisleiva\Actions\Decorators\JobDecorator;
|
||||
|
||||
public function configureJob(JobDecorator $job): void
|
||||
{
|
||||
$job->onConnection('my_connection')
|
||||
->onQueue('my_queue')
|
||||
->through(['my_middleware'])
|
||||
->chain(['my_chain'])
|
||||
->delay(60);
|
||||
}
|
||||
```
|
||||
|
||||
### `$jobConnection`
|
||||
|
||||
Defines queue connection.
|
||||
|
||||
```php
|
||||
public string $jobConnection = 'my_connection';
|
||||
```
|
||||
|
||||
### `$jobQueue`
|
||||
|
||||
Defines queue name.
|
||||
|
||||
```php
|
||||
public string $jobQueue = 'my_queue';
|
||||
```
|
||||
|
||||
### `$jobTries`
|
||||
|
||||
Defines max attempts.
|
||||
|
||||
```php
|
||||
public int $jobTries = 10;
|
||||
```
|
||||
|
||||
### `$jobMaxExceptions`
|
||||
|
||||
Defines max unhandled exceptions before failure.
|
||||
|
||||
```php
|
||||
public int $jobMaxExceptions = 3;
|
||||
```
|
||||
|
||||
### `$jobBackoff`
|
||||
|
||||
Defines retry delay seconds.
|
||||
|
||||
```php
|
||||
public int $jobBackoff = 60;
|
||||
```
|
||||
|
||||
### `getJobBackoff`
|
||||
|
||||
Defines retry delay (int or per-attempt array).
|
||||
|
||||
```php
|
||||
public function getJobBackoff(): int
|
||||
{
|
||||
return 60;
|
||||
}
|
||||
|
||||
public function getJobBackoff(): array
|
||||
{
|
||||
return [30, 60, 120];
|
||||
}
|
||||
```
|
||||
|
||||
### `$jobTimeout`
|
||||
|
||||
Defines timeout in seconds.
|
||||
|
||||
```php
|
||||
public int $jobTimeout = 60 * 30;
|
||||
```
|
||||
|
||||
### `$jobRetryUntil`
|
||||
|
||||
Defines timestamp retry deadline.
|
||||
|
||||
```php
|
||||
public int $jobRetryUntil = 1610191764;
|
||||
```
|
||||
|
||||
### `getJobRetryUntil`
|
||||
|
||||
Defines retry deadline as `DateTime`.
|
||||
|
||||
```php
|
||||
public function getJobRetryUntil(): DateTime
|
||||
{
|
||||
return now()->addMinutes(30);
|
||||
}
|
||||
```
|
||||
|
||||
### `getJobDisplayName`
|
||||
|
||||
Customizes queued job display name.
|
||||
|
||||
```php
|
||||
public function getJobDisplayName(): string
|
||||
{
|
||||
return 'Send team report email';
|
||||
}
|
||||
```
|
||||
|
||||
### `getJobTags`
|
||||
|
||||
Adds queue tags.
|
||||
|
||||
```php
|
||||
public function getJobTags(Team $team): array
|
||||
{
|
||||
return ['report', 'team:'.$team->id];
|
||||
}
|
||||
```
|
||||
|
||||
### `getJobUniqueId`
|
||||
|
||||
Defines uniqueness key when using `ShouldBeUnique`.
|
||||
|
||||
```php
|
||||
public function getJobUniqueId(Team $team): int
|
||||
{
|
||||
return $team->id;
|
||||
}
|
||||
```
|
||||
|
||||
### `$jobUniqueId`
|
||||
|
||||
Static uniqueness key alternative.
|
||||
|
||||
```php
|
||||
public string $jobUniqueId = 'some_static_key';
|
||||
```
|
||||
|
||||
### `getJobUniqueFor`
|
||||
|
||||
Defines uniqueness lock duration in seconds.
|
||||
|
||||
```php
|
||||
public function getJobUniqueFor(Team $team): int
|
||||
{
|
||||
return $team->role === 'premium' ? 1800 : 3600;
|
||||
}
|
||||
```
|
||||
|
||||
### `$jobUniqueFor`
|
||||
|
||||
Property alternative for uniqueness lock duration.
|
||||
|
||||
```php
|
||||
public int $jobUniqueFor = 3600;
|
||||
```
|
||||
|
||||
### `getJobUniqueVia`
|
||||
|
||||
Defines cache driver used for uniqueness lock.
|
||||
|
||||
```php
|
||||
public function getJobUniqueVia()
|
||||
{
|
||||
return Cache::driver('redis');
|
||||
}
|
||||
```
|
||||
|
||||
### `$jobDeleteWhenMissingModels`
|
||||
|
||||
Property alternative for missing model handling.
|
||||
|
||||
```php
|
||||
public bool $jobDeleteWhenMissingModels = true;
|
||||
```
|
||||
|
||||
### `getJobDeleteWhenMissingModels`
|
||||
|
||||
Defines whether jobs with missing models are deleted.
|
||||
|
||||
```php
|
||||
public function getJobDeleteWhenMissingModels(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### `jobFailed`
|
||||
|
||||
Handles job failure. Receives exception and dispatched parameters.
|
||||
|
||||
```php
|
||||
public function jobFailed(?Throwable $e, ...$parameters): void
|
||||
{
|
||||
// Notify users, report errors, trigger compensations...
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Async/sync dispatch method matches use-case (`dispatch`, `dispatchSync`, `dispatchAfterResponse`).
|
||||
- Queue config is explicit when needed (`$jobConnection`, `$jobQueue`, `configureJob`).
|
||||
- Retry/backoff/timeout policies are intentional.
|
||||
- `asJob(...)` delegates to `handle(...)` unless queue-specific branching is required.
|
||||
- Queue tests use `Queue::fake()` and action assertions (`assertPushed*`).
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Embedding domain logic only in `asJob(...)`.
|
||||
- Forgetting uniqueness/timeout/retry controls on heavy jobs.
|
||||
- Missing queue-specific assertions in tests.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-job.html
|
||||
81
.agents/skills/laravel-actions/references/listener.md
Normal file
81
.agents/skills/laravel-actions/references/listener.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Listener Entrypoint (`asListener`)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when wiring actions to domain/application events.
|
||||
|
||||
## Recap
|
||||
|
||||
- Shows how listener execution maps event payloads into `handle(...)` arguments.
|
||||
- Describes `asListener(...)` fallback behavior and adaptation role.
|
||||
- Includes event registration example for provider wiring.
|
||||
- Emphasizes test focus on dispatch and action interaction.
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Register action listener in `EventServiceProvider` (or project equivalent).
|
||||
- Use `asListener(Event $event)` for event adaptation.
|
||||
- Delegate core logic to `handle(...)`.
|
||||
|
||||
## Methods used (`ListenerDecorator`)
|
||||
|
||||
### `asListener`
|
||||
|
||||
Called when executed as an event listener. If missing, it falls back to `handle(...)`.
|
||||
|
||||
```php
|
||||
class SendOfferToNearbyDrivers
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Address $source, Address $destination): void
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
public function asListener(TaxiRequested $event): void
|
||||
{
|
||||
$this->handle($event->source, $event->destination);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Event registration
|
||||
|
||||
```php
|
||||
// app/Providers/EventServiceProvider.php
|
||||
protected $listen = [
|
||||
TaxiRequested::class => [
|
||||
SendOfferToNearbyDrivers::class,
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### Focused listener test
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
Event::fake();
|
||||
|
||||
TaxiRequested::dispatch($source, $destination);
|
||||
|
||||
Event::assertDispatched(TaxiRequested::class);
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Event-to-listener mapping is registered.
|
||||
- Listener method signature matches event contract.
|
||||
- Listener tests verify dispatch and action interaction.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Assuming automatic listener registration when explicit mapping is required.
|
||||
- Re-implementing business logic in `asListener(...)`.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-listener.html
|
||||
118
.agents/skills/laravel-actions/references/object.md
Normal file
118
.agents/skills/laravel-actions/references/object.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Object Entrypoint (`run`, `make`, DI)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when the action is invoked as a plain object.
|
||||
|
||||
## Recap
|
||||
|
||||
- Explains object-style invocation with `make`, `run`, `runIf`, `runUnless`.
|
||||
- Clarifies when to use static helpers versus DI/manual invocation.
|
||||
- Includes minimal examples for direct run and service-level injection.
|
||||
- Highlights boundaries: business logic stays in `handle(...)`.
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Keep core business logic in `handle(...)`.
|
||||
- Prefer `Action::run(...)` for readability.
|
||||
- Use `Action::make()->handle(...)` or DI only when needed.
|
||||
|
||||
## Methods provided
|
||||
|
||||
### `make`
|
||||
|
||||
Resolves the action from the container.
|
||||
|
||||
```php
|
||||
PublishArticle::make();
|
||||
|
||||
// Equivalent to:
|
||||
app(PublishArticle::class);
|
||||
```
|
||||
|
||||
### `run`
|
||||
|
||||
Resolves and executes the action.
|
||||
|
||||
```php
|
||||
PublishArticle::run($articleId);
|
||||
|
||||
// Equivalent to:
|
||||
PublishArticle::make()->handle($articleId);
|
||||
```
|
||||
|
||||
### `runIf`
|
||||
|
||||
Resolves and executes the action only if the condition is met.
|
||||
|
||||
```php
|
||||
PublishArticle::runIf($shouldPublish, $articleId);
|
||||
|
||||
// Equivalent mental model:
|
||||
if ($shouldPublish) {
|
||||
PublishArticle::run($articleId);
|
||||
}
|
||||
```
|
||||
|
||||
### `runUnless`
|
||||
|
||||
Resolves and executes the action only if the condition is not met.
|
||||
|
||||
```php
|
||||
PublishArticle::runUnless($alreadyPublished, $articleId);
|
||||
|
||||
// Equivalent mental model:
|
||||
if (! $alreadyPublished) {
|
||||
PublishArticle::run($articleId);
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Input/output types are explicit.
|
||||
- `handle(...)` has no transport concerns.
|
||||
- Business behavior is covered by direct `handle(...)` tests.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Putting HTTP/CLI/queue concerns in `handle(...)`.
|
||||
- Calling adapters from `handle(...)` instead of the reverse.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-object.html
|
||||
|
||||
## Examples
|
||||
|
||||
### Minimal object-style invocation
|
||||
|
||||
```php
|
||||
final class PublishArticle
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(int $articleId): bool
|
||||
{
|
||||
// Domain logic...
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$published = PublishArticle::run(42);
|
||||
```
|
||||
|
||||
### Dependency injection invocation
|
||||
|
||||
```php
|
||||
final class ArticleService
|
||||
{
|
||||
public function __construct(
|
||||
private PublishArticle $publishArticle
|
||||
) {}
|
||||
|
||||
public function publish(int $articleId): bool
|
||||
{
|
||||
return $this->publishArticle->handle($articleId);
|
||||
}
|
||||
}
|
||||
```
|
||||
160
.agents/skills/laravel-actions/references/testing-fakes.md
Normal file
160
.agents/skills/laravel-actions/references/testing-fakes.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# Testing and Action Fakes
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when isolating action orchestration in tests.
|
||||
|
||||
## Recap
|
||||
|
||||
- Summarizes all `AsFake` helpers (`mock`, `partialMock`, `spy`, `shouldRun`, `shouldNotRun`, `allowToRun`).
|
||||
- Clarifies when to assert execution versus non-execution.
|
||||
- Covers fake lifecycle checks/reset (`isFake`, `clearFake`).
|
||||
- Provides branch-oriented test examples for orchestration confidence.
|
||||
|
||||
## Core methods
|
||||
|
||||
- `mock()`
|
||||
- `partialMock()`
|
||||
- `spy()`
|
||||
- `shouldRun()`
|
||||
- `shouldNotRun()`
|
||||
- `allowToRun()`
|
||||
- `isFake()`
|
||||
- `clearFake()`
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Test `handle(...)` directly for business rules.
|
||||
- Test entrypoints for wiring/orchestration.
|
||||
- Fake only at the boundary under test.
|
||||
|
||||
## Methods provided (`AsFake` trait)
|
||||
|
||||
### `mock`
|
||||
|
||||
Swaps the action with a full mock.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::mock()
|
||||
->shouldReceive('handle')
|
||||
->with(42)
|
||||
->andReturn(['Loris', 'Will', 'Barney']);
|
||||
```
|
||||
|
||||
### `partialMock`
|
||||
|
||||
Swaps the action with a partial mock.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::partialMock()
|
||||
->shouldReceive('fetch')
|
||||
->with('some_google_identifier')
|
||||
->andReturn(['Loris', 'Will', 'Barney']);
|
||||
```
|
||||
|
||||
### `spy`
|
||||
|
||||
Swaps the action with a spy.
|
||||
|
||||
```php
|
||||
$spy = FetchContactsFromGoogle::spy()
|
||||
->allows('handle')
|
||||
->andReturn(['Loris', 'Will', 'Barney']);
|
||||
|
||||
// ...
|
||||
|
||||
$spy->shouldHaveReceived('handle')->with(42);
|
||||
```
|
||||
|
||||
### `shouldRun`
|
||||
|
||||
Helper adding expectation on `handle`.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::shouldRun();
|
||||
|
||||
// Equivalent to:
|
||||
FetchContactsFromGoogle::mock()->shouldReceive('handle');
|
||||
```
|
||||
|
||||
### `shouldNotRun`
|
||||
|
||||
Helper adding negative expectation on `handle`.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::shouldNotRun();
|
||||
|
||||
// Equivalent to:
|
||||
FetchContactsFromGoogle::mock()->shouldNotReceive('handle');
|
||||
```
|
||||
|
||||
### `allowToRun`
|
||||
|
||||
Helper allowing `handle` on a spy.
|
||||
|
||||
```php
|
||||
$spy = FetchContactsFromGoogle::allowToRun()
|
||||
->andReturn(['Loris', 'Will', 'Barney']);
|
||||
|
||||
// ...
|
||||
|
||||
$spy->shouldHaveReceived('handle')->with(42);
|
||||
```
|
||||
|
||||
### `isFake`
|
||||
|
||||
Returns whether the action has been swapped with a fake.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::isFake(); // false
|
||||
FetchContactsFromGoogle::mock();
|
||||
FetchContactsFromGoogle::isFake(); // true
|
||||
```
|
||||
|
||||
### `clearFake`
|
||||
|
||||
Clears the fake instance, if any.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::mock();
|
||||
FetchContactsFromGoogle::isFake(); // true
|
||||
FetchContactsFromGoogle::clearFake();
|
||||
FetchContactsFromGoogle::isFake(); // false
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Orchestration test
|
||||
|
||||
```php
|
||||
it('runs sync contacts for premium teams', function () {
|
||||
SyncGoogleContacts::shouldRun()->once()->with(42)->andReturnTrue();
|
||||
|
||||
ImportTeamContacts::run(42, isPremium: true);
|
||||
});
|
||||
```
|
||||
|
||||
### Guard-clause test
|
||||
|
||||
```php
|
||||
it('does not run sync when integration is disabled', function () {
|
||||
SyncGoogleContacts::shouldNotRun();
|
||||
|
||||
ImportTeamContacts::run(42, integrationEnabled: false);
|
||||
});
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Assertions verify call intent and argument contracts.
|
||||
- Fakes are cleared when leakage risk exists.
|
||||
- Branch tests use `shouldRun()` / `shouldNotRun()` where clearer.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Over-mocking and losing behavior confidence.
|
||||
- Asserting only dispatch, not business correctness.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-fake.html
|
||||
33
.agents/skills/laravel-actions/references/troubleshooting.md
Normal file
33
.agents/skills/laravel-actions/references/troubleshooting.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Troubleshooting
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when action wiring behaves unexpectedly.
|
||||
|
||||
## Recap
|
||||
|
||||
- Provides a fast triage flow for routing, queueing, events, and command wiring.
|
||||
- Lists recurring failure patterns and where to check first.
|
||||
- Encourages reproducing issues with focused tests before broad debugging.
|
||||
- Separates wiring diagnostics from domain logic verification.
|
||||
|
||||
## Fast checks
|
||||
|
||||
- Action class uses `AsAction`.
|
||||
- Namespace and autoloading are correct.
|
||||
- Entrypoint wiring (route, queue, event, command) is registered.
|
||||
- Method signatures and argument types match caller expectations.
|
||||
|
||||
## Failure patterns
|
||||
|
||||
- Controller route points to wrong class.
|
||||
- Queue worker/config mismatch.
|
||||
- Listener mapping not loaded.
|
||||
- Command signature mismatch.
|
||||
- Command not registered in the console kernel.
|
||||
|
||||
## Debug checklist
|
||||
|
||||
- Reproduce with a focused failing test.
|
||||
- Validate wiring layer first, then domain behavior.
|
||||
- Isolate dependencies with fakes/spies where appropriate.
|
||||
189
.agents/skills/laravel-actions/references/with-attributes.md
Normal file
189
.agents/skills/laravel-actions/references/with-attributes.md
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
# With Attributes (`WithAttributes` trait)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when an action stores and validates input via internal attributes instead of method arguments.
|
||||
|
||||
## Recap
|
||||
|
||||
- Documents attribute lifecycle APIs (`setRawAttributes`, `fill`, `fillFromRequest`, readers/writers).
|
||||
- Clarifies behavior of key collisions (`fillFromRequest`: request data wins over route params).
|
||||
- Lists validation/authorization hooks reused from controller validation pipeline.
|
||||
- Includes end-to-end example from fill to `validateAttributes()` and `handle(...)`.
|
||||
|
||||
## Methods provided (`WithAttributes` trait)
|
||||
|
||||
### `setRawAttributes`
|
||||
|
||||
Replaces all attributes with the provided payload.
|
||||
|
||||
```php
|
||||
$action->setRawAttributes([
|
||||
'key' => 'value',
|
||||
]);
|
||||
```
|
||||
|
||||
### `fill`
|
||||
|
||||
Merges provided attributes into existing attributes.
|
||||
|
||||
```php
|
||||
$action->fill([
|
||||
'key' => 'value',
|
||||
]);
|
||||
```
|
||||
|
||||
### `fillFromRequest`
|
||||
|
||||
Merges request input and route parameters into attributes. Request input has priority over route parameters when keys collide.
|
||||
|
||||
```php
|
||||
$action->fillFromRequest($request);
|
||||
```
|
||||
|
||||
### `all`
|
||||
|
||||
Returns all attributes.
|
||||
|
||||
```php
|
||||
$action->all();
|
||||
```
|
||||
|
||||
### `only`
|
||||
|
||||
Returns attributes matching the provided keys.
|
||||
|
||||
```php
|
||||
$action->only('title', 'body');
|
||||
```
|
||||
|
||||
### `except`
|
||||
|
||||
Returns attributes excluding the provided keys.
|
||||
|
||||
```php
|
||||
$action->except('body');
|
||||
```
|
||||
|
||||
### `has`
|
||||
|
||||
Returns whether an attribute exists for the given key.
|
||||
|
||||
```php
|
||||
$action->has('title');
|
||||
```
|
||||
|
||||
### `get`
|
||||
|
||||
Returns the attribute value by key, with optional default.
|
||||
|
||||
```php
|
||||
$action->get('title');
|
||||
$action->get('title', 'Untitled');
|
||||
```
|
||||
|
||||
### `set`
|
||||
|
||||
Sets an attribute value by key.
|
||||
|
||||
```php
|
||||
$action->set('title', 'My blog post');
|
||||
```
|
||||
|
||||
### `__get`
|
||||
|
||||
Accesses attributes as object properties.
|
||||
|
||||
```php
|
||||
$action->title;
|
||||
```
|
||||
|
||||
### `__set`
|
||||
|
||||
Updates attributes as object properties.
|
||||
|
||||
```php
|
||||
$action->title = 'My blog post';
|
||||
```
|
||||
|
||||
### `__isset`
|
||||
|
||||
Checks attribute existence as object properties.
|
||||
|
||||
```php
|
||||
isset($action->title);
|
||||
```
|
||||
|
||||
### `validateAttributes`
|
||||
|
||||
Runs authorization and validation using action attributes and returns validated data.
|
||||
|
||||
```php
|
||||
$validatedData = $action->validateAttributes();
|
||||
```
|
||||
|
||||
## Methods used (`AttributeValidator`)
|
||||
|
||||
`WithAttributes` uses the same authorization/validation hooks as `AsController`:
|
||||
|
||||
- `prepareForValidation`
|
||||
- `authorize`
|
||||
- `rules`
|
||||
- `withValidator`
|
||||
- `afterValidator`
|
||||
- `getValidator`
|
||||
- `getValidationData`
|
||||
- `getValidationMessages`
|
||||
- `getValidationAttributes`
|
||||
- `getValidationRedirect`
|
||||
- `getValidationErrorBag`
|
||||
- `getValidationFailure`
|
||||
- `getAuthorizationFailure`
|
||||
|
||||
## Example
|
||||
|
||||
```php
|
||||
class CreateArticle
|
||||
{
|
||||
use AsAction;
|
||||
use WithAttributes;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'min:8'],
|
||||
'body' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
public function handle(array $attributes): Article
|
||||
{
|
||||
return Article::create($attributes);
|
||||
}
|
||||
}
|
||||
|
||||
$action = CreateArticle::make()->fill([
|
||||
'title' => 'My first post',
|
||||
'body' => 'Hello world',
|
||||
]);
|
||||
|
||||
$validated = $action->validateAttributes();
|
||||
$article = $action->handle($validated);
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Attribute keys are explicit and stable.
|
||||
- Validation rules match expected attribute shape.
|
||||
- `validateAttributes()` is called before side effects when needed.
|
||||
- Validation/authorization hooks are tested in focused unit tests.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Mixing attribute-based and argument-based flows inconsistently in the same action.
|
||||
- Assuming route params override request input in `fillFromRequest` (they do not).
|
||||
- Skipping `validateAttributes()` when using external input.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/with-attributes.html
|
||||
190
.agents/skills/laravel-best-practices/SKILL.md
Normal file
190
.agents/skills/laravel-best-practices/SKILL.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
---
|
||||
name: laravel-best-practices
|
||||
description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Laravel Best Practices
|
||||
|
||||
Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.
|
||||
|
||||
## Consistency First
|
||||
|
||||
Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.
|
||||
|
||||
Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. Database Performance → `rules/db-performance.md`
|
||||
|
||||
- Eager load with `with()` to prevent N+1 queries
|
||||
- Enable `Model::preventLazyLoading()` in development
|
||||
- Select only needed columns, avoid `SELECT *`
|
||||
- `chunk()` / `chunkById()` for large datasets
|
||||
- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
|
||||
- `withCount()` instead of loading relations to count
|
||||
- `cursor()` for memory-efficient read-only iteration
|
||||
- Never query in Blade templates
|
||||
|
||||
### 2. Advanced Query Patterns → `rules/advanced-queries.md`
|
||||
|
||||
- `addSelect()` subqueries over eager-loading entire has-many for a single value
|
||||
- Dynamic relationships via subquery FK + `belongsTo`
|
||||
- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
|
||||
- `setRelation()` to prevent circular N+1 queries
|
||||
- `whereIn` + `pluck()` over `whereHas` for better index usage
|
||||
- Two simple queries can beat one complex query
|
||||
- Compound indexes matching `orderBy` column order
|
||||
- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)
|
||||
|
||||
### 3. Security → `rules/security.md`
|
||||
|
||||
- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
|
||||
- No raw SQL with user input — use Eloquent or query builder
|
||||
- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
|
||||
- Validate MIME type, extension, and size for file uploads
|
||||
- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields
|
||||
|
||||
### 4. Caching → `rules/caching.md`
|
||||
|
||||
- `Cache::remember()` over manual get/put
|
||||
- `Cache::flexible()` for stale-while-revalidate on high-traffic data
|
||||
- `Cache::memo()` to avoid redundant cache hits within a request
|
||||
- Cache tags to invalidate related groups
|
||||
- `Cache::add()` for atomic conditional writes
|
||||
- `once()` to memoize per-request or per-object lifetime
|
||||
- `Cache::lock()` / `lockForUpdate()` for race conditions
|
||||
- Failover cache stores in production
|
||||
|
||||
### 5. Eloquent Patterns → `rules/eloquent.md`
|
||||
|
||||
- Correct relationship types with return type hints
|
||||
- Local scopes for reusable query constraints
|
||||
- Global scopes sparingly — document their existence
|
||||
- Attribute casts in the `casts()` method
|
||||
- Cast date columns, use Carbon instances in templates
|
||||
- `whereBelongsTo($model)` for cleaner queries
|
||||
- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries
|
||||
|
||||
### 6. Validation & Forms → `rules/validation.md`
|
||||
|
||||
- Form Request classes, not inline validation
|
||||
- Array notation `['required', 'email']` for new code; follow existing convention
|
||||
- `$request->validated()` only — never `$request->all()`
|
||||
- `Rule::when()` for conditional validation
|
||||
- `after()` instead of `withValidator()`
|
||||
|
||||
### 7. Configuration → `rules/config.md`
|
||||
|
||||
- `env()` only inside config files
|
||||
- `App::environment()` or `app()->isProduction()`
|
||||
- Config, lang files, and constants over hardcoded text
|
||||
|
||||
### 8. Testing Patterns → `rules/testing.md`
|
||||
|
||||
- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
|
||||
- `assertModelExists()` over raw `assertDatabaseHas()`
|
||||
- Factory states and sequences over manual overrides
|
||||
- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
|
||||
- `recycle()` to share relationship instances across factories
|
||||
|
||||
### 9. Queue & Job Patterns → `rules/queue-jobs.md`
|
||||
|
||||
- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
|
||||
- `ShouldBeUnique` to prevent duplicates; `ShouldBeUniqueUntilProcessing` for early lock release
|
||||
- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
|
||||
- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
|
||||
- Horizon for complex multi-queue scenarios
|
||||
|
||||
### 10. Routing & Controllers → `rules/routing.md`
|
||||
|
||||
- Implicit route model binding
|
||||
- Scoped bindings for nested resources
|
||||
- `Route::resource()` or `apiResource()`
|
||||
- Methods under 10 lines — extract to actions/services
|
||||
- Type-hint Form Requests for auto-validation
|
||||
|
||||
### 11. HTTP Client → `rules/http-client.md`
|
||||
|
||||
- Explicit `timeout` and `connectTimeout` on every request
|
||||
- `retry()` with exponential backoff for external APIs
|
||||
- Check response status or use `throw()`
|
||||
- `Http::pool()` for concurrent independent requests
|
||||
- `Http::fake()` and `preventStrayRequests()` in tests
|
||||
|
||||
### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`
|
||||
|
||||
- Event discovery over manual registration; `event:cache` in production
|
||||
- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
|
||||
- Queue notifications and mailables with `ShouldQueue`
|
||||
- On-demand notifications for non-user recipients
|
||||
- `HasLocalePreference` on notifiable models
|
||||
- `assertQueued()` not `assertSent()` for queued mailables
|
||||
- Markdown mailables for transactional emails
|
||||
|
||||
### 13. Error Handling → `rules/error-handling.md`
|
||||
|
||||
- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
|
||||
- `ShouldntReport` for exceptions that should never log
|
||||
- Throttle high-volume exceptions to protect log sinks
|
||||
- `dontReportDuplicates()` for multi-catch scenarios
|
||||
- Force JSON rendering for API routes
|
||||
- Structured context via `context()` on exception classes
|
||||
|
||||
### 14. Task Scheduling → `rules/scheduling.md`
|
||||
|
||||
- `withoutOverlapping()` on variable-duration tasks
|
||||
- `onOneServer()` on multi-server deployments
|
||||
- `runInBackground()` for concurrent long tasks
|
||||
- `environments()` to restrict to appropriate environments
|
||||
- `takeUntilTimeout()` for time-bounded processing
|
||||
- Schedule groups for shared configuration
|
||||
|
||||
### 15. Architecture → `rules/architecture.md`
|
||||
|
||||
- Single-purpose Action classes; dependency injection over `app()` helper
|
||||
- Prefer official Laravel packages and follow conventions, don't override defaults
|
||||
- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
|
||||
- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution
|
||||
|
||||
### 16. Migrations → `rules/migrations.md`
|
||||
|
||||
- Generate migrations with `php artisan make:migration`
|
||||
- `constrained()` for foreign keys
|
||||
- Never modify migrations that have run in production
|
||||
- Add indexes in the migration, not as an afterthought
|
||||
- Mirror column defaults in model `$attributes`
|
||||
- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
|
||||
- One concern per migration — never mix DDL and DML
|
||||
|
||||
### 17. Collections → `rules/collections.md`
|
||||
|
||||
- Higher-order messages for simple collection operations
|
||||
- `cursor()` vs. `lazy()` — choose based on relationship needs
|
||||
- `lazyById()` when updating records while iterating
|
||||
- `toQuery()` for bulk operations on collections
|
||||
|
||||
### 18. Blade & Views → `rules/blade-views.md`
|
||||
|
||||
- `$attributes->merge()` in component templates
|
||||
- Blade components over `@include`; `@pushOnce` for per-component scripts
|
||||
- View Composers for shared view data
|
||||
- `@aware` for deeply nested component props
|
||||
|
||||
### 19. Conventions & Style → `rules/style.md`
|
||||
|
||||
- Follow Laravel naming conventions for all entities
|
||||
- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
|
||||
- No JS/CSS in Blade, no HTML in PHP classes
|
||||
- Code should be readable; comments only for config files
|
||||
|
||||
## How to Apply
|
||||
|
||||
Always use a sub-agent to read rule files and explore this skill's content.
|
||||
|
||||
1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
|
||||
2. Check sibling files for existing patterns — follow those first per Consistency First
|
||||
3. Verify API syntax with `search-docs` for the installed Laravel version
|
||||
106
.agents/skills/laravel-best-practices/rules/advanced-queries.md
Normal file
106
.agents/skills/laravel-best-practices/rules/advanced-queries.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Advanced Query Patterns
|
||||
|
||||
## Use `addSelect()` Subqueries for Single Values from Has-Many
|
||||
|
||||
Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries.
|
||||
|
||||
```php
|
||||
public function scopeWithLastLoginAt($query): void
|
||||
{
|
||||
$query->addSelect([
|
||||
'last_login_at' => Login::select('created_at')
|
||||
->whereColumn('user_id', 'users.id')
|
||||
->latest()
|
||||
->take(1),
|
||||
])->withCasts(['last_login_at' => 'datetime']);
|
||||
}
|
||||
```
|
||||
|
||||
## Create Dynamic Relationships via Subquery FK
|
||||
|
||||
Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection.
|
||||
|
||||
```php
|
||||
public function lastLogin(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Login::class);
|
||||
}
|
||||
|
||||
public function scopeWithLastLogin($query): void
|
||||
{
|
||||
$query->addSelect([
|
||||
'last_login_id' => Login::select('id')
|
||||
->whereColumn('user_id', 'users.id')
|
||||
->latest()
|
||||
->take(1),
|
||||
])->with('lastLogin');
|
||||
}
|
||||
```
|
||||
|
||||
## Use Conditional Aggregates Instead of Multiple Count Queries
|
||||
|
||||
Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values.
|
||||
|
||||
```php
|
||||
$statuses = Feature::toBase()
|
||||
->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
|
||||
->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
|
||||
->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
|
||||
->first();
|
||||
```
|
||||
|
||||
## Use `setRelation()` to Prevent Circular N+1
|
||||
|
||||
When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries.
|
||||
|
||||
```php
|
||||
$feature->load('comments.user');
|
||||
$feature->comments->each->setRelation('feature', $feature);
|
||||
```
|
||||
|
||||
## Prefer `whereIn` + Subquery Over `whereHas`
|
||||
|
||||
`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory.
|
||||
|
||||
Incorrect (correlated EXISTS re-executes per row):
|
||||
|
||||
```php
|
||||
$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));
|
||||
```
|
||||
|
||||
Correct (index-friendly subquery, no PHP memory overhead):
|
||||
|
||||
```php
|
||||
$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));
|
||||
```
|
||||
|
||||
## Sometimes Two Simple Queries Beat One Complex Query
|
||||
|
||||
Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index.
|
||||
|
||||
## Use Compound Indexes Matching `orderBy` Column Order
|
||||
|
||||
When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index.
|
||||
|
||||
```php
|
||||
// Migration
|
||||
$table->index(['last_name', 'first_name']);
|
||||
|
||||
// Query — column order must match the index
|
||||
User::query()->orderBy('last_name')->orderBy('first_name')->paginate();
|
||||
```
|
||||
|
||||
## Use Correlated Subqueries for Has-Many Ordering
|
||||
|
||||
When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading.
|
||||
|
||||
```php
|
||||
public function scopeOrderByLastLogin($query): void
|
||||
{
|
||||
$query->orderByDesc(Login::select('created_at')
|
||||
->whereColumn('user_id', 'users.id')
|
||||
->latest()
|
||||
->take(1)
|
||||
);
|
||||
}
|
||||
```
|
||||
202
.agents/skills/laravel-best-practices/rules/architecture.md
Normal file
202
.agents/skills/laravel-best-practices/rules/architecture.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# Architecture Best Practices
|
||||
|
||||
## Single-Purpose Action Classes
|
||||
|
||||
Extract discrete business operations into invokable Action classes.
|
||||
|
||||
```php
|
||||
class CreateOrderAction
|
||||
{
|
||||
public function __construct(private InventoryService $inventory) {}
|
||||
|
||||
public function execute(array $data): Order
|
||||
{
|
||||
$order = Order::create($data);
|
||||
$this->inventory->reserve($order);
|
||||
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Use Dependency Injection
|
||||
|
||||
Always use constructor injection. Avoid `app()` or `resolve()` inside classes.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
class OrderController extends Controller
|
||||
{
|
||||
public function store(StoreOrderRequest $request)
|
||||
{
|
||||
$service = app(OrderService::class);
|
||||
|
||||
return $service->create($request->validated());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
class OrderController extends Controller
|
||||
{
|
||||
public function __construct(private OrderService $service) {}
|
||||
|
||||
public function store(StoreOrderRequest $request)
|
||||
{
|
||||
return $this->service->create($request->validated());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Code to Interfaces
|
||||
|
||||
Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability.
|
||||
|
||||
Incorrect (concrete dependency):
|
||||
```php
|
||||
class OrderService
|
||||
{
|
||||
public function __construct(private StripeGateway $gateway) {}
|
||||
}
|
||||
```
|
||||
|
||||
Correct (interface dependency):
|
||||
```php
|
||||
interface PaymentGateway
|
||||
{
|
||||
public function charge(int $amount, string $customerId): PaymentResult;
|
||||
}
|
||||
|
||||
class OrderService
|
||||
{
|
||||
public function __construct(private PaymentGateway $gateway) {}
|
||||
}
|
||||
```
|
||||
|
||||
Bind in a service provider:
|
||||
|
||||
```php
|
||||
$this->app->bind(PaymentGateway::class, StripeGateway::class);
|
||||
```
|
||||
|
||||
## Default Sort by Descending
|
||||
|
||||
When no explicit order is specified, sort by `id` or `created_at` descending. Without an explicit `ORDER BY`, row order is undefined.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$posts = Post::paginate();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$posts = Post::latest()->paginate();
|
||||
```
|
||||
|
||||
## Use Atomic Locks for Race Conditions
|
||||
|
||||
Prevent race conditions with `Cache::lock()` or `lockForUpdate()`.
|
||||
|
||||
```php
|
||||
Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) {
|
||||
$order->process();
|
||||
});
|
||||
|
||||
// Or at query level
|
||||
$product = Product::where('id', $id)->lockForUpdate()->first();
|
||||
```
|
||||
|
||||
## Use `mb_*` String Functions
|
||||
|
||||
When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
strlen('José'); // 5 (bytes, not characters)
|
||||
strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
mb_strlen('José'); // 4 (characters)
|
||||
mb_strtolower('MÜNCHEN'); // 'münchen'
|
||||
|
||||
// Prefer Laravel's Str helpers when available
|
||||
Str::length('José'); // 4
|
||||
Str::lower('MÜNCHEN'); // 'münchen'
|
||||
```
|
||||
|
||||
## Use `defer()` for Post-Response Work
|
||||
|
||||
For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead.
|
||||
|
||||
Incorrect (job overhead for trivial work):
|
||||
```php
|
||||
dispatch(new LogPageView($page));
|
||||
```
|
||||
|
||||
Correct (runs after response, same process):
|
||||
```php
|
||||
defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()]));
|
||||
```
|
||||
|
||||
Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work.
|
||||
|
||||
## Use `Context` for Request-Scoped Data
|
||||
|
||||
The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually.
|
||||
|
||||
```php
|
||||
// In middleware
|
||||
Context::add('tenant_id', $request->header('X-Tenant-ID'));
|
||||
|
||||
// Anywhere later — controllers, jobs, log context
|
||||
$tenantId = Context::get('tenant_id');
|
||||
```
|
||||
|
||||
Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`.
|
||||
|
||||
## Use `Concurrency::run()` for Parallel Execution
|
||||
|
||||
Run independent operations in parallel using child processes — no async libraries needed.
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Concurrency;
|
||||
|
||||
[$users, $orders] = Concurrency::run([
|
||||
fn () => User::count(),
|
||||
fn () => Order::where('status', 'pending')->count(),
|
||||
]);
|
||||
```
|
||||
|
||||
Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially.
|
||||
|
||||
## Convention Over Configuration
|
||||
|
||||
Follow Laravel conventions. Don't override defaults unnecessarily.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
class Customer extends Model
|
||||
{
|
||||
protected $table = 'Customer';
|
||||
protected $primaryKey = 'customer_id';
|
||||
|
||||
public function roles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
class Customer extends Model
|
||||
{
|
||||
public function roles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
36
.agents/skills/laravel-best-practices/rules/blade-views.md
Normal file
36
.agents/skills/laravel-best-practices/rules/blade-views.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Blade & Views Best Practices
|
||||
|
||||
## Use `$attributes->merge()` in Component Templates
|
||||
|
||||
Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly.
|
||||
|
||||
```blade
|
||||
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
|
||||
{{ $message }}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Use `@pushOnce` for Per-Component Scripts
|
||||
|
||||
If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once.
|
||||
|
||||
## Prefer Blade Components Over `@include`
|
||||
|
||||
`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots.
|
||||
|
||||
## Use View Composers for Shared View Data
|
||||
|
||||
If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it.
|
||||
|
||||
## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
|
||||
|
||||
A single view can return either the full page or just a fragment, keeping routing clean.
|
||||
|
||||
```php
|
||||
return view('dashboard', compact('users'))
|
||||
->fragmentIf($request->hasHeader('HX-Request'), 'user-list');
|
||||
```
|
||||
|
||||
## Use `@aware` for Deeply Nested Component Props
|
||||
|
||||
Avoids re-passing parent props through every level of nested components.
|
||||
70
.agents/skills/laravel-best-practices/rules/caching.md
Normal file
70
.agents/skills/laravel-best-practices/rules/caching.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Caching Best Practices
|
||||
|
||||
## Use `Cache::remember()` Instead of Manual Get/Put
|
||||
|
||||
Cleaner cache-aside pattern that removes boilerplate. use `Cache::lock()` for race conditions.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$val = Cache::get('stats');
|
||||
if (! $val) {
|
||||
$val = $this->computeStats();
|
||||
Cache::put('stats', $val, 60);
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$val = Cache::remember('stats', 60, fn () => $this->computeStats());
|
||||
```
|
||||
|
||||
## Use `Cache::flexible()` for Stale-While-Revalidate
|
||||
|
||||
On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background.
|
||||
|
||||
Incorrect: `Cache::remember('users', 300, fn () => User::all());`
|
||||
|
||||
Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function.
|
||||
|
||||
## Use `Cache::memo()` to Avoid Redundant Hits Within a Request
|
||||
|
||||
If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory.
|
||||
|
||||
`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5.
|
||||
|
||||
## Use Cache Tags to Invalidate Related Groups
|
||||
|
||||
Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`.
|
||||
|
||||
```php
|
||||
Cache::tags(['user-1'])->flush();
|
||||
```
|
||||
|
||||
## Use `Cache::add()` for Atomic Conditional Writes
|
||||
|
||||
`add()` only writes if the key does not exist — atomic, no race condition between checking and writing.
|
||||
|
||||
Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }`
|
||||
|
||||
Correct: `Cache::add('lock', true, 10);`
|
||||
|
||||
## Use `once()` for Per-Request Memoization
|
||||
|
||||
`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory.
|
||||
|
||||
```php
|
||||
public function roles(): Collection
|
||||
{
|
||||
return once(fn () => $this->loadRoles());
|
||||
}
|
||||
```
|
||||
|
||||
Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching.
|
||||
|
||||
## Configure Failover Cache Stores in Production
|
||||
|
||||
If Redis goes down, the app falls back to a secondary store automatically.
|
||||
|
||||
```php
|
||||
'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
|
||||
```
|
||||
44
.agents/skills/laravel-best-practices/rules/collections.md
Normal file
44
.agents/skills/laravel-best-practices/rules/collections.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Collection Best Practices
|
||||
|
||||
## Use Higher-Order Messages for Simple Operations
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$users->each(function (User $user) {
|
||||
$user->markAsVip();
|
||||
});
|
||||
```
|
||||
|
||||
Correct: `$users->each->markAsVip();`
|
||||
|
||||
Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc.
|
||||
|
||||
## Choose `cursor()` vs. `lazy()` Correctly
|
||||
|
||||
- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk).
|
||||
- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading.
|
||||
|
||||
Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored.
|
||||
|
||||
Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work.
|
||||
|
||||
## Use `lazyById()` When Updating Records While Iterating
|
||||
|
||||
`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation.
|
||||
|
||||
## Use `toQuery()` for Bulk Operations on Collections
|
||||
|
||||
Avoids manual `whereIn` construction.
|
||||
|
||||
Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);`
|
||||
|
||||
Correct: `$users->toQuery()->update([...]);`
|
||||
|
||||
## Use `#[CollectedBy]` for Custom Collection Classes
|
||||
|
||||
More declarative than overriding `newCollection()`.
|
||||
|
||||
```php
|
||||
#[CollectedBy(UserCollection::class)]
|
||||
class User extends Model {}
|
||||
```
|
||||
73
.agents/skills/laravel-best-practices/rules/config.md
Normal file
73
.agents/skills/laravel-best-practices/rules/config.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Configuration Best Practices
|
||||
|
||||
## `env()` Only in Config Files
|
||||
|
||||
Direct `env()` calls may return `null` when config is cached.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$key = env('API_KEY');
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
// config/services.php
|
||||
'key' => env('API_KEY'),
|
||||
|
||||
// Application code
|
||||
$key = config('services.key');
|
||||
```
|
||||
|
||||
## Use Encrypted Env or External Secrets
|
||||
|
||||
Never store production secrets in plain `.env` files in version control.
|
||||
|
||||
Incorrect:
|
||||
```bash
|
||||
|
||||
# .env committed to repo or shared in Slack
|
||||
|
||||
STRIPE_SECRET=sk_live_abc123
|
||||
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI
|
||||
```
|
||||
|
||||
Correct:
|
||||
```bash
|
||||
php artisan env:encrypt --env=production --readable
|
||||
php artisan env:decrypt --env=production
|
||||
```
|
||||
|
||||
For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime.
|
||||
|
||||
## Use `App::environment()` for Environment Checks
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
if (env('APP_ENV') === 'production') {
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
if (app()->isProduction()) {
|
||||
// or
|
||||
if (App::environment('production')) {
|
||||
```
|
||||
|
||||
## Use Constants and Language Files
|
||||
|
||||
Use class constants instead of hardcoded magic strings for model states, types, and statuses.
|
||||
|
||||
```php
|
||||
// Incorrect
|
||||
return $this->type === 'normal';
|
||||
|
||||
// Correct
|
||||
return $this->type === self::TYPE_NORMAL;
|
||||
```
|
||||
|
||||
If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there.
|
||||
|
||||
```php
|
||||
// Only when lang files already exist in the project
|
||||
return back()->with('message', __('app.article_added'));
|
||||
```
|
||||
192
.agents/skills/laravel-best-practices/rules/db-performance.md
Normal file
192
.agents/skills/laravel-best-practices/rules/db-performance.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
# Database Performance Best Practices
|
||||
|
||||
## Always Eager Load Relationships
|
||||
|
||||
Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront.
|
||||
|
||||
Incorrect (N+1 — executes 1 + N queries):
|
||||
```php
|
||||
$posts = Post::all();
|
||||
foreach ($posts as $post) {
|
||||
echo $post->author->name;
|
||||
}
|
||||
```
|
||||
|
||||
Correct (2 queries total):
|
||||
```php
|
||||
$posts = Post::with('author')->get();
|
||||
foreach ($posts as $post) {
|
||||
echo $post->author->name;
|
||||
}
|
||||
```
|
||||
|
||||
Constrain eager loads to select only needed columns (always include the foreign key):
|
||||
|
||||
```php
|
||||
$users = User::with(['posts' => function ($query) {
|
||||
$query->select('id', 'user_id', 'title')
|
||||
->where('published', true)
|
||||
->latest()
|
||||
->limit(10);
|
||||
}])->get();
|
||||
```
|
||||
|
||||
## Prevent Lazy Loading in Development
|
||||
|
||||
Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development.
|
||||
|
||||
```php
|
||||
public function boot(): void
|
||||
{
|
||||
Model::preventLazyLoading(! app()->isProduction());
|
||||
}
|
||||
```
|
||||
|
||||
Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded.
|
||||
|
||||
## Select Only Needed Columns
|
||||
|
||||
Avoid `SELECT *` — especially when tables have large text or JSON columns.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$posts = Post::with('author')->get();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$posts = Post::select('id', 'title', 'user_id', 'created_at')
|
||||
->with(['author:id,name,avatar'])
|
||||
->get();
|
||||
```
|
||||
|
||||
When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match.
|
||||
|
||||
## Chunk Large Datasets
|
||||
|
||||
Never load thousands of records at once. Use chunking for batch processing.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$users = User::all();
|
||||
foreach ($users as $user) {
|
||||
$user->notify(new WeeklyDigest);
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
User::where('subscribed', true)->chunk(200, function ($users) {
|
||||
foreach ($users as $user) {
|
||||
$user->notify(new WeeklyDigest);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change:
|
||||
|
||||
```php
|
||||
User::where('active', false)->chunkById(200, function ($users) {
|
||||
$users->each->delete();
|
||||
});
|
||||
```
|
||||
|
||||
## Add Database Indexes
|
||||
|
||||
Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
Schema::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained();
|
||||
$table->string('status');
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
Schema::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->index()->constrained();
|
||||
$table->string('status')->index();
|
||||
$table->timestamps();
|
||||
$table->index(['status', 'created_at']);
|
||||
});
|
||||
```
|
||||
|
||||
Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`).
|
||||
|
||||
## Use `withCount()` for Counting Relations
|
||||
|
||||
Never load entire collections just to count them.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$posts = Post::all();
|
||||
foreach ($posts as $post) {
|
||||
echo $post->comments->count();
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$posts = Post::withCount('comments')->get();
|
||||
foreach ($posts as $post) {
|
||||
echo $post->comments_count;
|
||||
}
|
||||
```
|
||||
|
||||
Conditional counting:
|
||||
|
||||
```php
|
||||
$posts = Post::withCount([
|
||||
'comments',
|
||||
'comments as approved_comments_count' => function ($query) {
|
||||
$query->where('approved', true);
|
||||
},
|
||||
])->get();
|
||||
```
|
||||
|
||||
## Use `cursor()` for Memory-Efficient Iteration
|
||||
|
||||
For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$users = User::where('active', true)->get();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
foreach (User::where('active', true)->cursor() as $user) {
|
||||
ProcessUser::dispatch($user->id);
|
||||
}
|
||||
```
|
||||
|
||||
Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records.
|
||||
|
||||
## No Queries in Blade Templates
|
||||
|
||||
Never execute queries in Blade templates. Pass data from controllers.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
@foreach (User::all() as $user)
|
||||
{{ $user->profile->name }}
|
||||
@endforeach
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
// Controller
|
||||
$users = User::with('profile')->get();
|
||||
return view('users.index', compact('users'));
|
||||
```
|
||||
|
||||
```blade
|
||||
@foreach ($users as $user)
|
||||
{{ $user->profile->name }}
|
||||
@endforeach
|
||||
```
|
||||
148
.agents/skills/laravel-best-practices/rules/eloquent.md
Normal file
148
.agents/skills/laravel-best-practices/rules/eloquent.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# Eloquent Best Practices
|
||||
|
||||
## Use Correct Relationship Types
|
||||
|
||||
Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints.
|
||||
|
||||
```php
|
||||
public function comments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Comment::class);
|
||||
}
|
||||
|
||||
public function author(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
```
|
||||
|
||||
## Use Local Scopes for Reusable Queries
|
||||
|
||||
Extract reusable query constraints into local scopes to avoid duplication.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$active = User::where('verified', true)->whereNotNull('activated_at')->get();
|
||||
$articles = Article::whereHas('user', function ($q) {
|
||||
$q->where('verified', true)->whereNotNull('activated_at');
|
||||
})->get();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('verified', true)->whereNotNull('activated_at');
|
||||
}
|
||||
|
||||
// Usage
|
||||
$active = User::active()->get();
|
||||
$articles = Article::whereHas('user', fn ($q) => $q->active())->get();
|
||||
```
|
||||
|
||||
## Apply Global Scopes Sparingly
|
||||
|
||||
Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy.
|
||||
|
||||
Incorrect (global scope for a conditional filter):
|
||||
```php
|
||||
class PublishedScope implements Scope
|
||||
{
|
||||
public function apply(Builder $builder, Model $model): void
|
||||
{
|
||||
$builder->where('published', true);
|
||||
}
|
||||
}
|
||||
// Now admin panels, reports, and background jobs all silently skip drafts
|
||||
```
|
||||
|
||||
Correct (local scope you opt into):
|
||||
```php
|
||||
public function scopePublished(Builder $query): Builder
|
||||
{
|
||||
return $query->where('published', true);
|
||||
}
|
||||
|
||||
Post::published()->paginate(); // Explicit
|
||||
Post::paginate(); // Admin sees all
|
||||
```
|
||||
|
||||
## Define Attribute Casts
|
||||
|
||||
Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion.
|
||||
|
||||
```php
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'total' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Cast Date Columns Properly
|
||||
|
||||
Always cast date columns. Use Carbon instances in templates instead of formatting strings manually.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'ordered_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
```blade
|
||||
{{ $order->ordered_at->toDateString() }}
|
||||
{{ $order->ordered_at->format('m-d') }}
|
||||
```
|
||||
|
||||
## Use `whereBelongsTo()` for Relationship Queries
|
||||
|
||||
Cleaner than manually specifying foreign keys.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
Post::where('user_id', $user->id)->get();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
Post::whereBelongsTo($user)->get();
|
||||
Post::whereBelongsTo($user, 'author')->get();
|
||||
```
|
||||
|
||||
## Avoid Hardcoded Table Names in Queries
|
||||
|
||||
Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string).
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
DB::table('users')->where('active', true)->get();
|
||||
|
||||
$query->join('companies', 'companies.id', '=', 'users.company_id');
|
||||
|
||||
DB::select('SELECT * FROM orders WHERE status = ?', ['pending']);
|
||||
```
|
||||
|
||||
Correct — reference the model's table:
|
||||
```php
|
||||
DB::table((new User)->getTable())->where('active', true)->get();
|
||||
|
||||
// Even better — use Eloquent or the query builder instead of raw SQL
|
||||
User::where('active', true)->get();
|
||||
Order::where('status', 'pending')->get();
|
||||
```
|
||||
|
||||
Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable.
|
||||
|
||||
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
# Error Handling Best Practices
|
||||
|
||||
## Exception Reporting and Rendering
|
||||
|
||||
There are two valid approaches — choose one and apply it consistently across the project.
|
||||
|
||||
**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find:
|
||||
|
||||
```php
|
||||
class InvalidOrderException extends Exception
|
||||
{
|
||||
public function report(): void { /* custom reporting */ }
|
||||
|
||||
public function render(Request $request): Response
|
||||
{
|
||||
return response()->view('errors.invalid-order', status: 422);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture:
|
||||
|
||||
```php
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
$exceptions->report(function (InvalidOrderException $e) { /* ... */ });
|
||||
$exceptions->render(function (InvalidOrderException $e, Request $request) {
|
||||
return response()->view('errors.invalid-order', status: 422);
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
Check the existing codebase and follow whichever pattern is already established.
|
||||
|
||||
## Use `ShouldntReport` for Exceptions That Should Never Log
|
||||
|
||||
More discoverable than listing classes in `dontReport()`.
|
||||
|
||||
```php
|
||||
class PodcastProcessingException extends Exception implements ShouldntReport {}
|
||||
```
|
||||
|
||||
## Throttle High-Volume Exceptions
|
||||
|
||||
A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type.
|
||||
|
||||
## Enable `dontReportDuplicates()`
|
||||
|
||||
Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks.
|
||||
|
||||
## Force JSON Error Rendering for API Routes
|
||||
|
||||
Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes.
|
||||
|
||||
```php
|
||||
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
|
||||
return $request->is('api/*') || $request->expectsJson();
|
||||
});
|
||||
```
|
||||
|
||||
## Add Context to Exception Classes
|
||||
|
||||
Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry.
|
||||
|
||||
```php
|
||||
class InvalidOrderException extends Exception
|
||||
{
|
||||
public function context(): array
|
||||
{
|
||||
return ['order_id' => $this->orderId];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# Events & Notifications Best Practices
|
||||
|
||||
## Rely on Event Discovery
|
||||
|
||||
Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`.
|
||||
|
||||
## Run `event:cache` in Production Deploy
|
||||
|
||||
Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`.
|
||||
|
||||
## Use `ShouldDispatchAfterCommit` Inside Transactions
|
||||
|
||||
Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet.
|
||||
|
||||
```php
|
||||
class OrderShipped implements ShouldDispatchAfterCommit {}
|
||||
```
|
||||
|
||||
## Always Queue Notifications
|
||||
|
||||
Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response.
|
||||
|
||||
```php
|
||||
class InvoicePaid extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
}
|
||||
```
|
||||
|
||||
## Use `afterCommit()` on Notifications in Transactions
|
||||
|
||||
Same race condition as events — call `afterCommit()` to delay dispatch until the transaction commits.
|
||||
|
||||
```php
|
||||
$user->notify((new InvoicePaid($invoice))->afterCommit());
|
||||
```
|
||||
|
||||
## Route Notification Channels to Dedicated Queues
|
||||
|
||||
Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues.
|
||||
|
||||
## Use On-Demand Notifications for Non-User Recipients
|
||||
|
||||
Avoid creating dummy models to send notifications to arbitrary addresses.
|
||||
|
||||
```php
|
||||
Notification::route('mail', 'admin@example.com')->notify(new SystemAlert());
|
||||
```
|
||||
|
||||
## Implement `HasLocalePreference` on Notifiable Models
|
||||
|
||||
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
|
||||
160
.agents/skills/laravel-best-practices/rules/http-client.md
Normal file
160
.agents/skills/laravel-best-practices/rules/http-client.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# HTTP Client Best Practices
|
||||
|
||||
## Always Set Explicit Timeouts
|
||||
|
||||
The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$response = Http::get('https://api.example.com/users');
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$response = Http::timeout(5)
|
||||
->connectTimeout(3)
|
||||
->get('https://api.example.com/users');
|
||||
```
|
||||
|
||||
For service-specific clients, define timeouts in a macro:
|
||||
|
||||
```php
|
||||
Http::macro('github', function () {
|
||||
return Http::baseUrl('https://api.github.com')
|
||||
->timeout(10)
|
||||
->connectTimeout(3)
|
||||
->withToken(config('services.github.token'));
|
||||
});
|
||||
|
||||
$response = Http::github()->get('/repos/laravel/framework');
|
||||
```
|
||||
|
||||
## Use Retry with Backoff for External APIs
|
||||
|
||||
External APIs have transient failures. Use `retry()` with increasing delays.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$response = Http::post('https://api.stripe.com/v1/charges', $data);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new PaymentFailedException('Charge failed');
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$response = Http::retry([100, 500, 1000])
|
||||
->timeout(10)
|
||||
->post('https://api.stripe.com/v1/charges', $data);
|
||||
```
|
||||
|
||||
Only retry on specific errors:
|
||||
|
||||
```php
|
||||
$response = Http::retry(3, 100, function (Throwable $exception, PendingRequest $request) {
|
||||
return $exception instanceof ConnectionException
|
||||
|| ($exception instanceof RequestException && $exception->response->serverError());
|
||||
})->post('https://api.example.com/data');
|
||||
```
|
||||
|
||||
## Handle Errors Explicitly
|
||||
|
||||
The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$response = Http::get('https://api.example.com/users/1');
|
||||
$user = $response->json(); // Could be an error body
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$response = Http::timeout(5)
|
||||
->get('https://api.example.com/users/1')
|
||||
->throw();
|
||||
|
||||
$user = $response->json();
|
||||
```
|
||||
|
||||
For graceful degradation:
|
||||
|
||||
```php
|
||||
$response = Http::get('https://api.example.com/users/1');
|
||||
|
||||
if ($response->successful()) {
|
||||
return $response->json();
|
||||
}
|
||||
|
||||
if ($response->notFound()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response->throw();
|
||||
```
|
||||
|
||||
## Use Request Pooling for Concurrent Requests
|
||||
|
||||
When making multiple independent API calls, use `Http::pool()` instead of sequential calls.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$users = Http::get('https://api.example.com/users')->json();
|
||||
$posts = Http::get('https://api.example.com/posts')->json();
|
||||
$comments = Http::get('https://api.example.com/comments')->json();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
use Illuminate\Http\Client\Pool;
|
||||
|
||||
$responses = Http::pool(fn (Pool $pool) => [
|
||||
$pool->as('users')->get('https://api.example.com/users'),
|
||||
$pool->as('posts')->get('https://api.example.com/posts'),
|
||||
$pool->as('comments')->get('https://api.example.com/comments'),
|
||||
]);
|
||||
|
||||
$users = $responses['users']->json();
|
||||
$posts = $responses['posts']->json();
|
||||
```
|
||||
|
||||
## Fake HTTP Calls in Tests
|
||||
|
||||
Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
it('syncs user from API', function () {
|
||||
$service = new UserSyncService;
|
||||
$service->sync(1); // Hits the real API
|
||||
});
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
it('syncs user from API', function () {
|
||||
Http::preventStrayRequests();
|
||||
|
||||
Http::fake([
|
||||
'api.example.com/users/1' => Http::response([
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@example.com',
|
||||
]),
|
||||
]);
|
||||
|
||||
$service = new UserSyncService;
|
||||
$service->sync(1);
|
||||
|
||||
Http::assertSent(function (Request $request) {
|
||||
return $request->url() === 'https://api.example.com/users/1';
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Test failure scenarios too:
|
||||
|
||||
```php
|
||||
Http::fake([
|
||||
'api.example.com/*' => Http::failedConnection(),
|
||||
]);
|
||||
```
|
||||
27
.agents/skills/laravel-best-practices/rules/mail.md
Normal file
27
.agents/skills/laravel-best-practices/rules/mail.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Mail Best Practices
|
||||
|
||||
## Implement `ShouldQueue` on the Mailable Class
|
||||
|
||||
Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it.
|
||||
|
||||
## Use `afterCommit()` on Mailables Inside Transactions
|
||||
|
||||
A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor.
|
||||
|
||||
## Use `assertQueued()` Not `assertSent()` for Queued Mailables
|
||||
|
||||
`Mail::assertSent()` only catches synchronous mail. Queued mailables fail `assertSent` with a "Did you mean to use assertQueued()?" hint.
|
||||
|
||||
Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`.
|
||||
|
||||
Correct: `Mail::assertQueued(OrderShipped::class);`
|
||||
|
||||
## Use Markdown Mailables for Transactional Emails
|
||||
|
||||
Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag.
|
||||
|
||||
## Separate Content Tests from Sending Tests
|
||||
|
||||
Content tests: instantiate the mailable directly, call `assertSeeInHtml()`.
|
||||
Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`.
|
||||
Don't mix them — it conflates concerns and makes tests brittle.
|
||||
121
.agents/skills/laravel-best-practices/rules/migrations.md
Normal file
121
.agents/skills/laravel-best-practices/rules/migrations.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# Migration Best Practices
|
||||
|
||||
## Generate Migrations with Artisan
|
||||
|
||||
Always use `php artisan make:migration` for consistent naming and timestamps.
|
||||
|
||||
Incorrect (manually created file):
|
||||
```php
|
||||
// database/migrations/posts_migration.php ← wrong naming, no timestamp
|
||||
```
|
||||
|
||||
Correct (Artisan-generated):
|
||||
```bash
|
||||
php artisan make:migration create_posts_table
|
||||
php artisan make:migration add_slug_to_posts_table
|
||||
```
|
||||
|
||||
## Use `constrained()` for Foreign Keys
|
||||
|
||||
Automatic naming and referential integrity.
|
||||
|
||||
```php
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
|
||||
// Non-standard names
|
||||
$table->foreignId('author_id')->constrained('users');
|
||||
```
|
||||
|
||||
## Never Modify Deployed Migrations
|
||||
|
||||
Once a migration has run in production, treat it as immutable. Create a new migration to change the table.
|
||||
|
||||
Incorrect (editing a deployed migration):
|
||||
```php
|
||||
// 2024_01_01_create_posts_table.php — already in production
|
||||
$table->string('slug')->unique(); // ← added after deployment
|
||||
```
|
||||
|
||||
Correct (new migration to alter):
|
||||
```php
|
||||
// 2024_03_15_add_slug_to_posts_table.php
|
||||
Schema::table('posts', function (Blueprint $table) {
|
||||
$table->string('slug')->unique()->after('title');
|
||||
});
|
||||
```
|
||||
|
||||
## Add Indexes in the Migration
|
||||
|
||||
Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
Schema::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained();
|
||||
$table->string('status');
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
Schema::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->index();
|
||||
$table->string('status')->index();
|
||||
$table->timestamp('shipped_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
|
||||
## Mirror Defaults in Model `$attributes`
|
||||
|
||||
When a column has a database default, mirror it in the model so new instances have correct values before saving.
|
||||
|
||||
```php
|
||||
// Migration
|
||||
$table->string('status')->default('pending');
|
||||
|
||||
// Model
|
||||
protected $attributes = [
|
||||
'status' => 'pending',
|
||||
];
|
||||
```
|
||||
|
||||
## Write Reversible `down()` Methods by Default
|
||||
|
||||
Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments.
|
||||
|
||||
```php
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('posts', function (Blueprint $table) {
|
||||
$table->dropColumn('slug');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported.
|
||||
|
||||
## Keep Migrations Focused
|
||||
|
||||
One concern per migration. Never mix DDL (schema changes) and DML (data manipulation).
|
||||
|
||||
Incorrect (partial failure creates unrecoverable state):
|
||||
```php
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('settings', function (Blueprint $table) { ... });
|
||||
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
|
||||
}
|
||||
```
|
||||
|
||||
Correct (separate migrations):
|
||||
```php
|
||||
// Migration 1: create_settings_table
|
||||
Schema::create('settings', function (Blueprint $table) { ... });
|
||||
|
||||
// Migration 2: seed_default_settings
|
||||
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
|
||||
```
|
||||
144
.agents/skills/laravel-best-practices/rules/queue-jobs.md
Normal file
144
.agents/skills/laravel-best-practices/rules/queue-jobs.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# Queue & Job Best Practices
|
||||
|
||||
## Set `retry_after` Greater Than `timeout`
|
||||
|
||||
If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution.
|
||||
|
||||
Incorrect (`retry_after` ≤ `timeout`):
|
||||
```php
|
||||
class ProcessReport implements ShouldQueue
|
||||
{
|
||||
public $timeout = 120;
|
||||
}
|
||||
|
||||
// config/queue.php — retry_after: 90 ← job retried while still running!
|
||||
```
|
||||
|
||||
Correct (`retry_after` > `timeout`):
|
||||
```php
|
||||
class ProcessReport implements ShouldQueue
|
||||
{
|
||||
public $timeout = 120;
|
||||
}
|
||||
|
||||
// config/queue.php — retry_after: 180 ← safely longer than any job timeout
|
||||
```
|
||||
|
||||
## Use Exponential Backoff
|
||||
|
||||
Use progressively longer delays between retries to avoid hammering failing services.
|
||||
|
||||
Incorrect (fixed retry interval):
|
||||
```php
|
||||
class SyncWithStripe implements ShouldQueue
|
||||
{
|
||||
public $tries = 3;
|
||||
// Default: retries immediately, overwhelming the API
|
||||
}
|
||||
```
|
||||
|
||||
Correct (exponential backoff):
|
||||
```php
|
||||
class SyncWithStripe implements ShouldQueue
|
||||
{
|
||||
public $tries = 3;
|
||||
public $backoff = [1, 5, 10];
|
||||
}
|
||||
```
|
||||
|
||||
## Implement `ShouldBeUnique`
|
||||
|
||||
Prevent duplicate job processing.
|
||||
|
||||
```php
|
||||
class GenerateInvoice implements ShouldQueue, ShouldBeUnique
|
||||
{
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return $this->order->id;
|
||||
}
|
||||
|
||||
public $uniqueFor = 3600;
|
||||
}
|
||||
```
|
||||
|
||||
## Always Implement `failed()`
|
||||
|
||||
Handle errors explicitly — don't rely on silent failure.
|
||||
|
||||
```php
|
||||
public function failed(?Throwable $exception): void
|
||||
{
|
||||
$this->podcast->update(['status' => 'failed']);
|
||||
Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]);
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limit External API Calls in Jobs
|
||||
|
||||
Use `RateLimited` middleware to throttle jobs calling third-party APIs.
|
||||
|
||||
```php
|
||||
public function middleware(): array
|
||||
{
|
||||
return [new RateLimited('external-api')];
|
||||
}
|
||||
```
|
||||
|
||||
## Batch Related Jobs
|
||||
|
||||
Use `Bus::batch()` when jobs should succeed or fail together.
|
||||
|
||||
```php
|
||||
Bus::batch([
|
||||
new ImportCsvChunk($chunk1),
|
||||
new ImportCsvChunk($chunk2),
|
||||
])
|
||||
->then(fn (Batch $batch) => Notification::send($user, new ImportComplete))
|
||||
->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed'))
|
||||
->dispatch();
|
||||
```
|
||||
|
||||
## `retryUntil()` Needs `$tries = 0`
|
||||
|
||||
When using time-based retry limits, set `$tries = 0` to avoid premature failure.
|
||||
|
||||
```php
|
||||
public $tries = 0;
|
||||
|
||||
public function retryUntil(): \DateTimeInterface
|
||||
{
|
||||
return now()->addHours(4);
|
||||
}
|
||||
```
|
||||
|
||||
## Use `ShouldBeUniqueUntilProcessing` for Early Lock Release
|
||||
|
||||
`ShouldBeUnique` holds the lock until the job completes. `ShouldBeUniqueUntilProcessing` releases it when processing starts, allowing new instances to queue.
|
||||
|
||||
```php
|
||||
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
||||
{
|
||||
// Lock releases when processing begins, not when it finishes
|
||||
}
|
||||
```
|
||||
|
||||
## Use Horizon for Complex Queue Scenarios
|
||||
|
||||
Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities.
|
||||
|
||||
```php
|
||||
// config/horizon.php
|
||||
'environments' => [
|
||||
'production' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['high', 'default', 'low'],
|
||||
'balance' => 'auto',
|
||||
'minProcesses' => 1,
|
||||
'maxProcesses' => 10,
|
||||
'tries' => 3,
|
||||
],
|
||||
],
|
||||
],
|
||||
```
|
||||
99
.agents/skills/laravel-best-practices/rules/routing.md
Normal file
99
.agents/skills/laravel-best-practices/rules/routing.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# Routing & Controllers Best Practices
|
||||
|
||||
## Use Implicit Route Model Binding
|
||||
|
||||
Let Laravel resolve models automatically from route parameters.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
public function show(int $id)
|
||||
{
|
||||
$post = Post::findOrFail($id);
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function show(Post $post)
|
||||
{
|
||||
return view('posts.show', ['post' => $post]);
|
||||
}
|
||||
```
|
||||
|
||||
## Use Scoped Bindings for Nested Resources
|
||||
|
||||
Enforce parent-child relationships automatically.
|
||||
|
||||
```php
|
||||
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
|
||||
// $post is automatically scoped to $user
|
||||
})->scopeBindings();
|
||||
```
|
||||
|
||||
## Use Resource Controllers
|
||||
|
||||
Use `Route::resource()` or `apiResource()` for RESTful endpoints.
|
||||
|
||||
```php
|
||||
Route::resource('posts', PostController::class);
|
||||
// In routes/api.php — the /api prefix is applied automatically
|
||||
Route::apiResource('posts', Api\PostController::class);
|
||||
```
|
||||
|
||||
## Keep Controllers Thin
|
||||
|
||||
Aim for under 10 lines per method. Extract business logic to action or service classes.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([...]);
|
||||
if ($request->hasFile('image')) {
|
||||
$request->file('image')->move(public_path('images'));
|
||||
}
|
||||
$post = Post::create($validated);
|
||||
$post->tags()->sync($validated['tags']);
|
||||
event(new PostCreated($post));
|
||||
return redirect()->route('posts.show', $post);
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function store(StorePostRequest $request, CreatePostAction $create)
|
||||
{
|
||||
$post = $create->execute($request->validated());
|
||||
|
||||
return redirect()->route('posts.show', $post);
|
||||
}
|
||||
```
|
||||
|
||||
## Type-Hint Form Requests
|
||||
|
||||
Type-hinting Form Requests triggers automatic validation and authorization before the method executes.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => ['required', 'max:255'],
|
||||
'body' => ['required'],
|
||||
]);
|
||||
|
||||
Post::create($validated);
|
||||
|
||||
return redirect()->route('posts.index');
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function store(StorePostRequest $request): RedirectResponse
|
||||
{
|
||||
Post::create($request->validated());
|
||||
|
||||
return redirect()->route('posts.index');
|
||||
}
|
||||
```
|
||||
39
.agents/skills/laravel-best-practices/rules/scheduling.md
Normal file
39
.agents/skills/laravel-best-practices/rules/scheduling.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Task Scheduling Best Practices
|
||||
|
||||
## Use `withoutOverlapping()` on Variable-Duration Tasks
|
||||
|
||||
Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion.
|
||||
|
||||
## Use `onOneServer()` on Multi-Server Deployments
|
||||
|
||||
Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached).
|
||||
|
||||
## Use `runInBackground()` for Concurrent Long Tasks
|
||||
|
||||
By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes.
|
||||
|
||||
## Use `environments()` to Restrict Tasks
|
||||
|
||||
Prevent accidental execution of production-only tasks (billing, reporting) on staging.
|
||||
|
||||
```php
|
||||
Schedule::command('billing:charge')->monthly()->environments(['production']);
|
||||
```
|
||||
|
||||
## Use `takeUntilTimeout()` for Time-Bounded Processing
|
||||
|
||||
A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time.
|
||||
|
||||
## Use Schedule Groups for Shared Configuration
|
||||
|
||||
Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks.
|
||||
|
||||
```php
|
||||
Schedule::daily()
|
||||
->onOneServer()
|
||||
->timezone('America/New_York')
|
||||
->group(function () {
|
||||
Schedule::command('emails:send --force');
|
||||
Schedule::command('emails:prune');
|
||||
});
|
||||
```
|
||||
198
.agents/skills/laravel-best-practices/rules/security.md
Normal file
198
.agents/skills/laravel-best-practices/rules/security.md
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# Security Best Practices
|
||||
|
||||
## Mass Assignment Protection
|
||||
|
||||
Every model must define `$fillable` (whitelist) or `$guarded` (blacklist).
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
class User extends Model
|
||||
{
|
||||
protected $guarded = []; // All fields are mass assignable
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
class User extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Never use `$guarded = []` on models that accept user input.
|
||||
|
||||
## Authorize Every Action
|
||||
|
||||
Use policies or gates in controllers. Never skip authorization.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
public function update(UpdatePostRequest $request, Post $post)
|
||||
{
|
||||
$post->update($request->validated());
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function update(UpdatePostRequest $request, Post $post)
|
||||
{
|
||||
Gate::authorize('update', $post);
|
||||
|
||||
$post->update($request->validated());
|
||||
}
|
||||
```
|
||||
|
||||
Or via Form Request:
|
||||
|
||||
```php
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('update', $this->route('post'));
|
||||
}
|
||||
```
|
||||
|
||||
## Prevent SQL Injection
|
||||
|
||||
Always use parameter binding. Never interpolate user input into queries.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
DB::select("SELECT * FROM users WHERE name = '{$request->name}'");
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
User::where('name', $request->name)->get();
|
||||
|
||||
// Raw expressions with bindings
|
||||
User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get();
|
||||
```
|
||||
|
||||
## Escape Output to Prevent XSS
|
||||
|
||||
Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
{!! $user->bio !!}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```blade
|
||||
{{ $user->bio }}
|
||||
```
|
||||
|
||||
## CSRF Protection
|
||||
|
||||
Include `@csrf` in all POST/PUT/DELETE Blade forms. In Inertia apps, the `@csrf` directive is automatically applied.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
<form method="POST" action="/posts">
|
||||
<input type="text" name="title">
|
||||
</form>
|
||||
```
|
||||
|
||||
Correct:
|
||||
```blade
|
||||
<form method="POST" action="/posts">
|
||||
@csrf
|
||||
<input type="text" name="title">
|
||||
</form>
|
||||
```
|
||||
|
||||
## Rate Limit Auth and API Routes
|
||||
|
||||
Apply `throttle` middleware to authentication and API routes.
|
||||
|
||||
```php
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->ip());
|
||||
});
|
||||
|
||||
Route::post('/login', LoginController::class)->middleware('throttle:login');
|
||||
```
|
||||
|
||||
## Validate File Uploads
|
||||
|
||||
Validate extension, MIME type, and size. The `mimes` rule checks extensions; use `mimetypes` for actual MIME type validation. Never trust client-provided filenames.
|
||||
|
||||
```php
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Store with generated filenames:
|
||||
|
||||
```php
|
||||
$path = $request->file('avatar')->store('avatars', 'public');
|
||||
```
|
||||
|
||||
## Keep Secrets Out of Code
|
||||
|
||||
Never commit `.env`. Access secrets via `config()` only.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$key = env('API_KEY');
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
// config/services.php
|
||||
'api_key' => env('API_KEY'),
|
||||
|
||||
// In application code
|
||||
$key = config('services.api_key');
|
||||
```
|
||||
|
||||
## Audit Dependencies
|
||||
|
||||
Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment.
|
||||
|
||||
```bash
|
||||
composer audit
|
||||
```
|
||||
|
||||
## Encrypt Sensitive Database Fields
|
||||
|
||||
Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
class Integration extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'api_key' => 'string',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
class Integration extends Model
|
||||
{
|
||||
protected $hidden = ['api_key', 'api_secret'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'api_key' => 'encrypted',
|
||||
'api_secret' => 'encrypted',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
125
.agents/skills/laravel-best-practices/rules/style.md
Normal file
125
.agents/skills/laravel-best-practices/rules/style.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Conventions & Style
|
||||
|
||||
## Follow Laravel Naming Conventions
|
||||
|
||||
| What | Convention | Good | Bad |
|
||||
|------|-----------|------|-----|
|
||||
| Controller | singular | `ArticleController` | `ArticlesController` |
|
||||
| Model | singular | `User` | `Users` |
|
||||
| Table | plural, snake_case | `article_comments` | `articleComments` |
|
||||
| Pivot table | singular alphabetical | `article_user` | `user_article` |
|
||||
| Column | snake_case, no model name | `meta_title` | `article_meta_title` |
|
||||
| Foreign key | singular model + `_id` | `article_id` | `articles_id` |
|
||||
| Route | plural | `articles/1` | `article/1` |
|
||||
| Route name | snake_case with dots | `users.show_active` | `users.show-active` |
|
||||
| Method | camelCase | `getAll` | `get_all` |
|
||||
| Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` |
|
||||
| Collection | descriptive, plural | `$activeUsers` | `$data` |
|
||||
| Object | descriptive, singular | `$activeUser` | `$users` |
|
||||
| View | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` |
|
||||
| Config | snake_case | `google_calendar.php` | `googleCalendar.php` |
|
||||
| Enum | singular | `UserType` | `UserTypes` |
|
||||
|
||||
## Prefer Shorter Readable Syntax
|
||||
|
||||
| Verbose | Shorter |
|
||||
|---------|---------|
|
||||
| `Session::get('cart')` | `session('cart')` |
|
||||
| `$request->session()->get('cart')` | `session('cart')` |
|
||||
| `$request->input('name')` | `$request->name` |
|
||||
| `return Redirect::back()` | `return back()` |
|
||||
| `Carbon::now()` | `now()` |
|
||||
| `App::make('Class')` | `app('Class')` |
|
||||
| `->where('column', '=', 1)` | `->where('column', 1)` |
|
||||
| `->orderBy('created_at', 'desc')` | `->latest()` |
|
||||
| `->orderBy('created_at', 'asc')` | `->oldest()` |
|
||||
| `->first()->name` | `->value('name')` |
|
||||
|
||||
## Use Laravel String & Array Helpers
|
||||
|
||||
Laravel provides `Str`, `Arr`, `Number`, and `Uri` helper classes that are more readable, chainable, and UTF-8 safe than raw PHP functions. Always prefer them.
|
||||
|
||||
Strings — use `Str` and fluent `Str::of()` over raw PHP:
|
||||
```php
|
||||
// Incorrect
|
||||
$slug = strtolower(str_replace(' ', '-', $title));
|
||||
$short = substr($text, 0, 100) . '...';
|
||||
$class = substr(strrchr('App\Models\User', '\'), 1);
|
||||
|
||||
// Correct
|
||||
$slug = Str::slug($title);
|
||||
$short = Str::limit($text, 100);
|
||||
$class = class_basename('App\Models\User');
|
||||
```
|
||||
|
||||
Fluent strings — chain operations for complex transformations:
|
||||
```php
|
||||
// Incorrect
|
||||
$result = strtolower(trim(str_replace('_', '-', $input)));
|
||||
|
||||
// Correct
|
||||
$result = Str::of($input)->trim()->replace('_', '-')->lower();
|
||||
```
|
||||
|
||||
Key `Str` methods to prefer: `Str::slug()`, `Str::limit()`, `Str::contains()`, `Str::before()`, `Str::after()`, `Str::between()`, `Str::camel()`, `Str::snake()`, `Str::kebab()`, `Str::headline()`, `Str::squish()`, `Str::mask()`, `Str::uuid()`, `Str::ulid()`, `Str::random()`, `Str::is()`.
|
||||
|
||||
Arrays — use `Arr` over raw PHP:
|
||||
```php
|
||||
// Incorrect
|
||||
$name = isset($array['user']['name']) ? $array['user']['name'] : 'default';
|
||||
|
||||
// Correct
|
||||
$name = Arr::get($array, 'user.name', 'default');
|
||||
```
|
||||
|
||||
Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`.
|
||||
|
||||
Numbers — use `Number` for display formatting:
|
||||
```php
|
||||
Number::format(1000000); // "1,000,000"
|
||||
Number::currency(1500, 'USD'); // "$1,500.00"
|
||||
Number::abbreviate(1000000); // "1M"
|
||||
Number::fileSize(1024 * 1024); // "1 MB"
|
||||
Number::percentage(75.5); // "75.5%"
|
||||
```
|
||||
|
||||
URIs — use `Uri` for URL manipulation:
|
||||
```php
|
||||
$uri = Uri::of('https://example.com/search')
|
||||
->withQuery(['q' => 'laravel', 'page' => 1]);
|
||||
```
|
||||
|
||||
Use `$request->string('name')` to get a fluent `Stringable` directly from request input for immediate chaining.
|
||||
|
||||
Use `search-docs` for the full list of available methods — these helpers are extensive.
|
||||
|
||||
## No Inline JS/CSS in Blade
|
||||
|
||||
Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
let article = `{{ json_encode($article) }}`;
|
||||
```
|
||||
|
||||
Correct:
|
||||
```blade
|
||||
<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}</button>
|
||||
```
|
||||
|
||||
Pass data to JS via data attributes or use a dedicated PHP-to-JS package.
|
||||
|
||||
## No Unnecessary Comments
|
||||
|
||||
Code should be readable on its own. Use descriptive method and variable names instead of comments. The only exception is config files, where descriptive comments are expected.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
// Check if there are any joins
|
||||
if (count((array) $builder->getQuery()->joins) > 0)
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
if ($this->hasJoins())
|
||||
```
|
||||
43
.agents/skills/laravel-best-practices/rules/testing.md
Normal file
43
.agents/skills/laravel-best-practices/rules/testing.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Testing Best Practices
|
||||
|
||||
## Use `LazilyRefreshDatabase` Over `RefreshDatabase`
|
||||
|
||||
`RefreshDatabase` migrates once per process and wraps each test in a rolled-back transaction. `LazilyRefreshDatabase` skips even that first migration if the schema is already up to date.
|
||||
|
||||
## Use Model Assertions Over Raw Database Assertions
|
||||
|
||||
Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);`
|
||||
|
||||
Correct: `$this->assertModelExists($user);`
|
||||
|
||||
More expressive, type-safe, and fails with clearer messages.
|
||||
|
||||
## Use Factory States and Sequences
|
||||
|
||||
Named states make tests self-documenting. Sequences eliminate repetitive setup.
|
||||
|
||||
Incorrect: `User::factory()->create(['email_verified_at' => null]);`
|
||||
|
||||
Correct: `User::factory()->unverified()->create();`
|
||||
|
||||
## Use `Exceptions::fake()` to Assert Exception Reporting
|
||||
|
||||
Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally.
|
||||
|
||||
## Call `Event::fake()` After Factory Setup
|
||||
|
||||
Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models.
|
||||
|
||||
Incorrect: `Event::fake(); $user = User::factory()->create();`
|
||||
|
||||
Correct: `$user = User::factory()->create(); Event::fake();`
|
||||
|
||||
## Use `recycle()` to Share Relationship Instances Across Factories
|
||||
|
||||
Without `recycle()`, nested factories create separate instances of the same conceptual entity.
|
||||
|
||||
```php
|
||||
Ticket::factory()
|
||||
->recycle(Airline::factory()->create())
|
||||
->create();
|
||||
```
|
||||
75
.agents/skills/laravel-best-practices/rules/validation.md
Normal file
75
.agents/skills/laravel-best-practices/rules/validation.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Validation & Forms Best Practices
|
||||
|
||||
## Use Form Request Classes
|
||||
|
||||
Extract validation from controllers into dedicated Form Request classes.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'required|max:255',
|
||||
'body' => 'required',
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function store(StorePostRequest $request)
|
||||
{
|
||||
Post::create($request->validated());
|
||||
}
|
||||
```
|
||||
|
||||
## Array vs. String Notation for Rules
|
||||
|
||||
Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses.
|
||||
|
||||
```php
|
||||
// Preferred for new code
|
||||
'email' => ['required', 'email', Rule::unique('users')],
|
||||
|
||||
// Follow existing convention if the project uses string notation
|
||||
'email' => 'required|email|unique:users',
|
||||
```
|
||||
|
||||
## Always Use `validated()`
|
||||
|
||||
Get only validated data. Never use `$request->all()` for mass operations.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
Post::create($request->all());
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
Post::create($request->validated());
|
||||
```
|
||||
|
||||
## Use `Rule::when()` for Conditional Validation
|
||||
|
||||
```php
|
||||
'company_name' => [
|
||||
Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']),
|
||||
],
|
||||
```
|
||||
|
||||
## Use the `after()` Method for Custom Validation
|
||||
|
||||
Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields.
|
||||
|
||||
```php
|
||||
public function after(): array
|
||||
{
|
||||
return [
|
||||
function (Validator $validator) {
|
||||
if ($this->quantity > Product::find($this->product_id)?->stock) {
|
||||
$validator->errors()->add('quantity', 'Not enough stock.');
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
115
.agents/skills/livewire-development/SKILL.md
Normal file
115
.agents/skills/livewire-development/SKILL.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
---
|
||||
name: livewire-development
|
||||
description: "Use for any task or question involving Livewire. Activate if user mentions Livewire, wire: directives, or Livewire-specific concepts like wire:model, wire:click, invoke this skill. Covers building new components, debugging reactivity issues, real-time form validation, loading states, migrating from Livewire 2 to 3, converting component formats (SFC/MFC/class-based), and performance optimization. Do not use for non-Livewire reactive UI (React, Vue, Alpine-only, Inertia.js) or standard Laravel forms without Livewire."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Livewire Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Livewire 3 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Components
|
||||
|
||||
Use the `php artisan make:livewire [Posts\CreatePost]` Artisan command to create new components.
|
||||
|
||||
### Fundamental Concepts
|
||||
|
||||
- State should live on the server, with the UI reflecting it.
|
||||
- All Livewire requests hit the Laravel backend; they're like regular HTTP requests. Always validate form data and run authorization checks in Livewire actions.
|
||||
|
||||
## Livewire 3 Specifics
|
||||
|
||||
### Key Changes From Livewire 2
|
||||
|
||||
These things changed in Livewire 3, but may not have been updated in this application. Verify this application's setup to ensure you follow existing conventions.
|
||||
- Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default.
|
||||
- Components now use the `App\Livewire` namespace (not `App\Http\Livewire`).
|
||||
- Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`).
|
||||
- Use the `components.layouts.app` view as the typical layout path (not `layouts.app`).
|
||||
|
||||
### New Directives
|
||||
|
||||
- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use.
|
||||
|
||||
### Alpine Integration
|
||||
|
||||
- Alpine is now included with Livewire; don't manually include Alpine.js.
|
||||
- Plugins included with Alpine: persist, intersect, collapse, and focus.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Component Structure
|
||||
|
||||
- Livewire components require a single root element.
|
||||
- Use `wire:loading` and `wire:dirty` for delightful loading states.
|
||||
|
||||
### Using Keys in Loops
|
||||
|
||||
<!-- Wire Key in Loops -->
|
||||
```blade
|
||||
@foreach ($items as $item)
|
||||
<div wire:key="item-{{ $item->id }}">
|
||||
{{ $item->name }}
|
||||
</div>
|
||||
@endforeach
|
||||
```
|
||||
|
||||
### Lifecycle Hooks
|
||||
|
||||
Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
|
||||
|
||||
<!-- Lifecycle Hook Examples -->
|
||||
```php
|
||||
public function mount(User $user) { $this->user = $user; }
|
||||
public function updatedSearch() { $this->resetPage(); }
|
||||
```
|
||||
|
||||
## JavaScript Hooks
|
||||
|
||||
You can listen for `livewire:init` to hook into Livewire initialization:
|
||||
|
||||
<!-- Livewire Init Hook Example -->
|
||||
```js
|
||||
document.addEventListener('livewire:init', function () {
|
||||
Livewire.hook('request', ({ fail }) => {
|
||||
if (fail && fail.status === 419) {
|
||||
alert('Your session expired');
|
||||
}
|
||||
});
|
||||
|
||||
Livewire.hook('message.failed', (message, component) => {
|
||||
console.error(message);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
<!-- Example Livewire Component Test -->
|
||||
```php
|
||||
Livewire::test(Counter::class)
|
||||
->assertSet('count', 0)
|
||||
->call('increment')
|
||||
->assertSet('count', 1)
|
||||
->assertSee(1)
|
||||
->assertStatus(200);
|
||||
```
|
||||
|
||||
<!-- Testing Livewire Component Exists on Page -->
|
||||
```php
|
||||
$this->get('/posts/create')
|
||||
->assertSeeLivewire(CreatePost::class);
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Forgetting `wire:key` in loops causes unexpected behavior when items change
|
||||
- Using `wire:model` expecting real-time updates (use `wire:model.live` instead in v3)
|
||||
- Not validating/authorizing in Livewire actions (treat them like HTTP requests)
|
||||
- Including Alpine.js separately when it's already bundled with Livewire 3
|
||||
96
.agents/skills/mcp-development/SKILL.md
Normal file
96
.agents/skills/mcp-development/SKILL.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
name: mcp-development
|
||||
description: "Use this skill for Laravel MCP development only. Trigger when creating or editing MCP tools, resources, prompts, or servers in Laravel projects. Covers: artisan make:mcp-* generators, mcp:inspector, routes/ai.php, Tool/Resource/Prompt classes, schema validation, shouldRegister(), OAuth setup, URI templates, read-only attributes, and MCP debugging. Do not use for non-Laravel MCP projects or generic AI features without MCP."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# MCP Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Laravel MCP patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Register MCP servers in `routes/ai.php`:
|
||||
|
||||
<!-- Register MCP Server -->
|
||||
```php
|
||||
use Laravel\Mcp\Facades\Mcp;
|
||||
|
||||
Mcp::web();
|
||||
```
|
||||
|
||||
### Creating MCP Primitives
|
||||
|
||||
Create MCP tools, resources, prompts, and servers using artisan commands:
|
||||
|
||||
```bash
|
||||
php artisan make:mcp-tool ToolName # Create a tool
|
||||
|
||||
php artisan make:mcp-resource ResourceName # Create a resource
|
||||
|
||||
php artisan make:mcp-prompt PromptName # Create a prompt
|
||||
|
||||
php artisan make:mcp-server ServerName # Create a server
|
||||
|
||||
```
|
||||
|
||||
After creating primitives, register them in your server's `$tools`, `$resources`, or `$prompts` properties.
|
||||
|
||||
### Tools
|
||||
|
||||
<!-- MCP Tool Example -->
|
||||
```php
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Request;
|
||||
use Laravel\Mcp\Server\Response;
|
||||
|
||||
class MyTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
return new Response(['result' => 'success']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registering Primitives in a Server
|
||||
|
||||
Each MCP server must explicitly declare the tools, resources, and prompts it exposes.
|
||||
|
||||
<!-- Register Primitives in MCP Server -->
|
||||
```php
|
||||
use Laravel\Mcp\Server;
|
||||
|
||||
class AppServer extends Server
|
||||
{
|
||||
protected array $tools = [
|
||||
\App\Mcp\Tools\MyTool::class,
|
||||
];
|
||||
|
||||
protected array $resources = [
|
||||
\App\Mcp\Resources\MyResource::class,
|
||||
];
|
||||
|
||||
protected array $prompts = [
|
||||
\App\Mcp\Prompts\MyPrompt::class,
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Check `routes/ai.php` for proper registration
|
||||
2. Test tool via MCP client
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Running `mcp:start` command (it hangs waiting for input)
|
||||
- Using HTTPS locally with Node-based MCP clients
|
||||
- Not using `search-docs` for the latest MCP documentation
|
||||
- Not registering MCP server routes in `routes/ai.php`
|
||||
- Do not register `ai.php` in `bootstrap.php`; it is registered automatically.
|
||||
- OAuth registration supports custom URI schemes (e.g., `cursor://`, `vscode://`) for native desktop clients via `mcp.custom_schemes` config
|
||||
166
.agents/skills/pest-testing/SKILL.md
Normal file
166
.agents/skills/pest-testing/SKILL.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
---
|
||||
name: pest-testing
|
||||
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Pest Testing 4
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Tests
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
The `{name}` argument should include only the path and test name, but should not include the test suite.
|
||||
- Incorrect: `php artisan make:test --pest Feature/SomeFeatureTest` will generate `tests/Feature/Feature/SomeFeatureTest.php`
|
||||
- Correct: `php artisan make:test --pest SomeControllerTest` will generate `tests/Feature/SomeControllerTest.php`
|
||||
- Incorrect: `php artisan make:test --pest --unit Unit/SomeServiceTest` will generate `tests/Unit/Unit/SomeServiceTest.php`
|
||||
- Correct: `php artisan make:test --pest --unit SomeServiceTest` will generate `tests/Unit/SomeServiceTest.php`
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`.
|
||||
|
||||
<!-- Basic Pest Test Example -->
|
||||
```php
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<!-- Pest Response Assertion -->
|
||||
```php
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
```
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
| `assertNotFound()` | `assertStatus(404)` |
|
||||
| `assertForbidden()` | `assertStatus(403)` |
|
||||
|
||||
## Mocking
|
||||
|
||||
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
|
||||
<!-- Pest Dataset Example -->
|
||||
```php
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
```
|
||||
|
||||
## Pest 4 Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
|
||||
### Browser Test Example
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
|
||||
<!-- Pest Browser Test Example -->
|
||||
```php
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavaScriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
```
|
||||
|
||||
### Smoke Testing
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
|
||||
<!-- Pest Smoke Testing Example -->
|
||||
```php
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
||||
```
|
||||
|
||||
### Visual Regression Testing
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
|
||||
### Test Sharding
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
|
||||
### Architecture Testing
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
|
||||
<!-- Architecture Test Example -->
|
||||
```php
|
||||
arch('controllers')
|
||||
->expect('App\Http\Controllers')
|
||||
->toExtendNothing()
|
||||
->toHaveSuffix('Controller');
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
|
||||
80
.agents/skills/socialite-development/SKILL.md
Normal file
80
.agents/skills/socialite-development/SKILL.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
name: socialite-development
|
||||
description: "Manages OAuth social authentication with Laravel Socialite. Activate when adding social login providers; configuring OAuth redirect/callback flows; retrieving authenticated user details; customizing scopes or parameters; setting up community providers; testing with Socialite fakes; or when the user mentions social login, OAuth, Socialite, or third-party authentication."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Socialite Authentication
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Socialite patterns and documentation (installation, configuration, routing, callbacks, testing, scopes, stateless auth).
|
||||
|
||||
## Available Providers
|
||||
|
||||
Built-in: `facebook`, `twitter`, `twitter-oauth-2`, `linkedin`, `linkedin-openid`, `google`, `github`, `gitlab`, `bitbucket`, `slack`, `slack-openid`, `twitch`
|
||||
|
||||
Community: 150+ additional providers at [socialiteproviders.com](https://socialiteproviders.com). For provider-specific setup, use `WebFetch` on `https://socialiteproviders.com/{provider-name}`.
|
||||
|
||||
Configuration key in `config/services.php` must match the driver name exactly — note the hyphenated keys: `twitter-oauth-2`, `linkedin-openid`, `slack-openid`.
|
||||
|
||||
Twitter/X: Use `twitter-oauth-2` (OAuth 2.0) for new projects. The legacy `twitter` driver is OAuth 1.0. Driver names remain unchanged despite the platform rebrand.
|
||||
|
||||
Community providers differ from built-in providers in the following ways:
|
||||
- Installed via `composer require socialiteproviders/{name}`
|
||||
- Must register via event listener — NOT auto-discovered like built-in providers
|
||||
- Use `search-docs` for the registration pattern
|
||||
|
||||
## Adding a Provider
|
||||
|
||||
### 1. Configure the provider
|
||||
|
||||
Add the provider's `client_id`, `client_secret`, and `redirect` to `config/services.php`. The config key must match the driver name exactly.
|
||||
|
||||
### 2. Create redirect and callback routes
|
||||
|
||||
Two routes are needed: one that calls `Socialite::driver('provider')->redirect()` to send the user to the OAuth provider, and one that calls `Socialite::driver('provider')->user()` to receive the callback and retrieve user details.
|
||||
|
||||
### 3. Authenticate and store the user
|
||||
|
||||
In the callback, use `updateOrCreate` to find or create a user record from the provider's response (`id`, `name`, `email`, `token`, `refreshToken`), then call `Auth::login()`.
|
||||
|
||||
### 4. Customize the redirect (optional)
|
||||
|
||||
- `scopes()` — merge additional scopes with the provider's defaults
|
||||
- `setScopes()` — replace all scopes entirely
|
||||
- `with()` — pass optional parameters (e.g., `['hd' => 'example.com']` for Google)
|
||||
- `asBotUser()` — Slack only; generates a bot token (`xoxb-`) instead of a user token (`xoxp-`). Must be called before both `redirect()` and `user()`. Only the `token` property will be hydrated on the user object.
|
||||
- `stateless()` — for API/SPA contexts where session state is not maintained
|
||||
|
||||
### 5. Verify
|
||||
|
||||
1. Config key matches driver name exactly (check the list above for hyphenated names)
|
||||
2. `client_id`, `client_secret`, and `redirect` are all present
|
||||
3. Redirect URL matches what is registered in the provider's OAuth dashboard
|
||||
4. Callback route handles denied grants (when user declines authorization)
|
||||
|
||||
Use `search-docs` for complete code examples of each step.
|
||||
|
||||
## Additional Features
|
||||
|
||||
Use `search-docs` for usage details on: `enablePKCE()`, `userFromToken($token)`, `userFromTokenAndSecret($token, $secret)` (OAuth 1.0), retrieving user details.
|
||||
|
||||
User object: `getId()`, `getName()`, `getEmail()`, `getAvatar()`, `getNickname()`, `token`, `refreshToken`, `expiresIn`, `approvedScopes`
|
||||
|
||||
## Testing
|
||||
|
||||
Socialite provides `Socialite::fake()` for testing redirects and callbacks. Use `search-docs` for faking redirects, callback user data, custom token properties, and assertion methods.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Config key must match driver name exactly — hyphenated drivers need hyphenated keys (`linkedin-openid`, `slack-openid`, `twitter-oauth-2`). Mismatch silently fails.
|
||||
- Every provider needs `client_id`, `client_secret`, and `redirect` in `config/services.php`. Missing any one causes cryptic errors.
|
||||
- `scopes()` merges with defaults; `setScopes()` replaces all scopes entirely.
|
||||
- Missing `stateless()` in API/SPA contexts causes `InvalidStateException`.
|
||||
- Redirect URL in `config/services.php` must exactly match the provider's OAuth dashboard (including trailing slashes and protocol).
|
||||
- Do not pass `state`, `response_type`, `client_id`, `redirect_uri`, or `scope` via `with()` — these are reserved.
|
||||
- Community providers require event listener registration via `SocialiteWasCalled`.
|
||||
- `user()` throws when the user declines authorization. Always handle denied grants.
|
||||
119
.agents/skills/tailwindcss-development/SKILL.md
Normal file
119
.agents/skills/tailwindcss-development/SKILL.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
---
|
||||
name: tailwindcss-development
|
||||
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Tailwind CSS Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
|
||||
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
|
||||
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
|
||||
|
||||
## Tailwind CSS v4 Specifics
|
||||
|
||||
- Always use Tailwind CSS v4 and avoid deprecated utilities.
|
||||
- `corePlugins` is not supported in Tailwind v4.
|
||||
|
||||
### CSS-First Configuration
|
||||
|
||||
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
|
||||
|
||||
<!-- CSS-First Config -->
|
||||
```css
|
||||
@theme {
|
||||
--color-brand: oklch(0.72 0.11 178);
|
||||
}
|
||||
```
|
||||
|
||||
### Import Syntax
|
||||
|
||||
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
|
||||
|
||||
<!-- v4 Import Syntax -->
|
||||
```diff
|
||||
- @tailwind base;
|
||||
- @tailwind components;
|
||||
- @tailwind utilities;
|
||||
+ @import "tailwindcss";
|
||||
```
|
||||
|
||||
### Replaced Utilities
|
||||
|
||||
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
|
||||
|
||||
| Deprecated | Replacement |
|
||||
|------------|-------------|
|
||||
| bg-opacity-* | bg-black/* |
|
||||
| text-opacity-* | text-black/* |
|
||||
| border-opacity-* | border-black/* |
|
||||
| divide-opacity-* | divide-black/* |
|
||||
| ring-opacity-* | ring-black/* |
|
||||
| placeholder-opacity-* | placeholder-black/* |
|
||||
| flex-shrink-* | shrink-* |
|
||||
| flex-grow-* | grow-* |
|
||||
| overflow-ellipsis | text-ellipsis |
|
||||
| decoration-slice | box-decoration-slice |
|
||||
| decoration-clone | box-decoration-clone |
|
||||
|
||||
## Spacing
|
||||
|
||||
Use `gap` utilities instead of margins for spacing between siblings:
|
||||
|
||||
<!-- Gap Utilities -->
|
||||
```html
|
||||
<div class="flex gap-8">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
|
||||
|
||||
<!-- Dark Mode -->
|
||||
```html
|
||||
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
Content adapts to color scheme
|
||||
</div>
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Flexbox Layout
|
||||
|
||||
<!-- Flexbox Layout -->
|
||||
```html
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>Left content</div>
|
||||
<div>Right content</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Grid Layout
|
||||
|
||||
<!-- Grid Layout -->
|
||||
```html
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div>Card 1</div>
|
||||
<div>Card 2</div>
|
||||
<div>Card 3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
|
||||
- Using `@tailwind` directives instead of `@import "tailwindcss"`
|
||||
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
|
||||
- Using margins for spacing between siblings instead of gap utilities
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
404
.claude/skills/configure-nightwatch/SKILL.md
Normal file
404
.claude/skills/configure-nightwatch/SKILL.md
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
---
|
||||
name: configure-nightwatch
|
||||
description: Configures Laravel Nightwatch data collection, sampling rates, filtering rules, and redaction policies. Use when setting up Nightwatch, managing data volume, protecting sensitive data (PII), or optimizing event collection for production workloads.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Nightwatch Configuration Guide
|
||||
|
||||
This skill helps configure Laravel Nightwatch data collection to balance observability, performance, and privacy. Covers sampling strategies, filtering rules, and redaction methods across all event types.
|
||||
|
||||
## Documentation Reference
|
||||
|
||||
The [Nightwatch Documentation](https://nightwatch.laravel.com/docs) is the definitive and up-to-date source of information for all Nightwatch configuration options. This skill provides practical guidance and common patterns, but always consult the official documentation as the primary source of truth for specific details, environment variables, and API behavior. The documentation includes comprehensive coverage of:
|
||||
|
||||
- [Filtering and Configuration](https://nightwatch.laravel.com/docs/filtering) - Core concepts for sampling, filtering, and redaction
|
||||
- Individual event type pages with specific configuration options:
|
||||
- [Requests](https://nightwatch.laravel.com/docs/requests) - Request sampling, header handling, payload capture
|
||||
- [Commands](https://nightwatch.laravel.com/docs/commands) - Command sampling and redaction
|
||||
- [Queries](https://nightwatch.laravel.com/docs/queries) - Query filtering and redaction
|
||||
- [Cache](https://nightwatch.laravel.com/docs/cache) - Cache event filtering by key or pattern
|
||||
- [Jobs](https://nightwatch.laravel.com/docs/jobs) - Job filtering and sampling decoupling
|
||||
- [Mail](https://nightwatch.laravel.com/docs/mail) - Mail event filtering
|
||||
- [Notifications](https://nightwatch.laravel.com/docs/notifications) - Notification filtering by channel
|
||||
- [Exceptions](https://nightwatch.laravel.com/docs/exceptions) - Exception sampling and throttling
|
||||
- [Outgoing Requests](https://nightwatch.laravel.com/docs/outgoing-requests) - HTTP request filtering
|
||||
- [reference.md](reference.md) - Quick lookup table by event type, production presets, and verification checklist
|
||||
|
||||
## Data Collection Flow
|
||||
|
||||
Nightwatch processes events through three stages:
|
||||
|
||||
1. **Sampling** - Controls which entry points are captured (requests, commands, scheduled tasks)
|
||||
2. **Filtering** - Excludes specific events after sampling (queries, cache, mail, etc.)
|
||||
3. **Redaction** - Modifies captured data to remove/obfuscate sensitive information
|
||||
|
||||
```
|
||||
Request/Command/Scheduled Task
|
||||
|
|
||||
v
|
||||
[Sampling?] ----NO----> Drop entire trace
|
||||
| YES
|
||||
v
|
||||
Events generated
|
||||
|
|
||||
v
|
||||
[Filtering?] ----YES---> Drop specific event
|
||||
| NO
|
||||
v
|
||||
[Redaction] ----------> Store modified data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sampling Configuration
|
||||
|
||||
Sampling determines which entry points (requests, commands, scheduled tasks) trigger full trace collection. When an entry point is sampled, all related events are captured.
|
||||
|
||||
### Global Sample Rates
|
||||
|
||||
Configure via environment variables:
|
||||
|
||||
```bash
|
||||
|
||||
# Default: 100% sampling (all requests/commands captured)
|
||||
|
||||
NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1 # Recommended: 10% of requests
|
||||
|
||||
NIGHTWATCH_COMMAND_SAMPLE_RATE=1.0 # Capture all commands
|
||||
|
||||
NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0 # Always capture exceptions
|
||||
|
||||
```
|
||||
|
||||
**Recommendation**: Start with `0.1` (10%) for requests in production, adjust based on volume and needs.
|
||||
|
||||
### Route-Based Sampling
|
||||
|
||||
Apply different rates to specific routes using the `Sample` middleware:
|
||||
|
||||
```php routes/web.php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Laravel\Nightwatch\Http\Middleware\Sample;
|
||||
|
||||
// Sample admin routes at 100%
|
||||
Route::middleware(Sample::rate(1.0))->prefix('admin')->group(function () {
|
||||
// All admin routes sampled fully
|
||||
});
|
||||
|
||||
// Sample API routes at 5%
|
||||
Route::middleware(Sample::rate(0.05))->prefix('api')->group(function () {
|
||||
// API routes sampled sparingly
|
||||
});
|
||||
|
||||
// Always sample critical endpoints
|
||||
Route::post('/checkout', [CheckoutController::class, 'process'])
|
||||
->middleware(Sample::always());
|
||||
|
||||
// Never sample health checks
|
||||
Route::get('/health', [HealthController::class, 'check'])
|
||||
->middleware(Sample::never());
|
||||
```
|
||||
|
||||
### Unmatched Route Sampling
|
||||
|
||||
Handle 404/bot traffic with reduced sampling:
|
||||
|
||||
```php routes/web.php
|
||||
Route::fallback(fn () => abort(404))
|
||||
->middleware(Sample::rate(0.01)); // 1% sampling for unmatched routes
|
||||
```
|
||||
|
||||
### Dynamic Sampling
|
||||
|
||||
Sample based on runtime conditions (user role, request attributes):
|
||||
|
||||
```php app/Http/Middleware/SampleAdminRequests.php
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Nightwatch\Facades\Nightwatch;
|
||||
|
||||
class SampleAdminRequests
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($request->user()?->isAdmin()) {
|
||||
Nightwatch::sample(); // Always sample admin requests
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Command Sampling
|
||||
|
||||
Exclude specific commands from sampling:
|
||||
|
||||
```php AppServiceProvider.php
|
||||
use Illuminate\Console\Events\CommandStarting;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Laravel\Nightwatch\Facades\Nightwatch;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
Event::listen(function (CommandStarting $event) {
|
||||
if (in_array($event->command, ['schedule:finish', 'horizon:snapshot'])) {
|
||||
Nightwatch::dontSample();
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Vendor Commands
|
||||
|
||||
Nightwatch automatically ignores framework/internal commands. Opt-in to capture them:
|
||||
|
||||
```php
|
||||
Nightwatch::captureDefaultVendorCommands();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Filtering Configuration
|
||||
|
||||
Filtering excludes specific events from collection after sampling. Use filtering to reduce noise and quota usage.
|
||||
|
||||
### Database Queries
|
||||
|
||||
**Filter all queries** (disable query collection):
|
||||
|
||||
```bash
|
||||
NIGHTWATCH_IGNORE_QUERIES=true
|
||||
```
|
||||
|
||||
**Filter specific queries** by SQL pattern:
|
||||
|
||||
```php AppServiceProvider.php
|
||||
use Laravel\Nightwatch\Facades\Nightwatch;
|
||||
use Laravel\Nightwatch\Records\Query;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
// Filter job table queries (PostgreSQL)
|
||||
Nightwatch::rejectQueries(function (Query $query) {
|
||||
return str_contains($query->sql, 'into "jobs"');
|
||||
});
|
||||
|
||||
// Filter cache table queries (MySQL)
|
||||
Nightwatch::rejectQueries(function (Query $query) {
|
||||
return str_contains($query->sql, 'from `cache`')
|
||||
|| str_contains($query->sql, 'into `cache`');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Events
|
||||
|
||||
**Filter all cache events**:
|
||||
|
||||
```bash
|
||||
NIGHTWATCH_IGNORE_CACHE_EVENTS=true
|
||||
```
|
||||
|
||||
**Filter by cache key patterns**:
|
||||
|
||||
```php
|
||||
Nightwatch::rejectCacheKeys([
|
||||
'my-app:users', // Exact match
|
||||
'/^my-app:posts:/', // Regex: starts with my-app:posts:
|
||||
'/^[a-zA-Z0-9]{40}$/', // Regex: session IDs
|
||||
]);
|
||||
```
|
||||
|
||||
**Filter with callback**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\CacheEvent;
|
||||
|
||||
Nightwatch::rejectCacheEvents(function (CacheEvent $cacheEvent) {
|
||||
return str_starts_with($cacheEvent->key, 'temp:');
|
||||
});
|
||||
```
|
||||
|
||||
### Mail Events
|
||||
|
||||
**Filter all mail**:
|
||||
|
||||
```bash
|
||||
NIGHTWATCH_IGNORE_MAIL=true
|
||||
```
|
||||
|
||||
**Filter specific mail**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Mail;
|
||||
|
||||
Nightwatch::rejectMail(function (Mail $mail) {
|
||||
return str_contains($mail->subject, 'Newsletter');
|
||||
});
|
||||
```
|
||||
|
||||
### Notification Events
|
||||
|
||||
**Filter all notifications**:
|
||||
|
||||
```bash
|
||||
NIGHTWATCH_IGNORE_NOTIFICATIONS=true
|
||||
```
|
||||
|
||||
**Filter by channel**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Notification;
|
||||
|
||||
Nightwatch::rejectNotifications(function (Notification $notification) {
|
||||
return $notification->channel === 'database';
|
||||
});
|
||||
```
|
||||
|
||||
### Outgoing HTTP Requests
|
||||
|
||||
**Filter all outgoing requests**:
|
||||
|
||||
```bash
|
||||
NIGHTWATCH_IGNORE_OUTGOING_REQUESTS=true
|
||||
```
|
||||
|
||||
**Filter by URL**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\OutgoingRequest;
|
||||
|
||||
Nightwatch::rejectOutgoingRequests(function (OutgoingRequest $request) {
|
||||
return str_contains($request->url, 'analytics.example.com');
|
||||
});
|
||||
```
|
||||
|
||||
### Queued Jobs
|
||||
|
||||
**Filter specific jobs**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\QueuedJob;
|
||||
|
||||
Nightwatch::rejectQueuedJobs(function (QueuedJob $job) {
|
||||
return $job->name === 'App\Jobs\LowPriorityJob';
|
||||
});
|
||||
```
|
||||
|
||||
### Decoupling Job Sampling
|
||||
|
||||
Sample jobs independently from parent contexts:
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
Queue::before(fn () => Nightwatch::sample(rate: 0.5));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Redaction Configuration
|
||||
|
||||
Redaction modifies captured data to remove or obfuscate sensitive information. Unlike filtering, redaction keeps the event but sanitizes its content.
|
||||
|
||||
### Request Redaction
|
||||
|
||||
**Redact sensitive headers** (automatically redacts: Authorization, Cookie, X-XSRF-TOKEN):
|
||||
|
||||
```bash
|
||||
|
||||
# Customize redacted headers
|
||||
|
||||
NIGHTWATCH_REDACT_HEADERS=Authorization,Cookie,Proxy-Authorization,X-API-Key
|
||||
```
|
||||
|
||||
**Redact request payloads** (disabled by default):
|
||||
|
||||
```bash
|
||||
|
||||
# Enable payload capture
|
||||
|
||||
NIGHTWATCH_CAPTURE_REQUEST_PAYLOAD=true
|
||||
|
||||
# Customize redacted fields
|
||||
|
||||
NIGHTWATCH_REDACT_PAYLOAD_FIELDS=password,password_confirmation,ssn,credit_card
|
||||
```
|
||||
|
||||
**Programmatic redaction**:
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Facades\Nightwatch;
|
||||
use Laravel\Nightwatch\Records\Request;
|
||||
|
||||
Nightwatch::redactRequests(function (Request $request) {
|
||||
$request->url = str_replace('secret', '***', $request->url);
|
||||
$request->ip = preg_replace('/\d+$/', '***', $request->ip);
|
||||
});
|
||||
```
|
||||
|
||||
### Query Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Query;
|
||||
|
||||
Nightwatch::redactQueries(function (Query $query) {
|
||||
$query->sql = str_replace('secret_token', '***', $query->sql);
|
||||
});
|
||||
```
|
||||
|
||||
### Cache Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\CacheEvent;
|
||||
|
||||
Nightwatch::redactCacheEvents(function (CacheEvent $cacheEvent) {
|
||||
$cacheEvent->key = str_replace('user:', 'user:***:', $cacheEvent->key);
|
||||
});
|
||||
```
|
||||
|
||||
### Command Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Command;
|
||||
|
||||
Nightwatch::redactCommands(function (Command $command) {
|
||||
$command->command = preg_replace('/--password=\S+/', '--password=***', $command->command);
|
||||
});
|
||||
```
|
||||
|
||||
### Exception Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Exception;
|
||||
|
||||
Nightwatch::redactExceptions(function (Exception $exception) {
|
||||
$exception->message = str_replace('secret', '***', $exception->message);
|
||||
});
|
||||
```
|
||||
|
||||
### Mail Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\Mail;
|
||||
|
||||
Nightwatch::redactMail(function (Mail $mail) {
|
||||
$mail->subject = str_replace('Invoice #', 'Invoice ***', $mail->subject);
|
||||
});
|
||||
```
|
||||
|
||||
### Outgoing Request Redaction
|
||||
|
||||
```php
|
||||
use Laravel\Nightwatch\Records\OutgoingRequest;
|
||||
|
||||
Nightwatch::redactOutgoingRequests(function (OutgoingRequest $outgoingRequest) {
|
||||
$outgoingRequest->url = preg_replace('/api_key=\w+/', 'api_key=***', $outgoingRequest->url);
|
||||
});
|
||||
```
|
||||
108
.claude/skills/configure-nightwatch/reference.md
Normal file
108
.claude/skills/configure-nightwatch/reference.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Nightwatch Configuration Reference
|
||||
|
||||
## Configuration Summary by Event Type
|
||||
|
||||
| Event Type | Sampling | Filtering | Redaction |
|
||||
| --------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------- |
|
||||
| **Requests** | `NIGHTWATCH_REQUEST_SAMPLE_RATE`, Route middleware | Not applicable | Headers, payload, URL, IP |
|
||||
| **Commands** | `NIGHTWATCH_COMMAND_SAMPLE_RATE`, Event listener | Not applicable | Command arguments |
|
||||
| **Queries** | Parent context | `rejectQueries()`, `NIGHTWATCH_IGNORE_QUERIES` | SQL statement |
|
||||
| **Cache** | Parent context | `rejectCacheKeys()`, `rejectCacheEvents()`, `NIGHTWATCH_IGNORE_CACHE_EVENTS` | Cache key |
|
||||
| **Jobs** | Parent context, Queue::before | `rejectQueuedJobs()` | Not applicable |
|
||||
| **Mail** | Parent context | `rejectMail()`, `NIGHTWATCH_IGNORE_MAIL` | Subject |
|
||||
| **Notifications** | Parent context | `rejectNotifications()`, `NIGHTWATCH_IGNORE_NOTIFICATIONS` | Not applicable |
|
||||
| **Outgoing Requests** | Parent context | `rejectOutgoingRequests()`, `NIGHTWATCH_IGNORE_OUTGOING_REQUESTS` | URL |
|
||||
| **Exceptions** | `NIGHTWATCH_EXCEPTION_SAMPLE_RATE` | Not applicable | Exception message |
|
||||
|
||||
---
|
||||
|
||||
## Production Recommendations
|
||||
|
||||
### High-Traffic Applications
|
||||
|
||||
```bash
|
||||
|
||||
# Conservative sampling
|
||||
|
||||
NIGHTWATCH_REQUEST_SAMPLE_RATE=0.01 # 1% of requests
|
||||
|
||||
NIGHTWATCH_COMMAND_SAMPLE_RATE=0.1 # 10% of commands
|
||||
|
||||
NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0 # Always capture exceptions
|
||||
|
||||
# Filter noisy events
|
||||
|
||||
NIGHTWATCH_IGNORE_CACHE_EVENTS=true
|
||||
NIGHTWATCH_IGNORE_QUERIES=true # Or filter specific queries programmatically
|
||||
|
||||
```
|
||||
|
||||
### Privacy-Conscious Applications
|
||||
|
||||
```bash
|
||||
|
||||
# Disable sensitive data collection
|
||||
|
||||
NIGHTWATCH_CAPTURE_REQUEST_PAYLOAD=false
|
||||
NIGHTWATCH_REDACT_HEADERS=Authorization,Cookie,Proxy-Authorization,X-XSRF-TOKEN
|
||||
|
||||
# Or use redaction in AppServiceProvider
|
||||
|
||||
```
|
||||
|
||||
### Balanced Configuration (Recommended Start)
|
||||
|
||||
```bash
|
||||
|
||||
# Sample rates
|
||||
|
||||
NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1
|
||||
NIGHTWATCH_COMMAND_SAMPLE_RATE=1.0
|
||||
NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0
|
||||
|
||||
# Filter obvious noise programmatically
|
||||
|
||||
# Redact PII as needed
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After configuration:
|
||||
|
||||
- [ ] Sampling rates appropriate for traffic volume
|
||||
- [ ] Noisy events filtered (cache, certain queries)
|
||||
- [ ] Sensitive data redacted (PII, tokens, credentials)
|
||||
- [ ] Exceptions always captured for debugging
|
||||
- [ ] Test in development with `NIGHTWATCH_REQUEST_SAMPLE_RATE=1.0`
|
||||
- [ ] Monitor event quota usage in Nightwatch dashboard
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Filter Health Checks + Reduce Sampling
|
||||
|
||||
```php
|
||||
Route::get('/health', fn() => ['status' => 'ok'])
|
||||
->middleware(Sample::never());
|
||||
```
|
||||
|
||||
### Exclude Internal/Vendor Queries
|
||||
|
||||
```php
|
||||
Nightwatch::rejectQueries(fn($q) =>
|
||||
str_contains($q->sql, 'telescope') ||
|
||||
str_contains($q->sql, 'pulse')
|
||||
);
|
||||
```
|
||||
|
||||
### Protect User Data in Cache Keys
|
||||
|
||||
```php
|
||||
Nightwatch::redactCacheEvents(fn($e) =>
|
||||
$e->key = preg_replace('/user:\d+/', 'user:***', $e->key)
|
||||
);
|
||||
```
|
||||
85
.claude/skills/configuring-horizon/SKILL.md
Normal file
85
.claude/skills/configuring-horizon/SKILL.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
name: configuring-horizon
|
||||
description: "Use this skill whenever the user mentions Horizon by name in a Laravel context. Covers the full Horizon lifecycle: installing Horizon (horizon:install, Sail setup), configuring config/horizon.php (supervisor blocks, queue assignments, balancing strategies, minProcesses/maxProcesses), fixing the dashboard (authorization via Gate::define viewHorizon, blank metrics, horizon:snapshot scheduling), and troubleshooting production issues (worker crashes, timeout chain ordering, LongWaitDetected notifications, waits config). Also covers job tagging and silencing. Do not use for generic Laravel queues without Horizon, SQS or database drivers, standalone Redis setup, Linux supervisord, Telescope, or job batching."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Horizon Configuration
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Horizon patterns and documentation covering configuration, supervisors, balancing, dashboard authorization, tags, notifications, metrics, and deployment.
|
||||
|
||||
For deeper guidance on specific topics, read the relevant reference file before implementing:
|
||||
|
||||
- `references/supervisors.md` covers supervisor blocks, balancing strategies, multi-queue setups, and auto-scaling
|
||||
- `references/notifications.md` covers LongWaitDetected alerts, notification routing, and the `waits` config
|
||||
- `references/tags.md` covers job tagging, dashboard filtering, and silencing noisy jobs
|
||||
- `references/metrics.md` covers the blank metrics dashboard, snapshot scheduling, and retention config
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
php artisan horizon:install
|
||||
```
|
||||
|
||||
### Supervisor Configuration
|
||||
|
||||
Define supervisors in `config/horizon.php`. The `environments` array merges into `defaults` and does not replace the whole supervisor block:
|
||||
|
||||
<!-- Supervisor Config -->
|
||||
```php
|
||||
'defaults' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['default'],
|
||||
'balance' => 'auto',
|
||||
'minProcesses' => 1,
|
||||
'maxProcesses' => 10,
|
||||
'tries' => 3,
|
||||
],
|
||||
],
|
||||
|
||||
'environments' => [
|
||||
'production' => [
|
||||
'supervisor-1' => ['maxProcesses' => 20, 'balanceCooldown' => 3],
|
||||
],
|
||||
'local' => [
|
||||
'supervisor-1' => ['maxProcesses' => 2],
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
### Dashboard Authorization
|
||||
|
||||
Restrict access in `App\Providers\HorizonServiceProvider`:
|
||||
|
||||
<!-- Dashboard Gate -->
|
||||
```php
|
||||
protected function gate(): void
|
||||
{
|
||||
Gate::define('viewHorizon', function (User $user) {
|
||||
return $user->is_admin;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Run `php artisan horizon` and visit `/horizon`
|
||||
2. Confirm dashboard access is restricted as expected
|
||||
3. Check that metrics populate after scheduling `horizon:snapshot`
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Horizon only works with the Redis queue driver. Other drivers such as database and SQS are not supported.
|
||||
- Redis Cluster is not supported. Horizon requires a standalone Redis connection.
|
||||
- Always check `config/horizon.php` before making changes to understand the current supervisor and environment configuration.
|
||||
- The `environments` array overrides only the keys you specify. It merges into `defaults` and does not replace it.
|
||||
- The timeout chain must be ordered: job `timeout` less than supervisor `timeout` less than `retry_after`. The wrong order can cause jobs to be retried before Horizon finishes timing them out.
|
||||
- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `php artisan horizon` alone does not populate metrics.
|
||||
- Always use `search-docs` for the latest Horizon documentation rather than relying on this skill alone.
|
||||
21
.claude/skills/configuring-horizon/references/metrics.md
Normal file
21
.claude/skills/configuring-horizon/references/metrics.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Metrics & Snapshots
|
||||
|
||||
## Where to Find It
|
||||
|
||||
Search with `search-docs`:
|
||||
- `"horizon metrics snapshot"` for the snapshot command and scheduling
|
||||
- `"horizon trim snapshots"` for retention configuration
|
||||
|
||||
## What to Watch For
|
||||
|
||||
### Metrics dashboard stays blank until `horizon:snapshot` is scheduled
|
||||
|
||||
Running `horizon` artisan command does not populate metrics automatically. The metrics graph is built from snapshots, so `horizon:snapshot` must be scheduled to run every 5 minutes via Laravel's scheduler.
|
||||
|
||||
### Register the snapshot in the scheduler rather than running it manually
|
||||
|
||||
A single manual run populates the dashboard momentarily but will not keep it updated. Search `"horizon metrics snapshot"` for the exact scheduler registration syntax, which differs between Laravel 10 and 11+.
|
||||
|
||||
### `metrics.trim_snapshots` is a snapshot count, not a time duration
|
||||
|
||||
The `trim_snapshots.job` and `trim_snapshots.queue` values in `config/horizon.php` are counts of snapshots to keep, not minutes or hours. With the default of 24 snapshots at 5-minute intervals, that provides 2 hours of history. Increase the value to retain more history at the cost of Redis memory usage.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Notifications & Alerts
|
||||
|
||||
## Where to Find It
|
||||
|
||||
Search with `search-docs`:
|
||||
- `"horizon notifications"` for Horizon's built-in notification routing helpers
|
||||
- `"horizon long wait detected"` for LongWaitDetected event details
|
||||
|
||||
## What to Watch For
|
||||
|
||||
### `waits` in `config/horizon.php` controls the LongWaitDetected threshold
|
||||
|
||||
The `waits` array (e.g., `'redis:default' => 60`) defines how many seconds a job can wait in a queue before Horizon fires a `LongWaitDetected` event. This value is set in the config file, not in Horizon's notification routing. If alerts are firing too often or too late, adjust `waits` rather than the routing configuration.
|
||||
|
||||
### Use Horizon's built-in notification routing in `HorizonServiceProvider`
|
||||
|
||||
Configure notifications in the `boot()` method of `App\Providers\HorizonServiceProvider` using `Horizon::routeMailNotificationsTo()`, `Horizon::routeSlackNotificationsTo()`, or `Horizon::routeSmsNotificationsTo()`. Horizon already wires `LongWaitDetected` to its notification sender, so the documented setup is notification routing rather than manual listener registration.
|
||||
|
||||
### Failed job alerts are separate from Horizon's documented notification routing
|
||||
|
||||
Horizon's 12.x documentation covers built-in long-wait notifications. Do not assume the docs provide a `JobFailed` listener example in `HorizonServiceProvider`. If a user needs failed job alerts, treat that as custom queue event handling and consult the queue documentation instead of Horizon's notification-routing API.
|
||||
27
.claude/skills/configuring-horizon/references/supervisors.md
Normal file
27
.claude/skills/configuring-horizon/references/supervisors.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Supervisor & Balancing Configuration
|
||||
|
||||
## Where to Find It
|
||||
|
||||
Search with `search-docs` before writing any supervisor config, as option names and defaults change between Horizon versions:
|
||||
- `"horizon supervisor configuration"` for the full options list
|
||||
- `"horizon balancing strategies"` for auto, simple, and false modes
|
||||
- `"horizon autoscaling workers"` for autoScalingStrategy details
|
||||
- `"horizon environment configuration"` for the defaults and environments merge
|
||||
|
||||
## What to Watch For
|
||||
|
||||
### The `environments` array merges into `defaults` rather than replacing it
|
||||
|
||||
The `defaults` array defines the complete base supervisor config. The `environments` array patches it per environment, overriding only the keys listed. There is no need to repeat every key in each environment block. A common pattern is to define `connection`, `queue`, `balance`, `autoScalingStrategy`, `tries`, and `timeout` in `defaults`, then override only `maxProcesses`, `balanceMaxShift`, and `balanceCooldown` in `production`.
|
||||
|
||||
### Use separate named supervisors to enforce queue priority
|
||||
|
||||
Horizon does not enforce queue order when using `balance: auto` on a single supervisor. The `queue` array order is ignored for load balancing. To process `notifications` before `default`, use two separately named supervisors: one for the high-priority queue with a higher `maxProcesses`, and one for the low-priority queue with a lower cap. The docs include an explicit note about this.
|
||||
|
||||
### Use `balance: false` to keep a fixed number of workers on a dedicated queue
|
||||
|
||||
Auto-balancing suits variable load, but if a queue should always have exactly N workers such as a video-processing queue limited to 2, set `balance: false` and `maxProcesses: 2`. Auto-balancing would scale it up during bursts, which may be undesirable.
|
||||
|
||||
### Set `balanceCooldown` to prevent rapid worker scaling under bursty load
|
||||
|
||||
When using `balance: auto`, the supervisor can scale up and down rapidly under bursty load. Set `balanceCooldown` to the number of seconds between scaling decisions, typically 3 to 5, to smooth this out. `balanceMaxShift` limits how many processes are added or removed per cycle.
|
||||
21
.claude/skills/configuring-horizon/references/tags.md
Normal file
21
.claude/skills/configuring-horizon/references/tags.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Tags & Silencing
|
||||
|
||||
## Where to Find It
|
||||
|
||||
Search with `search-docs`:
|
||||
- `"horizon tags"` for the tagging API and auto-tagging behaviour
|
||||
- `"horizon silenced jobs"` for the `silenced` and `silenced_tags` config options
|
||||
|
||||
## What to Watch For
|
||||
|
||||
### Eloquent model jobs are tagged automatically without any extra code
|
||||
|
||||
If a job's constructor accepts Eloquent model instances, Horizon automatically tags the job with `ModelClass:id` such as `App\Models\User:42`. These tags are filterable in the dashboard without any changes to the job class. Only add a `tags()` method when custom tags beyond auto-tagging are needed.
|
||||
|
||||
### `silenced` hides jobs from the dashboard completed list but does not stop them from running
|
||||
|
||||
Adding a job class to the `silenced` array in `config/horizon.php` removes it from the completed jobs view. The job still runs normally. This is a dashboard noise-reduction tool, not a way to disable jobs.
|
||||
|
||||
### `silenced_tags` hides all jobs carrying a matching tag from the completed list
|
||||
|
||||
Any job carrying a matching tag string is hidden from the completed jobs view. This is useful for silencing a category of jobs such as all jobs tagged `notifications`, rather than silencing specific classes.
|
||||
|
|
@ -0,0 +1,414 @@
|
|||
---
|
||||
name: debugging-output-and-previewing-html-using-ray
|
||||
description: Use when user says "send to Ray," "show in Ray," "debug in Ray," "log to Ray," "display in Ray," or wants to visualize data, debug output, or show diagrams in the Ray desktop application.
|
||||
metadata:
|
||||
author: Spatie
|
||||
tags:
|
||||
- debugging
|
||||
- logging
|
||||
- visualization
|
||||
- ray
|
||||
---
|
||||
|
||||
# Ray Skill
|
||||
|
||||
## Overview
|
||||
|
||||
Ray is Spatie's desktop debugging application for developers. Send data directly to Ray by making HTTP requests to its local server.
|
||||
|
||||
This can be useful for debugging applications, or to preview design, logos, or other visual content.
|
||||
|
||||
This is what the `ray()` PHP function does under the hood.
|
||||
|
||||
## Connection Details
|
||||
|
||||
| Setting | Default | Environment Variable |
|
||||
|---------|---------|---------------------|
|
||||
| Host | `localhost` | `RAY_HOST` |
|
||||
| Port | `23517` | `RAY_PORT` |
|
||||
| URL | `http://localhost:23517/` | - |
|
||||
|
||||
## Request Format
|
||||
|
||||
**Method:** POST
|
||||
**Content-Type:** `application/json`
|
||||
**User-Agent:** `Ray 1.0`
|
||||
|
||||
### Basic Request Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "unique-identifier-for-this-ray-instance",
|
||||
"payloads": [
|
||||
{
|
||||
"type": "log",
|
||||
"content": { },
|
||||
"origin": {
|
||||
"file": "/path/to/file.php",
|
||||
"line_number": 42,
|
||||
"hostname": "my-machine"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"ray_package_version": "1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `uuid` | string | Unique identifier for this Ray instance. Reuse the same UUID to update an existing entry. |
|
||||
| `payloads` | array | Array of payload objects to send |
|
||||
| `meta` | object | Optional metadata (ray_package_version, project_name, php_version) |
|
||||
|
||||
### Origin Object
|
||||
|
||||
Every payload includes origin information:
|
||||
|
||||
```json
|
||||
{
|
||||
"file": "/Users/dev/project/app/Controller.php",
|
||||
"line_number": 42,
|
||||
"hostname": "dev-machine"
|
||||
}
|
||||
```
|
||||
|
||||
## Payload Types
|
||||
|
||||
### Log (Send Values)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "log",
|
||||
"content": {
|
||||
"values": ["Hello World", 42, {"key": "value"}]
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Custom (HTML/Text Content)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "custom",
|
||||
"content": {
|
||||
"content": "<h1>HTML Content</h1><p>With formatting</p>",
|
||||
"label": "My Label"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Table
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "table",
|
||||
"content": {
|
||||
"values": {"name": "John", "email": "john@example.com", "age": 30},
|
||||
"label": "User Data"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Color
|
||||
|
||||
Set the color of the preceding log entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "color",
|
||||
"content": {
|
||||
"color": "green"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
**Available colors:** `green`, `orange`, `red`, `purple`, `blue`, `gray`
|
||||
|
||||
### Screen Color
|
||||
|
||||
Set the background color of the screen:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "screen_color",
|
||||
"content": {
|
||||
"color": "green"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Label
|
||||
|
||||
Add a label to the entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "label",
|
||||
"content": {
|
||||
"label": "Important"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Size
|
||||
|
||||
Set the size of the entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "size",
|
||||
"content": {
|
||||
"size": "lg"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
**Available sizes:** `sm`, `lg`
|
||||
|
||||
### Notify (Desktop Notification)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "notify",
|
||||
"content": {
|
||||
"value": "Task completed!"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### New Screen
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "new_screen",
|
||||
"content": {
|
||||
"name": "Debug Session"
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
### Measure (Timing)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "measure",
|
||||
"content": {
|
||||
"name": "my-timer",
|
||||
"is_new_timer": true,
|
||||
"total_time": 0,
|
||||
"time_since_last_call": 0,
|
||||
"max_memory_usage_during_total_time": 0,
|
||||
"max_memory_usage_since_last_call": 0
|
||||
},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
For subsequent measurements, set `is_new_timer: false` and provide actual timing values.
|
||||
|
||||
### Simple Payloads (No Content)
|
||||
|
||||
These payloads only need a `type` and empty `content`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "separator",
|
||||
"content": {},
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
```
|
||||
|
||||
| Type | Purpose |
|
||||
|------|---------|
|
||||
| `separator` | Add visual divider |
|
||||
| `clear_all` | Clear all entries |
|
||||
| `hide` | Hide this entry |
|
||||
| `remove` | Remove this entry |
|
||||
| `confetti` | Show confetti animation |
|
||||
| `show_app` | Bring Ray to foreground |
|
||||
| `hide_app` | Hide Ray window |
|
||||
|
||||
## Combining Multiple Payloads
|
||||
|
||||
Send multiple payloads in one request. Use the same `uuid` to apply modifiers (color, label, size) to a log entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "abc-123",
|
||||
"payloads": [
|
||||
{
|
||||
"type": "log",
|
||||
"content": { "values": ["Important message"] },
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"content": { "color": "red" },
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"content": { "label": "ERROR" },
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
},
|
||||
{
|
||||
"type": "size",
|
||||
"content": { "size": "lg" },
|
||||
"origin": { "file": "test.php", "line_number": 1, "hostname": "localhost" }
|
||||
}
|
||||
],
|
||||
"meta": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Complete Request
|
||||
|
||||
Send a green, labeled log message:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:23517/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "User-Agent: Ray 1.0" \
|
||||
-d '{
|
||||
"uuid": "my-unique-id-123",
|
||||
"payloads": [
|
||||
{
|
||||
"type": "log",
|
||||
"content": {
|
||||
"values": ["User logged in", {"user_id": 42, "name": "John"}]
|
||||
},
|
||||
"origin": {
|
||||
"file": "/app/AuthController.php",
|
||||
"line_number": 55,
|
||||
"hostname": "dev-server"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"content": { "color": "green" },
|
||||
"origin": { "file": "/app/AuthController.php", "line_number": 55, "hostname": "dev-server" }
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"content": { "label": "Auth" },
|
||||
"origin": { "file": "/app/AuthController.php", "line_number": 55, "hostname": "dev-server" }
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"project_name": "my-app"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Availability Check
|
||||
|
||||
Before sending data, you can check if Ray is running:
|
||||
|
||||
```
|
||||
GET http://localhost:23517/_availability_check
|
||||
```
|
||||
|
||||
Ray responds with HTTP 404 when available (the endpoint doesn't exist, but the server is running).
|
||||
|
||||
## Getting Ray Information
|
||||
|
||||
### Get Windows
|
||||
|
||||
Retrieve information about all open Ray windows:
|
||||
|
||||
```
|
||||
GET http://localhost:23517/windows
|
||||
```
|
||||
|
||||
Returns an array of window objects with their IDs and names:
|
||||
|
||||
```json
|
||||
[
|
||||
{"id": 1, "name": "Window 1"},
|
||||
{"id": 2, "name": "Debug Session"}
|
||||
]
|
||||
```
|
||||
|
||||
### Get Theme Colors
|
||||
|
||||
Retrieve the current theme colors being used by Ray:
|
||||
|
||||
```
|
||||
GET http://localhost:23517/theme
|
||||
```
|
||||
|
||||
Returns the theme information including color palette:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Dark",
|
||||
"colors": {
|
||||
"primary": "#000000",
|
||||
"secondary": "#1a1a1a",
|
||||
"accent": "#3b82f6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case:** When sending custom HTML content to Ray, use these theme colors to ensure your content matches Ray's current theme and looks visually integrated.
|
||||
|
||||
**Example:** Send HTML with matching colors:
|
||||
|
||||
```bash
|
||||
|
||||
# First, get the theme
|
||||
|
||||
THEME=$(curl -s http://localhost:23517/theme)
|
||||
PRIMARY_COLOR=$(echo $THEME | jq -r '.colors.primary')
|
||||
|
||||
# Then send HTML using those colors
|
||||
|
||||
curl -X POST http://localhost:23517/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"uuid": "theme-matched-html",
|
||||
"payloads": [{
|
||||
"type": "custom",
|
||||
"content": {
|
||||
"content": "<div style=\"background: '"$PRIMARY_COLOR"'; padding: 20px;\"><h1>Themed Content</h1></div>",
|
||||
"label": "Themed HTML"
|
||||
},
|
||||
"origin": {"file": "script.sh", "line_number": 1, "hostname": "localhost"}
|
||||
}]
|
||||
}'
|
||||
```
|
||||
|
||||
## Payload Type Reference
|
||||
|
||||
| Type | Content Fields | Purpose |
|
||||
|------|----------------|---------|
|
||||
| `log` | `values` (array) | Send values to Ray |
|
||||
| `custom` | `content`, `label` | HTML or text content |
|
||||
| `table` | `values`, `label` | Display as table |
|
||||
| `color` | `color` | Set entry color |
|
||||
| `screen_color` | `color` | Set screen background |
|
||||
| `label` | `label` | Add label to entry |
|
||||
| `size` | `size` | Set entry size (sm/lg) |
|
||||
| `notify` | `value` | Desktop notification |
|
||||
| `new_screen` | `name` | Create new screen |
|
||||
| `measure` | `name`, `is_new_timer`, timing fields | Performance timing |
|
||||
| `separator` | (empty) | Visual divider |
|
||||
| `clear_all` | (empty) | Clear all entries |
|
||||
| `hide` | (empty) | Hide entry |
|
||||
| `remove` | (empty) | Remove entry |
|
||||
| `confetti` | (empty) | Confetti animation |
|
||||
| `show_app` | (empty) | Show Ray window |
|
||||
| `hide_app` | (empty) | Hide Ray window |
|
||||
151
.claude/skills/fortify-development/SKILL.md
Normal file
151
.claude/skills/fortify-development/SKILL.md
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
---
|
||||
name: fortify-development
|
||||
description: 'ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), passkeys, profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, passkeys, WebAuthn, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.'
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Laravel Fortify Development
|
||||
|
||||
Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Laravel Fortify patterns and documentation.
|
||||
|
||||
## Usage
|
||||
|
||||
- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints
|
||||
- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.)
|
||||
- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field
|
||||
- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.)
|
||||
- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc.
|
||||
|
||||
## Available Features
|
||||
|
||||
Enable in `config/fortify.php` features array:
|
||||
|
||||
- `Features::registration()` - User registration
|
||||
- `Features::resetPasswords()` - Password reset via email
|
||||
- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail`
|
||||
- `Features::updateProfileInformation()` - Profile updates
|
||||
- `Features::updatePasswords()` - Password changes
|
||||
- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes
|
||||
- `Features::passkeys()` - Passwordless authentication with WebAuthn passkeys
|
||||
|
||||
> Use `search-docs` for feature configuration options and customization patterns.
|
||||
|
||||
## Setup Workflows
|
||||
|
||||
### Two-Factor Authentication Setup
|
||||
|
||||
```
|
||||
- [ ] Add TwoFactorAuthenticatable trait to User model
|
||||
- [ ] Enable feature in config/fortify.php
|
||||
- [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
|
||||
- [ ] Set up view callbacks in FortifyServiceProvider
|
||||
- [ ] Create 2FA management UI
|
||||
- [ ] Test QR code and recovery codes
|
||||
```
|
||||
|
||||
> Use `search-docs` for TOTP implementation and recovery code handling patterns.
|
||||
|
||||
### Passkeys Setup
|
||||
|
||||
```
|
||||
- [ ] Add PasskeyAuthenticatable trait to User model and implement PasskeyUser
|
||||
- [ ] Enable passkeys feature in config/fortify.php
|
||||
- [ ] If the passkeys table migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
|
||||
- [ ] Configure passkeys relying_party_id, allowed_origins, user_handle_secret, and timeout if defaults are not suitable
|
||||
- [ ] Build UI with @laravel/passkeys for registration, login, confirmation, and deletion
|
||||
```
|
||||
|
||||
> Use `search-docs` for passkey configuration options. For `@laravel/passkeys` frontend usage, refer to the package's README on npm.
|
||||
|
||||
### Email Verification Setup
|
||||
|
||||
```
|
||||
- [ ] Enable emailVerification feature in config
|
||||
- [ ] Implement MustVerifyEmail interface on User model
|
||||
- [ ] Set up verifyEmailView callback
|
||||
- [ ] Add verified middleware to protected routes
|
||||
- [ ] Test verification email flow
|
||||
```
|
||||
|
||||
> Use `search-docs` for MustVerifyEmail implementation patterns.
|
||||
|
||||
### Password Reset Setup
|
||||
|
||||
```
|
||||
- [ ] Enable resetPasswords feature in config
|
||||
- [ ] Set up requestPasswordResetLinkView callback
|
||||
- [ ] Set up resetPasswordView callback
|
||||
- [ ] Define password.reset named route (if views disabled)
|
||||
- [ ] Test reset email and link flow
|
||||
```
|
||||
|
||||
> Use `search-docs` for custom password reset flow patterns.
|
||||
|
||||
### SPA Authentication Setup
|
||||
|
||||
```
|
||||
- [ ] Set 'views' => false in config/fortify.php
|
||||
- [ ] Install and configure Laravel Sanctum for session-based SPA authentication
|
||||
- [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication)
|
||||
- [ ] Set up CSRF token handling
|
||||
- [ ] Test XHR authentication flows
|
||||
```
|
||||
|
||||
> Use `search-docs` for integration and SPA authentication patterns.
|
||||
|
||||
#### Two-Factor Authentication in SPA Mode
|
||||
|
||||
When `views` is set to `false`, Fortify returns JSON responses instead of redirects.
|
||||
|
||||
If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required:
|
||||
|
||||
```json
|
||||
{
|
||||
"two_factor": true
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Custom Authentication Logic
|
||||
|
||||
Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects.
|
||||
|
||||
### Registration Customization
|
||||
|
||||
Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields.
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination.
|
||||
|
||||
## Key Endpoints
|
||||
|
||||
| Feature | Method | Endpoint |
|
||||
|------------------------|----------|---------------------------------------------|
|
||||
| Login | POST | `/login` |
|
||||
| Logout | POST | `/logout` |
|
||||
| Register | POST | `/register` |
|
||||
| Password Reset Request | POST | `/forgot-password` |
|
||||
| Password Reset | POST | `/reset-password` |
|
||||
| Email Verify Notice | GET | `/email/verify` |
|
||||
| Resend Verification | POST | `/email/verification-notification` |
|
||||
| Password Confirm | POST | `/user/confirm-password` |
|
||||
| Enable 2FA | POST | `/user/two-factor-authentication` |
|
||||
| Confirm 2FA | POST | `/user/confirmed-two-factor-authentication` |
|
||||
| 2FA Challenge | POST | `/two-factor-challenge` |
|
||||
| Get QR Code | GET | `/user/two-factor-qr-code` |
|
||||
| Recovery Codes | GET/POST | `/user/two-factor-recovery-codes` |
|
||||
| Passkey Login Options | GET | `/passkeys/login/options` |
|
||||
| Passkey Login | POST | `/passkeys/login` |
|
||||
| Passkey Confirm Options| GET | `/passkeys/confirm/options` |
|
||||
| Passkey Confirm | POST | `/passkeys/confirm` |
|
||||
| Passkey Options | GET | `/user/passkeys/options` |
|
||||
| Register Passkey | POST | `/user/passkeys` |
|
||||
| Delete Passkey | DELETE | `/user/passkeys/{passkey}` |
|
||||
302
.claude/skills/laravel-actions/SKILL.md
Normal file
302
.claude/skills/laravel-actions/SKILL.md
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
---
|
||||
name: laravel-actions
|
||||
description: Build, refactor, and troubleshoot Laravel Actions using lorisleiva/laravel-actions. Use when implementing reusable action classes (object/controller/job/listener/command), converting service classes/controllers/jobs into actions, orchestrating workflows via faked actions, or debugging action entrypoints and wiring.
|
||||
---
|
||||
|
||||
# Laravel Actions or `lorisleiva/laravel-actions`
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill to implement or update actions based on `lorisleiva/laravel-actions` with consistent structure and predictable testing patterns.
|
||||
|
||||
## Quick Workflow
|
||||
|
||||
1. Confirm the package is installed with `composer show lorisleiva/laravel-actions`.
|
||||
2. Create or edit an action class that uses `Lorisleiva\Actions\Concerns\AsAction`.
|
||||
3. Implement `handle(...)` with the core business logic first.
|
||||
4. Add adapter methods only when needed for the requested entrypoint:
|
||||
- `asController` (+ route/invokable controller usage)
|
||||
- `asJob` (+ dispatch)
|
||||
- `asListener` (+ event listener wiring)
|
||||
- `asCommand` (+ command signature/description)
|
||||
5. Add or update tests for the chosen entrypoint.
|
||||
6. When tests need isolation, use action fakes (`MyAction::fake()`) and assertions (`MyAction::assertDispatched()`).
|
||||
|
||||
## Base Action Pattern
|
||||
|
||||
Use this minimal skeleton and expand only what is needed.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class PublishArticle
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(int $articleId): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Project Conventions
|
||||
|
||||
- Place action classes in `App\Actions` unless an existing domain sub-namespace is already used.
|
||||
- Use descriptive `VerbNoun` naming (e.g. `PublishArticle`, `SyncVehicleTaxStatus`).
|
||||
- Keep domain/business logic in `handle(...)`; keep transport and framework concerns in adapter methods (`asController`, `asJob`, `asListener`, `asCommand`).
|
||||
- Prefer explicit parameter and return types in all action methods.
|
||||
- Prefer PHPDoc for complex data contracts (e.g. array shapes), not inline comments.
|
||||
|
||||
### When to Use an Action
|
||||
|
||||
- Use an Action when the same use-case needs multiple entrypoints (HTTP, queue, event, CLI) or benefits from first-class orchestration/faking.
|
||||
- Keep a plain service class when logic is local, single-entrypoint, and unlikely to be reused as an Action.
|
||||
|
||||
## Entrypoint Patterns
|
||||
|
||||
### Run as Object
|
||||
|
||||
- (prefer method) Use static helper from the trait: `PublishArticle::run($id)`.
|
||||
- Use make and call handle: `PublishArticle::make()->handle($id)`.
|
||||
- Call with dependency injection: `app(PublishArticle::class)->handle($id)`.
|
||||
|
||||
### Run as Controller
|
||||
|
||||
- Use route to class (invokable style), e.g. `Route::post('/articles/{id}/publish', PublishArticle::class)`.
|
||||
- Add `asController(...)` for HTTP-specific adaptation and return a response.
|
||||
- Add request validation (`rules()` or custom validator hooks) when input comes from HTTP.
|
||||
|
||||
### Run as Job
|
||||
|
||||
- Dispatch with `PublishArticle::dispatch($id)`.
|
||||
- Use `asJob(...)` only for queue-specific behavior; keep domain logic in `handle(...)`.
|
||||
- In this project, job Actions often define additional queue lifecycle methods and job properties for retries, uniqueness, and timing control.
|
||||
|
||||
#### Project Pattern: Job Action with Extra Methods
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Demo;
|
||||
|
||||
use App\Models\Demo;
|
||||
use DateTime;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Lorisleiva\Actions\Decorators\JobDecorator;
|
||||
|
||||
class GetDemoData
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public int $jobTries = 3;
|
||||
|
||||
public int $jobMaxExceptions = 3;
|
||||
|
||||
public function getJobRetryUntil(): DateTime
|
||||
{
|
||||
return now()->addMinutes(30);
|
||||
}
|
||||
|
||||
public function getJobBackoff(): array
|
||||
{
|
||||
return [60, 120];
|
||||
}
|
||||
|
||||
public function getJobUniqueId(Demo $demo): string
|
||||
{
|
||||
return $demo->id;
|
||||
}
|
||||
|
||||
public function handle(Demo $demo): void
|
||||
{
|
||||
// Core business logic.
|
||||
}
|
||||
|
||||
public function asJob(JobDecorator $job, Demo $demo): void
|
||||
{
|
||||
// Queue-specific orchestration and retry behavior.
|
||||
$this->handle($demo);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use these members only when needed:
|
||||
|
||||
- `$jobTries`: max attempts for the queued execution.
|
||||
- `$jobMaxExceptions`: max unhandled exceptions before failing.
|
||||
- `getJobRetryUntil()`: absolute retry deadline.
|
||||
- `getJobBackoff()`: retry delay strategy per attempt.
|
||||
- `getJobUniqueId(...)`: deduplication key for unique jobs.
|
||||
- `asJob(JobDecorator $job, ...)`: access attempt metadata and queue-only branching.
|
||||
|
||||
### Run as Listener
|
||||
|
||||
- Register the action class as listener in `EventServiceProvider`.
|
||||
- Use `asListener(EventName $event)` and delegate to `handle(...)`.
|
||||
|
||||
### Run as Command
|
||||
|
||||
- Define `$commandSignature` and `$commandDescription` properties.
|
||||
- Implement `asCommand(Command $command)` and keep console IO in this method only.
|
||||
- Import `Command` with `use Illuminate\Console\Command;`.
|
||||
|
||||
## Testing Guidance
|
||||
|
||||
Use a two-layer strategy:
|
||||
|
||||
1. `handle(...)` tests for business correctness.
|
||||
2. entrypoint tests (`asController`, `asJob`, `asListener`, `asCommand`) for wiring/orchestration.
|
||||
|
||||
### Deep Dive: `AsFake` methods (2.x)
|
||||
|
||||
Reference: https://www.laravelactions.com/2.x/as-fake.html
|
||||
|
||||
Use these methods intentionally based on what you want to prove.
|
||||
|
||||
#### `mock()`
|
||||
|
||||
- Replaces the action with a full mock.
|
||||
- Best when you need strict expectations and argument assertions.
|
||||
|
||||
```php
|
||||
PublishArticle::mock()
|
||||
->shouldReceive('handle')
|
||||
->once()
|
||||
->with(42)
|
||||
->andReturnTrue();
|
||||
```
|
||||
|
||||
#### `partialMock()`
|
||||
|
||||
- Replaces the action with a partial mock.
|
||||
- Best when you want to keep most real behavior but stub one expensive/internal method.
|
||||
|
||||
```php
|
||||
PublishArticle::partialMock()
|
||||
->shouldReceive('fetchRemoteData')
|
||||
->once()
|
||||
->andReturn(['ok' => true]);
|
||||
```
|
||||
|
||||
#### `spy()`
|
||||
|
||||
- Replaces the action with a spy.
|
||||
- Best for post-execution verification ("was called with X") without predefining all expectations.
|
||||
|
||||
```php
|
||||
$spy = PublishArticle::spy()->allows('handle')->andReturnTrue();
|
||||
|
||||
// execute code that triggers the action...
|
||||
|
||||
$spy->shouldHaveReceived('handle')->with(42);
|
||||
```
|
||||
|
||||
#### `shouldRun()`
|
||||
|
||||
- Shortcut for `mock()->shouldReceive('handle')`.
|
||||
- Best for compact orchestration assertions.
|
||||
|
||||
```php
|
||||
PublishArticle::shouldRun()->once()->with(42)->andReturnTrue();
|
||||
```
|
||||
|
||||
#### `shouldNotRun()`
|
||||
|
||||
- Shortcut for `mock()->shouldNotReceive('handle')`.
|
||||
- Best for guard-clause tests and branch coverage.
|
||||
|
||||
```php
|
||||
PublishArticle::shouldNotRun();
|
||||
```
|
||||
|
||||
#### `allowToRun()`
|
||||
|
||||
- Shortcut for spy + allowing `handle`.
|
||||
- Best when you want execution to proceed but still assert interaction.
|
||||
|
||||
```php
|
||||
$spy = PublishArticle::allowToRun()->andReturnTrue();
|
||||
// ...
|
||||
$spy->shouldHaveReceived('handle')->once();
|
||||
```
|
||||
|
||||
#### `isFake()` and `clearFake()`
|
||||
|
||||
- `isFake()` checks whether the class is currently swapped.
|
||||
- `clearFake()` resets the fake and prevents cross-test leakage.
|
||||
|
||||
```php
|
||||
expect(PublishArticle::isFake())->toBeFalse();
|
||||
PublishArticle::mock();
|
||||
expect(PublishArticle::isFake())->toBeTrue();
|
||||
PublishArticle::clearFake();
|
||||
expect(PublishArticle::isFake())->toBeFalse();
|
||||
```
|
||||
|
||||
### Recommended test matrix for Actions
|
||||
|
||||
- Business rule test: call `handle(...)` directly with real dependencies/factories.
|
||||
- HTTP wiring test: hit route/controller, fake downstream actions with `shouldRun` or `shouldNotRun`.
|
||||
- Job wiring test: dispatch action as job, assert expected downstream action calls.
|
||||
- Event listener test: dispatch event, assert action interaction via fake/spy.
|
||||
- Console test: run artisan command, assert action invocation and output.
|
||||
|
||||
### Practical defaults
|
||||
|
||||
- Prefer `shouldRun()` and `shouldNotRun()` for readability in branch tests.
|
||||
- Prefer `spy()`/`allowToRun()` when behavior is mostly real and you only need call verification.
|
||||
- Prefer `mock()` when interaction contracts are strict and should fail fast.
|
||||
- Use `clearFake()` in cleanup when a fake might leak into another test.
|
||||
- Keep side effects isolated: fake only the action under test boundary, not everything.
|
||||
|
||||
### Pest style examples
|
||||
|
||||
```php
|
||||
it('dispatches the downstream action', function () {
|
||||
SendInvoiceEmail::shouldRun()->once()->withArgs(fn (int $invoiceId) => $invoiceId > 0);
|
||||
|
||||
FinalizeInvoice::run(123);
|
||||
});
|
||||
|
||||
it('does not dispatch when invoice is already sent', function () {
|
||||
SendInvoiceEmail::shouldNotRun();
|
||||
|
||||
FinalizeInvoice::run(123, alreadySent: true);
|
||||
});
|
||||
```
|
||||
|
||||
Run the minimum relevant suite first, e.g. `php artisan test --compact --filter=PublishArticle` or by specific test file.
|
||||
|
||||
## Troubleshooting Checklist
|
||||
|
||||
- Ensure the class uses `AsAction` and namespace matches autoload.
|
||||
- Check route registration when used as controller.
|
||||
- Check queue config when using `dispatch`.
|
||||
- Verify event-to-listener mapping in `EventServiceProvider`.
|
||||
- Keep transport concerns in adapter methods (`asController`, `asCommand`, etc.), not in `handle(...)`.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Putting HTTP response/redirect logic inside `handle(...)` instead of `asController(...)`.
|
||||
- Duplicating business rules across `as*` methods rather than delegating to `handle(...)`.
|
||||
- Assuming listener wiring works without explicit registration where required.
|
||||
- Testing only entrypoints and skipping direct `handle(...)` behavior tests.
|
||||
- Overusing Actions for one-off, single-context logic with no reuse pressure.
|
||||
|
||||
## Topic References
|
||||
|
||||
Use these references for deep dives by entrypoint/topic. Keep `SKILL.md` focused on workflow and decision rules.
|
||||
|
||||
- Object entrypoint: `references/object.md`
|
||||
- Controller entrypoint: `references/controller.md`
|
||||
- Job entrypoint: `references/job.md`
|
||||
- Listener entrypoint: `references/listener.md`
|
||||
- Command entrypoint: `references/command.md`
|
||||
- With attributes: `references/with-attributes.md`
|
||||
- Testing and fakes: `references/testing-fakes.md`
|
||||
- Troubleshooting: `references/troubleshooting.md`
|
||||
160
.claude/skills/laravel-actions/references/command.md
Normal file
160
.claude/skills/laravel-actions/references/command.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# Command Entrypoint (`asCommand`)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when exposing actions as Artisan commands.
|
||||
|
||||
## Recap
|
||||
|
||||
- Documents command execution via `asCommand(...)` and fallback to `handle(...)`.
|
||||
- Covers command metadata via methods/properties (signature, description, help, hidden).
|
||||
- Includes registration example and focused artisan test pattern.
|
||||
- Reinforces separation between console I/O and domain logic.
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Define `$commandSignature` and `$commandDescription`.
|
||||
- Implement `asCommand(Command $command)` for console I/O.
|
||||
- Keep business logic in `handle(...)`.
|
||||
|
||||
## Methods used (`CommandDecorator`)
|
||||
|
||||
### `asCommand`
|
||||
|
||||
Called when executed as a command. If missing, it falls back to `handle(...)`.
|
||||
|
||||
```php
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UpdateUserRole
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $commandSignature = 'users:update-role {user_id} {role}';
|
||||
|
||||
public function handle(User $user, string $newRole): void
|
||||
{
|
||||
$user->update(['role' => $newRole]);
|
||||
}
|
||||
|
||||
public function asCommand(Command $command): void
|
||||
{
|
||||
$this->handle(
|
||||
User::findOrFail($command->argument('user_id')),
|
||||
$command->argument('role')
|
||||
);
|
||||
|
||||
$command->info('Done!');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `getCommandSignature`
|
||||
|
||||
Defines the command signature. Required when registering an action as a command if no `$commandSignature` property is set.
|
||||
|
||||
```php
|
||||
public function getCommandSignature(): string
|
||||
{
|
||||
return 'users:update-role {user_id} {role}';
|
||||
}
|
||||
```
|
||||
|
||||
### `$commandSignature`
|
||||
|
||||
Property alternative to `getCommandSignature`.
|
||||
|
||||
```php
|
||||
public string $commandSignature = 'users:update-role {user_id} {role}';
|
||||
```
|
||||
|
||||
### `getCommandDescription`
|
||||
|
||||
Provides command description.
|
||||
|
||||
```php
|
||||
public function getCommandDescription(): string
|
||||
{
|
||||
return 'Updates the role of a given user.';
|
||||
}
|
||||
```
|
||||
|
||||
### `$commandDescription`
|
||||
|
||||
Property alternative to `getCommandDescription`.
|
||||
|
||||
```php
|
||||
public string $commandDescription = 'Updates the role of a given user.';
|
||||
```
|
||||
|
||||
### `getCommandHelp`
|
||||
|
||||
Provides additional help text shown with `--help`.
|
||||
|
||||
```php
|
||||
public function getCommandHelp(): string
|
||||
{
|
||||
return 'My help message.';
|
||||
}
|
||||
```
|
||||
|
||||
### `$commandHelp`
|
||||
|
||||
Property alternative to `getCommandHelp`.
|
||||
|
||||
```php
|
||||
public string $commandHelp = 'My help message.';
|
||||
```
|
||||
|
||||
### `isCommandHidden`
|
||||
|
||||
Defines whether command should be hidden from artisan list. Default is `false`.
|
||||
|
||||
```php
|
||||
public function isCommandHidden(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### `$commandHidden`
|
||||
|
||||
Property alternative to `isCommandHidden`.
|
||||
|
||||
```php
|
||||
public bool $commandHidden = true;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Register in console kernel
|
||||
|
||||
```php
|
||||
// app/Console/Kernel.php
|
||||
protected $commands = [
|
||||
UpdateUserRole::class,
|
||||
];
|
||||
```
|
||||
|
||||
### Focused command test
|
||||
|
||||
```php
|
||||
$this->artisan('users:update-role 1 admin')
|
||||
->expectsOutput('Done!')
|
||||
->assertSuccessful();
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- `use Illuminate\Console\Command;` is imported.
|
||||
- Signature/options/arguments are documented.
|
||||
- Command test verifies invocation and output.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Mixing command I/O with domain logic in `handle(...)`.
|
||||
- Missing/ambiguous command signature.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-command.html
|
||||
339
.claude/skills/laravel-actions/references/controller.md
Normal file
339
.claude/skills/laravel-actions/references/controller.md
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
# Controller Entrypoint (`asController`)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when exposing an action through HTTP routes.
|
||||
|
||||
## Recap
|
||||
|
||||
- Documents controller lifecycle around `asController(...)` and response adapters.
|
||||
- Covers routing patterns, middleware, and optional in-action `routes()` registration.
|
||||
- Summarizes validation/authorization hooks used by `ActionRequest`.
|
||||
- Provides extension points for JSON/HTML responses and failure customization.
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Route directly to action class when appropriate.
|
||||
- Keep HTTP adaptation in controller methods (`asController`, `jsonResponse`, `htmlResponse`).
|
||||
- Keep domain logic in `handle(...)`.
|
||||
|
||||
## Methods provided (`AsController` trait)
|
||||
|
||||
### `__invoke`
|
||||
|
||||
Required so Laravel can register the action class as an invokable controller.
|
||||
|
||||
```php
|
||||
$action($someArguments);
|
||||
|
||||
// Equivalent to:
|
||||
$action->handle($someArguments);
|
||||
```
|
||||
|
||||
If the method does not exist, Laravel route registration fails for invokable controllers.
|
||||
|
||||
```php
|
||||
// Illuminate\Routing\RouteAction
|
||||
protected static function makeInvokable($action)
|
||||
{
|
||||
if (! method_exists($action, '__invoke')) {
|
||||
throw new UnexpectedValueException("Invalid route action: [{$action}].");
|
||||
}
|
||||
|
||||
return $action.'@__invoke';
|
||||
}
|
||||
```
|
||||
|
||||
If you need your own `__invoke`, alias the trait implementation:
|
||||
|
||||
```php
|
||||
class MyAction
|
||||
{
|
||||
use AsAction {
|
||||
__invoke as protected invokeFromLaravelActions;
|
||||
}
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
// Custom behavior...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Methods used (`ControllerDecorator` + `ActionRequest`)
|
||||
|
||||
### `asController`
|
||||
|
||||
Called when used as invokable controller. If missing, it falls back to `handle(...)`.
|
||||
|
||||
```php
|
||||
public function asController(User $user, Request $request): Response
|
||||
{
|
||||
$article = $this->handle(
|
||||
$user,
|
||||
$request->get('title'),
|
||||
$request->get('body')
|
||||
);
|
||||
|
||||
return redirect()->route('articles.show', [$article]);
|
||||
}
|
||||
```
|
||||
|
||||
### `jsonResponse`
|
||||
|
||||
Called after `asController` when request expects JSON.
|
||||
|
||||
```php
|
||||
public function jsonResponse(Article $article, Request $request): ArticleResource
|
||||
{
|
||||
return new ArticleResource($article);
|
||||
}
|
||||
```
|
||||
|
||||
### `htmlResponse`
|
||||
|
||||
Called after `asController` when request expects HTML.
|
||||
|
||||
```php
|
||||
public function htmlResponse(Article $article, Request $request): Response
|
||||
{
|
||||
return redirect()->route('articles.show', [$article]);
|
||||
}
|
||||
```
|
||||
|
||||
### `getControllerMiddleware`
|
||||
|
||||
Adds middleware directly on the action controller.
|
||||
|
||||
```php
|
||||
public function getControllerMiddleware(): array
|
||||
{
|
||||
return ['auth', MyCustomMiddleware::class];
|
||||
}
|
||||
```
|
||||
|
||||
### `routes`
|
||||
|
||||
Defines routes directly in the action.
|
||||
|
||||
```php
|
||||
public static function routes(Router $router)
|
||||
{
|
||||
$router->get('author/{author}/articles', static::class);
|
||||
}
|
||||
```
|
||||
|
||||
To enable this, register routes from actions in a service provider:
|
||||
|
||||
```php
|
||||
use Lorisleiva\Actions\Facades\Actions;
|
||||
|
||||
Actions::registerRoutes();
|
||||
Actions::registerRoutes('app/MyCustomActionsFolder');
|
||||
Actions::registerRoutes([
|
||||
'app/Authentication',
|
||||
'app/Billing',
|
||||
'app/TeamManagement',
|
||||
]);
|
||||
```
|
||||
|
||||
### `prepareForValidation`
|
||||
|
||||
Called before authorization and validation are resolved.
|
||||
|
||||
```php
|
||||
public function prepareForValidation(ActionRequest $request): void
|
||||
{
|
||||
$request->merge(['some' => 'additional data']);
|
||||
}
|
||||
```
|
||||
|
||||
### `authorize`
|
||||
|
||||
Defines authorization logic.
|
||||
|
||||
```php
|
||||
public function authorize(ActionRequest $request): bool
|
||||
{
|
||||
return $request->user()->role === 'author';
|
||||
}
|
||||
```
|
||||
|
||||
You can also return gate responses:
|
||||
|
||||
```php
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
public function authorize(ActionRequest $request): Response
|
||||
{
|
||||
if ($request->user()->role !== 'author') {
|
||||
return Response::deny('You must be an author to create a new article.');
|
||||
}
|
||||
|
||||
return Response::allow();
|
||||
}
|
||||
```
|
||||
|
||||
### `rules`
|
||||
|
||||
Defines validation rules.
|
||||
|
||||
```php
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'min:8'],
|
||||
'body' => ['required', IsValidMarkdown::class],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### `withValidator`
|
||||
|
||||
Adds custom validation logic with an after hook.
|
||||
|
||||
```php
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
public function withValidator(Validator $validator, ActionRequest $request): void
|
||||
{
|
||||
$validator->after(function (Validator $validator) use ($request) {
|
||||
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
|
||||
$validator->errors()->add('current_password', 'Wrong password.');
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### `afterValidator`
|
||||
|
||||
Alternative to add post-validation checks.
|
||||
|
||||
```php
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
public function afterValidator(Validator $validator, ActionRequest $request): void
|
||||
{
|
||||
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
|
||||
$validator->errors()->add('current_password', 'Wrong password.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidator`
|
||||
|
||||
Provides a custom validator instead of default rules pipeline.
|
||||
|
||||
```php
|
||||
use Illuminate\Validation\Factory;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
public function getValidator(Factory $factory, ActionRequest $request): Validator
|
||||
{
|
||||
return $factory->make($request->only('title', 'body'), [
|
||||
'title' => ['required', 'min:8'],
|
||||
'body' => ['required', IsValidMarkdown::class],
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationData`
|
||||
|
||||
Defines which data is validated (default: `$request->all()`).
|
||||
|
||||
```php
|
||||
public function getValidationData(ActionRequest $request): array
|
||||
{
|
||||
return $request->all();
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationMessages`
|
||||
|
||||
Custom validation error messages.
|
||||
|
||||
```php
|
||||
public function getValidationMessages(): array
|
||||
{
|
||||
return [
|
||||
'title.required' => 'Looks like you forgot the title.',
|
||||
'body.required' => 'Is that really all you have to say?',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationAttributes`
|
||||
|
||||
Human-friendly names for request attributes.
|
||||
|
||||
```php
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'headline',
|
||||
'body' => 'content',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationRedirect`
|
||||
|
||||
Custom redirect URL on validation failure.
|
||||
|
||||
```php
|
||||
public function getValidationRedirect(UrlGenerator $url): string
|
||||
{
|
||||
return $url->to('/my-custom-redirect-url');
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationErrorBag`
|
||||
|
||||
Custom error bag name on validation failure (default: `default`).
|
||||
|
||||
```php
|
||||
public function getValidationErrorBag(): string
|
||||
{
|
||||
return 'my_custom_error_bag';
|
||||
}
|
||||
```
|
||||
|
||||
### `getValidationFailure`
|
||||
|
||||
Override validation failure behavior.
|
||||
|
||||
```php
|
||||
public function getValidationFailure(): void
|
||||
{
|
||||
throw new MyCustomValidationException();
|
||||
}
|
||||
```
|
||||
|
||||
### `getAuthorizationFailure`
|
||||
|
||||
Override authorization failure behavior.
|
||||
|
||||
```php
|
||||
public function getAuthorizationFailure(): void
|
||||
{
|
||||
throw new MyCustomAuthorizationException();
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Route wiring points to the action class.
|
||||
- `asController(...)` delegates to `handle(...)`.
|
||||
- Validation/authorization methods are explicit where needed.
|
||||
- Response mapping is split by channel (`jsonResponse`, `htmlResponse`) when useful.
|
||||
- HTTP tests cover both success and validation/authorization failure branches.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Putting response/redirect logic in `handle(...)`.
|
||||
- Duplicating business rules in `asController(...)` instead of delegating.
|
||||
- Assuming action route discovery works without `Actions::registerRoutes(...)` when using in-action `routes()`.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-controller.html
|
||||
425
.claude/skills/laravel-actions/references/job.md
Normal file
425
.claude/skills/laravel-actions/references/job.md
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
# Job Entrypoint (`dispatch`, `asJob`)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when running an action through queues.
|
||||
|
||||
## Recap
|
||||
|
||||
- Lists async/sync dispatch helpers and conditional dispatch variants.
|
||||
- Covers job wrapping/chaining with `makeJob`, `makeUniqueJob`, and `withChain`.
|
||||
- Documents queue assertion helpers for tests (`assertPushed*`).
|
||||
- Summarizes `JobDecorator` hooks/properties for retries, uniqueness, timeout, and failure handling.
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Dispatch with `Action::dispatch(...)` for async execution.
|
||||
- Keep queue-specific orchestration in `asJob(...)`.
|
||||
- Keep reusable business logic in `handle(...)`.
|
||||
|
||||
## Methods provided (`AsJob` trait)
|
||||
|
||||
### `dispatch`
|
||||
|
||||
Dispatches the action asynchronously.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatch($team);
|
||||
```
|
||||
|
||||
### `dispatchIf`
|
||||
|
||||
Dispatches asynchronously only if condition is met.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatchIf($team->plan === 'premium', $team);
|
||||
```
|
||||
|
||||
### `dispatchUnless`
|
||||
|
||||
Dispatches asynchronously unless condition is met.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatchUnless($team->plan === 'free', $team);
|
||||
```
|
||||
|
||||
### `dispatchSync`
|
||||
|
||||
Dispatches synchronously.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatchSync($team);
|
||||
```
|
||||
|
||||
### `dispatchNow`
|
||||
|
||||
Alias of `dispatchSync`.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatchNow($team);
|
||||
```
|
||||
|
||||
### `dispatchAfterResponse`
|
||||
|
||||
Dispatches synchronously after the HTTP response is sent.
|
||||
|
||||
```php
|
||||
SendTeamReportEmail::dispatchAfterResponse($team);
|
||||
```
|
||||
|
||||
### `makeJob`
|
||||
|
||||
Creates a `JobDecorator` wrapper. Useful with `dispatch(...)` helper or chains.
|
||||
|
||||
```php
|
||||
dispatch(SendTeamReportEmail::makeJob($team));
|
||||
```
|
||||
|
||||
### `makeUniqueJob`
|
||||
|
||||
Creates a `UniqueJobDecorator` wrapper. Usually automatic with `ShouldBeUnique`, but can be forced.
|
||||
|
||||
```php
|
||||
dispatch(SendTeamReportEmail::makeUniqueJob($team));
|
||||
```
|
||||
|
||||
### `withChain`
|
||||
|
||||
Attaches jobs to run after successful processing.
|
||||
|
||||
```php
|
||||
$chain = [
|
||||
OptimizeTeamReport::makeJob($team),
|
||||
SendTeamReportEmail::makeJob($team),
|
||||
];
|
||||
|
||||
CreateNewTeamReport::withChain($chain)->dispatch($team);
|
||||
```
|
||||
|
||||
Equivalent using `Bus::chain(...)`:
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
Bus::chain([
|
||||
CreateNewTeamReport::makeJob($team),
|
||||
OptimizeTeamReport::makeJob($team),
|
||||
SendTeamReportEmail::makeJob($team),
|
||||
])->dispatch();
|
||||
```
|
||||
|
||||
Chain assertion example:
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
Bus::fake();
|
||||
|
||||
Bus::assertChained([
|
||||
CreateNewTeamReport::makeJob($team),
|
||||
OptimizeTeamReport::makeJob($team),
|
||||
SendTeamReportEmail::makeJob($team),
|
||||
]);
|
||||
```
|
||||
|
||||
### `assertPushed`
|
||||
|
||||
Asserts the action was queued.
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
Queue::fake();
|
||||
|
||||
SendTeamReportEmail::assertPushed();
|
||||
SendTeamReportEmail::assertPushed(3);
|
||||
SendTeamReportEmail::assertPushed($callback);
|
||||
SendTeamReportEmail::assertPushed(3, $callback);
|
||||
```
|
||||
|
||||
`$callback` receives:
|
||||
- Action instance.
|
||||
- Dispatched arguments.
|
||||
- `JobDecorator` instance.
|
||||
- Queue name.
|
||||
|
||||
### `assertNotPushed`
|
||||
|
||||
Asserts the action was not queued.
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
Queue::fake();
|
||||
|
||||
SendTeamReportEmail::assertNotPushed();
|
||||
SendTeamReportEmail::assertNotPushed($callback);
|
||||
```
|
||||
|
||||
### `assertPushedOn`
|
||||
|
||||
Asserts the action was queued on a specific queue.
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
Queue::fake();
|
||||
|
||||
SendTeamReportEmail::assertPushedOn('reports');
|
||||
SendTeamReportEmail::assertPushedOn('reports', 3);
|
||||
SendTeamReportEmail::assertPushedOn('reports', $callback);
|
||||
SendTeamReportEmail::assertPushedOn('reports', 3, $callback);
|
||||
```
|
||||
|
||||
## Methods used (`JobDecorator`)
|
||||
|
||||
### `asJob`
|
||||
|
||||
Called when dispatched as a job. Falls back to `handle(...)` if missing.
|
||||
|
||||
```php
|
||||
class SendTeamReportEmail
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Team $team, bool $fullReport = false): void
|
||||
{
|
||||
// Prepare report and send it to all $team->users.
|
||||
}
|
||||
|
||||
public function asJob(Team $team): void
|
||||
{
|
||||
$this->handle($team, true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `getJobMiddleware`
|
||||
|
||||
Adds middleware to the queued action.
|
||||
|
||||
```php
|
||||
public function getJobMiddleware(array $parameters): array
|
||||
{
|
||||
return [new RateLimited('reports')];
|
||||
}
|
||||
```
|
||||
|
||||
### `configureJob`
|
||||
|
||||
Configures `JobDecorator` options.
|
||||
|
||||
```php
|
||||
use Lorisleiva\Actions\Decorators\JobDecorator;
|
||||
|
||||
public function configureJob(JobDecorator $job): void
|
||||
{
|
||||
$job->onConnection('my_connection')
|
||||
->onQueue('my_queue')
|
||||
->through(['my_middleware'])
|
||||
->chain(['my_chain'])
|
||||
->delay(60);
|
||||
}
|
||||
```
|
||||
|
||||
### `$jobConnection`
|
||||
|
||||
Defines queue connection.
|
||||
|
||||
```php
|
||||
public string $jobConnection = 'my_connection';
|
||||
```
|
||||
|
||||
### `$jobQueue`
|
||||
|
||||
Defines queue name.
|
||||
|
||||
```php
|
||||
public string $jobQueue = 'my_queue';
|
||||
```
|
||||
|
||||
### `$jobTries`
|
||||
|
||||
Defines max attempts.
|
||||
|
||||
```php
|
||||
public int $jobTries = 10;
|
||||
```
|
||||
|
||||
### `$jobMaxExceptions`
|
||||
|
||||
Defines max unhandled exceptions before failure.
|
||||
|
||||
```php
|
||||
public int $jobMaxExceptions = 3;
|
||||
```
|
||||
|
||||
### `$jobBackoff`
|
||||
|
||||
Defines retry delay seconds.
|
||||
|
||||
```php
|
||||
public int $jobBackoff = 60;
|
||||
```
|
||||
|
||||
### `getJobBackoff`
|
||||
|
||||
Defines retry delay (int or per-attempt array).
|
||||
|
||||
```php
|
||||
public function getJobBackoff(): int
|
||||
{
|
||||
return 60;
|
||||
}
|
||||
|
||||
public function getJobBackoff(): array
|
||||
{
|
||||
return [30, 60, 120];
|
||||
}
|
||||
```
|
||||
|
||||
### `$jobTimeout`
|
||||
|
||||
Defines timeout in seconds.
|
||||
|
||||
```php
|
||||
public int $jobTimeout = 60 * 30;
|
||||
```
|
||||
|
||||
### `$jobRetryUntil`
|
||||
|
||||
Defines timestamp retry deadline.
|
||||
|
||||
```php
|
||||
public int $jobRetryUntil = 1610191764;
|
||||
```
|
||||
|
||||
### `getJobRetryUntil`
|
||||
|
||||
Defines retry deadline as `DateTime`.
|
||||
|
||||
```php
|
||||
public function getJobRetryUntil(): DateTime
|
||||
{
|
||||
return now()->addMinutes(30);
|
||||
}
|
||||
```
|
||||
|
||||
### `getJobDisplayName`
|
||||
|
||||
Customizes queued job display name.
|
||||
|
||||
```php
|
||||
public function getJobDisplayName(): string
|
||||
{
|
||||
return 'Send team report email';
|
||||
}
|
||||
```
|
||||
|
||||
### `getJobTags`
|
||||
|
||||
Adds queue tags.
|
||||
|
||||
```php
|
||||
public function getJobTags(Team $team): array
|
||||
{
|
||||
return ['report', 'team:'.$team->id];
|
||||
}
|
||||
```
|
||||
|
||||
### `getJobUniqueId`
|
||||
|
||||
Defines uniqueness key when using `ShouldBeUnique`.
|
||||
|
||||
```php
|
||||
public function getJobUniqueId(Team $team): int
|
||||
{
|
||||
return $team->id;
|
||||
}
|
||||
```
|
||||
|
||||
### `$jobUniqueId`
|
||||
|
||||
Static uniqueness key alternative.
|
||||
|
||||
```php
|
||||
public string $jobUniqueId = 'some_static_key';
|
||||
```
|
||||
|
||||
### `getJobUniqueFor`
|
||||
|
||||
Defines uniqueness lock duration in seconds.
|
||||
|
||||
```php
|
||||
public function getJobUniqueFor(Team $team): int
|
||||
{
|
||||
return $team->role === 'premium' ? 1800 : 3600;
|
||||
}
|
||||
```
|
||||
|
||||
### `$jobUniqueFor`
|
||||
|
||||
Property alternative for uniqueness lock duration.
|
||||
|
||||
```php
|
||||
public int $jobUniqueFor = 3600;
|
||||
```
|
||||
|
||||
### `getJobUniqueVia`
|
||||
|
||||
Defines cache driver used for uniqueness lock.
|
||||
|
||||
```php
|
||||
public function getJobUniqueVia()
|
||||
{
|
||||
return Cache::driver('redis');
|
||||
}
|
||||
```
|
||||
|
||||
### `$jobDeleteWhenMissingModels`
|
||||
|
||||
Property alternative for missing model handling.
|
||||
|
||||
```php
|
||||
public bool $jobDeleteWhenMissingModels = true;
|
||||
```
|
||||
|
||||
### `getJobDeleteWhenMissingModels`
|
||||
|
||||
Defines whether jobs with missing models are deleted.
|
||||
|
||||
```php
|
||||
public function getJobDeleteWhenMissingModels(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### `jobFailed`
|
||||
|
||||
Handles job failure. Receives exception and dispatched parameters.
|
||||
|
||||
```php
|
||||
public function jobFailed(?Throwable $e, ...$parameters): void
|
||||
{
|
||||
// Notify users, report errors, trigger compensations...
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Async/sync dispatch method matches use-case (`dispatch`, `dispatchSync`, `dispatchAfterResponse`).
|
||||
- Queue config is explicit when needed (`$jobConnection`, `$jobQueue`, `configureJob`).
|
||||
- Retry/backoff/timeout policies are intentional.
|
||||
- `asJob(...)` delegates to `handle(...)` unless queue-specific branching is required.
|
||||
- Queue tests use `Queue::fake()` and action assertions (`assertPushed*`).
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Embedding domain logic only in `asJob(...)`.
|
||||
- Forgetting uniqueness/timeout/retry controls on heavy jobs.
|
||||
- Missing queue-specific assertions in tests.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-job.html
|
||||
81
.claude/skills/laravel-actions/references/listener.md
Normal file
81
.claude/skills/laravel-actions/references/listener.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Listener Entrypoint (`asListener`)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when wiring actions to domain/application events.
|
||||
|
||||
## Recap
|
||||
|
||||
- Shows how listener execution maps event payloads into `handle(...)` arguments.
|
||||
- Describes `asListener(...)` fallback behavior and adaptation role.
|
||||
- Includes event registration example for provider wiring.
|
||||
- Emphasizes test focus on dispatch and action interaction.
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Register action listener in `EventServiceProvider` (or project equivalent).
|
||||
- Use `asListener(Event $event)` for event adaptation.
|
||||
- Delegate core logic to `handle(...)`.
|
||||
|
||||
## Methods used (`ListenerDecorator`)
|
||||
|
||||
### `asListener`
|
||||
|
||||
Called when executed as an event listener. If missing, it falls back to `handle(...)`.
|
||||
|
||||
```php
|
||||
class SendOfferToNearbyDrivers
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Address $source, Address $destination): void
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
public function asListener(TaxiRequested $event): void
|
||||
{
|
||||
$this->handle($event->source, $event->destination);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Event registration
|
||||
|
||||
```php
|
||||
// app/Providers/EventServiceProvider.php
|
||||
protected $listen = [
|
||||
TaxiRequested::class => [
|
||||
SendOfferToNearbyDrivers::class,
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### Focused listener test
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
Event::fake();
|
||||
|
||||
TaxiRequested::dispatch($source, $destination);
|
||||
|
||||
Event::assertDispatched(TaxiRequested::class);
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Event-to-listener mapping is registered.
|
||||
- Listener method signature matches event contract.
|
||||
- Listener tests verify dispatch and action interaction.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Assuming automatic listener registration when explicit mapping is required.
|
||||
- Re-implementing business logic in `asListener(...)`.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-listener.html
|
||||
118
.claude/skills/laravel-actions/references/object.md
Normal file
118
.claude/skills/laravel-actions/references/object.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Object Entrypoint (`run`, `make`, DI)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when the action is invoked as a plain object.
|
||||
|
||||
## Recap
|
||||
|
||||
- Explains object-style invocation with `make`, `run`, `runIf`, `runUnless`.
|
||||
- Clarifies when to use static helpers versus DI/manual invocation.
|
||||
- Includes minimal examples for direct run and service-level injection.
|
||||
- Highlights boundaries: business logic stays in `handle(...)`.
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Keep core business logic in `handle(...)`.
|
||||
- Prefer `Action::run(...)` for readability.
|
||||
- Use `Action::make()->handle(...)` or DI only when needed.
|
||||
|
||||
## Methods provided
|
||||
|
||||
### `make`
|
||||
|
||||
Resolves the action from the container.
|
||||
|
||||
```php
|
||||
PublishArticle::make();
|
||||
|
||||
// Equivalent to:
|
||||
app(PublishArticle::class);
|
||||
```
|
||||
|
||||
### `run`
|
||||
|
||||
Resolves and executes the action.
|
||||
|
||||
```php
|
||||
PublishArticle::run($articleId);
|
||||
|
||||
// Equivalent to:
|
||||
PublishArticle::make()->handle($articleId);
|
||||
```
|
||||
|
||||
### `runIf`
|
||||
|
||||
Resolves and executes the action only if the condition is met.
|
||||
|
||||
```php
|
||||
PublishArticle::runIf($shouldPublish, $articleId);
|
||||
|
||||
// Equivalent mental model:
|
||||
if ($shouldPublish) {
|
||||
PublishArticle::run($articleId);
|
||||
}
|
||||
```
|
||||
|
||||
### `runUnless`
|
||||
|
||||
Resolves and executes the action only if the condition is not met.
|
||||
|
||||
```php
|
||||
PublishArticle::runUnless($alreadyPublished, $articleId);
|
||||
|
||||
// Equivalent mental model:
|
||||
if (! $alreadyPublished) {
|
||||
PublishArticle::run($articleId);
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Input/output types are explicit.
|
||||
- `handle(...)` has no transport concerns.
|
||||
- Business behavior is covered by direct `handle(...)` tests.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Putting HTTP/CLI/queue concerns in `handle(...)`.
|
||||
- Calling adapters from `handle(...)` instead of the reverse.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-object.html
|
||||
|
||||
## Examples
|
||||
|
||||
### Minimal object-style invocation
|
||||
|
||||
```php
|
||||
final class PublishArticle
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(int $articleId): bool
|
||||
{
|
||||
// Domain logic...
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$published = PublishArticle::run(42);
|
||||
```
|
||||
|
||||
### Dependency injection invocation
|
||||
|
||||
```php
|
||||
final class ArticleService
|
||||
{
|
||||
public function __construct(
|
||||
private PublishArticle $publishArticle
|
||||
) {}
|
||||
|
||||
public function publish(int $articleId): bool
|
||||
{
|
||||
return $this->publishArticle->handle($articleId);
|
||||
}
|
||||
}
|
||||
```
|
||||
160
.claude/skills/laravel-actions/references/testing-fakes.md
Normal file
160
.claude/skills/laravel-actions/references/testing-fakes.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# Testing and Action Fakes
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when isolating action orchestration in tests.
|
||||
|
||||
## Recap
|
||||
|
||||
- Summarizes all `AsFake` helpers (`mock`, `partialMock`, `spy`, `shouldRun`, `shouldNotRun`, `allowToRun`).
|
||||
- Clarifies when to assert execution versus non-execution.
|
||||
- Covers fake lifecycle checks/reset (`isFake`, `clearFake`).
|
||||
- Provides branch-oriented test examples for orchestration confidence.
|
||||
|
||||
## Core methods
|
||||
|
||||
- `mock()`
|
||||
- `partialMock()`
|
||||
- `spy()`
|
||||
- `shouldRun()`
|
||||
- `shouldNotRun()`
|
||||
- `allowToRun()`
|
||||
- `isFake()`
|
||||
- `clearFake()`
|
||||
|
||||
## Recommended pattern
|
||||
|
||||
- Test `handle(...)` directly for business rules.
|
||||
- Test entrypoints for wiring/orchestration.
|
||||
- Fake only at the boundary under test.
|
||||
|
||||
## Methods provided (`AsFake` trait)
|
||||
|
||||
### `mock`
|
||||
|
||||
Swaps the action with a full mock.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::mock()
|
||||
->shouldReceive('handle')
|
||||
->with(42)
|
||||
->andReturn(['Loris', 'Will', 'Barney']);
|
||||
```
|
||||
|
||||
### `partialMock`
|
||||
|
||||
Swaps the action with a partial mock.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::partialMock()
|
||||
->shouldReceive('fetch')
|
||||
->with('some_google_identifier')
|
||||
->andReturn(['Loris', 'Will', 'Barney']);
|
||||
```
|
||||
|
||||
### `spy`
|
||||
|
||||
Swaps the action with a spy.
|
||||
|
||||
```php
|
||||
$spy = FetchContactsFromGoogle::spy()
|
||||
->allows('handle')
|
||||
->andReturn(['Loris', 'Will', 'Barney']);
|
||||
|
||||
// ...
|
||||
|
||||
$spy->shouldHaveReceived('handle')->with(42);
|
||||
```
|
||||
|
||||
### `shouldRun`
|
||||
|
||||
Helper adding expectation on `handle`.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::shouldRun();
|
||||
|
||||
// Equivalent to:
|
||||
FetchContactsFromGoogle::mock()->shouldReceive('handle');
|
||||
```
|
||||
|
||||
### `shouldNotRun`
|
||||
|
||||
Helper adding negative expectation on `handle`.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::shouldNotRun();
|
||||
|
||||
// Equivalent to:
|
||||
FetchContactsFromGoogle::mock()->shouldNotReceive('handle');
|
||||
```
|
||||
|
||||
### `allowToRun`
|
||||
|
||||
Helper allowing `handle` on a spy.
|
||||
|
||||
```php
|
||||
$spy = FetchContactsFromGoogle::allowToRun()
|
||||
->andReturn(['Loris', 'Will', 'Barney']);
|
||||
|
||||
// ...
|
||||
|
||||
$spy->shouldHaveReceived('handle')->with(42);
|
||||
```
|
||||
|
||||
### `isFake`
|
||||
|
||||
Returns whether the action has been swapped with a fake.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::isFake(); // false
|
||||
FetchContactsFromGoogle::mock();
|
||||
FetchContactsFromGoogle::isFake(); // true
|
||||
```
|
||||
|
||||
### `clearFake`
|
||||
|
||||
Clears the fake instance, if any.
|
||||
|
||||
```php
|
||||
FetchContactsFromGoogle::mock();
|
||||
FetchContactsFromGoogle::isFake(); // true
|
||||
FetchContactsFromGoogle::clearFake();
|
||||
FetchContactsFromGoogle::isFake(); // false
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Orchestration test
|
||||
|
||||
```php
|
||||
it('runs sync contacts for premium teams', function () {
|
||||
SyncGoogleContacts::shouldRun()->once()->with(42)->andReturnTrue();
|
||||
|
||||
ImportTeamContacts::run(42, isPremium: true);
|
||||
});
|
||||
```
|
||||
|
||||
### Guard-clause test
|
||||
|
||||
```php
|
||||
it('does not run sync when integration is disabled', function () {
|
||||
SyncGoogleContacts::shouldNotRun();
|
||||
|
||||
ImportTeamContacts::run(42, integrationEnabled: false);
|
||||
});
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Assertions verify call intent and argument contracts.
|
||||
- Fakes are cleared when leakage risk exists.
|
||||
- Branch tests use `shouldRun()` / `shouldNotRun()` where clearer.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Over-mocking and losing behavior confidence.
|
||||
- Asserting only dispatch, not business correctness.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/as-fake.html
|
||||
33
.claude/skills/laravel-actions/references/troubleshooting.md
Normal file
33
.claude/skills/laravel-actions/references/troubleshooting.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Troubleshooting
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when action wiring behaves unexpectedly.
|
||||
|
||||
## Recap
|
||||
|
||||
- Provides a fast triage flow for routing, queueing, events, and command wiring.
|
||||
- Lists recurring failure patterns and where to check first.
|
||||
- Encourages reproducing issues with focused tests before broad debugging.
|
||||
- Separates wiring diagnostics from domain logic verification.
|
||||
|
||||
## Fast checks
|
||||
|
||||
- Action class uses `AsAction`.
|
||||
- Namespace and autoloading are correct.
|
||||
- Entrypoint wiring (route, queue, event, command) is registered.
|
||||
- Method signatures and argument types match caller expectations.
|
||||
|
||||
## Failure patterns
|
||||
|
||||
- Controller route points to wrong class.
|
||||
- Queue worker/config mismatch.
|
||||
- Listener mapping not loaded.
|
||||
- Command signature mismatch.
|
||||
- Command not registered in the console kernel.
|
||||
|
||||
## Debug checklist
|
||||
|
||||
- Reproduce with a focused failing test.
|
||||
- Validate wiring layer first, then domain behavior.
|
||||
- Isolate dependencies with fakes/spies where appropriate.
|
||||
189
.claude/skills/laravel-actions/references/with-attributes.md
Normal file
189
.claude/skills/laravel-actions/references/with-attributes.md
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
# With Attributes (`WithAttributes` trait)
|
||||
|
||||
## Scope
|
||||
|
||||
Use this reference when an action stores and validates input via internal attributes instead of method arguments.
|
||||
|
||||
## Recap
|
||||
|
||||
- Documents attribute lifecycle APIs (`setRawAttributes`, `fill`, `fillFromRequest`, readers/writers).
|
||||
- Clarifies behavior of key collisions (`fillFromRequest`: request data wins over route params).
|
||||
- Lists validation/authorization hooks reused from controller validation pipeline.
|
||||
- Includes end-to-end example from fill to `validateAttributes()` and `handle(...)`.
|
||||
|
||||
## Methods provided (`WithAttributes` trait)
|
||||
|
||||
### `setRawAttributes`
|
||||
|
||||
Replaces all attributes with the provided payload.
|
||||
|
||||
```php
|
||||
$action->setRawAttributes([
|
||||
'key' => 'value',
|
||||
]);
|
||||
```
|
||||
|
||||
### `fill`
|
||||
|
||||
Merges provided attributes into existing attributes.
|
||||
|
||||
```php
|
||||
$action->fill([
|
||||
'key' => 'value',
|
||||
]);
|
||||
```
|
||||
|
||||
### `fillFromRequest`
|
||||
|
||||
Merges request input and route parameters into attributes. Request input has priority over route parameters when keys collide.
|
||||
|
||||
```php
|
||||
$action->fillFromRequest($request);
|
||||
```
|
||||
|
||||
### `all`
|
||||
|
||||
Returns all attributes.
|
||||
|
||||
```php
|
||||
$action->all();
|
||||
```
|
||||
|
||||
### `only`
|
||||
|
||||
Returns attributes matching the provided keys.
|
||||
|
||||
```php
|
||||
$action->only('title', 'body');
|
||||
```
|
||||
|
||||
### `except`
|
||||
|
||||
Returns attributes excluding the provided keys.
|
||||
|
||||
```php
|
||||
$action->except('body');
|
||||
```
|
||||
|
||||
### `has`
|
||||
|
||||
Returns whether an attribute exists for the given key.
|
||||
|
||||
```php
|
||||
$action->has('title');
|
||||
```
|
||||
|
||||
### `get`
|
||||
|
||||
Returns the attribute value by key, with optional default.
|
||||
|
||||
```php
|
||||
$action->get('title');
|
||||
$action->get('title', 'Untitled');
|
||||
```
|
||||
|
||||
### `set`
|
||||
|
||||
Sets an attribute value by key.
|
||||
|
||||
```php
|
||||
$action->set('title', 'My blog post');
|
||||
```
|
||||
|
||||
### `__get`
|
||||
|
||||
Accesses attributes as object properties.
|
||||
|
||||
```php
|
||||
$action->title;
|
||||
```
|
||||
|
||||
### `__set`
|
||||
|
||||
Updates attributes as object properties.
|
||||
|
||||
```php
|
||||
$action->title = 'My blog post';
|
||||
```
|
||||
|
||||
### `__isset`
|
||||
|
||||
Checks attribute existence as object properties.
|
||||
|
||||
```php
|
||||
isset($action->title);
|
||||
```
|
||||
|
||||
### `validateAttributes`
|
||||
|
||||
Runs authorization and validation using action attributes and returns validated data.
|
||||
|
||||
```php
|
||||
$validatedData = $action->validateAttributes();
|
||||
```
|
||||
|
||||
## Methods used (`AttributeValidator`)
|
||||
|
||||
`WithAttributes` uses the same authorization/validation hooks as `AsController`:
|
||||
|
||||
- `prepareForValidation`
|
||||
- `authorize`
|
||||
- `rules`
|
||||
- `withValidator`
|
||||
- `afterValidator`
|
||||
- `getValidator`
|
||||
- `getValidationData`
|
||||
- `getValidationMessages`
|
||||
- `getValidationAttributes`
|
||||
- `getValidationRedirect`
|
||||
- `getValidationErrorBag`
|
||||
- `getValidationFailure`
|
||||
- `getAuthorizationFailure`
|
||||
|
||||
## Example
|
||||
|
||||
```php
|
||||
class CreateArticle
|
||||
{
|
||||
use AsAction;
|
||||
use WithAttributes;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'min:8'],
|
||||
'body' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
public function handle(array $attributes): Article
|
||||
{
|
||||
return Article::create($attributes);
|
||||
}
|
||||
}
|
||||
|
||||
$action = CreateArticle::make()->fill([
|
||||
'title' => 'My first post',
|
||||
'body' => 'Hello world',
|
||||
]);
|
||||
|
||||
$validated = $action->validateAttributes();
|
||||
$article = $action->handle($validated);
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- Attribute keys are explicit and stable.
|
||||
- Validation rules match expected attribute shape.
|
||||
- `validateAttributes()` is called before side effects when needed.
|
||||
- Validation/authorization hooks are tested in focused unit tests.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Mixing attribute-based and argument-based flows inconsistently in the same action.
|
||||
- Assuming route params override request input in `fillFromRequest` (they do not).
|
||||
- Skipping `validateAttributes()` when using external input.
|
||||
|
||||
## References
|
||||
|
||||
- https://www.laravelactions.com/2.x/with-attributes.html
|
||||
190
.claude/skills/laravel-best-practices/SKILL.md
Normal file
190
.claude/skills/laravel-best-practices/SKILL.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
---
|
||||
name: laravel-best-practices
|
||||
description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Laravel Best Practices
|
||||
|
||||
Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.
|
||||
|
||||
## Consistency First
|
||||
|
||||
Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.
|
||||
|
||||
Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. Database Performance → `rules/db-performance.md`
|
||||
|
||||
- Eager load with `with()` to prevent N+1 queries
|
||||
- Enable `Model::preventLazyLoading()` in development
|
||||
- Select only needed columns, avoid `SELECT *`
|
||||
- `chunk()` / `chunkById()` for large datasets
|
||||
- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
|
||||
- `withCount()` instead of loading relations to count
|
||||
- `cursor()` for memory-efficient read-only iteration
|
||||
- Never query in Blade templates
|
||||
|
||||
### 2. Advanced Query Patterns → `rules/advanced-queries.md`
|
||||
|
||||
- `addSelect()` subqueries over eager-loading entire has-many for a single value
|
||||
- Dynamic relationships via subquery FK + `belongsTo`
|
||||
- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
|
||||
- `setRelation()` to prevent circular N+1 queries
|
||||
- `whereIn` + `pluck()` over `whereHas` for better index usage
|
||||
- Two simple queries can beat one complex query
|
||||
- Compound indexes matching `orderBy` column order
|
||||
- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)
|
||||
|
||||
### 3. Security → `rules/security.md`
|
||||
|
||||
- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
|
||||
- No raw SQL with user input — use Eloquent or query builder
|
||||
- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
|
||||
- Validate MIME type, extension, and size for file uploads
|
||||
- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields
|
||||
|
||||
### 4. Caching → `rules/caching.md`
|
||||
|
||||
- `Cache::remember()` over manual get/put
|
||||
- `Cache::flexible()` for stale-while-revalidate on high-traffic data
|
||||
- `Cache::memo()` to avoid redundant cache hits within a request
|
||||
- Cache tags to invalidate related groups
|
||||
- `Cache::add()` for atomic conditional writes
|
||||
- `once()` to memoize per-request or per-object lifetime
|
||||
- `Cache::lock()` / `lockForUpdate()` for race conditions
|
||||
- Failover cache stores in production
|
||||
|
||||
### 5. Eloquent Patterns → `rules/eloquent.md`
|
||||
|
||||
- Correct relationship types with return type hints
|
||||
- Local scopes for reusable query constraints
|
||||
- Global scopes sparingly — document their existence
|
||||
- Attribute casts in the `casts()` method
|
||||
- Cast date columns, use Carbon instances in templates
|
||||
- `whereBelongsTo($model)` for cleaner queries
|
||||
- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries
|
||||
|
||||
### 6. Validation & Forms → `rules/validation.md`
|
||||
|
||||
- Form Request classes, not inline validation
|
||||
- Array notation `['required', 'email']` for new code; follow existing convention
|
||||
- `$request->validated()` only — never `$request->all()`
|
||||
- `Rule::when()` for conditional validation
|
||||
- `after()` instead of `withValidator()`
|
||||
|
||||
### 7. Configuration → `rules/config.md`
|
||||
|
||||
- `env()` only inside config files
|
||||
- `App::environment()` or `app()->isProduction()`
|
||||
- Config, lang files, and constants over hardcoded text
|
||||
|
||||
### 8. Testing Patterns → `rules/testing.md`
|
||||
|
||||
- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
|
||||
- `assertModelExists()` over raw `assertDatabaseHas()`
|
||||
- Factory states and sequences over manual overrides
|
||||
- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
|
||||
- `recycle()` to share relationship instances across factories
|
||||
|
||||
### 9. Queue & Job Patterns → `rules/queue-jobs.md`
|
||||
|
||||
- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
|
||||
- `ShouldBeUnique` to prevent duplicates; `ShouldBeUniqueUntilProcessing` for early lock release
|
||||
- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
|
||||
- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
|
||||
- Horizon for complex multi-queue scenarios
|
||||
|
||||
### 10. Routing & Controllers → `rules/routing.md`
|
||||
|
||||
- Implicit route model binding
|
||||
- Scoped bindings for nested resources
|
||||
- `Route::resource()` or `apiResource()`
|
||||
- Methods under 10 lines — extract to actions/services
|
||||
- Type-hint Form Requests for auto-validation
|
||||
|
||||
### 11. HTTP Client → `rules/http-client.md`
|
||||
|
||||
- Explicit `timeout` and `connectTimeout` on every request
|
||||
- `retry()` with exponential backoff for external APIs
|
||||
- Check response status or use `throw()`
|
||||
- `Http::pool()` for concurrent independent requests
|
||||
- `Http::fake()` and `preventStrayRequests()` in tests
|
||||
|
||||
### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`
|
||||
|
||||
- Event discovery over manual registration; `event:cache` in production
|
||||
- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
|
||||
- Queue notifications and mailables with `ShouldQueue`
|
||||
- On-demand notifications for non-user recipients
|
||||
- `HasLocalePreference` on notifiable models
|
||||
- `assertQueued()` not `assertSent()` for queued mailables
|
||||
- Markdown mailables for transactional emails
|
||||
|
||||
### 13. Error Handling → `rules/error-handling.md`
|
||||
|
||||
- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
|
||||
- `ShouldntReport` for exceptions that should never log
|
||||
- Throttle high-volume exceptions to protect log sinks
|
||||
- `dontReportDuplicates()` for multi-catch scenarios
|
||||
- Force JSON rendering for API routes
|
||||
- Structured context via `context()` on exception classes
|
||||
|
||||
### 14. Task Scheduling → `rules/scheduling.md`
|
||||
|
||||
- `withoutOverlapping()` on variable-duration tasks
|
||||
- `onOneServer()` on multi-server deployments
|
||||
- `runInBackground()` for concurrent long tasks
|
||||
- `environments()` to restrict to appropriate environments
|
||||
- `takeUntilTimeout()` for time-bounded processing
|
||||
- Schedule groups for shared configuration
|
||||
|
||||
### 15. Architecture → `rules/architecture.md`
|
||||
|
||||
- Single-purpose Action classes; dependency injection over `app()` helper
|
||||
- Prefer official Laravel packages and follow conventions, don't override defaults
|
||||
- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
|
||||
- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution
|
||||
|
||||
### 16. Migrations → `rules/migrations.md`
|
||||
|
||||
- Generate migrations with `php artisan make:migration`
|
||||
- `constrained()` for foreign keys
|
||||
- Never modify migrations that have run in production
|
||||
- Add indexes in the migration, not as an afterthought
|
||||
- Mirror column defaults in model `$attributes`
|
||||
- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
|
||||
- One concern per migration — never mix DDL and DML
|
||||
|
||||
### 17. Collections → `rules/collections.md`
|
||||
|
||||
- Higher-order messages for simple collection operations
|
||||
- `cursor()` vs. `lazy()` — choose based on relationship needs
|
||||
- `lazyById()` when updating records while iterating
|
||||
- `toQuery()` for bulk operations on collections
|
||||
|
||||
### 18. Blade & Views → `rules/blade-views.md`
|
||||
|
||||
- `$attributes->merge()` in component templates
|
||||
- Blade components over `@include`; `@pushOnce` for per-component scripts
|
||||
- View Composers for shared view data
|
||||
- `@aware` for deeply nested component props
|
||||
|
||||
### 19. Conventions & Style → `rules/style.md`
|
||||
|
||||
- Follow Laravel naming conventions for all entities
|
||||
- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
|
||||
- No JS/CSS in Blade, no HTML in PHP classes
|
||||
- Code should be readable; comments only for config files
|
||||
|
||||
## How to Apply
|
||||
|
||||
Always use a sub-agent to read rule files and explore this skill's content.
|
||||
|
||||
1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
|
||||
2. Check sibling files for existing patterns — follow those first per Consistency First
|
||||
3. Verify API syntax with `search-docs` for the installed Laravel version
|
||||
106
.claude/skills/laravel-best-practices/rules/advanced-queries.md
Normal file
106
.claude/skills/laravel-best-practices/rules/advanced-queries.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Advanced Query Patterns
|
||||
|
||||
## Use `addSelect()` Subqueries for Single Values from Has-Many
|
||||
|
||||
Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries.
|
||||
|
||||
```php
|
||||
public function scopeWithLastLoginAt($query): void
|
||||
{
|
||||
$query->addSelect([
|
||||
'last_login_at' => Login::select('created_at')
|
||||
->whereColumn('user_id', 'users.id')
|
||||
->latest()
|
||||
->take(1),
|
||||
])->withCasts(['last_login_at' => 'datetime']);
|
||||
}
|
||||
```
|
||||
|
||||
## Create Dynamic Relationships via Subquery FK
|
||||
|
||||
Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection.
|
||||
|
||||
```php
|
||||
public function lastLogin(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Login::class);
|
||||
}
|
||||
|
||||
public function scopeWithLastLogin($query): void
|
||||
{
|
||||
$query->addSelect([
|
||||
'last_login_id' => Login::select('id')
|
||||
->whereColumn('user_id', 'users.id')
|
||||
->latest()
|
||||
->take(1),
|
||||
])->with('lastLogin');
|
||||
}
|
||||
```
|
||||
|
||||
## Use Conditional Aggregates Instead of Multiple Count Queries
|
||||
|
||||
Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values.
|
||||
|
||||
```php
|
||||
$statuses = Feature::toBase()
|
||||
->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
|
||||
->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
|
||||
->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
|
||||
->first();
|
||||
```
|
||||
|
||||
## Use `setRelation()` to Prevent Circular N+1
|
||||
|
||||
When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries.
|
||||
|
||||
```php
|
||||
$feature->load('comments.user');
|
||||
$feature->comments->each->setRelation('feature', $feature);
|
||||
```
|
||||
|
||||
## Prefer `whereIn` + Subquery Over `whereHas`
|
||||
|
||||
`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory.
|
||||
|
||||
Incorrect (correlated EXISTS re-executes per row):
|
||||
|
||||
```php
|
||||
$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));
|
||||
```
|
||||
|
||||
Correct (index-friendly subquery, no PHP memory overhead):
|
||||
|
||||
```php
|
||||
$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));
|
||||
```
|
||||
|
||||
## Sometimes Two Simple Queries Beat One Complex Query
|
||||
|
||||
Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index.
|
||||
|
||||
## Use Compound Indexes Matching `orderBy` Column Order
|
||||
|
||||
When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index.
|
||||
|
||||
```php
|
||||
// Migration
|
||||
$table->index(['last_name', 'first_name']);
|
||||
|
||||
// Query — column order must match the index
|
||||
User::query()->orderBy('last_name')->orderBy('first_name')->paginate();
|
||||
```
|
||||
|
||||
## Use Correlated Subqueries for Has-Many Ordering
|
||||
|
||||
When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading.
|
||||
|
||||
```php
|
||||
public function scopeOrderByLastLogin($query): void
|
||||
{
|
||||
$query->orderByDesc(Login::select('created_at')
|
||||
->whereColumn('user_id', 'users.id')
|
||||
->latest()
|
||||
->take(1)
|
||||
);
|
||||
}
|
||||
```
|
||||
202
.claude/skills/laravel-best-practices/rules/architecture.md
Normal file
202
.claude/skills/laravel-best-practices/rules/architecture.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# Architecture Best Practices
|
||||
|
||||
## Single-Purpose Action Classes
|
||||
|
||||
Extract discrete business operations into invokable Action classes.
|
||||
|
||||
```php
|
||||
class CreateOrderAction
|
||||
{
|
||||
public function __construct(private InventoryService $inventory) {}
|
||||
|
||||
public function execute(array $data): Order
|
||||
{
|
||||
$order = Order::create($data);
|
||||
$this->inventory->reserve($order);
|
||||
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Use Dependency Injection
|
||||
|
||||
Always use constructor injection. Avoid `app()` or `resolve()` inside classes.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
class OrderController extends Controller
|
||||
{
|
||||
public function store(StoreOrderRequest $request)
|
||||
{
|
||||
$service = app(OrderService::class);
|
||||
|
||||
return $service->create($request->validated());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
class OrderController extends Controller
|
||||
{
|
||||
public function __construct(private OrderService $service) {}
|
||||
|
||||
public function store(StoreOrderRequest $request)
|
||||
{
|
||||
return $this->service->create($request->validated());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Code to Interfaces
|
||||
|
||||
Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability.
|
||||
|
||||
Incorrect (concrete dependency):
|
||||
```php
|
||||
class OrderService
|
||||
{
|
||||
public function __construct(private StripeGateway $gateway) {}
|
||||
}
|
||||
```
|
||||
|
||||
Correct (interface dependency):
|
||||
```php
|
||||
interface PaymentGateway
|
||||
{
|
||||
public function charge(int $amount, string $customerId): PaymentResult;
|
||||
}
|
||||
|
||||
class OrderService
|
||||
{
|
||||
public function __construct(private PaymentGateway $gateway) {}
|
||||
}
|
||||
```
|
||||
|
||||
Bind in a service provider:
|
||||
|
||||
```php
|
||||
$this->app->bind(PaymentGateway::class, StripeGateway::class);
|
||||
```
|
||||
|
||||
## Default Sort by Descending
|
||||
|
||||
When no explicit order is specified, sort by `id` or `created_at` descending. Without an explicit `ORDER BY`, row order is undefined.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$posts = Post::paginate();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$posts = Post::latest()->paginate();
|
||||
```
|
||||
|
||||
## Use Atomic Locks for Race Conditions
|
||||
|
||||
Prevent race conditions with `Cache::lock()` or `lockForUpdate()`.
|
||||
|
||||
```php
|
||||
Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) {
|
||||
$order->process();
|
||||
});
|
||||
|
||||
// Or at query level
|
||||
$product = Product::where('id', $id)->lockForUpdate()->first();
|
||||
```
|
||||
|
||||
## Use `mb_*` String Functions
|
||||
|
||||
When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
strlen('José'); // 5 (bytes, not characters)
|
||||
strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
mb_strlen('José'); // 4 (characters)
|
||||
mb_strtolower('MÜNCHEN'); // 'münchen'
|
||||
|
||||
// Prefer Laravel's Str helpers when available
|
||||
Str::length('José'); // 4
|
||||
Str::lower('MÜNCHEN'); // 'münchen'
|
||||
```
|
||||
|
||||
## Use `defer()` for Post-Response Work
|
||||
|
||||
For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead.
|
||||
|
||||
Incorrect (job overhead for trivial work):
|
||||
```php
|
||||
dispatch(new LogPageView($page));
|
||||
```
|
||||
|
||||
Correct (runs after response, same process):
|
||||
```php
|
||||
defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()]));
|
||||
```
|
||||
|
||||
Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work.
|
||||
|
||||
## Use `Context` for Request-Scoped Data
|
||||
|
||||
The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually.
|
||||
|
||||
```php
|
||||
// In middleware
|
||||
Context::add('tenant_id', $request->header('X-Tenant-ID'));
|
||||
|
||||
// Anywhere later — controllers, jobs, log context
|
||||
$tenantId = Context::get('tenant_id');
|
||||
```
|
||||
|
||||
Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`.
|
||||
|
||||
## Use `Concurrency::run()` for Parallel Execution
|
||||
|
||||
Run independent operations in parallel using child processes — no async libraries needed.
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Concurrency;
|
||||
|
||||
[$users, $orders] = Concurrency::run([
|
||||
fn () => User::count(),
|
||||
fn () => Order::where('status', 'pending')->count(),
|
||||
]);
|
||||
```
|
||||
|
||||
Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially.
|
||||
|
||||
## Convention Over Configuration
|
||||
|
||||
Follow Laravel conventions. Don't override defaults unnecessarily.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
class Customer extends Model
|
||||
{
|
||||
protected $table = 'Customer';
|
||||
protected $primaryKey = 'customer_id';
|
||||
|
||||
public function roles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
class Customer extends Model
|
||||
{
|
||||
public function roles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
36
.claude/skills/laravel-best-practices/rules/blade-views.md
Normal file
36
.claude/skills/laravel-best-practices/rules/blade-views.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Blade & Views Best Practices
|
||||
|
||||
## Use `$attributes->merge()` in Component Templates
|
||||
|
||||
Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly.
|
||||
|
||||
```blade
|
||||
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
|
||||
{{ $message }}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Use `@pushOnce` for Per-Component Scripts
|
||||
|
||||
If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once.
|
||||
|
||||
## Prefer Blade Components Over `@include`
|
||||
|
||||
`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots.
|
||||
|
||||
## Use View Composers for Shared View Data
|
||||
|
||||
If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it.
|
||||
|
||||
## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
|
||||
|
||||
A single view can return either the full page or just a fragment, keeping routing clean.
|
||||
|
||||
```php
|
||||
return view('dashboard', compact('users'))
|
||||
->fragmentIf($request->hasHeader('HX-Request'), 'user-list');
|
||||
```
|
||||
|
||||
## Use `@aware` for Deeply Nested Component Props
|
||||
|
||||
Avoids re-passing parent props through every level of nested components.
|
||||
70
.claude/skills/laravel-best-practices/rules/caching.md
Normal file
70
.claude/skills/laravel-best-practices/rules/caching.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Caching Best Practices
|
||||
|
||||
## Use `Cache::remember()` Instead of Manual Get/Put
|
||||
|
||||
Cleaner cache-aside pattern that removes boilerplate. use `Cache::lock()` for race conditions.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$val = Cache::get('stats');
|
||||
if (! $val) {
|
||||
$val = $this->computeStats();
|
||||
Cache::put('stats', $val, 60);
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$val = Cache::remember('stats', 60, fn () => $this->computeStats());
|
||||
```
|
||||
|
||||
## Use `Cache::flexible()` for Stale-While-Revalidate
|
||||
|
||||
On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background.
|
||||
|
||||
Incorrect: `Cache::remember('users', 300, fn () => User::all());`
|
||||
|
||||
Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function.
|
||||
|
||||
## Use `Cache::memo()` to Avoid Redundant Hits Within a Request
|
||||
|
||||
If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory.
|
||||
|
||||
`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5.
|
||||
|
||||
## Use Cache Tags to Invalidate Related Groups
|
||||
|
||||
Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`.
|
||||
|
||||
```php
|
||||
Cache::tags(['user-1'])->flush();
|
||||
```
|
||||
|
||||
## Use `Cache::add()` for Atomic Conditional Writes
|
||||
|
||||
`add()` only writes if the key does not exist — atomic, no race condition between checking and writing.
|
||||
|
||||
Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }`
|
||||
|
||||
Correct: `Cache::add('lock', true, 10);`
|
||||
|
||||
## Use `once()` for Per-Request Memoization
|
||||
|
||||
`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory.
|
||||
|
||||
```php
|
||||
public function roles(): Collection
|
||||
{
|
||||
return once(fn () => $this->loadRoles());
|
||||
}
|
||||
```
|
||||
|
||||
Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching.
|
||||
|
||||
## Configure Failover Cache Stores in Production
|
||||
|
||||
If Redis goes down, the app falls back to a secondary store automatically.
|
||||
|
||||
```php
|
||||
'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
|
||||
```
|
||||
44
.claude/skills/laravel-best-practices/rules/collections.md
Normal file
44
.claude/skills/laravel-best-practices/rules/collections.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Collection Best Practices
|
||||
|
||||
## Use Higher-Order Messages for Simple Operations
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$users->each(function (User $user) {
|
||||
$user->markAsVip();
|
||||
});
|
||||
```
|
||||
|
||||
Correct: `$users->each->markAsVip();`
|
||||
|
||||
Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc.
|
||||
|
||||
## Choose `cursor()` vs. `lazy()` Correctly
|
||||
|
||||
- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk).
|
||||
- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading.
|
||||
|
||||
Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored.
|
||||
|
||||
Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work.
|
||||
|
||||
## Use `lazyById()` When Updating Records While Iterating
|
||||
|
||||
`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation.
|
||||
|
||||
## Use `toQuery()` for Bulk Operations on Collections
|
||||
|
||||
Avoids manual `whereIn` construction.
|
||||
|
||||
Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);`
|
||||
|
||||
Correct: `$users->toQuery()->update([...]);`
|
||||
|
||||
## Use `#[CollectedBy]` for Custom Collection Classes
|
||||
|
||||
More declarative than overriding `newCollection()`.
|
||||
|
||||
```php
|
||||
#[CollectedBy(UserCollection::class)]
|
||||
class User extends Model {}
|
||||
```
|
||||
73
.claude/skills/laravel-best-practices/rules/config.md
Normal file
73
.claude/skills/laravel-best-practices/rules/config.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Configuration Best Practices
|
||||
|
||||
## `env()` Only in Config Files
|
||||
|
||||
Direct `env()` calls may return `null` when config is cached.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$key = env('API_KEY');
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
// config/services.php
|
||||
'key' => env('API_KEY'),
|
||||
|
||||
// Application code
|
||||
$key = config('services.key');
|
||||
```
|
||||
|
||||
## Use Encrypted Env or External Secrets
|
||||
|
||||
Never store production secrets in plain `.env` files in version control.
|
||||
|
||||
Incorrect:
|
||||
```bash
|
||||
|
||||
# .env committed to repo or shared in Slack
|
||||
|
||||
STRIPE_SECRET=sk_live_abc123
|
||||
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI
|
||||
```
|
||||
|
||||
Correct:
|
||||
```bash
|
||||
php artisan env:encrypt --env=production --readable
|
||||
php artisan env:decrypt --env=production
|
||||
```
|
||||
|
||||
For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime.
|
||||
|
||||
## Use `App::environment()` for Environment Checks
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
if (env('APP_ENV') === 'production') {
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
if (app()->isProduction()) {
|
||||
// or
|
||||
if (App::environment('production')) {
|
||||
```
|
||||
|
||||
## Use Constants and Language Files
|
||||
|
||||
Use class constants instead of hardcoded magic strings for model states, types, and statuses.
|
||||
|
||||
```php
|
||||
// Incorrect
|
||||
return $this->type === 'normal';
|
||||
|
||||
// Correct
|
||||
return $this->type === self::TYPE_NORMAL;
|
||||
```
|
||||
|
||||
If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there.
|
||||
|
||||
```php
|
||||
// Only when lang files already exist in the project
|
||||
return back()->with('message', __('app.article_added'));
|
||||
```
|
||||
192
.claude/skills/laravel-best-practices/rules/db-performance.md
Normal file
192
.claude/skills/laravel-best-practices/rules/db-performance.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
# Database Performance Best Practices
|
||||
|
||||
## Always Eager Load Relationships
|
||||
|
||||
Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront.
|
||||
|
||||
Incorrect (N+1 — executes 1 + N queries):
|
||||
```php
|
||||
$posts = Post::all();
|
||||
foreach ($posts as $post) {
|
||||
echo $post->author->name;
|
||||
}
|
||||
```
|
||||
|
||||
Correct (2 queries total):
|
||||
```php
|
||||
$posts = Post::with('author')->get();
|
||||
foreach ($posts as $post) {
|
||||
echo $post->author->name;
|
||||
}
|
||||
```
|
||||
|
||||
Constrain eager loads to select only needed columns (always include the foreign key):
|
||||
|
||||
```php
|
||||
$users = User::with(['posts' => function ($query) {
|
||||
$query->select('id', 'user_id', 'title')
|
||||
->where('published', true)
|
||||
->latest()
|
||||
->limit(10);
|
||||
}])->get();
|
||||
```
|
||||
|
||||
## Prevent Lazy Loading in Development
|
||||
|
||||
Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development.
|
||||
|
||||
```php
|
||||
public function boot(): void
|
||||
{
|
||||
Model::preventLazyLoading(! app()->isProduction());
|
||||
}
|
||||
```
|
||||
|
||||
Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded.
|
||||
|
||||
## Select Only Needed Columns
|
||||
|
||||
Avoid `SELECT *` — especially when tables have large text or JSON columns.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$posts = Post::with('author')->get();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$posts = Post::select('id', 'title', 'user_id', 'created_at')
|
||||
->with(['author:id,name,avatar'])
|
||||
->get();
|
||||
```
|
||||
|
||||
When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match.
|
||||
|
||||
## Chunk Large Datasets
|
||||
|
||||
Never load thousands of records at once. Use chunking for batch processing.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$users = User::all();
|
||||
foreach ($users as $user) {
|
||||
$user->notify(new WeeklyDigest);
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
User::where('subscribed', true)->chunk(200, function ($users) {
|
||||
foreach ($users as $user) {
|
||||
$user->notify(new WeeklyDigest);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change:
|
||||
|
||||
```php
|
||||
User::where('active', false)->chunkById(200, function ($users) {
|
||||
$users->each->delete();
|
||||
});
|
||||
```
|
||||
|
||||
## Add Database Indexes
|
||||
|
||||
Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
Schema::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained();
|
||||
$table->string('status');
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
Schema::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->index()->constrained();
|
||||
$table->string('status')->index();
|
||||
$table->timestamps();
|
||||
$table->index(['status', 'created_at']);
|
||||
});
|
||||
```
|
||||
|
||||
Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`).
|
||||
|
||||
## Use `withCount()` for Counting Relations
|
||||
|
||||
Never load entire collections just to count them.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$posts = Post::all();
|
||||
foreach ($posts as $post) {
|
||||
echo $post->comments->count();
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$posts = Post::withCount('comments')->get();
|
||||
foreach ($posts as $post) {
|
||||
echo $post->comments_count;
|
||||
}
|
||||
```
|
||||
|
||||
Conditional counting:
|
||||
|
||||
```php
|
||||
$posts = Post::withCount([
|
||||
'comments',
|
||||
'comments as approved_comments_count' => function ($query) {
|
||||
$query->where('approved', true);
|
||||
},
|
||||
])->get();
|
||||
```
|
||||
|
||||
## Use `cursor()` for Memory-Efficient Iteration
|
||||
|
||||
For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$users = User::where('active', true)->get();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
foreach (User::where('active', true)->cursor() as $user) {
|
||||
ProcessUser::dispatch($user->id);
|
||||
}
|
||||
```
|
||||
|
||||
Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records.
|
||||
|
||||
## No Queries in Blade Templates
|
||||
|
||||
Never execute queries in Blade templates. Pass data from controllers.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
@foreach (User::all() as $user)
|
||||
{{ $user->profile->name }}
|
||||
@endforeach
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
// Controller
|
||||
$users = User::with('profile')->get();
|
||||
return view('users.index', compact('users'));
|
||||
```
|
||||
|
||||
```blade
|
||||
@foreach ($users as $user)
|
||||
{{ $user->profile->name }}
|
||||
@endforeach
|
||||
```
|
||||
148
.claude/skills/laravel-best-practices/rules/eloquent.md
Normal file
148
.claude/skills/laravel-best-practices/rules/eloquent.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# Eloquent Best Practices
|
||||
|
||||
## Use Correct Relationship Types
|
||||
|
||||
Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints.
|
||||
|
||||
```php
|
||||
public function comments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Comment::class);
|
||||
}
|
||||
|
||||
public function author(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
```
|
||||
|
||||
## Use Local Scopes for Reusable Queries
|
||||
|
||||
Extract reusable query constraints into local scopes to avoid duplication.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$active = User::where('verified', true)->whereNotNull('activated_at')->get();
|
||||
$articles = Article::whereHas('user', function ($q) {
|
||||
$q->where('verified', true)->whereNotNull('activated_at');
|
||||
})->get();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('verified', true)->whereNotNull('activated_at');
|
||||
}
|
||||
|
||||
// Usage
|
||||
$active = User::active()->get();
|
||||
$articles = Article::whereHas('user', fn ($q) => $q->active())->get();
|
||||
```
|
||||
|
||||
## Apply Global Scopes Sparingly
|
||||
|
||||
Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy.
|
||||
|
||||
Incorrect (global scope for a conditional filter):
|
||||
```php
|
||||
class PublishedScope implements Scope
|
||||
{
|
||||
public function apply(Builder $builder, Model $model): void
|
||||
{
|
||||
$builder->where('published', true);
|
||||
}
|
||||
}
|
||||
// Now admin panels, reports, and background jobs all silently skip drafts
|
||||
```
|
||||
|
||||
Correct (local scope you opt into):
|
||||
```php
|
||||
public function scopePublished(Builder $query): Builder
|
||||
{
|
||||
return $query->where('published', true);
|
||||
}
|
||||
|
||||
Post::published()->paginate(); // Explicit
|
||||
Post::paginate(); // Admin sees all
|
||||
```
|
||||
|
||||
## Define Attribute Casts
|
||||
|
||||
Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion.
|
||||
|
||||
```php
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'total' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Cast Date Columns Properly
|
||||
|
||||
Always cast date columns. Use Carbon instances in templates instead of formatting strings manually.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'ordered_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
```blade
|
||||
{{ $order->ordered_at->toDateString() }}
|
||||
{{ $order->ordered_at->format('m-d') }}
|
||||
```
|
||||
|
||||
## Use `whereBelongsTo()` for Relationship Queries
|
||||
|
||||
Cleaner than manually specifying foreign keys.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
Post::where('user_id', $user->id)->get();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
Post::whereBelongsTo($user)->get();
|
||||
Post::whereBelongsTo($user, 'author')->get();
|
||||
```
|
||||
|
||||
## Avoid Hardcoded Table Names in Queries
|
||||
|
||||
Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string).
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
DB::table('users')->where('active', true)->get();
|
||||
|
||||
$query->join('companies', 'companies.id', '=', 'users.company_id');
|
||||
|
||||
DB::select('SELECT * FROM orders WHERE status = ?', ['pending']);
|
||||
```
|
||||
|
||||
Correct — reference the model's table:
|
||||
```php
|
||||
DB::table((new User)->getTable())->where('active', true)->get();
|
||||
|
||||
// Even better — use Eloquent or the query builder instead of raw SQL
|
||||
User::where('active', true)->get();
|
||||
Order::where('status', 'pending')->get();
|
||||
```
|
||||
|
||||
Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable.
|
||||
|
||||
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
# Error Handling Best Practices
|
||||
|
||||
## Exception Reporting and Rendering
|
||||
|
||||
There are two valid approaches — choose one and apply it consistently across the project.
|
||||
|
||||
**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find:
|
||||
|
||||
```php
|
||||
class InvalidOrderException extends Exception
|
||||
{
|
||||
public function report(): void { /* custom reporting */ }
|
||||
|
||||
public function render(Request $request): Response
|
||||
{
|
||||
return response()->view('errors.invalid-order', status: 422);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture:
|
||||
|
||||
```php
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
$exceptions->report(function (InvalidOrderException $e) { /* ... */ });
|
||||
$exceptions->render(function (InvalidOrderException $e, Request $request) {
|
||||
return response()->view('errors.invalid-order', status: 422);
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
Check the existing codebase and follow whichever pattern is already established.
|
||||
|
||||
## Use `ShouldntReport` for Exceptions That Should Never Log
|
||||
|
||||
More discoverable than listing classes in `dontReport()`.
|
||||
|
||||
```php
|
||||
class PodcastProcessingException extends Exception implements ShouldntReport {}
|
||||
```
|
||||
|
||||
## Throttle High-Volume Exceptions
|
||||
|
||||
A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type.
|
||||
|
||||
## Enable `dontReportDuplicates()`
|
||||
|
||||
Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks.
|
||||
|
||||
## Force JSON Error Rendering for API Routes
|
||||
|
||||
Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes.
|
||||
|
||||
```php
|
||||
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
|
||||
return $request->is('api/*') || $request->expectsJson();
|
||||
});
|
||||
```
|
||||
|
||||
## Add Context to Exception Classes
|
||||
|
||||
Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry.
|
||||
|
||||
```php
|
||||
class InvalidOrderException extends Exception
|
||||
{
|
||||
public function context(): array
|
||||
{
|
||||
return ['order_id' => $this->orderId];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# Events & Notifications Best Practices
|
||||
|
||||
## Rely on Event Discovery
|
||||
|
||||
Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`.
|
||||
|
||||
## Run `event:cache` in Production Deploy
|
||||
|
||||
Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`.
|
||||
|
||||
## Use `ShouldDispatchAfterCommit` Inside Transactions
|
||||
|
||||
Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet.
|
||||
|
||||
```php
|
||||
class OrderShipped implements ShouldDispatchAfterCommit {}
|
||||
```
|
||||
|
||||
## Always Queue Notifications
|
||||
|
||||
Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response.
|
||||
|
||||
```php
|
||||
class InvoicePaid extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
}
|
||||
```
|
||||
|
||||
## Use `afterCommit()` on Notifications in Transactions
|
||||
|
||||
Same race condition as events — call `afterCommit()` to delay dispatch until the transaction commits.
|
||||
|
||||
```php
|
||||
$user->notify((new InvoicePaid($invoice))->afterCommit());
|
||||
```
|
||||
|
||||
## Route Notification Channels to Dedicated Queues
|
||||
|
||||
Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues.
|
||||
|
||||
## Use On-Demand Notifications for Non-User Recipients
|
||||
|
||||
Avoid creating dummy models to send notifications to arbitrary addresses.
|
||||
|
||||
```php
|
||||
Notification::route('mail', 'admin@example.com')->notify(new SystemAlert());
|
||||
```
|
||||
|
||||
## Implement `HasLocalePreference` on Notifiable Models
|
||||
|
||||
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
|
||||
160
.claude/skills/laravel-best-practices/rules/http-client.md
Normal file
160
.claude/skills/laravel-best-practices/rules/http-client.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# HTTP Client Best Practices
|
||||
|
||||
## Always Set Explicit Timeouts
|
||||
|
||||
The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$response = Http::get('https://api.example.com/users');
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$response = Http::timeout(5)
|
||||
->connectTimeout(3)
|
||||
->get('https://api.example.com/users');
|
||||
```
|
||||
|
||||
For service-specific clients, define timeouts in a macro:
|
||||
|
||||
```php
|
||||
Http::macro('github', function () {
|
||||
return Http::baseUrl('https://api.github.com')
|
||||
->timeout(10)
|
||||
->connectTimeout(3)
|
||||
->withToken(config('services.github.token'));
|
||||
});
|
||||
|
||||
$response = Http::github()->get('/repos/laravel/framework');
|
||||
```
|
||||
|
||||
## Use Retry with Backoff for External APIs
|
||||
|
||||
External APIs have transient failures. Use `retry()` with increasing delays.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$response = Http::post('https://api.stripe.com/v1/charges', $data);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new PaymentFailedException('Charge failed');
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$response = Http::retry([100, 500, 1000])
|
||||
->timeout(10)
|
||||
->post('https://api.stripe.com/v1/charges', $data);
|
||||
```
|
||||
|
||||
Only retry on specific errors:
|
||||
|
||||
```php
|
||||
$response = Http::retry(3, 100, function (Throwable $exception, PendingRequest $request) {
|
||||
return $exception instanceof ConnectionException
|
||||
|| ($exception instanceof RequestException && $exception->response->serverError());
|
||||
})->post('https://api.example.com/data');
|
||||
```
|
||||
|
||||
## Handle Errors Explicitly
|
||||
|
||||
The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$response = Http::get('https://api.example.com/users/1');
|
||||
$user = $response->json(); // Could be an error body
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
$response = Http::timeout(5)
|
||||
->get('https://api.example.com/users/1')
|
||||
->throw();
|
||||
|
||||
$user = $response->json();
|
||||
```
|
||||
|
||||
For graceful degradation:
|
||||
|
||||
```php
|
||||
$response = Http::get('https://api.example.com/users/1');
|
||||
|
||||
if ($response->successful()) {
|
||||
return $response->json();
|
||||
}
|
||||
|
||||
if ($response->notFound()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response->throw();
|
||||
```
|
||||
|
||||
## Use Request Pooling for Concurrent Requests
|
||||
|
||||
When making multiple independent API calls, use `Http::pool()` instead of sequential calls.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$users = Http::get('https://api.example.com/users')->json();
|
||||
$posts = Http::get('https://api.example.com/posts')->json();
|
||||
$comments = Http::get('https://api.example.com/comments')->json();
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
use Illuminate\Http\Client\Pool;
|
||||
|
||||
$responses = Http::pool(fn (Pool $pool) => [
|
||||
$pool->as('users')->get('https://api.example.com/users'),
|
||||
$pool->as('posts')->get('https://api.example.com/posts'),
|
||||
$pool->as('comments')->get('https://api.example.com/comments'),
|
||||
]);
|
||||
|
||||
$users = $responses['users']->json();
|
||||
$posts = $responses['posts']->json();
|
||||
```
|
||||
|
||||
## Fake HTTP Calls in Tests
|
||||
|
||||
Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
it('syncs user from API', function () {
|
||||
$service = new UserSyncService;
|
||||
$service->sync(1); // Hits the real API
|
||||
});
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
it('syncs user from API', function () {
|
||||
Http::preventStrayRequests();
|
||||
|
||||
Http::fake([
|
||||
'api.example.com/users/1' => Http::response([
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@example.com',
|
||||
]),
|
||||
]);
|
||||
|
||||
$service = new UserSyncService;
|
||||
$service->sync(1);
|
||||
|
||||
Http::assertSent(function (Request $request) {
|
||||
return $request->url() === 'https://api.example.com/users/1';
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Test failure scenarios too:
|
||||
|
||||
```php
|
||||
Http::fake([
|
||||
'api.example.com/*' => Http::failedConnection(),
|
||||
]);
|
||||
```
|
||||
27
.claude/skills/laravel-best-practices/rules/mail.md
Normal file
27
.claude/skills/laravel-best-practices/rules/mail.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Mail Best Practices
|
||||
|
||||
## Implement `ShouldQueue` on the Mailable Class
|
||||
|
||||
Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it.
|
||||
|
||||
## Use `afterCommit()` on Mailables Inside Transactions
|
||||
|
||||
A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor.
|
||||
|
||||
## Use `assertQueued()` Not `assertSent()` for Queued Mailables
|
||||
|
||||
`Mail::assertSent()` only catches synchronous mail. Queued mailables fail `assertSent` with a "Did you mean to use assertQueued()?" hint.
|
||||
|
||||
Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`.
|
||||
|
||||
Correct: `Mail::assertQueued(OrderShipped::class);`
|
||||
|
||||
## Use Markdown Mailables for Transactional Emails
|
||||
|
||||
Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag.
|
||||
|
||||
## Separate Content Tests from Sending Tests
|
||||
|
||||
Content tests: instantiate the mailable directly, call `assertSeeInHtml()`.
|
||||
Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`.
|
||||
Don't mix them — it conflates concerns and makes tests brittle.
|
||||
121
.claude/skills/laravel-best-practices/rules/migrations.md
Normal file
121
.claude/skills/laravel-best-practices/rules/migrations.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# Migration Best Practices
|
||||
|
||||
## Generate Migrations with Artisan
|
||||
|
||||
Always use `php artisan make:migration` for consistent naming and timestamps.
|
||||
|
||||
Incorrect (manually created file):
|
||||
```php
|
||||
// database/migrations/posts_migration.php ← wrong naming, no timestamp
|
||||
```
|
||||
|
||||
Correct (Artisan-generated):
|
||||
```bash
|
||||
php artisan make:migration create_posts_table
|
||||
php artisan make:migration add_slug_to_posts_table
|
||||
```
|
||||
|
||||
## Use `constrained()` for Foreign Keys
|
||||
|
||||
Automatic naming and referential integrity.
|
||||
|
||||
```php
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
|
||||
// Non-standard names
|
||||
$table->foreignId('author_id')->constrained('users');
|
||||
```
|
||||
|
||||
## Never Modify Deployed Migrations
|
||||
|
||||
Once a migration has run in production, treat it as immutable. Create a new migration to change the table.
|
||||
|
||||
Incorrect (editing a deployed migration):
|
||||
```php
|
||||
// 2024_01_01_create_posts_table.php — already in production
|
||||
$table->string('slug')->unique(); // ← added after deployment
|
||||
```
|
||||
|
||||
Correct (new migration to alter):
|
||||
```php
|
||||
// 2024_03_15_add_slug_to_posts_table.php
|
||||
Schema::table('posts', function (Blueprint $table) {
|
||||
$table->string('slug')->unique()->after('title');
|
||||
});
|
||||
```
|
||||
|
||||
## Add Indexes in the Migration
|
||||
|
||||
Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
Schema::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained();
|
||||
$table->string('status');
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
Schema::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->index();
|
||||
$table->string('status')->index();
|
||||
$table->timestamp('shipped_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
|
||||
## Mirror Defaults in Model `$attributes`
|
||||
|
||||
When a column has a database default, mirror it in the model so new instances have correct values before saving.
|
||||
|
||||
```php
|
||||
// Migration
|
||||
$table->string('status')->default('pending');
|
||||
|
||||
// Model
|
||||
protected $attributes = [
|
||||
'status' => 'pending',
|
||||
];
|
||||
```
|
||||
|
||||
## Write Reversible `down()` Methods by Default
|
||||
|
||||
Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments.
|
||||
|
||||
```php
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('posts', function (Blueprint $table) {
|
||||
$table->dropColumn('slug');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported.
|
||||
|
||||
## Keep Migrations Focused
|
||||
|
||||
One concern per migration. Never mix DDL (schema changes) and DML (data manipulation).
|
||||
|
||||
Incorrect (partial failure creates unrecoverable state):
|
||||
```php
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('settings', function (Blueprint $table) { ... });
|
||||
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
|
||||
}
|
||||
```
|
||||
|
||||
Correct (separate migrations):
|
||||
```php
|
||||
// Migration 1: create_settings_table
|
||||
Schema::create('settings', function (Blueprint $table) { ... });
|
||||
|
||||
// Migration 2: seed_default_settings
|
||||
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
|
||||
```
|
||||
144
.claude/skills/laravel-best-practices/rules/queue-jobs.md
Normal file
144
.claude/skills/laravel-best-practices/rules/queue-jobs.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# Queue & Job Best Practices
|
||||
|
||||
## Set `retry_after` Greater Than `timeout`
|
||||
|
||||
If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution.
|
||||
|
||||
Incorrect (`retry_after` ≤ `timeout`):
|
||||
```php
|
||||
class ProcessReport implements ShouldQueue
|
||||
{
|
||||
public $timeout = 120;
|
||||
}
|
||||
|
||||
// config/queue.php — retry_after: 90 ← job retried while still running!
|
||||
```
|
||||
|
||||
Correct (`retry_after` > `timeout`):
|
||||
```php
|
||||
class ProcessReport implements ShouldQueue
|
||||
{
|
||||
public $timeout = 120;
|
||||
}
|
||||
|
||||
// config/queue.php — retry_after: 180 ← safely longer than any job timeout
|
||||
```
|
||||
|
||||
## Use Exponential Backoff
|
||||
|
||||
Use progressively longer delays between retries to avoid hammering failing services.
|
||||
|
||||
Incorrect (fixed retry interval):
|
||||
```php
|
||||
class SyncWithStripe implements ShouldQueue
|
||||
{
|
||||
public $tries = 3;
|
||||
// Default: retries immediately, overwhelming the API
|
||||
}
|
||||
```
|
||||
|
||||
Correct (exponential backoff):
|
||||
```php
|
||||
class SyncWithStripe implements ShouldQueue
|
||||
{
|
||||
public $tries = 3;
|
||||
public $backoff = [1, 5, 10];
|
||||
}
|
||||
```
|
||||
|
||||
## Implement `ShouldBeUnique`
|
||||
|
||||
Prevent duplicate job processing.
|
||||
|
||||
```php
|
||||
class GenerateInvoice implements ShouldQueue, ShouldBeUnique
|
||||
{
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return $this->order->id;
|
||||
}
|
||||
|
||||
public $uniqueFor = 3600;
|
||||
}
|
||||
```
|
||||
|
||||
## Always Implement `failed()`
|
||||
|
||||
Handle errors explicitly — don't rely on silent failure.
|
||||
|
||||
```php
|
||||
public function failed(?Throwable $exception): void
|
||||
{
|
||||
$this->podcast->update(['status' => 'failed']);
|
||||
Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]);
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limit External API Calls in Jobs
|
||||
|
||||
Use `RateLimited` middleware to throttle jobs calling third-party APIs.
|
||||
|
||||
```php
|
||||
public function middleware(): array
|
||||
{
|
||||
return [new RateLimited('external-api')];
|
||||
}
|
||||
```
|
||||
|
||||
## Batch Related Jobs
|
||||
|
||||
Use `Bus::batch()` when jobs should succeed or fail together.
|
||||
|
||||
```php
|
||||
Bus::batch([
|
||||
new ImportCsvChunk($chunk1),
|
||||
new ImportCsvChunk($chunk2),
|
||||
])
|
||||
->then(fn (Batch $batch) => Notification::send($user, new ImportComplete))
|
||||
->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed'))
|
||||
->dispatch();
|
||||
```
|
||||
|
||||
## `retryUntil()` Needs `$tries = 0`
|
||||
|
||||
When using time-based retry limits, set `$tries = 0` to avoid premature failure.
|
||||
|
||||
```php
|
||||
public $tries = 0;
|
||||
|
||||
public function retryUntil(): \DateTimeInterface
|
||||
{
|
||||
return now()->addHours(4);
|
||||
}
|
||||
```
|
||||
|
||||
## Use `ShouldBeUniqueUntilProcessing` for Early Lock Release
|
||||
|
||||
`ShouldBeUnique` holds the lock until the job completes. `ShouldBeUniqueUntilProcessing` releases it when processing starts, allowing new instances to queue.
|
||||
|
||||
```php
|
||||
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
||||
{
|
||||
// Lock releases when processing begins, not when it finishes
|
||||
}
|
||||
```
|
||||
|
||||
## Use Horizon for Complex Queue Scenarios
|
||||
|
||||
Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities.
|
||||
|
||||
```php
|
||||
// config/horizon.php
|
||||
'environments' => [
|
||||
'production' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['high', 'default', 'low'],
|
||||
'balance' => 'auto',
|
||||
'minProcesses' => 1,
|
||||
'maxProcesses' => 10,
|
||||
'tries' => 3,
|
||||
],
|
||||
],
|
||||
],
|
||||
```
|
||||
99
.claude/skills/laravel-best-practices/rules/routing.md
Normal file
99
.claude/skills/laravel-best-practices/rules/routing.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# Routing & Controllers Best Practices
|
||||
|
||||
## Use Implicit Route Model Binding
|
||||
|
||||
Let Laravel resolve models automatically from route parameters.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
public function show(int $id)
|
||||
{
|
||||
$post = Post::findOrFail($id);
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function show(Post $post)
|
||||
{
|
||||
return view('posts.show', ['post' => $post]);
|
||||
}
|
||||
```
|
||||
|
||||
## Use Scoped Bindings for Nested Resources
|
||||
|
||||
Enforce parent-child relationships automatically.
|
||||
|
||||
```php
|
||||
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
|
||||
// $post is automatically scoped to $user
|
||||
})->scopeBindings();
|
||||
```
|
||||
|
||||
## Use Resource Controllers
|
||||
|
||||
Use `Route::resource()` or `apiResource()` for RESTful endpoints.
|
||||
|
||||
```php
|
||||
Route::resource('posts', PostController::class);
|
||||
// In routes/api.php — the /api prefix is applied automatically
|
||||
Route::apiResource('posts', Api\PostController::class);
|
||||
```
|
||||
|
||||
## Keep Controllers Thin
|
||||
|
||||
Aim for under 10 lines per method. Extract business logic to action or service classes.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([...]);
|
||||
if ($request->hasFile('image')) {
|
||||
$request->file('image')->move(public_path('images'));
|
||||
}
|
||||
$post = Post::create($validated);
|
||||
$post->tags()->sync($validated['tags']);
|
||||
event(new PostCreated($post));
|
||||
return redirect()->route('posts.show', $post);
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function store(StorePostRequest $request, CreatePostAction $create)
|
||||
{
|
||||
$post = $create->execute($request->validated());
|
||||
|
||||
return redirect()->route('posts.show', $post);
|
||||
}
|
||||
```
|
||||
|
||||
## Type-Hint Form Requests
|
||||
|
||||
Type-hinting Form Requests triggers automatic validation and authorization before the method executes.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => ['required', 'max:255'],
|
||||
'body' => ['required'],
|
||||
]);
|
||||
|
||||
Post::create($validated);
|
||||
|
||||
return redirect()->route('posts.index');
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function store(StorePostRequest $request): RedirectResponse
|
||||
{
|
||||
Post::create($request->validated());
|
||||
|
||||
return redirect()->route('posts.index');
|
||||
}
|
||||
```
|
||||
39
.claude/skills/laravel-best-practices/rules/scheduling.md
Normal file
39
.claude/skills/laravel-best-practices/rules/scheduling.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Task Scheduling Best Practices
|
||||
|
||||
## Use `withoutOverlapping()` on Variable-Duration Tasks
|
||||
|
||||
Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion.
|
||||
|
||||
## Use `onOneServer()` on Multi-Server Deployments
|
||||
|
||||
Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached).
|
||||
|
||||
## Use `runInBackground()` for Concurrent Long Tasks
|
||||
|
||||
By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes.
|
||||
|
||||
## Use `environments()` to Restrict Tasks
|
||||
|
||||
Prevent accidental execution of production-only tasks (billing, reporting) on staging.
|
||||
|
||||
```php
|
||||
Schedule::command('billing:charge')->monthly()->environments(['production']);
|
||||
```
|
||||
|
||||
## Use `takeUntilTimeout()` for Time-Bounded Processing
|
||||
|
||||
A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time.
|
||||
|
||||
## Use Schedule Groups for Shared Configuration
|
||||
|
||||
Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks.
|
||||
|
||||
```php
|
||||
Schedule::daily()
|
||||
->onOneServer()
|
||||
->timezone('America/New_York')
|
||||
->group(function () {
|
||||
Schedule::command('emails:send --force');
|
||||
Schedule::command('emails:prune');
|
||||
});
|
||||
```
|
||||
198
.claude/skills/laravel-best-practices/rules/security.md
Normal file
198
.claude/skills/laravel-best-practices/rules/security.md
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# Security Best Practices
|
||||
|
||||
## Mass Assignment Protection
|
||||
|
||||
Every model must define `$fillable` (whitelist) or `$guarded` (blacklist).
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
class User extends Model
|
||||
{
|
||||
protected $guarded = []; // All fields are mass assignable
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
class User extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Never use `$guarded = []` on models that accept user input.
|
||||
|
||||
## Authorize Every Action
|
||||
|
||||
Use policies or gates in controllers. Never skip authorization.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
public function update(UpdatePostRequest $request, Post $post)
|
||||
{
|
||||
$post->update($request->validated());
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function update(UpdatePostRequest $request, Post $post)
|
||||
{
|
||||
Gate::authorize('update', $post);
|
||||
|
||||
$post->update($request->validated());
|
||||
}
|
||||
```
|
||||
|
||||
Or via Form Request:
|
||||
|
||||
```php
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('update', $this->route('post'));
|
||||
}
|
||||
```
|
||||
|
||||
## Prevent SQL Injection
|
||||
|
||||
Always use parameter binding. Never interpolate user input into queries.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
DB::select("SELECT * FROM users WHERE name = '{$request->name}'");
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
User::where('name', $request->name)->get();
|
||||
|
||||
// Raw expressions with bindings
|
||||
User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get();
|
||||
```
|
||||
|
||||
## Escape Output to Prevent XSS
|
||||
|
||||
Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
{!! $user->bio !!}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```blade
|
||||
{{ $user->bio }}
|
||||
```
|
||||
|
||||
## CSRF Protection
|
||||
|
||||
Include `@csrf` in all POST/PUT/DELETE Blade forms. In Inertia apps, the `@csrf` directive is automatically applied.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
<form method="POST" action="/posts">
|
||||
<input type="text" name="title">
|
||||
</form>
|
||||
```
|
||||
|
||||
Correct:
|
||||
```blade
|
||||
<form method="POST" action="/posts">
|
||||
@csrf
|
||||
<input type="text" name="title">
|
||||
</form>
|
||||
```
|
||||
|
||||
## Rate Limit Auth and API Routes
|
||||
|
||||
Apply `throttle` middleware to authentication and API routes.
|
||||
|
||||
```php
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->ip());
|
||||
});
|
||||
|
||||
Route::post('/login', LoginController::class)->middleware('throttle:login');
|
||||
```
|
||||
|
||||
## Validate File Uploads
|
||||
|
||||
Validate extension, MIME type, and size. The `mimes` rule checks extensions; use `mimetypes` for actual MIME type validation. Never trust client-provided filenames.
|
||||
|
||||
```php
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Store with generated filenames:
|
||||
|
||||
```php
|
||||
$path = $request->file('avatar')->store('avatars', 'public');
|
||||
```
|
||||
|
||||
## Keep Secrets Out of Code
|
||||
|
||||
Never commit `.env`. Access secrets via `config()` only.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
$key = env('API_KEY');
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
// config/services.php
|
||||
'api_key' => env('API_KEY'),
|
||||
|
||||
// In application code
|
||||
$key = config('services.api_key');
|
||||
```
|
||||
|
||||
## Audit Dependencies
|
||||
|
||||
Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment.
|
||||
|
||||
```bash
|
||||
composer audit
|
||||
```
|
||||
|
||||
## Encrypt Sensitive Database Fields
|
||||
|
||||
Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
class Integration extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'api_key' => 'string',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
class Integration extends Model
|
||||
{
|
||||
protected $hidden = ['api_key', 'api_secret'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'api_key' => 'encrypted',
|
||||
'api_secret' => 'encrypted',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
125
.claude/skills/laravel-best-practices/rules/style.md
Normal file
125
.claude/skills/laravel-best-practices/rules/style.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Conventions & Style
|
||||
|
||||
## Follow Laravel Naming Conventions
|
||||
|
||||
| What | Convention | Good | Bad |
|
||||
|------|-----------|------|-----|
|
||||
| Controller | singular | `ArticleController` | `ArticlesController` |
|
||||
| Model | singular | `User` | `Users` |
|
||||
| Table | plural, snake_case | `article_comments` | `articleComments` |
|
||||
| Pivot table | singular alphabetical | `article_user` | `user_article` |
|
||||
| Column | snake_case, no model name | `meta_title` | `article_meta_title` |
|
||||
| Foreign key | singular model + `_id` | `article_id` | `articles_id` |
|
||||
| Route | plural | `articles/1` | `article/1` |
|
||||
| Route name | snake_case with dots | `users.show_active` | `users.show-active` |
|
||||
| Method | camelCase | `getAll` | `get_all` |
|
||||
| Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` |
|
||||
| Collection | descriptive, plural | `$activeUsers` | `$data` |
|
||||
| Object | descriptive, singular | `$activeUser` | `$users` |
|
||||
| View | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` |
|
||||
| Config | snake_case | `google_calendar.php` | `googleCalendar.php` |
|
||||
| Enum | singular | `UserType` | `UserTypes` |
|
||||
|
||||
## Prefer Shorter Readable Syntax
|
||||
|
||||
| Verbose | Shorter |
|
||||
|---------|---------|
|
||||
| `Session::get('cart')` | `session('cart')` |
|
||||
| `$request->session()->get('cart')` | `session('cart')` |
|
||||
| `$request->input('name')` | `$request->name` |
|
||||
| `return Redirect::back()` | `return back()` |
|
||||
| `Carbon::now()` | `now()` |
|
||||
| `App::make('Class')` | `app('Class')` |
|
||||
| `->where('column', '=', 1)` | `->where('column', 1)` |
|
||||
| `->orderBy('created_at', 'desc')` | `->latest()` |
|
||||
| `->orderBy('created_at', 'asc')` | `->oldest()` |
|
||||
| `->first()->name` | `->value('name')` |
|
||||
|
||||
## Use Laravel String & Array Helpers
|
||||
|
||||
Laravel provides `Str`, `Arr`, `Number`, and `Uri` helper classes that are more readable, chainable, and UTF-8 safe than raw PHP functions. Always prefer them.
|
||||
|
||||
Strings — use `Str` and fluent `Str::of()` over raw PHP:
|
||||
```php
|
||||
// Incorrect
|
||||
$slug = strtolower(str_replace(' ', '-', $title));
|
||||
$short = substr($text, 0, 100) . '...';
|
||||
$class = substr(strrchr('App\Models\User', '\'), 1);
|
||||
|
||||
// Correct
|
||||
$slug = Str::slug($title);
|
||||
$short = Str::limit($text, 100);
|
||||
$class = class_basename('App\Models\User');
|
||||
```
|
||||
|
||||
Fluent strings — chain operations for complex transformations:
|
||||
```php
|
||||
// Incorrect
|
||||
$result = strtolower(trim(str_replace('_', '-', $input)));
|
||||
|
||||
// Correct
|
||||
$result = Str::of($input)->trim()->replace('_', '-')->lower();
|
||||
```
|
||||
|
||||
Key `Str` methods to prefer: `Str::slug()`, `Str::limit()`, `Str::contains()`, `Str::before()`, `Str::after()`, `Str::between()`, `Str::camel()`, `Str::snake()`, `Str::kebab()`, `Str::headline()`, `Str::squish()`, `Str::mask()`, `Str::uuid()`, `Str::ulid()`, `Str::random()`, `Str::is()`.
|
||||
|
||||
Arrays — use `Arr` over raw PHP:
|
||||
```php
|
||||
// Incorrect
|
||||
$name = isset($array['user']['name']) ? $array['user']['name'] : 'default';
|
||||
|
||||
// Correct
|
||||
$name = Arr::get($array, 'user.name', 'default');
|
||||
```
|
||||
|
||||
Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`.
|
||||
|
||||
Numbers — use `Number` for display formatting:
|
||||
```php
|
||||
Number::format(1000000); // "1,000,000"
|
||||
Number::currency(1500, 'USD'); // "$1,500.00"
|
||||
Number::abbreviate(1000000); // "1M"
|
||||
Number::fileSize(1024 * 1024); // "1 MB"
|
||||
Number::percentage(75.5); // "75.5%"
|
||||
```
|
||||
|
||||
URIs — use `Uri` for URL manipulation:
|
||||
```php
|
||||
$uri = Uri::of('https://example.com/search')
|
||||
->withQuery(['q' => 'laravel', 'page' => 1]);
|
||||
```
|
||||
|
||||
Use `$request->string('name')` to get a fluent `Stringable` directly from request input for immediate chaining.
|
||||
|
||||
Use `search-docs` for the full list of available methods — these helpers are extensive.
|
||||
|
||||
## No Inline JS/CSS in Blade
|
||||
|
||||
Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes.
|
||||
|
||||
Incorrect:
|
||||
```blade
|
||||
let article = `{{ json_encode($article) }}`;
|
||||
```
|
||||
|
||||
Correct:
|
||||
```blade
|
||||
<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}</button>
|
||||
```
|
||||
|
||||
Pass data to JS via data attributes or use a dedicated PHP-to-JS package.
|
||||
|
||||
## No Unnecessary Comments
|
||||
|
||||
Code should be readable on its own. Use descriptive method and variable names instead of comments. The only exception is config files, where descriptive comments are expected.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
// Check if there are any joins
|
||||
if (count((array) $builder->getQuery()->joins) > 0)
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
if ($this->hasJoins())
|
||||
```
|
||||
43
.claude/skills/laravel-best-practices/rules/testing.md
Normal file
43
.claude/skills/laravel-best-practices/rules/testing.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Testing Best Practices
|
||||
|
||||
## Use `LazilyRefreshDatabase` Over `RefreshDatabase`
|
||||
|
||||
`RefreshDatabase` migrates once per process and wraps each test in a rolled-back transaction. `LazilyRefreshDatabase` skips even that first migration if the schema is already up to date.
|
||||
|
||||
## Use Model Assertions Over Raw Database Assertions
|
||||
|
||||
Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);`
|
||||
|
||||
Correct: `$this->assertModelExists($user);`
|
||||
|
||||
More expressive, type-safe, and fails with clearer messages.
|
||||
|
||||
## Use Factory States and Sequences
|
||||
|
||||
Named states make tests self-documenting. Sequences eliminate repetitive setup.
|
||||
|
||||
Incorrect: `User::factory()->create(['email_verified_at' => null]);`
|
||||
|
||||
Correct: `User::factory()->unverified()->create();`
|
||||
|
||||
## Use `Exceptions::fake()` to Assert Exception Reporting
|
||||
|
||||
Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally.
|
||||
|
||||
## Call `Event::fake()` After Factory Setup
|
||||
|
||||
Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models.
|
||||
|
||||
Incorrect: `Event::fake(); $user = User::factory()->create();`
|
||||
|
||||
Correct: `$user = User::factory()->create(); Event::fake();`
|
||||
|
||||
## Use `recycle()` to Share Relationship Instances Across Factories
|
||||
|
||||
Without `recycle()`, nested factories create separate instances of the same conceptual entity.
|
||||
|
||||
```php
|
||||
Ticket::factory()
|
||||
->recycle(Airline::factory()->create())
|
||||
->create();
|
||||
```
|
||||
75
.claude/skills/laravel-best-practices/rules/validation.md
Normal file
75
.claude/skills/laravel-best-practices/rules/validation.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Validation & Forms Best Practices
|
||||
|
||||
## Use Form Request Classes
|
||||
|
||||
Extract validation from controllers into dedicated Form Request classes.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'required|max:255',
|
||||
'body' => 'required',
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
public function store(StorePostRequest $request)
|
||||
{
|
||||
Post::create($request->validated());
|
||||
}
|
||||
```
|
||||
|
||||
## Array vs. String Notation for Rules
|
||||
|
||||
Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses.
|
||||
|
||||
```php
|
||||
// Preferred for new code
|
||||
'email' => ['required', 'email', Rule::unique('users')],
|
||||
|
||||
// Follow existing convention if the project uses string notation
|
||||
'email' => 'required|email|unique:users',
|
||||
```
|
||||
|
||||
## Always Use `validated()`
|
||||
|
||||
Get only validated data. Never use `$request->all()` for mass operations.
|
||||
|
||||
Incorrect:
|
||||
```php
|
||||
Post::create($request->all());
|
||||
```
|
||||
|
||||
Correct:
|
||||
```php
|
||||
Post::create($request->validated());
|
||||
```
|
||||
|
||||
## Use `Rule::when()` for Conditional Validation
|
||||
|
||||
```php
|
||||
'company_name' => [
|
||||
Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']),
|
||||
],
|
||||
```
|
||||
|
||||
## Use the `after()` Method for Custom Validation
|
||||
|
||||
Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields.
|
||||
|
||||
```php
|
||||
public function after(): array
|
||||
{
|
||||
return [
|
||||
function (Validator $validator) {
|
||||
if ($this->quantity > Product::find($this->product_id)?->stock) {
|
||||
$validator->errors()->add('quantity', 'Not enough stock.');
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
115
.claude/skills/livewire-development/SKILL.md
Normal file
115
.claude/skills/livewire-development/SKILL.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
---
|
||||
name: livewire-development
|
||||
description: "Use for any task or question involving Livewire. Activate if user mentions Livewire, wire: directives, or Livewire-specific concepts like wire:model, wire:click, invoke this skill. Covers building new components, debugging reactivity issues, real-time form validation, loading states, migrating from Livewire 2 to 3, converting component formats (SFC/MFC/class-based), and performance optimization. Do not use for non-Livewire reactive UI (React, Vue, Alpine-only, Inertia.js) or standard Laravel forms without Livewire."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Livewire Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Livewire 3 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Components
|
||||
|
||||
Use the `php artisan make:livewire [Posts\CreatePost]` Artisan command to create new components.
|
||||
|
||||
### Fundamental Concepts
|
||||
|
||||
- State should live on the server, with the UI reflecting it.
|
||||
- All Livewire requests hit the Laravel backend; they're like regular HTTP requests. Always validate form data and run authorization checks in Livewire actions.
|
||||
|
||||
## Livewire 3 Specifics
|
||||
|
||||
### Key Changes From Livewire 2
|
||||
|
||||
These things changed in Livewire 3, but may not have been updated in this application. Verify this application's setup to ensure you follow existing conventions.
|
||||
- Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default.
|
||||
- Components now use the `App\Livewire` namespace (not `App\Http\Livewire`).
|
||||
- Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`).
|
||||
- Use the `components.layouts.app` view as the typical layout path (not `layouts.app`).
|
||||
|
||||
### New Directives
|
||||
|
||||
- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use.
|
||||
|
||||
### Alpine Integration
|
||||
|
||||
- Alpine is now included with Livewire; don't manually include Alpine.js.
|
||||
- Plugins included with Alpine: persist, intersect, collapse, and focus.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Component Structure
|
||||
|
||||
- Livewire components require a single root element.
|
||||
- Use `wire:loading` and `wire:dirty` for delightful loading states.
|
||||
|
||||
### Using Keys in Loops
|
||||
|
||||
<!-- Wire Key in Loops -->
|
||||
```blade
|
||||
@foreach ($items as $item)
|
||||
<div wire:key="item-{{ $item->id }}">
|
||||
{{ $item->name }}
|
||||
</div>
|
||||
@endforeach
|
||||
```
|
||||
|
||||
### Lifecycle Hooks
|
||||
|
||||
Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
|
||||
|
||||
<!-- Lifecycle Hook Examples -->
|
||||
```php
|
||||
public function mount(User $user) { $this->user = $user; }
|
||||
public function updatedSearch() { $this->resetPage(); }
|
||||
```
|
||||
|
||||
## JavaScript Hooks
|
||||
|
||||
You can listen for `livewire:init` to hook into Livewire initialization:
|
||||
|
||||
<!-- Livewire Init Hook Example -->
|
||||
```js
|
||||
document.addEventListener('livewire:init', function () {
|
||||
Livewire.hook('request', ({ fail }) => {
|
||||
if (fail && fail.status === 419) {
|
||||
alert('Your session expired');
|
||||
}
|
||||
});
|
||||
|
||||
Livewire.hook('message.failed', (message, component) => {
|
||||
console.error(message);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
<!-- Example Livewire Component Test -->
|
||||
```php
|
||||
Livewire::test(Counter::class)
|
||||
->assertSet('count', 0)
|
||||
->call('increment')
|
||||
->assertSet('count', 1)
|
||||
->assertSee(1)
|
||||
->assertStatus(200);
|
||||
```
|
||||
|
||||
<!-- Testing Livewire Component Exists on Page -->
|
||||
```php
|
||||
$this->get('/posts/create')
|
||||
->assertSeeLivewire(CreatePost::class);
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Forgetting `wire:key` in loops causes unexpected behavior when items change
|
||||
- Using `wire:model` expecting real-time updates (use `wire:model.live` instead in v3)
|
||||
- Not validating/authorizing in Livewire actions (treat them like HTTP requests)
|
||||
- Including Alpine.js separately when it's already bundled with Livewire 3
|
||||
96
.claude/skills/mcp-development/SKILL.md
Normal file
96
.claude/skills/mcp-development/SKILL.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
name: mcp-development
|
||||
description: "Use this skill for Laravel MCP development only. Trigger when creating or editing MCP tools, resources, prompts, or servers in Laravel projects. Covers: artisan make:mcp-* generators, mcp:inspector, routes/ai.php, Tool/Resource/Prompt classes, schema validation, shouldRegister(), OAuth setup, URI templates, read-only attributes, and MCP debugging. Do not use for non-Laravel MCP projects or generic AI features without MCP."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# MCP Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Laravel MCP patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Register MCP servers in `routes/ai.php`:
|
||||
|
||||
<!-- Register MCP Server -->
|
||||
```php
|
||||
use Laravel\Mcp\Facades\Mcp;
|
||||
|
||||
Mcp::web();
|
||||
```
|
||||
|
||||
### Creating MCP Primitives
|
||||
|
||||
Create MCP tools, resources, prompts, and servers using artisan commands:
|
||||
|
||||
```bash
|
||||
php artisan make:mcp-tool ToolName # Create a tool
|
||||
|
||||
php artisan make:mcp-resource ResourceName # Create a resource
|
||||
|
||||
php artisan make:mcp-prompt PromptName # Create a prompt
|
||||
|
||||
php artisan make:mcp-server ServerName # Create a server
|
||||
|
||||
```
|
||||
|
||||
After creating primitives, register them in your server's `$tools`, `$resources`, or `$prompts` properties.
|
||||
|
||||
### Tools
|
||||
|
||||
<!-- MCP Tool Example -->
|
||||
```php
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Request;
|
||||
use Laravel\Mcp\Server\Response;
|
||||
|
||||
class MyTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
return new Response(['result' => 'success']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registering Primitives in a Server
|
||||
|
||||
Each MCP server must explicitly declare the tools, resources, and prompts it exposes.
|
||||
|
||||
<!-- Register Primitives in MCP Server -->
|
||||
```php
|
||||
use Laravel\Mcp\Server;
|
||||
|
||||
class AppServer extends Server
|
||||
{
|
||||
protected array $tools = [
|
||||
\App\Mcp\Tools\MyTool::class,
|
||||
];
|
||||
|
||||
protected array $resources = [
|
||||
\App\Mcp\Resources\MyResource::class,
|
||||
];
|
||||
|
||||
protected array $prompts = [
|
||||
\App\Mcp\Prompts\MyPrompt::class,
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Check `routes/ai.php` for proper registration
|
||||
2. Test tool via MCP client
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Running `mcp:start` command (it hangs waiting for input)
|
||||
- Using HTTPS locally with Node-based MCP clients
|
||||
- Not using `search-docs` for the latest MCP documentation
|
||||
- Not registering MCP server routes in `routes/ai.php`
|
||||
- Do not register `ai.php` in `bootstrap.php`; it is registered automatically.
|
||||
- OAuth registration supports custom URI schemes (e.g., `cursor://`, `vscode://`) for native desktop clients via `mcp.custom_schemes` config
|
||||
166
.claude/skills/pest-testing/SKILL.md
Normal file
166
.claude/skills/pest-testing/SKILL.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
---
|
||||
name: pest-testing
|
||||
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Pest Testing 4
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Tests
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
The `{name}` argument should include only the path and test name, but should not include the test suite.
|
||||
- Incorrect: `php artisan make:test --pest Feature/SomeFeatureTest` will generate `tests/Feature/Feature/SomeFeatureTest.php`
|
||||
- Correct: `php artisan make:test --pest SomeControllerTest` will generate `tests/Feature/SomeControllerTest.php`
|
||||
- Incorrect: `php artisan make:test --pest --unit Unit/SomeServiceTest` will generate `tests/Unit/Unit/SomeServiceTest.php`
|
||||
- Correct: `php artisan make:test --pest --unit SomeServiceTest` will generate `tests/Unit/SomeServiceTest.php`
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`.
|
||||
|
||||
<!-- Basic Pest Test Example -->
|
||||
```php
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<!-- Pest Response Assertion -->
|
||||
```php
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
```
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
| `assertNotFound()` | `assertStatus(404)` |
|
||||
| `assertForbidden()` | `assertStatus(403)` |
|
||||
|
||||
## Mocking
|
||||
|
||||
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
|
||||
<!-- Pest Dataset Example -->
|
||||
```php
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
```
|
||||
|
||||
## Pest 4 Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
|
||||
### Browser Test Example
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
|
||||
<!-- Pest Browser Test Example -->
|
||||
```php
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavaScriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
```
|
||||
|
||||
### Smoke Testing
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
|
||||
<!-- Pest Smoke Testing Example -->
|
||||
```php
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
||||
```
|
||||
|
||||
### Visual Regression Testing
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
|
||||
### Test Sharding
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
|
||||
### Architecture Testing
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
|
||||
<!-- Architecture Test Example -->
|
||||
```php
|
||||
arch('controllers')
|
||||
->expect('App\Http\Controllers')
|
||||
->toExtendNothing()
|
||||
->toHaveSuffix('Controller');
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
|
||||
80
.claude/skills/socialite-development/SKILL.md
Normal file
80
.claude/skills/socialite-development/SKILL.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
name: socialite-development
|
||||
description: "Manages OAuth social authentication with Laravel Socialite. Activate when adding social login providers; configuring OAuth redirect/callback flows; retrieving authenticated user details; customizing scopes or parameters; setting up community providers; testing with Socialite fakes; or when the user mentions social login, OAuth, Socialite, or third-party authentication."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Socialite Authentication
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Socialite patterns and documentation (installation, configuration, routing, callbacks, testing, scopes, stateless auth).
|
||||
|
||||
## Available Providers
|
||||
|
||||
Built-in: `facebook`, `twitter`, `twitter-oauth-2`, `linkedin`, `linkedin-openid`, `google`, `github`, `gitlab`, `bitbucket`, `slack`, `slack-openid`, `twitch`
|
||||
|
||||
Community: 150+ additional providers at [socialiteproviders.com](https://socialiteproviders.com). For provider-specific setup, use `WebFetch` on `https://socialiteproviders.com/{provider-name}`.
|
||||
|
||||
Configuration key in `config/services.php` must match the driver name exactly — note the hyphenated keys: `twitter-oauth-2`, `linkedin-openid`, `slack-openid`.
|
||||
|
||||
Twitter/X: Use `twitter-oauth-2` (OAuth 2.0) for new projects. The legacy `twitter` driver is OAuth 1.0. Driver names remain unchanged despite the platform rebrand.
|
||||
|
||||
Community providers differ from built-in providers in the following ways:
|
||||
- Installed via `composer require socialiteproviders/{name}`
|
||||
- Must register via event listener — NOT auto-discovered like built-in providers
|
||||
- Use `search-docs` for the registration pattern
|
||||
|
||||
## Adding a Provider
|
||||
|
||||
### 1. Configure the provider
|
||||
|
||||
Add the provider's `client_id`, `client_secret`, and `redirect` to `config/services.php`. The config key must match the driver name exactly.
|
||||
|
||||
### 2. Create redirect and callback routes
|
||||
|
||||
Two routes are needed: one that calls `Socialite::driver('provider')->redirect()` to send the user to the OAuth provider, and one that calls `Socialite::driver('provider')->user()` to receive the callback and retrieve user details.
|
||||
|
||||
### 3. Authenticate and store the user
|
||||
|
||||
In the callback, use `updateOrCreate` to find or create a user record from the provider's response (`id`, `name`, `email`, `token`, `refreshToken`), then call `Auth::login()`.
|
||||
|
||||
### 4. Customize the redirect (optional)
|
||||
|
||||
- `scopes()` — merge additional scopes with the provider's defaults
|
||||
- `setScopes()` — replace all scopes entirely
|
||||
- `with()` — pass optional parameters (e.g., `['hd' => 'example.com']` for Google)
|
||||
- `asBotUser()` — Slack only; generates a bot token (`xoxb-`) instead of a user token (`xoxp-`). Must be called before both `redirect()` and `user()`. Only the `token` property will be hydrated on the user object.
|
||||
- `stateless()` — for API/SPA contexts where session state is not maintained
|
||||
|
||||
### 5. Verify
|
||||
|
||||
1. Config key matches driver name exactly (check the list above for hyphenated names)
|
||||
2. `client_id`, `client_secret`, and `redirect` are all present
|
||||
3. Redirect URL matches what is registered in the provider's OAuth dashboard
|
||||
4. Callback route handles denied grants (when user declines authorization)
|
||||
|
||||
Use `search-docs` for complete code examples of each step.
|
||||
|
||||
## Additional Features
|
||||
|
||||
Use `search-docs` for usage details on: `enablePKCE()`, `userFromToken($token)`, `userFromTokenAndSecret($token, $secret)` (OAuth 1.0), retrieving user details.
|
||||
|
||||
User object: `getId()`, `getName()`, `getEmail()`, `getAvatar()`, `getNickname()`, `token`, `refreshToken`, `expiresIn`, `approvedScopes`
|
||||
|
||||
## Testing
|
||||
|
||||
Socialite provides `Socialite::fake()` for testing redirects and callbacks. Use `search-docs` for faking redirects, callback user data, custom token properties, and assertion methods.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Config key must match driver name exactly — hyphenated drivers need hyphenated keys (`linkedin-openid`, `slack-openid`, `twitter-oauth-2`). Mismatch silently fails.
|
||||
- Every provider needs `client_id`, `client_secret`, and `redirect` in `config/services.php`. Missing any one causes cryptic errors.
|
||||
- `scopes()` merges with defaults; `setScopes()` replaces all scopes entirely.
|
||||
- Missing `stateless()` in API/SPA contexts causes `InvalidStateException`.
|
||||
- Redirect URL in `config/services.php` must exactly match the provider's OAuth dashboard (including trailing slashes and protocol).
|
||||
- Do not pass `state`, `response_type`, `client_id`, `redirect_uri`, or `scope` via `with()` — these are reserved.
|
||||
- Community providers require event listener registration via `SocialiteWasCalled`.
|
||||
- `user()` throws when the user declines authorization. Always handle denied grants.
|
||||
119
.claude/skills/tailwindcss-development/SKILL.md
Normal file
119
.claude/skills/tailwindcss-development/SKILL.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
---
|
||||
name: tailwindcss-development
|
||||
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Tailwind CSS Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
|
||||
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
|
||||
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
|
||||
|
||||
## Tailwind CSS v4 Specifics
|
||||
|
||||
- Always use Tailwind CSS v4 and avoid deprecated utilities.
|
||||
- `corePlugins` is not supported in Tailwind v4.
|
||||
|
||||
### CSS-First Configuration
|
||||
|
||||
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
|
||||
|
||||
<!-- CSS-First Config -->
|
||||
```css
|
||||
@theme {
|
||||
--color-brand: oklch(0.72 0.11 178);
|
||||
}
|
||||
```
|
||||
|
||||
### Import Syntax
|
||||
|
||||
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
|
||||
|
||||
<!-- v4 Import Syntax -->
|
||||
```diff
|
||||
- @tailwind base;
|
||||
- @tailwind components;
|
||||
- @tailwind utilities;
|
||||
+ @import "tailwindcss";
|
||||
```
|
||||
|
||||
### Replaced Utilities
|
||||
|
||||
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
|
||||
|
||||
| Deprecated | Replacement |
|
||||
|------------|-------------|
|
||||
| bg-opacity-* | bg-black/* |
|
||||
| text-opacity-* | text-black/* |
|
||||
| border-opacity-* | border-black/* |
|
||||
| divide-opacity-* | divide-black/* |
|
||||
| ring-opacity-* | ring-black/* |
|
||||
| placeholder-opacity-* | placeholder-black/* |
|
||||
| flex-shrink-* | shrink-* |
|
||||
| flex-grow-* | grow-* |
|
||||
| overflow-ellipsis | text-ellipsis |
|
||||
| decoration-slice | box-decoration-slice |
|
||||
| decoration-clone | box-decoration-clone |
|
||||
|
||||
## Spacing
|
||||
|
||||
Use `gap` utilities instead of margins for spacing between siblings:
|
||||
|
||||
<!-- Gap Utilities -->
|
||||
```html
|
||||
<div class="flex gap-8">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
|
||||
|
||||
<!-- Dark Mode -->
|
||||
```html
|
||||
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
Content adapts to color scheme
|
||||
</div>
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Flexbox Layout
|
||||
|
||||
<!-- Flexbox Layout -->
|
||||
```html
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>Left content</div>
|
||||
<div>Right content</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Grid Layout
|
||||
|
||||
<!-- Grid Layout -->
|
||||
```html
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div>Card 1</div>
|
||||
<div>Card 2</div>
|
||||
<div>Card 3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
|
||||
- Using `@tailwind` directives instead of `@import "tailwindcss"`
|
||||
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
|
||||
- Using margins for spacing between siblings instead of gap utilities
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
4
.codex/config.toml
Normal file
4
.codex/config.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[mcp_servers.laravel-boost]
|
||||
command = "php"
|
||||
args = ["artisan", "boost:mcp"]
|
||||
cwd = "/Users/heyandras/devel/coolify"
|
||||
11
.cursor/mcp.json
Normal file
11
.cursor/mcp.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"laravel-boost": {
|
||||
"command": "php",
|
||||
"args": [
|
||||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,292 +0,0 @@
|
|||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Coolify Cursor Rules - Complete Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This comprehensive set of Cursor Rules provides deep insights into **Coolify**, an open-source self-hostable alternative to Heroku/Netlify/Vercel. These rules will help you understand, navigate, and contribute to this complex Laravel-based deployment platform.
|
||||
|
||||
## Rule Categories
|
||||
|
||||
### 🏗️ Architecture & Foundation
|
||||
- **[project-overview.mdc](mdc:.cursor/rules/project-overview.mdc)** - What Coolify is and its core mission
|
||||
- **[technology-stack.mdc](mdc:.cursor/rules/technology-stack.mdc)** - Complete technology stack and dependencies
|
||||
- **[application-architecture.mdc](mdc:.cursor/rules/application-architecture.mdc)** - Laravel application structure and patterns
|
||||
|
||||
### 🎨 Frontend Development
|
||||
- **[frontend-patterns.mdc](mdc:.cursor/rules/frontend-patterns.mdc)** - Livewire + Alpine.js + Tailwind architecture
|
||||
|
||||
### 🗄️ Data & Backend
|
||||
- **[database-patterns.mdc](mdc:.cursor/rules/database-patterns.mdc)** - Database architecture, models, and data management
|
||||
- **[deployment-architecture.mdc](mdc:.cursor/rules/deployment-architecture.mdc)** - Docker orchestration and deployment workflows
|
||||
|
||||
### 🌐 API & Communication
|
||||
- **[api-and-routing.mdc](mdc:.cursor/rules/api-and-routing.mdc)** - RESTful APIs, webhooks, and routing patterns
|
||||
|
||||
### 🧪 Quality Assurance
|
||||
- **[testing-patterns.mdc](mdc:.cursor/rules/testing-patterns.mdc)** - Testing strategies with Pest PHP and Laravel Dusk
|
||||
|
||||
### 🔧 Development Process
|
||||
- **[development-workflow.mdc](mdc:.cursor/rules/development-workflow.mdc)** - Development setup, coding standards, and contribution guidelines
|
||||
|
||||
### 🔒 Security
|
||||
- **[security-patterns.mdc](mdc:.cursor/rules/security-patterns.mdc)** - Security architecture, authentication, and best practices
|
||||
|
||||
## Quick Navigation
|
||||
|
||||
### Core Application Files
|
||||
- **[app/Models/Application.php](mdc:app/Models/Application.php)** - Main application entity (74KB, highly complex)
|
||||
- **[app/Models/Server.php](mdc:app/Models/Server.php)** - Server management (46KB, complex)
|
||||
- **[app/Models/Service.php](mdc:app/Models/Service.php)** - Service definitions (58KB, complex)
|
||||
- **[app/Models/Team.php](mdc:app/Models/Team.php)** - Multi-tenant structure (8.9KB)
|
||||
|
||||
### Configuration Files
|
||||
- **[composer.json](mdc:composer.json)** - PHP dependencies and Laravel setup
|
||||
- **[package.json](mdc:package.json)** - Frontend dependencies and build scripts
|
||||
- **[vite.config.js](mdc:vite.config.js)** - Frontend build configuration
|
||||
- **[docker-compose.dev.yml](mdc:docker-compose.dev.yml)** - Development environment
|
||||
|
||||
### API Documentation
|
||||
- **[openapi.json](mdc:openapi.json)** - Complete API documentation (373KB)
|
||||
- **[routes/api.php](mdc:routes/api.php)** - API endpoint definitions (13KB)
|
||||
- **[routes/web.php](mdc:routes/web.php)** - Web application routes (21KB)
|
||||
|
||||
## Key Concepts to Understand
|
||||
|
||||
### 1. Multi-Tenant Architecture
|
||||
Coolify uses a **team-based multi-tenancy** model where:
|
||||
- Users belong to multiple teams
|
||||
- Resources are scoped to teams
|
||||
- Access control is team-based
|
||||
- Data isolation is enforced at the database level
|
||||
|
||||
### 2. Deployment Philosophy
|
||||
- **Docker-first** approach for all deployments
|
||||
- **Zero-downtime** deployments with health checks
|
||||
- **Git-based** workflows with webhook integration
|
||||
- **Multi-server** support with SSH connections
|
||||
|
||||
### 3. Technology Stack
|
||||
- **Backend**: Laravel 11 + PHP 8.4
|
||||
- **Frontend**: Livewire 3.5 + Alpine.js + Tailwind CSS 4.1
|
||||
- **Database**: PostgreSQL 15 + Redis 7
|
||||
- **Containerization**: Docker + Docker Compose
|
||||
- **Testing**: Pest PHP 3.8 + Laravel Dusk
|
||||
|
||||
### 4. Security Model
|
||||
- **Defense-in-depth** security architecture
|
||||
- **OAuth integration** with multiple providers
|
||||
- **API token** authentication with Sanctum
|
||||
- **Encrypted storage** for sensitive data
|
||||
- **SSH key** management for server access
|
||||
|
||||
## Development Quick Start
|
||||
|
||||
### Local Setup
|
||||
```bash
|
||||
# Clone and setup
|
||||
git clone https://github.com/coollabsio/coolify.git
|
||||
cd coolify
|
||||
cp .env.example .env
|
||||
|
||||
# Docker development (recommended)
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
docker-compose exec app composer install
|
||||
docker-compose exec app npm install
|
||||
docker-compose exec app php artisan migrate
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# PHP code style
|
||||
./vendor/bin/pint
|
||||
|
||||
# Static analysis
|
||||
./vendor/bin/phpstan analyse
|
||||
|
||||
# Run tests
|
||||
./vendor/bin/pest
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Livewire Components
|
||||
```php
|
||||
class ApplicationShow extends Component
|
||||
{
|
||||
public Application $application;
|
||||
|
||||
protected $listeners = [
|
||||
'deployment.started' => 'refresh',
|
||||
'deployment.completed' => 'refresh',
|
||||
];
|
||||
|
||||
public function deploy(): void
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
app(ApplicationDeploymentService::class)->deploy($this->application);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Controllers
|
||||
```php
|
||||
class ApplicationController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth:sanctum');
|
||||
$this->middleware('team.access');
|
||||
}
|
||||
|
||||
public function deploy(Application $application): JsonResponse
|
||||
{
|
||||
$this->authorize('deploy', $application);
|
||||
$deployment = app(ApplicationDeploymentService::class)->deploy($application);
|
||||
return response()->json(['deployment_id' => $deployment->id]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Queue Jobs
|
||||
```php
|
||||
class DeployApplicationJob implements ShouldQueue
|
||||
{
|
||||
public function handle(DockerService $dockerService): void
|
||||
{
|
||||
$this->deployment->update(['status' => 'running']);
|
||||
|
||||
try {
|
||||
$dockerService->deployContainer($this->deployment->application);
|
||||
$this->deployment->update(['status' => 'success']);
|
||||
} catch (Exception $e) {
|
||||
$this->deployment->update(['status' => 'failed']);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Feature Tests
|
||||
```php
|
||||
test('user can deploy application via API', function () {
|
||||
$user = User::factory()->create();
|
||||
$application = Application::factory()->create(['team_id' => $user->currentTeam->id]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->postJson("/api/v1/applications/{$application->id}/deploy");
|
||||
|
||||
$response->assertStatus(200);
|
||||
expect($application->deployments()->count())->toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
### Browser Tests
|
||||
```php
|
||||
test('user can create application through UI', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user) {
|
||||
$browser->loginAs($user)
|
||||
->visit('/applications/create')
|
||||
->type('name', 'Test App')
|
||||
->press('Create Application')
|
||||
->assertSee('Application created successfully');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Authentication
|
||||
- Multi-provider OAuth support
|
||||
- API token authentication
|
||||
- Team-based access control
|
||||
- Session management
|
||||
|
||||
### Data Protection
|
||||
- Encrypted environment variables
|
||||
- Secure SSH key storage
|
||||
- Input validation and sanitization
|
||||
- SQL injection prevention
|
||||
|
||||
### Container Security
|
||||
- Non-root container users
|
||||
- Minimal capabilities
|
||||
- Read-only filesystems
|
||||
- Network isolation
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Database
|
||||
- Eager loading relationships
|
||||
- Query optimization
|
||||
- Connection pooling
|
||||
- Caching strategies
|
||||
|
||||
### Frontend
|
||||
- Lazy loading components
|
||||
- Asset optimization
|
||||
- CDN integration
|
||||
- Real-time updates via WebSockets
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
### Code Standards
|
||||
- PSR-12 PHP coding standards
|
||||
- Laravel best practices
|
||||
- Comprehensive test coverage
|
||||
- Security-first approach
|
||||
|
||||
### Pull Request Process
|
||||
1. Fork repository
|
||||
2. Create feature branch
|
||||
3. Implement with tests
|
||||
4. Run quality checks
|
||||
5. Submit PR with clear description
|
||||
|
||||
## Useful Commands
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Start development environment
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# Run tests
|
||||
./vendor/bin/pest
|
||||
|
||||
# Code formatting
|
||||
./vendor/bin/pint
|
||||
|
||||
# Frontend development
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
# Install Coolify
|
||||
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
|
||||
|
||||
# Update Coolify
|
||||
./scripts/upgrade.sh
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
### Documentation
|
||||
- **[README.md](mdc:README.md)** - Project overview and installation
|
||||
- **[CONTRIBUTING.md](mdc:CONTRIBUTING.md)** - Contribution guidelines
|
||||
- **[CHANGELOG.md](mdc:CHANGELOG.md)** - Release history
|
||||
- **[TECH_STACK.md](mdc:TECH_STACK.md)** - Technology overview
|
||||
|
||||
### Configuration
|
||||
- **[config/](mdc:config)** - Laravel configuration files
|
||||
- **[database/migrations/](mdc:database/migrations)** - Database schema
|
||||
- **[tests/](mdc:tests)** - Test suite
|
||||
|
||||
This comprehensive rule set provides everything needed to understand, develop, and contribute to the Coolify project effectively. Each rule focuses on specific aspects while maintaining connections to the broader architecture.
|
||||
|
|
@ -1,474 +0,0 @@
|
|||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Coolify API & Routing Architecture
|
||||
|
||||
## Routing Structure
|
||||
|
||||
Coolify implements **multi-layered routing** with web interfaces, RESTful APIs, webhook endpoints, and real-time communication channels.
|
||||
|
||||
## Route Files
|
||||
|
||||
### Core Route Definitions
|
||||
- **[routes/web.php](mdc:routes/web.php)** - Web application routes (21KB, 362 lines)
|
||||
- **[routes/api.php](mdc:routes/api.php)** - RESTful API endpoints (13KB, 185 lines)
|
||||
- **[routes/webhooks.php](mdc:routes/webhooks.php)** - Webhook receivers (815B, 22 lines)
|
||||
- **[routes/channels.php](mdc:routes/channels.php)** - WebSocket channel definitions (829B, 33 lines)
|
||||
- **[routes/console.php](mdc:routes/console.php)** - Artisan command routes (592B, 20 lines)
|
||||
|
||||
## Web Application Routing
|
||||
|
||||
### Authentication Routes
|
||||
```php
|
||||
// Laravel Fortify authentication
|
||||
Route::middleware('guest')->group(function () {
|
||||
Route::get('/login', [AuthController::class, 'login']);
|
||||
Route::get('/register', [AuthController::class, 'register']);
|
||||
Route::get('/forgot-password', [AuthController::class, 'forgotPassword']);
|
||||
});
|
||||
```
|
||||
|
||||
### Dashboard & Core Features
|
||||
```php
|
||||
// Main application routes
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('/dashboard', Dashboard::class)->name('dashboard');
|
||||
Route::get('/projects', ProjectIndex::class)->name('projects');
|
||||
Route::get('/servers', ServerIndex::class)->name('servers');
|
||||
Route::get('/teams', TeamIndex::class)->name('teams');
|
||||
});
|
||||
```
|
||||
|
||||
### Resource Management Routes
|
||||
```php
|
||||
// Server management
|
||||
Route::prefix('servers')->group(function () {
|
||||
Route::get('/{server}', ServerShow::class)->name('server.show');
|
||||
Route::get('/{server}/edit', ServerEdit::class)->name('server.edit');
|
||||
Route::get('/{server}/logs', ServerLogs::class)->name('server.logs');
|
||||
});
|
||||
|
||||
// Application management
|
||||
Route::prefix('applications')->group(function () {
|
||||
Route::get('/{application}', ApplicationShow::class)->name('application.show');
|
||||
Route::get('/{application}/deployments', ApplicationDeployments::class);
|
||||
Route::get('/{application}/environment-variables', ApplicationEnvironmentVariables::class);
|
||||
Route::get('/{application}/logs', ApplicationLogs::class);
|
||||
});
|
||||
```
|
||||
|
||||
## RESTful API Architecture
|
||||
|
||||
### API Versioning
|
||||
```php
|
||||
// API route structure
|
||||
Route::prefix('v1')->group(function () {
|
||||
// Application endpoints
|
||||
Route::apiResource('applications', ApplicationController::class);
|
||||
Route::apiResource('servers', ServerController::class);
|
||||
Route::apiResource('teams', TeamController::class);
|
||||
});
|
||||
```
|
||||
|
||||
### Authentication & Authorization
|
||||
```php
|
||||
// Sanctum API authentication
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::get('/user', function (Request $request) {
|
||||
return $request->user();
|
||||
});
|
||||
|
||||
// Team-scoped resources
|
||||
Route::middleware('team.access')->group(function () {
|
||||
Route::apiResource('applications', ApplicationController::class);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Application Management API
|
||||
```php
|
||||
// Application CRUD operations
|
||||
Route::prefix('applications')->group(function () {
|
||||
Route::get('/', [ApplicationController::class, 'index']);
|
||||
Route::post('/', [ApplicationController::class, 'store']);
|
||||
Route::get('/{application}', [ApplicationController::class, 'show']);
|
||||
Route::patch('/{application}', [ApplicationController::class, 'update']);
|
||||
Route::delete('/{application}', [ApplicationController::class, 'destroy']);
|
||||
|
||||
// Deployment operations
|
||||
Route::post('/{application}/deploy', [ApplicationController::class, 'deploy']);
|
||||
Route::post('/{application}/restart', [ApplicationController::class, 'restart']);
|
||||
Route::post('/{application}/stop', [ApplicationController::class, 'stop']);
|
||||
Route::get('/{application}/logs', [ApplicationController::class, 'logs']);
|
||||
});
|
||||
```
|
||||
|
||||
### Server Management API
|
||||
```php
|
||||
// Server operations
|
||||
Route::prefix('servers')->group(function () {
|
||||
Route::get('/', [ServerController::class, 'index']);
|
||||
Route::post('/', [ServerController::class, 'store']);
|
||||
Route::get('/{server}', [ServerController::class, 'show']);
|
||||
Route::patch('/{server}', [ServerController::class, 'update']);
|
||||
Route::delete('/{server}', [ServerController::class, 'destroy']);
|
||||
|
||||
// Server actions
|
||||
Route::post('/{server}/validate', [ServerController::class, 'validate']);
|
||||
Route::get('/{server}/usage', [ServerController::class, 'usage']);
|
||||
Route::post('/{server}/cleanup', [ServerController::class, 'cleanup']);
|
||||
});
|
||||
```
|
||||
|
||||
### Database Management API
|
||||
```php
|
||||
// Database operations
|
||||
Route::prefix('databases')->group(function () {
|
||||
Route::get('/', [DatabaseController::class, 'index']);
|
||||
Route::post('/', [DatabaseController::class, 'store']);
|
||||
Route::get('/{database}', [DatabaseController::class, 'show']);
|
||||
Route::patch('/{database}', [DatabaseController::class, 'update']);
|
||||
Route::delete('/{database}', [DatabaseController::class, 'destroy']);
|
||||
|
||||
// Database actions
|
||||
Route::post('/{database}/backup', [DatabaseController::class, 'backup']);
|
||||
Route::post('/{database}/restore', [DatabaseController::class, 'restore']);
|
||||
Route::get('/{database}/logs', [DatabaseController::class, 'logs']);
|
||||
});
|
||||
```
|
||||
|
||||
## Webhook Architecture
|
||||
|
||||
### Git Integration Webhooks
|
||||
```php
|
||||
// GitHub webhook endpoints
|
||||
Route::post('/webhooks/github/{application}', [GitHubWebhookController::class, 'handle'])
|
||||
->name('webhooks.github');
|
||||
|
||||
// GitLab webhook endpoints
|
||||
Route::post('/webhooks/gitlab/{application}', [GitLabWebhookController::class, 'handle'])
|
||||
->name('webhooks.gitlab');
|
||||
|
||||
// Generic Git webhooks
|
||||
Route::post('/webhooks/git/{application}', [GitWebhookController::class, 'handle'])
|
||||
->name('webhooks.git');
|
||||
```
|
||||
|
||||
### Deployment Webhooks
|
||||
```php
|
||||
// Deployment status webhooks
|
||||
Route::post('/webhooks/deployment/{deployment}/success', [DeploymentWebhookController::class, 'success']);
|
||||
Route::post('/webhooks/deployment/{deployment}/failure', [DeploymentWebhookController::class, 'failure']);
|
||||
Route::post('/webhooks/deployment/{deployment}/progress', [DeploymentWebhookController::class, 'progress']);
|
||||
```
|
||||
|
||||
### Third-Party Integration Webhooks
|
||||
```php
|
||||
// Monitoring webhooks
|
||||
Route::post('/webhooks/monitoring/{server}', [MonitoringWebhookController::class, 'handle']);
|
||||
|
||||
// Backup status webhooks
|
||||
Route::post('/webhooks/backup/{backup}', [BackupWebhookController::class, 'handle']);
|
||||
|
||||
// SSL certificate webhooks
|
||||
Route::post('/webhooks/ssl/{certificate}', [SslWebhookController::class, 'handle']);
|
||||
```
|
||||
|
||||
## WebSocket Channel Definitions
|
||||
|
||||
### Real-Time Channels
|
||||
```php
|
||||
// Private channels for team members
|
||||
Broadcast::channel('team.{teamId}', function ($user, $teamId) {
|
||||
return $user->teams->contains('id', $teamId);
|
||||
});
|
||||
|
||||
// Application deployment channels
|
||||
Broadcast::channel('application.{applicationId}', function ($user, $applicationId) {
|
||||
return $user->hasAccessToApplication($applicationId);
|
||||
});
|
||||
|
||||
// Server monitoring channels
|
||||
Broadcast::channel('server.{serverId}', function ($user, $serverId) {
|
||||
return $user->hasAccessToServer($serverId);
|
||||
});
|
||||
```
|
||||
|
||||
### Presence Channels
|
||||
```php
|
||||
// Team collaboration presence
|
||||
Broadcast::channel('team.{teamId}.presence', function ($user, $teamId) {
|
||||
if ($user->teams->contains('id', $teamId)) {
|
||||
return ['id' => $user->id, 'name' => $user->name];
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## API Controllers
|
||||
|
||||
### Location: [app/Http/Controllers/Api/](mdc:app/Http/Controllers)
|
||||
|
||||
#### Resource Controllers
|
||||
```php
|
||||
class ApplicationController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApplicationResource::collection(
|
||||
$request->user()->currentTeam->applications()
|
||||
->with(['server', 'environment'])
|
||||
->paginate()
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreApplicationRequest $request)
|
||||
{
|
||||
$application = $request->user()->currentTeam
|
||||
->applications()
|
||||
->create($request->validated());
|
||||
|
||||
return new ApplicationResource($application);
|
||||
}
|
||||
|
||||
public function deploy(Application $application)
|
||||
{
|
||||
$deployment = $application->deploy();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Deployment started',
|
||||
'deployment_id' => $deployment->id
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Responses & Resources
|
||||
```php
|
||||
// API Resource classes
|
||||
class ApplicationResource extends JsonResource
|
||||
{
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'fqdn' => $this->fqdn,
|
||||
'status' => $this->status,
|
||||
'git_repository' => $this->git_repository,
|
||||
'git_branch' => $this->git_branch,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'server' => new ServerResource($this->whenLoaded('server')),
|
||||
'environment' => new EnvironmentResource($this->whenLoaded('environment')),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Authentication
|
||||
|
||||
### Sanctum Token Authentication
|
||||
```php
|
||||
// API token generation
|
||||
Route::post('/auth/tokens', function (Request $request) {
|
||||
$request->validate([
|
||||
'name' => 'required|string',
|
||||
'abilities' => 'array'
|
||||
]);
|
||||
|
||||
$token = $request->user()->createToken(
|
||||
$request->name,
|
||||
$request->abilities ?? []
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'token' => $token->plainTextToken,
|
||||
'abilities' => $token->accessToken->abilities
|
||||
]);
|
||||
});
|
||||
```
|
||||
|
||||
### Team-Based Authorization
|
||||
```php
|
||||
// Team access middleware
|
||||
class EnsureTeamAccess
|
||||
{
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$teamId = $request->route('team');
|
||||
|
||||
if (!$request->user()->teams->contains('id', $teamId)) {
|
||||
abort(403, 'Access denied to team resources');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### API Rate Limits
|
||||
```php
|
||||
// API throttling configuration
|
||||
RateLimiter::for('api', function (Request $request) {
|
||||
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
|
||||
});
|
||||
|
||||
// Deployment rate limiting
|
||||
RateLimiter::for('deployments', function (Request $request) {
|
||||
return Limit::perMinute(10)->by($request->user()->id);
|
||||
});
|
||||
```
|
||||
|
||||
### Webhook Rate Limiting
|
||||
```php
|
||||
// Webhook throttling
|
||||
RateLimiter::for('webhooks', function (Request $request) {
|
||||
return Limit::perMinute(100)->by($request->ip());
|
||||
});
|
||||
```
|
||||
|
||||
## Route Model Binding
|
||||
|
||||
### Custom Route Bindings
|
||||
```php
|
||||
// Custom model binding for applications
|
||||
Route::bind('application', function ($value) {
|
||||
return Application::where('uuid', $value)
|
||||
->orWhere('id', $value)
|
||||
->firstOrFail();
|
||||
});
|
||||
|
||||
// Team-scoped model binding
|
||||
Route::bind('team_application', function ($value, $route) {
|
||||
$teamId = $route->parameter('team');
|
||||
return Application::whereHas('environment.project', function ($query) use ($teamId) {
|
||||
$query->where('team_id', $teamId);
|
||||
})->findOrFail($value);
|
||||
});
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
### OpenAPI Specification
|
||||
- **[openapi.json](mdc:openapi.json)** - API documentation (373KB, 8316 lines)
|
||||
- **[openapi.yaml](mdc:openapi.yaml)** - YAML format documentation (184KB, 5579 lines)
|
||||
|
||||
### Documentation Generation
|
||||
```php
|
||||
// Swagger/OpenAPI annotations
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/applications",
|
||||
* summary="List applications",
|
||||
* tags={"Applications"},
|
||||
* security={{"bearerAuth":{}}},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="List of applications",
|
||||
* @OA\JsonContent(type="array", @OA\Items(ref="#/components/schemas/Application"))
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### API Error Responses
|
||||
```php
|
||||
// Standardized error response format
|
||||
class ApiExceptionHandler
|
||||
{
|
||||
public function render($request, Throwable $exception)
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'message' => $exception->getMessage(),
|
||||
'error_code' => $this->getErrorCode($exception),
|
||||
'timestamp' => now()->toISOString()
|
||||
], $this->getStatusCode($exception));
|
||||
}
|
||||
|
||||
return parent::render($request, $exception);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Error Handling
|
||||
```php
|
||||
// Form request validation
|
||||
class StoreApplicationRequest extends FormRequest
|
||||
{
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'git_repository' => 'required|url',
|
||||
'git_branch' => 'required|string',
|
||||
'server_id' => 'required|exists:servers,id',
|
||||
'environment_id' => 'required|exists:environments,id'
|
||||
];
|
||||
}
|
||||
|
||||
public function failedValidation(Validator $validator)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
response()->json([
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $validator->errors()
|
||||
], 422)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Real-Time API Integration
|
||||
|
||||
### WebSocket Events
|
||||
```php
|
||||
// Broadcasting deployment events
|
||||
class DeploymentStarted implements ShouldBroadcast
|
||||
{
|
||||
public $application;
|
||||
public $deployment;
|
||||
|
||||
public function broadcastOn()
|
||||
{
|
||||
return [
|
||||
new PrivateChannel("application.{$this->application->id}"),
|
||||
new PrivateChannel("team.{$this->application->team->id}")
|
||||
];
|
||||
}
|
||||
|
||||
public function broadcastWith()
|
||||
{
|
||||
return [
|
||||
'deployment_id' => $this->deployment->id,
|
||||
'status' => 'started',
|
||||
'timestamp' => now()
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Event Streaming
|
||||
```php
|
||||
// Server-Sent Events for real-time updates
|
||||
Route::get('/api/v1/applications/{application}/events', function (Application $application) {
|
||||
return response()->stream(function () use ($application) {
|
||||
while (true) {
|
||||
$events = $application->getRecentEvents();
|
||||
foreach ($events as $event) {
|
||||
echo "data: " . json_encode($event) . "\n\n";
|
||||
}
|
||||
usleep(1000000); // 1 second
|
||||
}
|
||||
}, 200, [
|
||||
'Content-Type' => 'text/event-stream',
|
||||
'Cache-Control' => 'no-cache',
|
||||
]);
|
||||
});
|
||||
```
|
||||
|
|
@ -1,368 +0,0 @@
|
|||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Coolify Application Architecture
|
||||
|
||||
## Laravel Project Structure
|
||||
|
||||
### **Core Application Directory** ([app/](mdc:app))
|
||||
|
||||
```
|
||||
app/
|
||||
├── Actions/ # Business logic actions (Action pattern)
|
||||
├── Console/ # Artisan commands
|
||||
├── Contracts/ # Interface definitions
|
||||
├── Data/ # Data Transfer Objects (Spatie Laravel Data)
|
||||
├── Enums/ # Enumeration classes
|
||||
├── Events/ # Event classes
|
||||
├── Exceptions/ # Custom exception classes
|
||||
├── Helpers/ # Utility helper classes
|
||||
├── Http/ # HTTP layer (Controllers, Middleware, Requests)
|
||||
├── Jobs/ # Background job classes
|
||||
├── Listeners/ # Event listeners
|
||||
├── Livewire/ # Livewire components (Frontend)
|
||||
├── Models/ # Eloquent models (Domain entities)
|
||||
├── Notifications/ # Notification classes
|
||||
├── Policies/ # Authorization policies
|
||||
├── Providers/ # Service providers
|
||||
├── Repositories/ # Repository pattern implementations
|
||||
├── Services/ # Service layer classes
|
||||
├── Traits/ # Reusable trait classes
|
||||
└── View/ # View composers and creators
|
||||
```
|
||||
|
||||
## Core Domain Models
|
||||
|
||||
### **Infrastructure Management**
|
||||
|
||||
#### **[Server.php](mdc:app/Models/Server.php)** (46KB, 1343 lines)
|
||||
- **Purpose**: Physical/virtual server management
|
||||
- **Key Relationships**:
|
||||
- `hasMany(Application::class)` - Deployed applications
|
||||
- `hasMany(StandalonePostgresql::class)` - Database instances
|
||||
- `belongsTo(Team::class)` - Team ownership
|
||||
- **Key Features**:
|
||||
- SSH connection management
|
||||
- Resource monitoring
|
||||
- Proxy configuration (Traefik/Caddy)
|
||||
- Docker daemon interaction
|
||||
|
||||
#### **[Application.php](mdc:app/Models/Application.php)** (74KB, 1734 lines)
|
||||
- **Purpose**: Application deployment and management
|
||||
- **Key Relationships**:
|
||||
- `belongsTo(Server::class)` - Deployment target
|
||||
- `belongsTo(Environment::class)` - Environment context
|
||||
- `hasMany(ApplicationDeploymentQueue::class)` - Deployment history
|
||||
- **Key Features**:
|
||||
- Git repository integration
|
||||
- Docker build and deployment
|
||||
- Environment variable management
|
||||
- SSL certificate handling
|
||||
|
||||
#### **[Service.php](mdc:app/Models/Service.php)** (58KB, 1325 lines)
|
||||
- **Purpose**: Multi-container service orchestration
|
||||
- **Key Relationships**:
|
||||
- `hasMany(ServiceApplication::class)` - Service components
|
||||
- `hasMany(ServiceDatabase::class)` - Service databases
|
||||
- `belongsTo(Environment::class)` - Environment context
|
||||
- **Key Features**:
|
||||
- Docker Compose generation
|
||||
- Service dependency management
|
||||
- Health check configuration
|
||||
|
||||
### **Team & Project Organization**
|
||||
|
||||
#### **[Team.php](mdc:app/Models/Team.php)** (8.9KB, 308 lines)
|
||||
- **Purpose**: Multi-tenant team management
|
||||
- **Key Relationships**:
|
||||
- `hasMany(User::class)` - Team members
|
||||
- `hasMany(Project::class)` - Team projects
|
||||
- `hasMany(Server::class)` - Team servers
|
||||
- **Key Features**:
|
||||
- Resource limits and quotas
|
||||
- Team-based access control
|
||||
- Subscription management
|
||||
|
||||
#### **[Project.php](mdc:app/Models/Project.php)** (4.3KB, 156 lines)
|
||||
- **Purpose**: Project organization and grouping
|
||||
- **Key Relationships**:
|
||||
- `hasMany(Environment::class)` - Project environments
|
||||
- `belongsTo(Team::class)` - Team ownership
|
||||
- **Key Features**:
|
||||
- Environment isolation
|
||||
- Resource organization
|
||||
|
||||
#### **[Environment.php](mdc:app/Models/Environment.php)**
|
||||
- **Purpose**: Environment-specific configuration
|
||||
- **Key Relationships**:
|
||||
- `hasMany(Application::class)` - Environment applications
|
||||
- `hasMany(Service::class)` - Environment services
|
||||
- `belongsTo(Project::class)` - Project context
|
||||
|
||||
### **Database Management Models**
|
||||
|
||||
#### **Standalone Database Models**
|
||||
- **[StandalonePostgresql.php](mdc:app/Models/StandalonePostgresql.php)** (11KB, 351 lines)
|
||||
- **[StandaloneMysql.php](mdc:app/Models/StandaloneMysql.php)** (11KB, 351 lines)
|
||||
- **[StandaloneMariadb.php](mdc:app/Models/StandaloneMariadb.php)** (10KB, 337 lines)
|
||||
- **[StandaloneMongodb.php](mdc:app/Models/StandaloneMongodb.php)** (12KB, 370 lines)
|
||||
- **[StandaloneRedis.php](mdc:app/Models/StandaloneRedis.php)** (12KB, 394 lines)
|
||||
- **[StandaloneKeydb.php](mdc:app/Models/StandaloneKeydb.php)** (11KB, 347 lines)
|
||||
- **[StandaloneDragonfly.php](mdc:app/Models/StandaloneDragonfly.php)** (11KB, 347 lines)
|
||||
- **[StandaloneClickhouse.php](mdc:app/Models/StandaloneClickhouse.php)** (10KB, 336 lines)
|
||||
|
||||
**Common Features**:
|
||||
- Database configuration management
|
||||
- Backup scheduling and execution
|
||||
- Connection string generation
|
||||
- Health monitoring
|
||||
|
||||
### **Configuration & Settings**
|
||||
|
||||
#### **[EnvironmentVariable.php](mdc:app/Models/EnvironmentVariable.php)** (7.6KB, 219 lines)
|
||||
- **Purpose**: Application environment variable management
|
||||
- **Key Features**:
|
||||
- Encrypted value storage
|
||||
- Build-time vs runtime variables
|
||||
- Shared variable inheritance
|
||||
|
||||
#### **[InstanceSettings.php](mdc:app/Models/InstanceSettings.php)** (3.2KB, 124 lines)
|
||||
- **Purpose**: Global Coolify instance configuration
|
||||
- **Key Features**:
|
||||
- FQDN and port configuration
|
||||
- Auto-update settings
|
||||
- Security configurations
|
||||
|
||||
## Architectural Patterns
|
||||
|
||||
### **Action Pattern** ([app/Actions/](mdc:app/Actions))
|
||||
|
||||
Using [lorisleiva/laravel-actions](mdc:composer.json) for business logic encapsulation:
|
||||
|
||||
```php
|
||||
// Example Action structure
|
||||
class DeployApplication extends Action
|
||||
{
|
||||
public function handle(Application $application): void
|
||||
{
|
||||
// Business logic for deployment
|
||||
}
|
||||
|
||||
public function asJob(Application $application): void
|
||||
{
|
||||
// Queue job implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Action Categories**:
|
||||
- **Application/**: Deployment and management actions
|
||||
- **Database/**: Database operations
|
||||
- **Server/**: Server management actions
|
||||
- **Service/**: Service orchestration actions
|
||||
|
||||
### **Repository Pattern** ([app/Repositories/](mdc:app/Repositories))
|
||||
|
||||
Data access abstraction layer:
|
||||
- Encapsulates database queries
|
||||
- Provides testable data layer
|
||||
- Abstracts complex query logic
|
||||
|
||||
### **Service Layer** ([app/Services/](mdc:app/Services))
|
||||
|
||||
Business logic services:
|
||||
- External API integrations
|
||||
- Complex business operations
|
||||
- Cross-cutting concerns
|
||||
|
||||
## Data Flow Architecture
|
||||
|
||||
### **Request Lifecycle**
|
||||
|
||||
1. **HTTP Request** → [routes/web.php](mdc:routes/web.php)
|
||||
2. **Middleware** → Authentication, authorization
|
||||
3. **Livewire Component** → [app/Livewire/](mdc:app/Livewire)
|
||||
4. **Action/Service** → Business logic execution
|
||||
5. **Model/Repository** → Data persistence
|
||||
6. **Response** → Livewire reactive update
|
||||
|
||||
### **Background Processing**
|
||||
|
||||
1. **Job Dispatch** → Queue system (Redis)
|
||||
2. **Job Processing** → [app/Jobs/](mdc:app/Jobs)
|
||||
3. **Action Execution** → Business logic
|
||||
4. **Event Broadcasting** → Real-time updates
|
||||
5. **Notification** → User feedback
|
||||
|
||||
## Security Architecture
|
||||
|
||||
### **Multi-Tenant Isolation**
|
||||
|
||||
```php
|
||||
// Team-based query scoping
|
||||
class Application extends Model
|
||||
{
|
||||
public function scopeOwnedByCurrentTeam($query)
|
||||
{
|
||||
return $query->whereHas('environment.project.team', function ($q) {
|
||||
$q->where('id', currentTeam()->id);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **Authorization Layers**
|
||||
|
||||
1. **Team Membership** → User belongs to team
|
||||
2. **Resource Ownership** → Resource belongs to team
|
||||
3. **Policy Authorization** → [app/Policies/](mdc:app/Policies)
|
||||
4. **Environment Isolation** → Project/environment boundaries
|
||||
|
||||
### **Data Protection**
|
||||
|
||||
- **Environment Variables**: Encrypted at rest
|
||||
- **SSH Keys**: Secure storage and transmission
|
||||
- **API Tokens**: Sanctum-based authentication
|
||||
- **Audit Logging**: [spatie/laravel-activitylog](mdc:composer.json)
|
||||
|
||||
## Configuration Hierarchy
|
||||
|
||||
### **Global Configuration**
|
||||
- **[InstanceSettings](mdc:app/Models/InstanceSettings.php)**: System-wide settings
|
||||
- **[config/](mdc:config)**: Laravel configuration files
|
||||
|
||||
### **Team Configuration**
|
||||
- **[Team](mdc:app/Models/Team.php)**: Team-specific settings
|
||||
- **[ServerSetting](mdc:app/Models/ServerSetting.php)**: Server configurations
|
||||
|
||||
### **Project Configuration**
|
||||
- **[ProjectSetting](mdc:app/Models/ProjectSetting.php)**: Project settings
|
||||
- **[Environment](mdc:app/Models/Environment.php)**: Environment variables
|
||||
|
||||
### **Application Configuration**
|
||||
- **[ApplicationSetting](mdc:app/Models/ApplicationSetting.php)**: App-specific settings
|
||||
- **[EnvironmentVariable](mdc:app/Models/EnvironmentVariable.php)**: Runtime configuration
|
||||
|
||||
## Event-Driven Architecture
|
||||
|
||||
### **Event Broadcasting** ([app/Events/](mdc:app/Events))
|
||||
|
||||
Real-time updates using Laravel Echo and WebSockets:
|
||||
|
||||
```php
|
||||
// Example event structure
|
||||
class ApplicationDeploymentStarted implements ShouldBroadcast
|
||||
{
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PrivateChannel("team.{$this->application->team->id}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **Event Listeners** ([app/Listeners/](mdc:app/Listeners))
|
||||
|
||||
- Deployment status updates
|
||||
- Resource monitoring alerts
|
||||
- Notification dispatching
|
||||
- Audit log creation
|
||||
|
||||
## Database Design Patterns
|
||||
|
||||
### **Polymorphic Relationships**
|
||||
|
||||
```php
|
||||
// Environment variables can belong to multiple resource types
|
||||
class EnvironmentVariable extends Model
|
||||
{
|
||||
public function resource(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **Team-Based Soft Scoping**
|
||||
|
||||
All major resources include team-based query scoping:
|
||||
|
||||
```php
|
||||
// Automatic team filtering
|
||||
$applications = Application::ownedByCurrentTeam()->get();
|
||||
$servers = Server::ownedByCurrentTeam()->get();
|
||||
```
|
||||
|
||||
### **Configuration Inheritance**
|
||||
|
||||
Environment variables cascade from:
|
||||
1. **Shared Variables** → Team-wide defaults
|
||||
2. **Project Variables** → Project-specific overrides
|
||||
3. **Application Variables** → Application-specific values
|
||||
|
||||
## Integration Patterns
|
||||
|
||||
### **Git Provider Integration**
|
||||
|
||||
Abstracted git operations supporting:
|
||||
- **GitHub**: [app/Models/GithubApp.php](mdc:app/Models/GithubApp.php)
|
||||
- **GitLab**: [app/Models/GitlabApp.php](mdc:app/Models/GitlabApp.php)
|
||||
- **Bitbucket**: Webhook integration
|
||||
- **Gitea**: Self-hosted Git support
|
||||
|
||||
### **Docker Integration**
|
||||
|
||||
- **Container Management**: Direct Docker API communication
|
||||
- **Image Building**: Dockerfile and Buildpack support
|
||||
- **Network Management**: Custom Docker networks
|
||||
- **Volume Management**: Persistent storage handling
|
||||
|
||||
### **SSH Communication**
|
||||
|
||||
- **[phpseclib/phpseclib](mdc:composer.json)**: Secure SSH connections
|
||||
- **Multiplexing**: Connection pooling for efficiency
|
||||
- **Key Management**: [PrivateKey](mdc:app/Models/PrivateKey.php) model
|
||||
|
||||
## Testing Architecture
|
||||
|
||||
### **Test Structure** ([tests/](mdc:tests))
|
||||
|
||||
```
|
||||
tests/
|
||||
├── Feature/ # Integration tests
|
||||
├── Unit/ # Unit tests
|
||||
├── Browser/ # Dusk browser tests
|
||||
├── Traits/ # Test helper traits
|
||||
├── Pest.php # Pest configuration
|
||||
└── TestCase.php # Base test case
|
||||
```
|
||||
|
||||
### **Testing Patterns**
|
||||
|
||||
- **Feature Tests**: Full request lifecycle testing
|
||||
- **Unit Tests**: Individual class/method testing
|
||||
- **Browser Tests**: End-to-end user workflows
|
||||
- **Database Testing**: Factories and seeders
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### **Query Optimization**
|
||||
|
||||
- **Eager Loading**: Prevent N+1 queries
|
||||
- **Query Scoping**: Team-based filtering
|
||||
- **Database Indexing**: Optimized for common queries
|
||||
|
||||
### **Caching Strategy**
|
||||
|
||||
- **Redis**: Session and cache storage
|
||||
- **Model Caching**: Frequently accessed data
|
||||
- **Query Caching**: Expensive query results
|
||||
|
||||
### **Background Processing**
|
||||
|
||||
- **Queue Workers**: Horizon-managed job processing
|
||||
- **Job Batching**: Related job grouping
|
||||
- **Failed Job Handling**: Automatic retry logic
|
||||
156
.cursor/rules/coolify-ai-docs.mdc
Normal file
156
.cursor/rules/coolify-ai-docs.mdc
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
title: Coolify AI Documentation
|
||||
description: Master reference to all Coolify AI documentation in .ai/ directory
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Coolify AI Documentation
|
||||
|
||||
All Coolify AI documentation has been consolidated in the **`.ai/`** directory for better organization and single source of truth.
|
||||
|
||||
## Quick Start
|
||||
|
||||
- **For Claude Code**: Start with `CLAUDE.md` in the root directory
|
||||
- **For Cursor IDE**: Start with `.ai/README.md` for navigation
|
||||
- **For All AI Tools**: Browse `.ai/` directory by topic
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
All detailed documentation lives in `.ai/` with the following organization:
|
||||
|
||||
### 📚 Core Documentation
|
||||
- **[Technology Stack](.ai/core/technology-stack.md)** - All versions, packages, dependencies (SINGLE SOURCE OF TRUTH for versions)
|
||||
- **[Project Overview](.ai/core/project-overview.md)** - What Coolify is, high-level architecture
|
||||
- **[Application Architecture](.ai/core/application-architecture.md)** - System design, components, relationships
|
||||
- **[Deployment Architecture](.ai/core/deployment-architecture.md)** - Deployment flows, Docker, proxies
|
||||
|
||||
### 💻 Development
|
||||
- **[Development Workflow](.ai/development/development-workflow.md)** - Dev setup, commands, daily workflows
|
||||
- **[Testing Patterns](.ai/development/testing-patterns.md)** - How to write/run tests, Docker requirements
|
||||
- **[Laravel Boost](.ai/development/laravel-boost.md)** - Laravel-specific guidelines (SINGLE SOURCE for Laravel Boost)
|
||||
|
||||
### 🎨 Code Patterns
|
||||
- **[Database Patterns](.ai/patterns/database-patterns.md)** - Eloquent, migrations, relationships
|
||||
- **[Frontend Patterns](.ai/patterns/frontend-patterns.md)** - Livewire, Alpine.js, Tailwind CSS
|
||||
- **[Security Patterns](.ai/patterns/security-patterns.md)** - Auth, authorization, security
|
||||
- **[Form Components](.ai/patterns/form-components.md)** - Enhanced forms with authorization
|
||||
- **[API & Routing](.ai/patterns/api-and-routing.md)** - API design, routing conventions
|
||||
|
||||
### 📖 Meta
|
||||
- **[Maintaining Docs](.ai/meta/maintaining-docs.md)** - How to update/improve documentation
|
||||
- **[Sync Guide](.ai/meta/sync-guide.md)** - Keeping docs synchronized
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
**What are you working on?**
|
||||
|
||||
### Running Commands
|
||||
→ `.ai/development/development-workflow.md`
|
||||
- `npm run dev` / `npm run build` - Frontend
|
||||
- `php artisan serve` / `php artisan migrate` - Backend
|
||||
- `docker exec coolify php artisan test` - Feature tests (requires Docker)
|
||||
- `./vendor/bin/pest tests/Unit` - Unit tests (no Docker needed)
|
||||
- `./vendor/bin/pint` - Code formatting
|
||||
|
||||
### Writing Tests
|
||||
→ `.ai/development/testing-patterns.md`
|
||||
- **Unit tests**: No database, use mocking, run outside Docker
|
||||
- **Feature tests**: Can use database, MUST run inside Docker
|
||||
- Critical: Docker execution requirements prevent database connection errors
|
||||
|
||||
### Building UI
|
||||
→ `.ai/patterns/frontend-patterns.md` + `.ai/patterns/form-components.md`
|
||||
- Livewire 3.5.20 with server-side state
|
||||
- Alpine.js for client interactions
|
||||
- Tailwind CSS 4.1.4 styling
|
||||
- Form components with `canGate` authorization
|
||||
|
||||
### Database Work
|
||||
→ `.ai/patterns/database-patterns.md`
|
||||
- Eloquent ORM patterns
|
||||
- Migration best practices
|
||||
- Relationship definitions
|
||||
- Query optimization
|
||||
|
||||
### Security & Authorization
|
||||
→ `.ai/patterns/security-patterns.md` + `.ai/patterns/form-components.md`
|
||||
- Team-based access control
|
||||
- Policy and gate patterns
|
||||
- Form authorization (`canGate`, `canResource`)
|
||||
- API security with Sanctum
|
||||
|
||||
### Laravel-Specific
|
||||
→ `.ai/development/laravel-boost.md`
|
||||
- Laravel 12.4.1 patterns
|
||||
- Livewire 3 best practices
|
||||
- Pest testing patterns
|
||||
- Laravel conventions
|
||||
|
||||
### Version Numbers
|
||||
→ `.ai/core/technology-stack.md`
|
||||
- **SINGLE SOURCE OF TRUTH** for all version numbers
|
||||
- Laravel 12.4.1, PHP 8.4.7, Tailwind 4.1.4, etc.
|
||||
- Never duplicate versions - always reference this file
|
||||
|
||||
## Critical Patterns (Always Follow)
|
||||
|
||||
### Testing Commands
|
||||
```bash
|
||||
# Unit tests (no database, outside Docker)
|
||||
./vendor/bin/pest tests/Unit
|
||||
|
||||
# Feature tests (requires database, inside Docker)
|
||||
docker exec coolify php artisan test
|
||||
```
|
||||
|
||||
**NEVER** run Feature tests outside Docker - they will fail with database connection errors.
|
||||
|
||||
### Form Authorization
|
||||
ALWAYS include authorization on form components:
|
||||
```blade
|
||||
<x-forms.input canGate="update" :canResource="$resource" id="name" label="Name" />
|
||||
```
|
||||
|
||||
### Livewire Components
|
||||
MUST have exactly ONE root element. No exceptions.
|
||||
|
||||
### Version Numbers
|
||||
Use exact versions from `technology-stack.md`:
|
||||
- ✅ Laravel 12.4.1
|
||||
- ❌ Laravel 12 or "v12"
|
||||
|
||||
### Code Style
|
||||
```bash
|
||||
# Always run before committing
|
||||
./vendor/bin/pint
|
||||
```
|
||||
|
||||
## For AI Assistants
|
||||
|
||||
### Important Notes
|
||||
1. **Single Source of Truth**: Each piece of information exists in ONE location only
|
||||
2. **Cross-Reference, Don't Duplicate**: Link to other files instead of copying content
|
||||
3. **Version Precision**: Always use exact versions from `technology-stack.md`
|
||||
4. **Docker for Feature Tests**: This is non-negotiable for database-dependent tests
|
||||
5. **Form Authorization**: Security requirement, not optional
|
||||
|
||||
### When to Use Which File
|
||||
- **Quick commands**: `CLAUDE.md` or `development-workflow.md`
|
||||
- **Detailed patterns**: Topic-specific files in `.ai/patterns/`
|
||||
- **Testing**: `.ai/development/testing-patterns.md`
|
||||
- **Laravel specifics**: `.ai/development/laravel-boost.md`
|
||||
- **Versions**: `.ai/core/technology-stack.md`
|
||||
|
||||
## Maintaining Documentation
|
||||
|
||||
When updating documentation:
|
||||
1. Read `.ai/meta/maintaining-docs.md` first
|
||||
2. Follow single source of truth principle
|
||||
3. Update cross-references when moving content
|
||||
4. Test all links work
|
||||
5. See `.ai/meta/sync-guide.md` for sync guidelines
|
||||
|
||||
## Migration Note
|
||||
|
||||
This file replaces all previous `.cursor/rules/*.mdc` files. All content has been migrated to `.ai/` directory for better organization and to serve as single source of truth for all AI tools (Claude Code, Cursor IDE, etc.).
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
---
|
||||
description: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness.
|
||||
globs: .cursor/rules/*.mdc
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
- **Required Rule Structure:**
|
||||
```markdown
|
||||
---
|
||||
description: Clear, one-line description of what the rule enforces
|
||||
globs: path/to/files/*.ext, other/path/**/*
|
||||
alwaysApply: boolean
|
||||
---
|
||||
|
||||
- **Main Points in Bold**
|
||||
- Sub-points with details
|
||||
- Examples and explanations
|
||||
```
|
||||
|
||||
- **File References:**
|
||||
- Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files
|
||||
- Example: [prisma.mdc](mdc:.cursor/rules/prisma.mdc) for rule references
|
||||
- Example: [schema.prisma](mdc:prisma/schema.prisma) for code references
|
||||
|
||||
- **Code Examples:**
|
||||
- Use language-specific code blocks
|
||||
```typescript
|
||||
// ✅ DO: Show good examples
|
||||
const goodExample = true;
|
||||
|
||||
// ❌ DON'T: Show anti-patterns
|
||||
const badExample = false;
|
||||
```
|
||||
|
||||
- **Rule Content Guidelines:**
|
||||
- Start with high-level overview
|
||||
- Include specific, actionable requirements
|
||||
- Show examples of correct implementation
|
||||
- Reference existing code when possible
|
||||
- Keep rules DRY by referencing other rules
|
||||
|
||||
- **Rule Maintenance:**
|
||||
- Update rules when new patterns emerge
|
||||
- Add examples from actual codebase
|
||||
- Remove outdated patterns
|
||||
- Cross-reference related rules
|
||||
|
||||
- **Best Practices:**
|
||||
- Use bullet points for clarity
|
||||
- Keep descriptions concise
|
||||
- Include both DO and DON'T examples
|
||||
- Reference actual code over theoretical examples
|
||||
- Use consistent formatting across rules
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Coolify Database Architecture & Patterns
|
||||
|
||||
## Database Strategy
|
||||
|
||||
Coolify uses **PostgreSQL 15** as the primary database with **Redis 7** for caching and real-time features. The architecture supports managing multiple external databases across different servers.
|
||||
|
||||
## Primary Database (PostgreSQL)
|
||||
|
||||
### Core Tables & Models
|
||||
|
||||
#### User & Team Management
|
||||
- **[User.php](mdc:app/Models/User.php)** - User authentication and profiles
|
||||
- **[Team.php](mdc:app/Models/Team.php)** - Multi-tenant organization structure
|
||||
- **[TeamInvitation.php](mdc:app/Models/TeamInvitation.php)** - Team collaboration invitations
|
||||
- **[PersonalAccessToken.php](mdc:app/Models/PersonalAccessToken.php)** - API token management
|
||||
|
||||
#### Infrastructure Management
|
||||
- **[Server.php](mdc:app/Models/Server.php)** - Physical/virtual server definitions (46KB, complex)
|
||||
- **[PrivateKey.php](mdc:app/Models/PrivateKey.php)** - SSH key management
|
||||
- **[ServerSetting.php](mdc:app/Models/ServerSetting.php)** - Server-specific configurations
|
||||
|
||||
#### Project Organization
|
||||
- **[Project.php](mdc:app/Models/Project.php)** - Project containers for applications
|
||||
- **[Environment.php](mdc:app/Models/Environment.php)** - Environment isolation (staging, production, etc.)
|
||||
- **[ProjectSetting.php](mdc:app/Models/ProjectSetting.php)** - Project-specific settings
|
||||
|
||||
#### Application Deployment
|
||||
- **[Application.php](mdc:app/Models/Application.php)** - Main application entity (74KB, highly complex)
|
||||
- **[ApplicationSetting.php](mdc:app/Models/ApplicationSetting.php)** - Application configurations
|
||||
- **[ApplicationDeploymentQueue.php](mdc:app/Models/ApplicationDeploymentQueue.php)** - Deployment orchestration
|
||||
- **[ApplicationPreview.php](mdc:app/Models/ApplicationPreview.php)** - Preview environment management
|
||||
|
||||
#### Service Management
|
||||
- **[Service.php](mdc:app/Models/Service.php)** - Service definitions (58KB, complex)
|
||||
- **[ServiceApplication.php](mdc:app/Models/ServiceApplication.php)** - Service components
|
||||
- **[ServiceDatabase.php](mdc:app/Models/ServiceDatabase.php)** - Service-attached databases
|
||||
|
||||
## Database Type Support
|
||||
|
||||
### Standalone Database Models
|
||||
Each database type has its own dedicated model with specific configurations:
|
||||
|
||||
#### SQL Databases
|
||||
- **[StandalonePostgresql.php](mdc:app/Models/StandalonePostgresql.php)** - PostgreSQL instances
|
||||
- **[StandaloneMysql.php](mdc:app/Models/StandaloneMysql.php)** - MySQL instances
|
||||
- **[StandaloneMariadb.php](mdc:app/Models/StandaloneMariadb.php)** - MariaDB instances
|
||||
|
||||
#### NoSQL & Analytics
|
||||
- **[StandaloneMongodb.php](mdc:app/Models/StandaloneMongodb.php)** - MongoDB instances
|
||||
- **[StandaloneClickhouse.php](mdc:app/Models/StandaloneClickhouse.php)** - ClickHouse analytics
|
||||
|
||||
#### Caching & In-Memory
|
||||
- **[StandaloneRedis.php](mdc:app/Models/StandaloneRedis.php)** - Redis instances
|
||||
- **[StandaloneKeydb.php](mdc:app/Models/StandaloneKeydb.php)** - KeyDB instances
|
||||
- **[StandaloneDragonfly.php](mdc:app/Models/StandaloneDragonfly.php)** - Dragonfly instances
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### Environment Variables
|
||||
- **[EnvironmentVariable.php](mdc:app/Models/EnvironmentVariable.php)** - Application-specific environment variables
|
||||
- **[SharedEnvironmentVariable.php](mdc:app/Models/SharedEnvironmentVariable.php)** - Shared across applications
|
||||
|
||||
### Settings Hierarchy
|
||||
- **[InstanceSettings.php](mdc:app/Models/InstanceSettings.php)** - Global Coolify instance settings
|
||||
- **[ServerSetting.php](mdc:app/Models/ServerSetting.php)** - Server-specific settings
|
||||
- **[ProjectSetting.php](mdc:app/Models/ProjectSetting.php)** - Project-level settings
|
||||
- **[ApplicationSetting.php](mdc:app/Models/ApplicationSetting.php)** - Application settings
|
||||
|
||||
## Storage & Backup Systems
|
||||
|
||||
### Storage Management
|
||||
- **[S3Storage.php](mdc:app/Models/S3Storage.php)** - S3-compatible storage configurations
|
||||
- **[LocalFileVolume.php](mdc:app/Models/LocalFileVolume.php)** - Local filesystem volumes
|
||||
- **[LocalPersistentVolume.php](mdc:app/Models/LocalPersistentVolume.php)** - Persistent volume management
|
||||
|
||||
### Backup Infrastructure
|
||||
- **[ScheduledDatabaseBackup.php](mdc:app/Models/ScheduledDatabaseBackup.php)** - Automated backup scheduling
|
||||
- **[ScheduledDatabaseBackupExecution.php](mdc:app/Models/ScheduledDatabaseBackupExecution.php)** - Backup execution tracking
|
||||
|
||||
### Task Scheduling
|
||||
- **[ScheduledTask.php](mdc:app/Models/ScheduledTask.php)** - Cron job management
|
||||
- **[ScheduledTaskExecution.php](mdc:app/Models/ScheduledTaskExecution.php)** - Task execution history
|
||||
|
||||
## Notification & Integration Models
|
||||
|
||||
### Notification Channels
|
||||
- **[EmailNotificationSettings.php](mdc:app/Models/EmailNotificationSettings.php)** - Email notifications
|
||||
- **[DiscordNotificationSettings.php](mdc:app/Models/DiscordNotificationSettings.php)** - Discord integration
|
||||
- **[SlackNotificationSettings.php](mdc:app/Models/SlackNotificationSettings.php)** - Slack integration
|
||||
- **[TelegramNotificationSettings.php](mdc:app/Models/TelegramNotificationSettings.php)** - Telegram bot
|
||||
- **[PushoverNotificationSettings.php](mdc:app/Models/PushoverNotificationSettings.php)** - Pushover notifications
|
||||
|
||||
### Source Control Integration
|
||||
- **[GithubApp.php](mdc:app/Models/GithubApp.php)** - GitHub App integration
|
||||
- **[GitlabApp.php](mdc:app/Models/GitlabApp.php)** - GitLab integration
|
||||
|
||||
### OAuth & Authentication
|
||||
- **[OauthSetting.php](mdc:app/Models/OauthSetting.php)** - OAuth provider configurations
|
||||
|
||||
## Docker & Container Management
|
||||
|
||||
### Container Orchestration
|
||||
- **[StandaloneDocker.php](mdc:app/Models/StandaloneDocker.php)** - Standalone Docker containers
|
||||
- **[SwarmDocker.php](mdc:app/Models/SwarmDocker.php)** - Docker Swarm management
|
||||
|
||||
### SSL & Security
|
||||
- **[SslCertificate.php](mdc:app/Models/SslCertificate.php)** - SSL certificate management
|
||||
|
||||
## Database Migration Strategy
|
||||
|
||||
### Migration Location: [database/migrations/](mdc:database/migrations)
|
||||
|
||||
#### Migration Patterns
|
||||
```php
|
||||
// Typical Coolify migration structure
|
||||
Schema::create('applications', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('fqdn')->nullable();
|
||||
$table->json('environment_variables')->nullable();
|
||||
$table->foreignId('destination_id');
|
||||
$table->foreignId('source_id');
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
|
||||
### Schema Versioning
|
||||
- **Incremental migrations** for database evolution
|
||||
- **Data migrations** for complex transformations
|
||||
- **Rollback support** for deployment safety
|
||||
|
||||
## Eloquent Model Patterns
|
||||
|
||||
### Base Model Structure
|
||||
- **[BaseModel.php](mdc:app/Models/BaseModel.php)** - Common model functionality
|
||||
- **UUID primary keys** for distributed systems
|
||||
- **Soft deletes** for audit trails
|
||||
- **Activity logging** with Spatie package
|
||||
|
||||
### Relationship Patterns
|
||||
```php
|
||||
// Typical relationship structure in Application model
|
||||
class Application extends Model
|
||||
{
|
||||
public function server()
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
public function environment()
|
||||
{
|
||||
return $this->belongsTo(Environment::class);
|
||||
}
|
||||
|
||||
public function deployments()
|
||||
{
|
||||
return $this->hasMany(ApplicationDeploymentQueue::class);
|
||||
}
|
||||
|
||||
public function environmentVariables()
|
||||
{
|
||||
return $this->hasMany(EnvironmentVariable::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Model Traits
|
||||
```php
|
||||
// Common traits used across models
|
||||
use SoftDeletes;
|
||||
use LogsActivity;
|
||||
use HasFactory;
|
||||
use HasUuids;
|
||||
```
|
||||
|
||||
## Caching Strategy (Redis)
|
||||
|
||||
### Cache Usage Patterns
|
||||
- **Session storage** - User authentication sessions
|
||||
- **Queue backend** - Background job processing
|
||||
- **Model caching** - Expensive query results
|
||||
- **Real-time data** - WebSocket state management
|
||||
|
||||
### Cache Keys Structure
|
||||
```
|
||||
coolify:session:{session_id}
|
||||
coolify:server:{server_id}:status
|
||||
coolify:deployment:{deployment_id}:logs
|
||||
coolify:user:{user_id}:teams
|
||||
```
|
||||
|
||||
## Query Optimization Patterns
|
||||
|
||||
### Eager Loading
|
||||
```php
|
||||
// Optimized queries with relationships
|
||||
$applications = Application::with([
|
||||
'server',
|
||||
'environment.project',
|
||||
'environmentVariables',
|
||||
'deployments' => function ($query) {
|
||||
$query->latest()->limit(5);
|
||||
}
|
||||
])->get();
|
||||
```
|
||||
|
||||
### Chunking for Large Datasets
|
||||
```php
|
||||
// Processing large datasets efficiently
|
||||
Server::chunk(100, function ($servers) {
|
||||
foreach ($servers as $server) {
|
||||
// Process server monitoring
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Database Indexes
|
||||
- **Primary keys** on all tables
|
||||
- **Foreign key indexes** for relationships
|
||||
- **Composite indexes** for common queries
|
||||
- **Unique constraints** for business rules
|
||||
|
||||
## Data Consistency Patterns
|
||||
|
||||
### Database Transactions
|
||||
```php
|
||||
// Atomic operations for deployment
|
||||
DB::transaction(function () {
|
||||
$application = Application::create($data);
|
||||
$application->environmentVariables()->createMany($envVars);
|
||||
$application->deployments()->create(['status' => 'queued']);
|
||||
});
|
||||
```
|
||||
|
||||
### Model Events
|
||||
```php
|
||||
// Automatic cleanup on model deletion
|
||||
class Application extends Model
|
||||
{
|
||||
protected static function booted()
|
||||
{
|
||||
static::deleting(function ($application) {
|
||||
$application->environmentVariables()->delete();
|
||||
$application->deployments()->delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Backup & Recovery
|
||||
|
||||
### Database Backup Strategy
|
||||
- **Automated PostgreSQL backups** via scheduled tasks
|
||||
- **Point-in-time recovery** capability
|
||||
- **Cross-region backup** replication
|
||||
- **Backup verification** and testing
|
||||
|
||||
### Data Export/Import
|
||||
- **Application configurations** export/import
|
||||
- **Environment variable** bulk operations
|
||||
- **Server configurations** backup and restore
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
### Query Performance
|
||||
- **Laravel Telescope** for development debugging
|
||||
- **Slow query logging** in production
|
||||
- **Database connection** pooling
|
||||
- **Read replica** support for scaling
|
||||
|
||||
### Metrics Collection
|
||||
- **Database size** monitoring
|
||||
- **Connection count** tracking
|
||||
- **Query execution time** analysis
|
||||
- **Cache hit rates** monitoring
|
||||
|
||||
## Multi-Tenancy Pattern
|
||||
|
||||
### Team-Based Isolation
|
||||
```php
|
||||
// Global scope for team-based filtering
|
||||
class Application extends Model
|
||||
{
|
||||
protected static function booted()
|
||||
{
|
||||
static::addGlobalScope('team', function (Builder $builder) {
|
||||
if (auth()->user()) {
|
||||
$builder->whereHas('environment.project', function ($query) {
|
||||
$query->where('team_id', auth()->user()->currentTeam->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Data Separation
|
||||
- **Team-scoped queries** by default
|
||||
- **Cross-team access** controls
|
||||
- **Admin access** patterns
|
||||
- **Data isolation** guarantees
|
||||
|
|
@ -1,310 +0,0 @@
|
|||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Coolify Deployment Architecture
|
||||
|
||||
## Deployment Philosophy
|
||||
|
||||
Coolify orchestrates **Docker-based deployments** across multiple servers with automated configuration generation, zero-downtime deployments, and comprehensive monitoring.
|
||||
|
||||
## Core Deployment Components
|
||||
|
||||
### Deployment Models
|
||||
- **[Application.php](mdc:app/Models/Application.php)** - Main application entity with deployment configurations
|
||||
- **[ApplicationDeploymentQueue.php](mdc:app/Models/ApplicationDeploymentQueue.php)** - Deployment job orchestration
|
||||
- **[Service.php](mdc:app/Models/Service.php)** - Multi-container service definitions
|
||||
- **[Server.php](mdc:app/Models/Server.php)** - Target deployment infrastructure
|
||||
|
||||
### Infrastructure Management
|
||||
- **[PrivateKey.php](mdc:app/Models/PrivateKey.php)** - SSH key management for secure server access
|
||||
- **[StandaloneDocker.php](mdc:app/Models/StandaloneDocker.php)** - Single container deployments
|
||||
- **[SwarmDocker.php](mdc:app/Models/SwarmDocker.php)** - Docker Swarm orchestration
|
||||
|
||||
## Deployment Workflow
|
||||
|
||||
### 1. Source Code Integration
|
||||
```
|
||||
Git Repository → Webhook → Coolify → Build & Deploy
|
||||
```
|
||||
|
||||
#### Source Control Models
|
||||
- **[GithubApp.php](mdc:app/Models/GithubApp.php)** - GitHub integration and webhooks
|
||||
- **[GitlabApp.php](mdc:app/Models/GitlabApp.php)** - GitLab CI/CD integration
|
||||
|
||||
#### Deployment Triggers
|
||||
- **Git push** to configured branches
|
||||
- **Manual deployment** via UI
|
||||
- **Scheduled deployments** via cron
|
||||
- **API-triggered** deployments
|
||||
|
||||
### 2. Build Process
|
||||
```
|
||||
Source Code → Docker Build → Image Registry → Deployment
|
||||
```
|
||||
|
||||
#### Build Configurations
|
||||
- **Dockerfile detection** and custom Dockerfile support
|
||||
- **Buildpack integration** for framework detection
|
||||
- **Multi-stage builds** for optimization
|
||||
- **Cache layer** management for faster builds
|
||||
|
||||
### 3. Deployment Orchestration
|
||||
```
|
||||
Queue Job → Configuration Generation → Container Deployment → Health Checks
|
||||
```
|
||||
|
||||
## Deployment Actions
|
||||
|
||||
### Location: [app/Actions/](mdc:app/Actions)
|
||||
|
||||
#### Application Deployment Actions
|
||||
- **Application/** - Core application deployment logic
|
||||
- **Docker/** - Docker container management
|
||||
- **Service/** - Multi-container service orchestration
|
||||
- **Proxy/** - Reverse proxy configuration
|
||||
|
||||
#### Database Actions
|
||||
- **Database/** - Database deployment and management
|
||||
- Automated backup scheduling
|
||||
- Connection management and health checks
|
||||
|
||||
#### Server Management Actions
|
||||
- **Server/** - Server provisioning and configuration
|
||||
- SSH connection establishment
|
||||
- Docker daemon management
|
||||
|
||||
## Configuration Generation
|
||||
|
||||
### Dynamic Configuration
|
||||
- **[ConfigurationGenerator.php](mdc:app/Services/ConfigurationGenerator.php)** - Generates deployment configurations
|
||||
- **[ConfigurationRepository.php](mdc:app/Services/ConfigurationRepository.php)** - Configuration management
|
||||
|
||||
### Generated Configurations
|
||||
#### Docker Compose Files
|
||||
```yaml
|
||||
# Generated docker-compose.yml structure
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
image: ${APP_IMAGE}
|
||||
environment:
|
||||
- ${ENV_VARIABLES}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.app.rule=Host(`${FQDN}`)
|
||||
volumes:
|
||||
- ${VOLUME_MAPPINGS}
|
||||
networks:
|
||||
- coolify
|
||||
```
|
||||
|
||||
#### Nginx Configurations
|
||||
- **Reverse proxy** setup
|
||||
- **SSL termination** with automatic certificates
|
||||
- **Load balancing** for multiple instances
|
||||
- **Custom headers** and routing rules
|
||||
|
||||
## Container Orchestration
|
||||
|
||||
### Docker Integration
|
||||
- **[DockerImageParser.php](mdc:app/Services/DockerImageParser.php)** - Parse and validate Docker images
|
||||
- **Container lifecycle** management
|
||||
- **Resource allocation** and limits
|
||||
- **Network isolation** and communication
|
||||
|
||||
### Volume Management
|
||||
- **[LocalFileVolume.php](mdc:app/Models/LocalFileVolume.php)** - Persistent file storage
|
||||
- **[LocalPersistentVolume.php](mdc:app/Models/LocalPersistentVolume.php)** - Data persistence
|
||||
- **Backup integration** for volume data
|
||||
|
||||
### Network Configuration
|
||||
- **Custom Docker networks** for isolation
|
||||
- **Service discovery** between containers
|
||||
- **Port mapping** and exposure
|
||||
- **SSL/TLS termination**
|
||||
|
||||
## Environment Management
|
||||
|
||||
### Environment Isolation
|
||||
- **[Environment.php](mdc:app/Models/Environment.php)** - Development, staging, production environments
|
||||
- **[EnvironmentVariable.php](mdc:app/Models/EnvironmentVariable.php)** - Application-specific variables
|
||||
- **[SharedEnvironmentVariable.php](mdc:app/Models/SharedEnvironmentVariable.php)** - Cross-application variables
|
||||
|
||||
### Configuration Hierarchy
|
||||
```
|
||||
Instance Settings → Server Settings → Project Settings → Application Settings
|
||||
```
|
||||
|
||||
## Preview Environments
|
||||
|
||||
### Git-Based Previews
|
||||
- **[ApplicationPreview.php](mdc:app/Models/ApplicationPreview.php)** - Preview environment management
|
||||
- **Automatic PR/MR previews** for feature branches
|
||||
- **Isolated environments** for testing
|
||||
- **Automatic cleanup** after merge/close
|
||||
|
||||
### Preview Workflow
|
||||
```
|
||||
Feature Branch → Auto-Deploy → Preview URL → Review → Cleanup
|
||||
```
|
||||
|
||||
## SSL & Security
|
||||
|
||||
### Certificate Management
|
||||
- **[SslCertificate.php](mdc:app/Models/SslCertificate.php)** - SSL certificate automation
|
||||
- **Let's Encrypt** integration for free certificates
|
||||
- **Custom certificate** upload support
|
||||
- **Automatic renewal** and monitoring
|
||||
|
||||
### Security Patterns
|
||||
- **Private Docker networks** for container isolation
|
||||
- **SSH key-based** server authentication
|
||||
- **Environment variable** encryption
|
||||
- **Access control** via team permissions
|
||||
|
||||
## Backup & Recovery
|
||||
|
||||
### Database Backups
|
||||
- **[ScheduledDatabaseBackup.php](mdc:app/Models/ScheduledDatabaseBackup.php)** - Automated database backups
|
||||
- **[ScheduledDatabaseBackupExecution.php](mdc:app/Models/ScheduledDatabaseBackupExecution.php)** - Backup execution tracking
|
||||
- **S3-compatible storage** for backup destinations
|
||||
|
||||
### Application Backups
|
||||
- **Volume snapshots** for persistent data
|
||||
- **Configuration export** for disaster recovery
|
||||
- **Cross-region replication** for high availability
|
||||
|
||||
## Monitoring & Logging
|
||||
|
||||
### Real-Time Monitoring
|
||||
- **[ActivityMonitor.php](mdc:app/Livewire/ActivityMonitor.php)** - Live deployment monitoring
|
||||
- **WebSocket-based** log streaming
|
||||
- **Container health checks** and alerts
|
||||
- **Resource usage** tracking
|
||||
|
||||
### Deployment Logs
|
||||
- **Build process** logging
|
||||
- **Container startup** logs
|
||||
- **Application runtime** logs
|
||||
- **Error tracking** and alerting
|
||||
|
||||
## Queue System
|
||||
|
||||
### Background Jobs
|
||||
Location: [app/Jobs/](mdc:app/Jobs)
|
||||
- **Deployment jobs** for async processing
|
||||
- **Server monitoring** jobs
|
||||
- **Backup scheduling** jobs
|
||||
- **Notification delivery** jobs
|
||||
|
||||
### Queue Processing
|
||||
- **Redis-backed** job queues
|
||||
- **Laravel Horizon** for queue monitoring
|
||||
- **Failed job** retry mechanisms
|
||||
- **Queue worker** auto-scaling
|
||||
|
||||
## Multi-Server Deployment
|
||||
|
||||
### Server Types
|
||||
- **Standalone servers** - Single Docker host
|
||||
- **Docker Swarm** - Multi-node orchestration
|
||||
- **Remote servers** - SSH-based deployment
|
||||
- **Local development** - Docker Desktop integration
|
||||
|
||||
### Load Balancing
|
||||
- **Traefik integration** for automatic load balancing
|
||||
- **Health check** based routing
|
||||
- **Blue-green deployments** for zero downtime
|
||||
- **Rolling updates** with configurable strategies
|
||||
|
||||
## Deployment Strategies
|
||||
|
||||
### Zero-Downtime Deployment
|
||||
```
|
||||
Old Container → New Container Build → Health Check → Traffic Switch → Old Container Cleanup
|
||||
```
|
||||
|
||||
### Blue-Green Deployment
|
||||
- **Parallel environments** for safe deployments
|
||||
- **Instant rollback** capability
|
||||
- **Database migration** handling
|
||||
- **Configuration synchronization**
|
||||
|
||||
### Rolling Updates
|
||||
- **Gradual instance** replacement
|
||||
- **Configurable update** strategy
|
||||
- **Automatic rollback** on failure
|
||||
- **Health check** validation
|
||||
|
||||
## API Integration
|
||||
|
||||
### Deployment API
|
||||
Routes: [routes/api.php](mdc:routes/api.php)
|
||||
- **RESTful endpoints** for deployment management
|
||||
- **Webhook receivers** for CI/CD integration
|
||||
- **Status reporting** endpoints
|
||||
- **Deployment triggering** via API
|
||||
|
||||
### Authentication
|
||||
- **Laravel Sanctum** API tokens
|
||||
- **Team-based** access control
|
||||
- **Rate limiting** for API calls
|
||||
- **Audit logging** for API usage
|
||||
|
||||
## Error Handling & Recovery
|
||||
|
||||
### Deployment Failure Recovery
|
||||
- **Automatic rollback** on deployment failure
|
||||
- **Health check** failure handling
|
||||
- **Container crash** recovery
|
||||
- **Resource exhaustion** protection
|
||||
|
||||
### Monitoring & Alerting
|
||||
- **Failed deployment** notifications
|
||||
- **Resource threshold** alerts
|
||||
- **SSL certificate** expiry warnings
|
||||
- **Backup failure** notifications
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Build Optimization
|
||||
- **Docker layer** caching
|
||||
- **Multi-stage builds** for smaller images
|
||||
- **Build artifact** reuse
|
||||
- **Parallel build** processing
|
||||
|
||||
### Runtime Optimization
|
||||
- **Container resource** limits
|
||||
- **Auto-scaling** based on metrics
|
||||
- **Connection pooling** for databases
|
||||
- **CDN integration** for static assets
|
||||
|
||||
## Compliance & Governance
|
||||
|
||||
### Audit Trail
|
||||
- **Deployment history** tracking
|
||||
- **Configuration changes** logging
|
||||
- **User action** auditing
|
||||
- **Resource access** monitoring
|
||||
|
||||
### Backup Compliance
|
||||
- **Retention policies** for backups
|
||||
- **Encryption at rest** for sensitive data
|
||||
- **Cross-region** backup replication
|
||||
- **Recovery testing** automation
|
||||
|
||||
## Integration Patterns
|
||||
|
||||
### CI/CD Integration
|
||||
- **GitHub Actions** compatibility
|
||||
- **GitLab CI** pipeline integration
|
||||
- **Custom webhook** endpoints
|
||||
- **Build status** reporting
|
||||
|
||||
### External Services
|
||||
- **S3-compatible** storage integration
|
||||
- **External database** connections
|
||||
- **Third-party monitoring** tools
|
||||
- **Custom notification** channels
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
---
|
||||
description: Guide for using Task Master to manage task-driven development workflows
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
# Task Master Development Workflow
|
||||
|
||||
This guide outlines the typical process for using Task Master to manage software development projects.
|
||||
|
||||
## Primary Interaction: MCP Server vs. CLI
|
||||
|
||||
Task Master offers two primary ways to interact:
|
||||
|
||||
1. **MCP Server (Recommended for Integrated Tools)**:
|
||||
- For AI agents and integrated development environments (like Cursor), interacting via the **MCP server is the preferred method**.
|
||||
- The MCP server exposes Task Master functionality through a set of tools (e.g., `get_tasks`, `add_subtask`).
|
||||
- This method offers better performance, structured data exchange, and richer error handling compared to CLI parsing.
|
||||
- Refer to [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc) for details on the MCP architecture and available tools.
|
||||
- A comprehensive list and description of MCP tools and their corresponding CLI commands can be found in [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc).
|
||||
- **Restart the MCP server** if core logic in `scripts/modules` or MCP tool/direct function definitions change.
|
||||
|
||||
2. **`task-master` CLI (For Users & Fallback)**:
|
||||
- The global `task-master` command provides a user-friendly interface for direct terminal interaction.
|
||||
- It can also serve as a fallback if the MCP server is inaccessible or a specific function isn't exposed via MCP.
|
||||
- Install globally with `npm install -g task-master-ai` or use locally via `npx task-master-ai ...`.
|
||||
- The CLI commands often mirror the MCP tools (e.g., `task-master list` corresponds to `get_tasks`).
|
||||
- Refer to [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc) for a detailed command reference.
|
||||
|
||||
## Standard Development Workflow Process
|
||||
|
||||
- Start new projects by running `initialize_project` tool / `task-master init` or `parse_prd` / `task-master parse-prd --input='<prd-file.txt>'` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to generate initial tasks.json
|
||||
- Begin coding sessions with `get_tasks` / `task-master list` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to see current tasks, status, and IDs
|
||||
- Determine the next task to work on using `next_task` / `task-master next` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)).
|
||||
- Analyze task complexity with `analyze_project_complexity` / `task-master analyze-complexity --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) before breaking down tasks
|
||||
- Review complexity report using `complexity_report` / `task-master complexity-report` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)).
|
||||
- Select tasks based on dependencies (all marked 'done'), priority level, and ID order
|
||||
- Clarify tasks by checking task files in tasks/ directory or asking for user input
|
||||
- View specific task details using `get_task` / `task-master show <id>` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to understand implementation requirements
|
||||
- Break down complex tasks using `expand_task` / `task-master expand --id=<id> --force --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) with appropriate flags like `--force` (to replace existing subtasks) and `--research`.
|
||||
- Clear existing subtasks if needed using `clear_subtasks` / `task-master clear-subtasks --id=<id>` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) before regenerating
|
||||
- Implement code following task details, dependencies, and project standards
|
||||
- Verify tasks according to test strategies before marking as complete (See [`tests.mdc`](mdc:.cursor/rules/tests.mdc))
|
||||
- Mark completed tasks with `set_task_status` / `task-master set-status --id=<id> --status=done` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc))
|
||||
- Update dependent tasks when implementation differs from original plan using `update` / `task-master update --from=<id> --prompt="..."` or `update_task` / `task-master update-task --id=<id> --prompt="..."` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc))
|
||||
- Add new tasks discovered during implementation using `add_task` / `task-master add-task --prompt="..." --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)).
|
||||
- Add new subtasks as needed using `add_subtask` / `task-master add-subtask --parent=<id> --title="..."` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)).
|
||||
- Append notes or details to subtasks using `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='Add implementation notes here...\nMore details...'` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)).
|
||||
- Generate task files with `generate` / `task-master generate` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) after updating tasks.json
|
||||
- Maintain valid dependency structure with `add_dependency`/`remove_dependency` tools or `task-master add-dependency`/`remove-dependency` commands, `validate_dependencies` / `task-master validate-dependencies`, and `fix_dependencies` / `task-master fix-dependencies` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) when needed
|
||||
- Respect dependency chains and task priorities when selecting work
|
||||
- Report progress regularly using `get_tasks` / `task-master list`
|
||||
|
||||
## Task Complexity Analysis
|
||||
|
||||
- Run `analyze_project_complexity` / `task-master analyze-complexity --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) for comprehensive analysis
|
||||
- Review complexity report via `complexity_report` / `task-master complexity-report` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) for a formatted, readable version.
|
||||
- Focus on tasks with highest complexity scores (8-10) for detailed breakdown
|
||||
- Use analysis results to determine appropriate subtask allocation
|
||||
- Note that reports are automatically used by the `expand_task` tool/command
|
||||
|
||||
## Task Breakdown Process
|
||||
|
||||
- Use `expand_task` / `task-master expand --id=<id>`. It automatically uses the complexity report if found, otherwise generates default number of subtasks.
|
||||
- Use `--num=<number>` to specify an explicit number of subtasks, overriding defaults or complexity report recommendations.
|
||||
- Add `--research` flag to leverage Perplexity AI for research-backed expansion.
|
||||
- Add `--force` flag to clear existing subtasks before generating new ones (default is to append).
|
||||
- Use `--prompt="<context>"` to provide additional context when needed.
|
||||
- Review and adjust generated subtasks as necessary.
|
||||
- Use `expand_all` tool or `task-master expand --all` to expand multiple pending tasks at once, respecting flags like `--force` and `--research`.
|
||||
- If subtasks need complete replacement (regardless of the `--force` flag on `expand`), clear them first with `clear_subtasks` / `task-master clear-subtasks --id=<id>`.
|
||||
|
||||
## Implementation Drift Handling
|
||||
|
||||
- When implementation differs significantly from planned approach
|
||||
- When future tasks need modification due to current implementation choices
|
||||
- When new dependencies or requirements emerge
|
||||
- Use `update` / `task-master update --from=<futureTaskId> --prompt='<explanation>\nUpdate context...' --research` to update multiple future tasks.
|
||||
- Use `update_task` / `task-master update-task --id=<taskId> --prompt='<explanation>\nUpdate context...' --research` to update a single specific task.
|
||||
|
||||
## Task Status Management
|
||||
|
||||
- Use 'pending' for tasks ready to be worked on
|
||||
- Use 'done' for completed and verified tasks
|
||||
- Use 'deferred' for postponed tasks
|
||||
- Add custom status values as needed for project-specific workflows
|
||||
|
||||
## Task Structure Fields
|
||||
|
||||
- **id**: Unique identifier for the task (Example: `1`, `1.1`)
|
||||
- **title**: Brief, descriptive title (Example: `"Initialize Repo"`)
|
||||
- **description**: Concise summary of what the task involves (Example: `"Create a new repository, set up initial structure."`)
|
||||
- **status**: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`)
|
||||
- **dependencies**: IDs of prerequisite tasks (Example: `[1, 2.1]`)
|
||||
- Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending)
|
||||
- This helps quickly identify which prerequisite tasks are blocking work
|
||||
- **priority**: Importance level (Example: `"high"`, `"medium"`, `"low"`)
|
||||
- **details**: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`)
|
||||
- **testStrategy**: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`)
|
||||
- **subtasks**: List of smaller, more specific tasks (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`)
|
||||
- Refer to task structure details (previously linked to `tasks.mdc`).
|
||||
|
||||
## Configuration Management (Updated)
|
||||
|
||||
Taskmaster configuration is managed through two main mechanisms:
|
||||
|
||||
1. **`.taskmasterconfig` File (Primary):**
|
||||
* Located in the project root directory.
|
||||
* Stores most configuration settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default subtasks/priority, project name, etc.
|
||||
* **Managed via `task-master models --setup` command.** Do not edit manually unless you know what you are doing.
|
||||
* **View/Set specific models via `task-master models` command or `models` MCP tool.**
|
||||
* Created automatically when you run `task-master models --setup` for the first time.
|
||||
|
||||
2. **Environment Variables (`.env` / `mcp.json`):**
|
||||
* Used **only** for sensitive API keys and specific endpoint URLs.
|
||||
* Place API keys (one per provider) in a `.env` file in the project root for CLI usage.
|
||||
* For MCP/Cursor integration, configure these keys in the `env` section of `.cursor/mcp.json`.
|
||||
* Available keys/variables: See `assets/env.example` or the Configuration section in the command reference (previously linked to `taskmaster.mdc`).
|
||||
|
||||
**Important:** Non-API key settings (like model selections, `MAX_TOKENS`, `TASKMASTER_LOG_LEVEL`) are **no longer configured via environment variables**. Use the `task-master models` command (or `--setup` for interactive configuration) or the `models` MCP tool.
|
||||
**If AI commands FAIL in MCP** verify that the API key for the selected provider is present in the `env` section of `.cursor/mcp.json`.
|
||||
**If AI commands FAIL in CLI** verify that the API key for the selected provider is present in the `.env` file in the root of the project.
|
||||
|
||||
## Determining the Next Task
|
||||
|
||||
- Run `next_task` / `task-master next` to show the next task to work on.
|
||||
- The command identifies tasks with all dependencies satisfied
|
||||
- Tasks are prioritized by priority level, dependency count, and ID
|
||||
- The command shows comprehensive task information including:
|
||||
- Basic task details and description
|
||||
- Implementation details
|
||||
- Subtasks (if they exist)
|
||||
- Contextual suggested actions
|
||||
- Recommended before starting any new development work
|
||||
- Respects your project's dependency structure
|
||||
- Ensures tasks are completed in the appropriate sequence
|
||||
- Provides ready-to-use commands for common task actions
|
||||
|
||||
## Viewing Specific Task Details
|
||||
|
||||
- Run `get_task` / `task-master show <id>` to view a specific task.
|
||||
- Use dot notation for subtasks: `task-master show 1.2` (shows subtask 2 of task 1)
|
||||
- Displays comprehensive information similar to the next command, but for a specific task
|
||||
- For parent tasks, shows all subtasks and their current status
|
||||
- For subtasks, shows parent task information and relationship
|
||||
- Provides contextual suggested actions appropriate for the specific task
|
||||
- Useful for examining task details before implementation or checking status
|
||||
|
||||
## Managing Task Dependencies
|
||||
|
||||
- Use `add_dependency` / `task-master add-dependency --id=<id> --depends-on=<id>` to add a dependency.
|
||||
- Use `remove_dependency` / `task-master remove-dependency --id=<id> --depends-on=<id>` to remove a dependency.
|
||||
- The system prevents circular dependencies and duplicate dependency entries
|
||||
- Dependencies are checked for existence before being added or removed
|
||||
- Task files are automatically regenerated after dependency changes
|
||||
- Dependencies are visualized with status indicators in task listings and files
|
||||
|
||||
## Iterative Subtask Implementation
|
||||
|
||||
Once a task has been broken down into subtasks using `expand_task` or similar methods, follow this iterative process for implementation:
|
||||
|
||||
1. **Understand the Goal (Preparation):**
|
||||
* Use `get_task` / `task-master show <subtaskId>` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to thoroughly understand the specific goals and requirements of the subtask.
|
||||
|
||||
2. **Initial Exploration & Planning (Iteration 1):**
|
||||
* This is the first attempt at creating a concrete implementation plan.
|
||||
* Explore the codebase to identify the precise files, functions, and even specific lines of code that will need modification.
|
||||
* Determine the intended code changes (diffs) and their locations.
|
||||
* Gather *all* relevant details from this exploration phase.
|
||||
|
||||
3. **Log the Plan:**
|
||||
* Run `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<detailed plan>'`.
|
||||
* Provide the *complete and detailed* findings from the exploration phase in the prompt. Include file paths, line numbers, proposed diffs, reasoning, and any potential challenges identified. Do not omit details. The goal is to create a rich, timestamped log within the subtask's `details`.
|
||||
|
||||
4. **Verify the Plan:**
|
||||
* Run `get_task` / `task-master show <subtaskId>` again to confirm that the detailed implementation plan has been successfully appended to the subtask's details.
|
||||
|
||||
5. **Begin Implementation:**
|
||||
* Set the subtask status using `set_task_status` / `task-master set-status --id=<subtaskId> --status=in-progress`.
|
||||
* Start coding based on the logged plan.
|
||||
|
||||
6. **Refine and Log Progress (Iteration 2+):**
|
||||
* As implementation progresses, you will encounter challenges, discover nuances, or confirm successful approaches.
|
||||
* **Before appending new information**: Briefly review the *existing* details logged in the subtask (using `get_task` or recalling from context) to ensure the update adds fresh insights and avoids redundancy.
|
||||
* **Regularly** use `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<update details>\n- What worked...\n- What didn't work...'` to append new findings.
|
||||
* **Crucially, log:**
|
||||
* What worked ("fundamental truths" discovered).
|
||||
* What didn't work and why (to avoid repeating mistakes).
|
||||
* Specific code snippets or configurations that were successful.
|
||||
* Decisions made, especially if confirmed with user input.
|
||||
* Any deviations from the initial plan and the reasoning.
|
||||
* The objective is to continuously enrich the subtask's details, creating a log of the implementation journey that helps the AI (and human developers) learn, adapt, and avoid repeating errors.
|
||||
|
||||
7. **Review & Update Rules (Post-Implementation):**
|
||||
* Once the implementation for the subtask is functionally complete, review all code changes and the relevant chat history.
|
||||
* Identify any new or modified code patterns, conventions, or best practices established during the implementation.
|
||||
* Create new or update existing rules following internal guidelines (previously linked to `cursor_rules.mdc` and `self_improve.mdc`).
|
||||
|
||||
8. **Mark Task Complete:**
|
||||
* After verifying the implementation and updating any necessary rules, mark the subtask as completed: `set_task_status` / `task-master set-status --id=<subtaskId> --status=done`.
|
||||
|
||||
9. **Commit Changes (If using Git):**
|
||||
* Stage the relevant code changes and any updated/new rule files (`git add .`).
|
||||
* Craft a comprehensive Git commit message summarizing the work done for the subtask, including both code implementation and any rule adjustments.
|
||||
* Execute the commit command directly in the terminal (e.g., `git commit -m 'feat(module): Implement feature X for subtask <subtaskId>\n\n- Details about changes...\n- Updated rule Y for pattern Z'`).
|
||||
* Consider if a Changeset is needed according to internal versioning guidelines (previously linked to `changeset.mdc`). If so, run `npm run changeset`, stage the generated file, and amend the commit or create a new one.
|
||||
|
||||
10. **Proceed to Next Subtask:**
|
||||
* Identify the next subtask (e.g., using `next_task` / `task-master next`).
|
||||
|
||||
## Code Analysis & Refactoring Techniques
|
||||
|
||||
- **Top-Level Function Search**:
|
||||
- Useful for understanding module structure or planning refactors.
|
||||
- Use grep/ripgrep to find exported functions/constants:
|
||||
`rg "export (async function|function|const) \w+"` or similar patterns.
|
||||
- Can help compare functions between files during migrations or identify potential naming conflicts.
|
||||
|
||||
---
|
||||
*This workflow provides a general guideline. Adapt it based on your specific project needs and team practices.*
|
||||
|
|
@ -1,653 +0,0 @@
|
|||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Coolify Development Workflow
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
- **PHP 8.4+** - Latest PHP version for modern features
|
||||
- **Node.js 18+** - For frontend asset compilation
|
||||
- **Docker & Docker Compose** - Container orchestration
|
||||
- **PostgreSQL 15** - Primary database
|
||||
- **Redis 7** - Caching and queues
|
||||
|
||||
### Local Development Setup
|
||||
|
||||
#### Using Docker (Recommended)
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/coollabsio/coolify.git
|
||||
cd coolify
|
||||
|
||||
# Copy environment configuration
|
||||
cp .env.example .env
|
||||
|
||||
# Start development environment
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# Install PHP dependencies
|
||||
docker-compose exec app composer install
|
||||
|
||||
# Install Node.js dependencies
|
||||
docker-compose exec app npm install
|
||||
|
||||
# Generate application key
|
||||
docker-compose exec app php artisan key:generate
|
||||
|
||||
# Run database migrations
|
||||
docker-compose exec app php artisan migrate
|
||||
|
||||
# Seed development data
|
||||
docker-compose exec app php artisan db:seed
|
||||
```
|
||||
|
||||
#### Native Development
|
||||
```bash
|
||||
# Install PHP dependencies
|
||||
composer install
|
||||
|
||||
# Install Node.js dependencies
|
||||
npm install
|
||||
|
||||
# Setup environment
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
|
||||
# Setup database
|
||||
createdb coolify_dev
|
||||
php artisan migrate
|
||||
php artisan db:seed
|
||||
|
||||
# Start development servers
|
||||
php artisan serve &
|
||||
npm run dev &
|
||||
php artisan queue:work &
|
||||
```
|
||||
|
||||
## Development Tools & Configuration
|
||||
|
||||
### Code Quality Tools
|
||||
- **[Laravel Pint](mdc:pint.json)** - PHP code style fixer
|
||||
- **[Rector](mdc:rector.php)** - PHP automated refactoring (989B, 35 lines)
|
||||
- **PHPStan** - Static analysis for type safety
|
||||
- **ESLint** - JavaScript code quality
|
||||
|
||||
### Development Configuration Files
|
||||
- **[docker-compose.dev.yml](mdc:docker-compose.dev.yml)** - Development Docker setup (3.4KB, 126 lines)
|
||||
- **[vite.config.js](mdc:vite.config.js)** - Frontend build configuration (1.0KB, 42 lines)
|
||||
- **[.editorconfig](mdc:.editorconfig)** - Code formatting standards (258B, 19 lines)
|
||||
|
||||
### Git Configuration
|
||||
- **[.gitignore](mdc:.gitignore)** - Version control exclusions (522B, 40 lines)
|
||||
- **[.gitattributes](mdc:.gitattributes)** - Git file handling (185B, 11 lines)
|
||||
|
||||
## Development Workflow Process
|
||||
|
||||
### 1. Feature Development
|
||||
```bash
|
||||
# Create feature branch
|
||||
git checkout -b feature/new-deployment-strategy
|
||||
|
||||
# Make changes following coding standards
|
||||
# Run code quality checks
|
||||
./vendor/bin/pint
|
||||
./vendor/bin/rector process --dry-run
|
||||
./vendor/bin/phpstan analyse
|
||||
|
||||
# Run tests
|
||||
./vendor/bin/pest
|
||||
./vendor/bin/pest --coverage
|
||||
|
||||
# Commit changes
|
||||
git add .
|
||||
git commit -m "feat: implement blue-green deployment strategy"
|
||||
```
|
||||
|
||||
### 2. Code Review Process
|
||||
```bash
|
||||
# Push feature branch
|
||||
git push origin feature/new-deployment-strategy
|
||||
|
||||
# Create pull request with:
|
||||
# - Clear description of changes
|
||||
# - Screenshots for UI changes
|
||||
# - Test coverage information
|
||||
# - Breaking change documentation
|
||||
```
|
||||
|
||||
### 3. Testing Requirements
|
||||
- **Unit tests** for new models and services
|
||||
- **Feature tests** for API endpoints
|
||||
- **Browser tests** for UI changes
|
||||
- **Integration tests** for deployment workflows
|
||||
|
||||
## Coding Standards & Conventions
|
||||
|
||||
### PHP Coding Standards
|
||||
```php
|
||||
// Follow PSR-12 coding standards
|
||||
class ApplicationDeploymentService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DockerService $dockerService,
|
||||
private readonly ConfigurationGenerator $configGenerator
|
||||
) {}
|
||||
|
||||
public function deploy(Application $application): ApplicationDeploymentQueue
|
||||
{
|
||||
return DB::transaction(function () use ($application) {
|
||||
$deployment = $application->deployments()->create([
|
||||
'status' => 'queued',
|
||||
'commit_sha' => $application->getLatestCommitSha(),
|
||||
]);
|
||||
|
||||
DeployApplicationJob::dispatch($deployment);
|
||||
|
||||
return $deployment;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Laravel Best Practices
|
||||
```php
|
||||
// Use Laravel conventions
|
||||
class Application extends Model
|
||||
{
|
||||
// Mass assignment protection
|
||||
protected $fillable = [
|
||||
'name', 'git_repository', 'git_branch', 'fqdn'
|
||||
];
|
||||
|
||||
// Type casting
|
||||
protected $casts = [
|
||||
'environment_variables' => 'array',
|
||||
'build_pack' => BuildPack::class,
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
public function server(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
public function deployments(): HasMany
|
||||
{
|
||||
return $this->hasMany(ApplicationDeploymentQueue::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend Standards
|
||||
```javascript
|
||||
// Alpine.js component structure
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('deploymentMonitor', () => ({
|
||||
status: 'idle',
|
||||
logs: [],
|
||||
|
||||
init() {
|
||||
this.connectWebSocket();
|
||||
},
|
||||
|
||||
connectWebSocket() {
|
||||
Echo.private(`application.${this.applicationId}`)
|
||||
.listen('DeploymentStarted', (e) => {
|
||||
this.status = 'deploying';
|
||||
})
|
||||
.listen('DeploymentCompleted', (e) => {
|
||||
this.status = 'completed';
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
```
|
||||
|
||||
### CSS/Tailwind Standards
|
||||
```html
|
||||
<!-- Use semantic class names and consistent spacing -->
|
||||
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Application Status
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<!-- Content with consistent spacing -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Database Development
|
||||
|
||||
### Migration Best Practices
|
||||
```php
|
||||
// Create descriptive migration files
|
||||
class CreateApplicationDeploymentQueuesTable extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('application_deployment_queues', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('application_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('status')->default('queued');
|
||||
$table->string('commit_sha')->nullable();
|
||||
$table->text('build_logs')->nullable();
|
||||
$table->text('deployment_logs')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('finished_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['application_id', 'status']);
|
||||
$table->index('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('application_deployment_queues');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Model Factory Development
|
||||
```php
|
||||
// Create comprehensive factories for testing
|
||||
class ApplicationFactory extends Factory
|
||||
{
|
||||
protected $model = Application::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->words(2, true),
|
||||
'fqdn' => $this->faker->domainName,
|
||||
'git_repository' => 'https://github.com/' . $this->faker->userName . '/' . $this->faker->word . '.git',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => BuildPack::NIXPACKS,
|
||||
'server_id' => Server::factory(),
|
||||
'environment_id' => Environment::factory(),
|
||||
];
|
||||
}
|
||||
|
||||
public function withCustomDomain(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'fqdn' => $this->faker->domainName,
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Development
|
||||
|
||||
### Controller Standards
|
||||
```php
|
||||
class ApplicationController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth:sanctum');
|
||||
$this->middleware('team.access');
|
||||
}
|
||||
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$applications = $request->user()
|
||||
->currentTeam
|
||||
->applications()
|
||||
->with(['server', 'environment', 'latestDeployment'])
|
||||
->paginate();
|
||||
|
||||
return ApplicationResource::collection($applications);
|
||||
}
|
||||
|
||||
public function store(StoreApplicationRequest $request): ApplicationResource
|
||||
{
|
||||
$application = $request->user()
|
||||
->currentTeam
|
||||
->applications()
|
||||
->create($request->validated());
|
||||
|
||||
return new ApplicationResource($application);
|
||||
}
|
||||
|
||||
public function deploy(Application $application): JsonResponse
|
||||
{
|
||||
$this->authorize('deploy', $application);
|
||||
|
||||
$deployment = app(ApplicationDeploymentService::class)
|
||||
->deploy($application);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Deployment started successfully',
|
||||
'deployment_id' => $deployment->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Resource Development
|
||||
```php
|
||||
class ApplicationResource extends JsonResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'fqdn' => $this->fqdn,
|
||||
'status' => $this->status,
|
||||
'git_repository' => $this->git_repository,
|
||||
'git_branch' => $this->git_branch,
|
||||
'build_pack' => $this->build_pack,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
|
||||
// Conditional relationships
|
||||
'server' => new ServerResource($this->whenLoaded('server')),
|
||||
'environment' => new EnvironmentResource($this->whenLoaded('environment')),
|
||||
'latest_deployment' => new DeploymentResource($this->whenLoaded('latestDeployment')),
|
||||
|
||||
// Computed attributes
|
||||
'deployment_url' => $this->getDeploymentUrl(),
|
||||
'can_deploy' => $this->canDeploy(),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Livewire Component Development
|
||||
|
||||
### Component Structure
|
||||
```php
|
||||
class ApplicationShow extends Component
|
||||
{
|
||||
public Application $application;
|
||||
public bool $showLogs = false;
|
||||
|
||||
protected $listeners = [
|
||||
'deployment.started' => 'refreshDeploymentStatus',
|
||||
'deployment.completed' => 'refreshDeploymentStatus',
|
||||
];
|
||||
|
||||
public function mount(Application $application): void
|
||||
{
|
||||
$this->authorize('view', $application);
|
||||
$this->application = $application;
|
||||
}
|
||||
|
||||
public function deploy(): void
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
try {
|
||||
app(ApplicationDeploymentService::class)->deploy($this->application);
|
||||
|
||||
$this->dispatch('deployment.started', [
|
||||
'application_id' => $this->application->id
|
||||
]);
|
||||
|
||||
session()->flash('success', 'Deployment started successfully');
|
||||
} catch (Exception $e) {
|
||||
session()->flash('error', 'Failed to start deployment: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function refreshDeploymentStatus(): void
|
||||
{
|
||||
$this->application->refresh();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.application.show', [
|
||||
'deployments' => $this->application
|
||||
->deployments()
|
||||
->latest()
|
||||
->limit(10)
|
||||
->get()
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Queue Job Development
|
||||
|
||||
### Job Structure
|
||||
```php
|
||||
class DeployApplicationJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
public int $maxExceptions = 1;
|
||||
|
||||
public function __construct(
|
||||
public ApplicationDeploymentQueue $deployment
|
||||
) {}
|
||||
|
||||
public function handle(
|
||||
DockerService $dockerService,
|
||||
ConfigurationGenerator $configGenerator
|
||||
): void {
|
||||
$this->deployment->update(['status' => 'running', 'started_at' => now()]);
|
||||
|
||||
try {
|
||||
// Generate configuration
|
||||
$config = $configGenerator->generateDockerCompose($this->deployment->application);
|
||||
|
||||
// Build and deploy
|
||||
$imageTag = $dockerService->buildImage($this->deployment->application);
|
||||
$dockerService->deployContainer($this->deployment->application, $imageTag);
|
||||
|
||||
$this->deployment->update([
|
||||
'status' => 'success',
|
||||
'finished_at' => now()
|
||||
]);
|
||||
|
||||
// Broadcast success
|
||||
broadcast(new DeploymentCompleted($this->deployment));
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->deployment->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $e->getMessage(),
|
||||
'finished_at' => now()
|
||||
]);
|
||||
|
||||
broadcast(new DeploymentFailed($this->deployment));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function backoff(): array
|
||||
{
|
||||
return [1, 5, 10];
|
||||
}
|
||||
|
||||
public function failed(Throwable $exception): void
|
||||
{
|
||||
$this->deployment->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $exception->getMessage(),
|
||||
'finished_at' => now()
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Development
|
||||
|
||||
### Test Structure
|
||||
```php
|
||||
// Feature test example
|
||||
test('user can deploy application via API', function () {
|
||||
$user = User::factory()->create();
|
||||
$application = Application::factory()->create([
|
||||
'team_id' => $user->currentTeam->id
|
||||
]);
|
||||
|
||||
// Mock external services
|
||||
$this->mock(DockerService::class, function ($mock) {
|
||||
$mock->shouldReceive('buildImage')->andReturn('app:latest');
|
||||
$mock->shouldReceive('deployContainer')->andReturn(true);
|
||||
});
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->postJson("/api/v1/applications/{$application->id}/deploy");
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'message' => 'Deployment started successfully'
|
||||
]);
|
||||
|
||||
expect($application->deployments()->count())->toBe(1);
|
||||
expect($application->deployments()->first()->status)->toBe('queued');
|
||||
});
|
||||
```
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### Code Documentation
|
||||
```php
|
||||
/**
|
||||
* Deploy an application to the specified server.
|
||||
*
|
||||
* This method creates a new deployment queue entry and dispatches
|
||||
* a background job to handle the actual deployment process.
|
||||
*
|
||||
* @param Application $application The application to deploy
|
||||
* @param array $options Additional deployment options
|
||||
* @return ApplicationDeploymentQueue The created deployment queue entry
|
||||
*
|
||||
* @throws DeploymentException When deployment cannot be started
|
||||
* @throws ServerConnectionException When server is unreachable
|
||||
*/
|
||||
public function deploy(Application $application, array $options = []): ApplicationDeploymentQueue
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
### API Documentation
|
||||
```php
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/applications/{application}/deploy",
|
||||
* summary="Deploy an application",
|
||||
* description="Triggers a new deployment for the specified application",
|
||||
* operationId="deployApplication",
|
||||
* tags={"Applications"},
|
||||
* security={{"bearerAuth":{}}},
|
||||
* @OA\Parameter(
|
||||
* name="application",
|
||||
* in="path",
|
||||
* required=true,
|
||||
* @OA\Schema(type="integer"),
|
||||
* description="Application ID"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Deployment started successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="deployment_id", type="integer")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Database Optimization
|
||||
```php
|
||||
// Use eager loading to prevent N+1 queries
|
||||
$applications = Application::with([
|
||||
'server:id,name,ip',
|
||||
'environment:id,name',
|
||||
'latestDeployment:id,application_id,status,created_at'
|
||||
])->get();
|
||||
|
||||
// Use database transactions for consistency
|
||||
DB::transaction(function () use ($application) {
|
||||
$deployment = $application->deployments()->create(['status' => 'queued']);
|
||||
$application->update(['last_deployment_at' => now()]);
|
||||
DeployApplicationJob::dispatch($deployment);
|
||||
});
|
||||
```
|
||||
|
||||
### Caching Strategies
|
||||
```php
|
||||
// Cache expensive operations
|
||||
public function getServerMetrics(Server $server): array
|
||||
{
|
||||
return Cache::remember(
|
||||
"server.{$server->id}.metrics",
|
||||
now()->addMinutes(5),
|
||||
fn () => $this->fetchServerMetrics($server)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment & Release Process
|
||||
|
||||
### Version Management
|
||||
- **[versions.json](mdc:versions.json)** - Version tracking (355B, 19 lines)
|
||||
- **[CHANGELOG.md](mdc:CHANGELOG.md)** - Release notes (187KB, 7411 lines)
|
||||
- **[cliff.toml](mdc:cliff.toml)** - Changelog generation (3.2KB, 85 lines)
|
||||
|
||||
### Release Workflow
|
||||
```bash
|
||||
# Create release branch
|
||||
git checkout -b release/v4.1.0
|
||||
|
||||
# Update version numbers
|
||||
# Update CHANGELOG.md
|
||||
# Run full test suite
|
||||
./vendor/bin/pest
|
||||
npm run test
|
||||
|
||||
# Create release commit
|
||||
git commit -m "chore: release v4.1.0"
|
||||
|
||||
# Create and push tag
|
||||
git tag v4.1.0
|
||||
git push origin v4.1.0
|
||||
|
||||
# Merge to main
|
||||
git checkout main
|
||||
git merge release/v4.1.0
|
||||
```
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
### Pull Request Process
|
||||
1. **Fork** the repository
|
||||
2. **Create** feature branch from `main`
|
||||
3. **Implement** changes with tests
|
||||
4. **Run** code quality checks
|
||||
5. **Submit** pull request with clear description
|
||||
6. **Address** review feedback
|
||||
7. **Merge** after approval
|
||||
|
||||
### Code Review Checklist
|
||||
- [ ] Code follows project standards
|
||||
- [ ] Tests cover new functionality
|
||||
- [ ] Documentation is updated
|
||||
- [ ] No breaking changes without migration
|
||||
- [ ] Performance impact considered
|
||||
- [ ] Security implications reviewed
|
||||
|
||||
### Issue Reporting
|
||||
- Use issue templates
|
||||
- Provide reproduction steps
|
||||
- Include environment details
|
||||
- Add relevant logs/screenshots
|
||||
- Label appropriately
|
||||
|
|
@ -1,319 +0,0 @@
|
|||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Coolify Frontend Architecture & Patterns
|
||||
|
||||
## Frontend Philosophy
|
||||
|
||||
Coolify uses a **server-side first** approach with minimal JavaScript, leveraging Livewire for reactivity and Alpine.js for lightweight client-side interactions.
|
||||
|
||||
## Core Frontend Stack
|
||||
|
||||
### Livewire 3.5+ (Primary Framework)
|
||||
- **Server-side rendering** with reactive components
|
||||
- **Real-time updates** without page refreshes
|
||||
- **State management** handled on the server
|
||||
- **WebSocket integration** for live updates
|
||||
|
||||
### Alpine.js (Client-Side Interactivity)
|
||||
- **Lightweight JavaScript** for DOM manipulation
|
||||
- **Declarative directives** in HTML
|
||||
- **Component-like behavior** without build steps
|
||||
- **Perfect companion** to Livewire
|
||||
|
||||
### Tailwind CSS 4.1+ (Styling)
|
||||
- **Utility-first** CSS framework
|
||||
- **Custom design system** for deployment platform
|
||||
- **Responsive design** built-in
|
||||
- **Dark mode support**
|
||||
|
||||
## Livewire Component Structure
|
||||
|
||||
### Location: [app/Livewire/](mdc:app/Livewire)
|
||||
|
||||
#### Core Application Components
|
||||
- **[Dashboard.php](mdc:app/Livewire/Dashboard.php)** - Main dashboard interface
|
||||
- **[ActivityMonitor.php](mdc:app/Livewire/ActivityMonitor.php)** - Real-time activity tracking
|
||||
- **[MonacoEditor.php](mdc:app/Livewire/MonacoEditor.php)** - Code editor component
|
||||
|
||||
#### Server Management
|
||||
- **Server/** directory - Server configuration and monitoring
|
||||
- Real-time server status updates
|
||||
- SSH connection management
|
||||
- Resource monitoring
|
||||
|
||||
#### Project & Application Management
|
||||
- **Project/** directory - Project organization
|
||||
- Application deployment interfaces
|
||||
- Environment variable management
|
||||
- Service configuration
|
||||
|
||||
#### Settings & Configuration
|
||||
- **Settings/** directory - System configuration
|
||||
- **[SettingsEmail.php](mdc:app/Livewire/SettingsEmail.php)** - Email notification setup
|
||||
- **[SettingsOauth.php](mdc:app/Livewire/SettingsOauth.php)** - OAuth provider configuration
|
||||
- **[SettingsBackup.php](mdc:app/Livewire/SettingsBackup.php)** - Backup configuration
|
||||
|
||||
#### User & Team Management
|
||||
- **Team/** directory - Team collaboration features
|
||||
- **Profile/** directory - User profile management
|
||||
- **Security/** directory - Security settings
|
||||
|
||||
## Blade Template Organization
|
||||
|
||||
### Location: [resources/views/](mdc:resources/views)
|
||||
|
||||
#### Layout Structure
|
||||
- **layouts/** - Base layout templates
|
||||
- **components/** - Reusable UI components
|
||||
- **livewire/** - Livewire component views
|
||||
|
||||
#### Feature-Specific Views
|
||||
- **server/** - Server management interfaces
|
||||
- **auth/** - Authentication pages
|
||||
- **emails/** - Email templates
|
||||
- **errors/** - Error pages
|
||||
|
||||
## Interactive Components
|
||||
|
||||
### Monaco Editor Integration
|
||||
- **Code editing** for configuration files
|
||||
- **Syntax highlighting** for multiple languages
|
||||
- **Live validation** and error detection
|
||||
- **Integration** with deployment process
|
||||
|
||||
### Terminal Emulation (XTerm.js)
|
||||
- **Real-time terminal** access to servers
|
||||
- **WebSocket-based** communication
|
||||
- **Multi-session** support
|
||||
- **Secure connection** through SSH
|
||||
|
||||
### Real-Time Updates
|
||||
- **WebSocket connections** via Laravel Echo
|
||||
- **Live deployment logs** streaming
|
||||
- **Server monitoring** with live metrics
|
||||
- **Activity notifications** in real-time
|
||||
|
||||
## Alpine.js Patterns
|
||||
|
||||
### Common Directives Used
|
||||
```html
|
||||
<!-- State management -->
|
||||
<div x-data="{ open: false }">
|
||||
|
||||
<!-- Event handling -->
|
||||
<button x-on:click="open = !open">
|
||||
|
||||
<!-- Conditional rendering -->
|
||||
<div x-show="open">
|
||||
|
||||
<!-- Data binding -->
|
||||
<input x-model="searchTerm">
|
||||
|
||||
<!-- Component initialization -->
|
||||
<div x-init="initializeComponent()">
|
||||
```
|
||||
|
||||
### Integration with Livewire
|
||||
```html
|
||||
<!-- Livewire actions with Alpine state -->
|
||||
<button
|
||||
x-data="{ loading: false }"
|
||||
x-on:click="loading = true"
|
||||
wire:click="deploy"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="deploy"
|
||||
>
|
||||
<span x-show="!loading">Deploy</span>
|
||||
<span x-show="loading">Deploying...</span>
|
||||
</button>
|
||||
```
|
||||
|
||||
## Tailwind CSS Patterns
|
||||
|
||||
### Design System
|
||||
- **Consistent spacing** using Tailwind scale
|
||||
- **Color palette** optimized for deployment platform
|
||||
- **Typography** hierarchy for technical content
|
||||
- **Component classes** for reusable elements
|
||||
|
||||
### Responsive Design
|
||||
```html
|
||||
<!-- Mobile-first responsive design -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Content adapts to screen size -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Dark Mode Support
|
||||
```html
|
||||
<!-- Dark mode variants -->
|
||||
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||
<!-- Automatic dark mode switching -->
|
||||
</div>
|
||||
```
|
||||
|
||||
## Build Process
|
||||
|
||||
### Vite Configuration ([vite.config.js](mdc:vite.config.js))
|
||||
- **Fast development** with hot module replacement
|
||||
- **Optimized production** builds
|
||||
- **Asset versioning** for cache busting
|
||||
- **CSS processing** with PostCSS
|
||||
|
||||
### Asset Compilation
|
||||
```bash
|
||||
# Development
|
||||
npm run dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
```
|
||||
|
||||
## State Management Patterns
|
||||
|
||||
### Server-Side State (Livewire)
|
||||
- **Component properties** for persistent state
|
||||
- **Session storage** for user preferences
|
||||
- **Database models** for application state
|
||||
- **Cache layer** for performance
|
||||
|
||||
### Client-Side State (Alpine.js)
|
||||
- **Local component state** for UI interactions
|
||||
- **Form validation** and user feedback
|
||||
- **Modal and dropdown** state management
|
||||
- **Temporary UI states** (loading, hover, etc.)
|
||||
|
||||
## Real-Time Features
|
||||
|
||||
### WebSocket Integration
|
||||
```php
|
||||
// Livewire component with real-time updates
|
||||
class ActivityMonitor extends Component
|
||||
{
|
||||
public function getListeners()
|
||||
{
|
||||
return [
|
||||
'deployment.started' => 'refresh',
|
||||
'deployment.finished' => 'refresh',
|
||||
'server.status.changed' => 'updateServerStatus',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event Broadcasting
|
||||
- **Laravel Echo** for client-side WebSocket handling
|
||||
- **Pusher protocol** for real-time communication
|
||||
- **Private channels** for user-specific events
|
||||
- **Presence channels** for collaborative features
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
### Lazy Loading
|
||||
```php
|
||||
// Livewire lazy loading
|
||||
class ServerList extends Component
|
||||
{
|
||||
public function placeholder()
|
||||
{
|
||||
return view('components.loading-skeleton');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Caching Strategies
|
||||
- **Fragment caching** for expensive operations
|
||||
- **Image optimization** with lazy loading
|
||||
- **Asset bundling** and compression
|
||||
- **CDN integration** for static assets
|
||||
|
||||
## Form Handling Patterns
|
||||
|
||||
### Livewire Forms
|
||||
```php
|
||||
class ServerCreateForm extends Component
|
||||
{
|
||||
public $name;
|
||||
public $ip;
|
||||
|
||||
protected $rules = [
|
||||
'name' => 'required|min:3',
|
||||
'ip' => 'required|ip',
|
||||
];
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
// Save logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Real-Time Validation
|
||||
- **Live validation** as user types
|
||||
- **Server-side validation** rules
|
||||
- **Error message** display
|
||||
- **Success feedback** patterns
|
||||
|
||||
## Component Communication
|
||||
|
||||
### Parent-Child Communication
|
||||
```php
|
||||
// Parent component
|
||||
$this->emit('serverCreated', $server->id);
|
||||
|
||||
// Child component
|
||||
protected $listeners = ['serverCreated' => 'refresh'];
|
||||
```
|
||||
|
||||
### Cross-Component Events
|
||||
- **Global events** for application-wide updates
|
||||
- **Scoped events** for feature-specific communication
|
||||
- **Browser events** for JavaScript integration
|
||||
|
||||
## Error Handling & UX
|
||||
|
||||
### Loading States
|
||||
- **Skeleton screens** during data loading
|
||||
- **Progress indicators** for long operations
|
||||
- **Optimistic updates** with rollback capability
|
||||
|
||||
### Error Display
|
||||
- **Toast notifications** for user feedback
|
||||
- **Inline validation** errors
|
||||
- **Global error** handling
|
||||
- **Retry mechanisms** for failed operations
|
||||
|
||||
## Accessibility Patterns
|
||||
|
||||
### ARIA Labels and Roles
|
||||
```html
|
||||
<button
|
||||
aria-label="Deploy application"
|
||||
aria-describedby="deploy-help"
|
||||
wire:click="deploy"
|
||||
>
|
||||
Deploy
|
||||
</button>
|
||||
```
|
||||
|
||||
### Keyboard Navigation
|
||||
- **Tab order** management
|
||||
- **Keyboard shortcuts** for power users
|
||||
- **Focus management** in modals and forms
|
||||
- **Screen reader** compatibility
|
||||
|
||||
## Mobile Optimization
|
||||
|
||||
### Touch-Friendly Interface
|
||||
- **Larger tap targets** for mobile devices
|
||||
- **Swipe gestures** where appropriate
|
||||
- **Mobile-optimized** forms and navigation
|
||||
|
||||
### Progressive Enhancement
|
||||
- **Core functionality** works without JavaScript
|
||||
- **Enhanced experience** with JavaScript enabled
|
||||
- **Offline capabilities** where possible
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue