Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a0af13548 | ||
|
|
f25b1997bf | ||
|
|
561e746cab | ||
|
|
b61ada089b | ||
|
|
19236b7c97 | ||
|
|
191239d6fc | ||
|
|
0bedeed913 | ||
|
|
9ef6bcf371 | ||
|
|
9001610eda | ||
|
|
0c7730de8f |
No files matched your search
@@ -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
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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']],
|
||||
```
|
||||
@@ -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 {}
|
||||
```
|
||||
@@ -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'));
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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(),
|
||||
]);
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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']);
|
||||
```
|
||||
@@ -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,
|
||||
],
|
||||
],
|
||||
],
|
||||
```
|
||||
@@ -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');
|
||||
}
|
||||
```
|
||||
@@ -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');
|
||||
});
|
||||
```
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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())
|
||||
```
|
||||
@@ -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();
|
||||
```
|
||||
@@ -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.');
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: mcp-development
|
||||
description: "Develops MCP servers, tools, resources, and prompts. Activates when creating MCP tools, resources, or prompts; setting up AI integrations; debugging MCP connections; working with routes/ai.php; or when the user mentions MCP, Model Context Protocol, AI tools, AI server, or building tools for AI assistants."
|
||||
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
|
||||
@@ -24,12 +24,12 @@ search-docs(['mcp tools', 'mcp resources', 'mcp validation'])
|
||||
|
||||
### Artisan Commands
|
||||
|
||||
<!-- Create MCP Primitives -->
|
||||
Create MCP Primitives
|
||||
```bash
|
||||
{{ $assist->artisanCommand('make:mcp-tool ToolName') }}
|
||||
{{ $assist->artisanCommand('make:mcp-resource ResourceName') }}
|
||||
{{ $assist->artisanCommand('make:mcp-prompt PromptName') }}
|
||||
{{ $assist->artisanCommand('make:mcp-server ServerName') }}
|
||||
php artisan make:mcp-tool ToolName
|
||||
php artisan make:mcp-resource ResourceName
|
||||
php artisan make:mcp-prompt PromptName
|
||||
php artisan make:mcp-server ServerName
|
||||
```
|
||||
|
||||
### Basic Tool Implementation
|
||||
@@ -114,11 +114,11 @@ $response->assertSentNotification('event/type', ['data' => 'value']);
|
||||
|
||||
Test interactively using the inspector:
|
||||
|
||||
<!-- Launch MCP Inspector -->
|
||||
<!--Launch MCP Inspector-->
|
||||
```bash
|
||||
{{ $assist->artisanCommand('mcp:inspector mcp/my-server') }} # Web server
|
||||
php artisan mcp:inspector mcp/my-server # Web server
|
||||
|
||||
{{ $assist->artisanCommand('mcp:inspector my-server') }} # Local server
|
||||
php artisan mcp:inspector my-server # Local server
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: pest-testing
|
||||
description: "Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works."
|
||||
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
|
||||
@@ -8,16 +8,6 @@ metadata:
|
||||
|
||||
# Pest Testing 4
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating new tests (unit, feature, or browser)
|
||||
- Modifying existing tests
|
||||
- Debugging test failures
|
||||
- Working with browser testing or smoke testing
|
||||
- Writing architecture tests or visual regression tests
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
@@ -28,6 +18,12 @@ Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
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.
|
||||
@@ -36,6 +32,8 @@ All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
### 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 () {
|
||||
@@ -164,4 +162,5 @@ arch('controllers')
|
||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: tailwindcss-development
|
||||
description: "Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components, working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors, typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle, hero section, cards, buttons, or any visual/UI changes."
|
||||
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
|
||||
@@ -8,16 +8,6 @@ metadata:
|
||||
|
||||
# Tailwind CSS Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Adding styles to components or pages
|
||||
- Working with responsive design
|
||||
- Implementing dark mode
|
||||
- Extracting repeated patterns into components
|
||||
- Debugging spacing or layout issues
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
|
||||
|
||||
+59
-28
@@ -21,6 +21,7 @@ namespace PHPSTORM_META {
|
||||
'Illuminate\Contracts\Database\LostConnectionDetector' => \Illuminate\Database\LostConnectionDetector::class,
|
||||
'Illuminate\Contracts\Debug\ExceptionHandler' => \NunoMaduro\Collision\Adapters\Laravel\ExceptionHandler::class,
|
||||
'Illuminate\Contracts\Foundation\MaintenanceMode' => \Illuminate\Foundation\FileBasedMaintenanceMode::class,
|
||||
'Illuminate\Contracts\Http\Kernel' => \Illuminate\Foundation\Http\Kernel::class,
|
||||
'Illuminate\Contracts\Log\ContextLogProcessor' => \Illuminate\Log\Context\ContextLogProcessor::class,
|
||||
'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class,
|
||||
'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class,
|
||||
@@ -87,6 +88,7 @@ namespace PHPSTORM_META {
|
||||
'Illuminate\Contracts\Database\LostConnectionDetector' => \Illuminate\Database\LostConnectionDetector::class,
|
||||
'Illuminate\Contracts\Debug\ExceptionHandler' => \NunoMaduro\Collision\Adapters\Laravel\ExceptionHandler::class,
|
||||
'Illuminate\Contracts\Foundation\MaintenanceMode' => \Illuminate\Foundation\FileBasedMaintenanceMode::class,
|
||||
'Illuminate\Contracts\Http\Kernel' => \Illuminate\Foundation\Http\Kernel::class,
|
||||
'Illuminate\Contracts\Log\ContextLogProcessor' => \Illuminate\Log\Context\ContextLogProcessor::class,
|
||||
'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class,
|
||||
'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class,
|
||||
@@ -153,6 +155,7 @@ namespace PHPSTORM_META {
|
||||
'Illuminate\Contracts\Database\LostConnectionDetector' => \Illuminate\Database\LostConnectionDetector::class,
|
||||
'Illuminate\Contracts\Debug\ExceptionHandler' => \NunoMaduro\Collision\Adapters\Laravel\ExceptionHandler::class,
|
||||
'Illuminate\Contracts\Foundation\MaintenanceMode' => \Illuminate\Foundation\FileBasedMaintenanceMode::class,
|
||||
'Illuminate\Contracts\Http\Kernel' => \Illuminate\Foundation\Http\Kernel::class,
|
||||
'Illuminate\Contracts\Log\ContextLogProcessor' => \Illuminate\Log\Context\ContextLogProcessor::class,
|
||||
'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class,
|
||||
'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class,
|
||||
@@ -219,6 +222,7 @@ namespace PHPSTORM_META {
|
||||
'Illuminate\Contracts\Database\LostConnectionDetector' => \Illuminate\Database\LostConnectionDetector::class,
|
||||
'Illuminate\Contracts\Debug\ExceptionHandler' => \NunoMaduro\Collision\Adapters\Laravel\ExceptionHandler::class,
|
||||
'Illuminate\Contracts\Foundation\MaintenanceMode' => \Illuminate\Foundation\FileBasedMaintenanceMode::class,
|
||||
'Illuminate\Contracts\Http\Kernel' => \Illuminate\Foundation\Http\Kernel::class,
|
||||
'Illuminate\Contracts\Log\ContextLogProcessor' => \Illuminate\Log\Context\ContextLogProcessor::class,
|
||||
'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class,
|
||||
'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class,
|
||||
@@ -285,6 +289,7 @@ namespace PHPSTORM_META {
|
||||
'Illuminate\Contracts\Database\LostConnectionDetector' => \Illuminate\Database\LostConnectionDetector::class,
|
||||
'Illuminate\Contracts\Debug\ExceptionHandler' => \NunoMaduro\Collision\Adapters\Laravel\ExceptionHandler::class,
|
||||
'Illuminate\Contracts\Foundation\MaintenanceMode' => \Illuminate\Foundation\FileBasedMaintenanceMode::class,
|
||||
'Illuminate\Contracts\Http\Kernel' => \Illuminate\Foundation\Http\Kernel::class,
|
||||
'Illuminate\Contracts\Log\ContextLogProcessor' => \Illuminate\Log\Context\ContextLogProcessor::class,
|
||||
'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class,
|
||||
'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class,
|
||||
@@ -351,6 +356,7 @@ namespace PHPSTORM_META {
|
||||
'Illuminate\Contracts\Database\LostConnectionDetector' => \Illuminate\Database\LostConnectionDetector::class,
|
||||
'Illuminate\Contracts\Debug\ExceptionHandler' => \NunoMaduro\Collision\Adapters\Laravel\ExceptionHandler::class,
|
||||
'Illuminate\Contracts\Foundation\MaintenanceMode' => \Illuminate\Foundation\FileBasedMaintenanceMode::class,
|
||||
'Illuminate\Contracts\Http\Kernel' => \Illuminate\Foundation\Http\Kernel::class,
|
||||
'Illuminate\Contracts\Log\ContextLogProcessor' => \Illuminate\Log\Context\ContextLogProcessor::class,
|
||||
'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class,
|
||||
'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class,
|
||||
@@ -417,6 +423,7 @@ namespace PHPSTORM_META {
|
||||
'Illuminate\Contracts\Database\LostConnectionDetector' => \Illuminate\Database\LostConnectionDetector::class,
|
||||
'Illuminate\Contracts\Debug\ExceptionHandler' => \NunoMaduro\Collision\Adapters\Laravel\ExceptionHandler::class,
|
||||
'Illuminate\Contracts\Foundation\MaintenanceMode' => \Illuminate\Foundation\FileBasedMaintenanceMode::class,
|
||||
'Illuminate\Contracts\Http\Kernel' => \Illuminate\Foundation\Http\Kernel::class,
|
||||
'Illuminate\Contracts\Log\ContextLogProcessor' => \Illuminate\Log\Context\ContextLogProcessor::class,
|
||||
'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class,
|
||||
'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class,
|
||||
@@ -483,6 +490,7 @@ namespace PHPSTORM_META {
|
||||
'Illuminate\Contracts\Database\LostConnectionDetector' => \Illuminate\Database\LostConnectionDetector::class,
|
||||
'Illuminate\Contracts\Debug\ExceptionHandler' => \NunoMaduro\Collision\Adapters\Laravel\ExceptionHandler::class,
|
||||
'Illuminate\Contracts\Foundation\MaintenanceMode' => \Illuminate\Foundation\FileBasedMaintenanceMode::class,
|
||||
'Illuminate\Contracts\Http\Kernel' => \Illuminate\Foundation\Http\Kernel::class,
|
||||
'Illuminate\Contracts\Log\ContextLogProcessor' => \Illuminate\Log\Context\ContextLogProcessor::class,
|
||||
'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class,
|
||||
'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class,
|
||||
@@ -549,6 +557,7 @@ namespace PHPSTORM_META {
|
||||
'Illuminate\Contracts\Database\LostConnectionDetector' => \Illuminate\Database\LostConnectionDetector::class,
|
||||
'Illuminate\Contracts\Debug\ExceptionHandler' => \NunoMaduro\Collision\Adapters\Laravel\ExceptionHandler::class,
|
||||
'Illuminate\Contracts\Foundation\MaintenanceMode' => \Illuminate\Foundation\FileBasedMaintenanceMode::class,
|
||||
'Illuminate\Contracts\Http\Kernel' => \Illuminate\Foundation\Http\Kernel::class,
|
||||
'Illuminate\Contracts\Log\ContextLogProcessor' => \Illuminate\Log\Context\ContextLogProcessor::class,
|
||||
'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class,
|
||||
'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class,
|
||||
@@ -615,6 +624,7 @@ namespace PHPSTORM_META {
|
||||
'Illuminate\Contracts\Database\LostConnectionDetector' => \Illuminate\Database\LostConnectionDetector::class,
|
||||
'Illuminate\Contracts\Debug\ExceptionHandler' => \NunoMaduro\Collision\Adapters\Laravel\ExceptionHandler::class,
|
||||
'Illuminate\Contracts\Foundation\MaintenanceMode' => \Illuminate\Foundation\FileBasedMaintenanceMode::class,
|
||||
'Illuminate\Contracts\Http\Kernel' => \Illuminate\Foundation\Http\Kernel::class,
|
||||
'Illuminate\Contracts\Log\ContextLogProcessor' => \Illuminate\Log\Context\ContextLogProcessor::class,
|
||||
'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class,
|
||||
'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class,
|
||||
@@ -681,6 +691,7 @@ namespace PHPSTORM_META {
|
||||
'Illuminate\Contracts\Database\LostConnectionDetector' => \Illuminate\Database\LostConnectionDetector::class,
|
||||
'Illuminate\Contracts\Debug\ExceptionHandler' => \NunoMaduro\Collision\Adapters\Laravel\ExceptionHandler::class,
|
||||
'Illuminate\Contracts\Foundation\MaintenanceMode' => \Illuminate\Foundation\FileBasedMaintenanceMode::class,
|
||||
'Illuminate\Contracts\Http\Kernel' => \Illuminate\Foundation\Http\Kernel::class,
|
||||
'Illuminate\Contracts\Log\ContextLogProcessor' => \Illuminate\Log\Context\ContextLogProcessor::class,
|
||||
'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class,
|
||||
'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class,
|
||||
@@ -1096,6 +1107,7 @@ namespace PHPSTORM_META {
|
||||
'mail.from.name' => 'string',
|
||||
'mail.markdown.theme' => 'string',
|
||||
'mail.markdown.paths' => 'array',
|
||||
'mail.markdown.extensions' => 'array',
|
||||
'queue.default' => 'string',
|
||||
'queue.connections.sync.driver' => 'string',
|
||||
'queue.connections.database.driver' => 'string',
|
||||
@@ -1162,12 +1174,13 @@ namespace PHPSTORM_META {
|
||||
'boost.executable_paths.composer' => 'NULL',
|
||||
'boost.executable_paths.npm' => 'NULL',
|
||||
'boost.executable_paths.vendor_bin' => 'NULL',
|
||||
'boost.executable_paths.current_directory' => 'string',
|
||||
'mcp.redirect_domains' => 'array',
|
||||
'ide-helper.filename' => 'string',
|
||||
'ide-helper.models_filename' => 'string',
|
||||
'ide-helper.meta_filename' => 'string',
|
||||
'ide-helper.include_fluent' => 'boolean',
|
||||
'ide-helper.include_factory_builders' => 'boolean',
|
||||
'ide-helper.write_query_methods' => 'boolean',
|
||||
'ide-helper.write_model_magic_where' => 'boolean',
|
||||
'ide-helper.write_model_external_builder_methods' => 'boolean',
|
||||
'ide-helper.write_model_relation_count_properties' => 'boolean',
|
||||
@@ -1192,6 +1205,7 @@ namespace PHPSTORM_META {
|
||||
'ide-helper.additional_relation_types' => 'array',
|
||||
'ide-helper.additional_relation_return_types' => 'array',
|
||||
'ide-helper.enforce_nullable_relationships' => 'boolean',
|
||||
'ide-helper.soft_deletes_force_nullable' => 'boolean',
|
||||
'ide-helper.post_migrate' => 'array',
|
||||
'tinker.commands' => 'array',
|
||||
'tinker.alias' => 'array',
|
||||
@@ -1537,6 +1551,7 @@ namespace PHPSTORM_META {
|
||||
'mail.from.name' => 'string',
|
||||
'mail.markdown.theme' => 'string',
|
||||
'mail.markdown.paths' => 'array',
|
||||
'mail.markdown.extensions' => 'array',
|
||||
'queue.default' => 'string',
|
||||
'queue.connections.sync.driver' => 'string',
|
||||
'queue.connections.database.driver' => 'string',
|
||||
@@ -1603,12 +1618,13 @@ namespace PHPSTORM_META {
|
||||
'boost.executable_paths.composer' => 'NULL',
|
||||
'boost.executable_paths.npm' => 'NULL',
|
||||
'boost.executable_paths.vendor_bin' => 'NULL',
|
||||
'boost.executable_paths.current_directory' => 'string',
|
||||
'mcp.redirect_domains' => 'array',
|
||||
'ide-helper.filename' => 'string',
|
||||
'ide-helper.models_filename' => 'string',
|
||||
'ide-helper.meta_filename' => 'string',
|
||||
'ide-helper.include_fluent' => 'boolean',
|
||||
'ide-helper.include_factory_builders' => 'boolean',
|
||||
'ide-helper.write_query_methods' => 'boolean',
|
||||
'ide-helper.write_model_magic_where' => 'boolean',
|
||||
'ide-helper.write_model_external_builder_methods' => 'boolean',
|
||||
'ide-helper.write_model_relation_count_properties' => 'boolean',
|
||||
@@ -1633,6 +1649,7 @@ namespace PHPSTORM_META {
|
||||
'ide-helper.additional_relation_types' => 'array',
|
||||
'ide-helper.additional_relation_return_types' => 'array',
|
||||
'ide-helper.enforce_nullable_relationships' => 'boolean',
|
||||
'ide-helper.soft_deletes_force_nullable' => 'boolean',
|
||||
'ide-helper.post_migrate' => 'array',
|
||||
'tinker.commands' => 'array',
|
||||
'tinker.alias' => 'array',
|
||||
@@ -1978,6 +1995,7 @@ namespace PHPSTORM_META {
|
||||
'mail.from.name' => 'string',
|
||||
'mail.markdown.theme' => 'string',
|
||||
'mail.markdown.paths' => 'array',
|
||||
'mail.markdown.extensions' => 'array',
|
||||
'queue.default' => 'string',
|
||||
'queue.connections.sync.driver' => 'string',
|
||||
'queue.connections.database.driver' => 'string',
|
||||
@@ -2044,12 +2062,13 @@ namespace PHPSTORM_META {
|
||||
'boost.executable_paths.composer' => 'NULL',
|
||||
'boost.executable_paths.npm' => 'NULL',
|
||||
'boost.executable_paths.vendor_bin' => 'NULL',
|
||||
'boost.executable_paths.current_directory' => 'string',
|
||||
'mcp.redirect_domains' => 'array',
|
||||
'ide-helper.filename' => 'string',
|
||||
'ide-helper.models_filename' => 'string',
|
||||
'ide-helper.meta_filename' => 'string',
|
||||
'ide-helper.include_fluent' => 'boolean',
|
||||
'ide-helper.include_factory_builders' => 'boolean',
|
||||
'ide-helper.write_query_methods' => 'boolean',
|
||||
'ide-helper.write_model_magic_where' => 'boolean',
|
||||
'ide-helper.write_model_external_builder_methods' => 'boolean',
|
||||
'ide-helper.write_model_relation_count_properties' => 'boolean',
|
||||
@@ -2074,6 +2093,7 @@ namespace PHPSTORM_META {
|
||||
'ide-helper.additional_relation_types' => 'array',
|
||||
'ide-helper.additional_relation_return_types' => 'array',
|
||||
'ide-helper.enforce_nullable_relationships' => 'boolean',
|
||||
'ide-helper.soft_deletes_force_nullable' => 'boolean',
|
||||
'ide-helper.post_migrate' => 'array',
|
||||
'tinker.commands' => 'array',
|
||||
'tinker.alias' => 'array',
|
||||
@@ -2081,7 +2101,6 @@ namespace PHPSTORM_META {
|
||||
'tinker.trust_project' => 'string',
|
||||
]));
|
||||
|
||||
|
||||
override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::mock(0), map(["" => "@&\Mockery\MockInterface"]));
|
||||
override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::partialMock(0), map(["" => "@&\Mockery\MockInterface"]));
|
||||
override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::instance(0), type(1));
|
||||
@@ -2186,33 +2205,45 @@ namespace PHPSTORM_META {
|
||||
'mail.mailers.ses.transport','mail.mailers.postmark.transport','mail.mailers.resend.transport','mail.mailers.sendmail.transport','mail.mailers.sendmail.path',
|
||||
'mail.mailers.log.transport','mail.mailers.log.channel','mail.mailers.array.transport','mail.mailers.failover.transport','mail.mailers.failover.mailers',
|
||||
'mail.mailers.failover.retry_after','mail.mailers.roundrobin.transport','mail.mailers.roundrobin.mailers','mail.mailers.roundrobin.retry_after','mail.from.address',
|
||||
'mail.from.name','mail.markdown.theme','mail.markdown.paths','queue.default','queue.connections.sync.driver',
|
||||
'queue.connections.database.driver','queue.connections.database.connection','queue.connections.database.table','queue.connections.database.queue','queue.connections.database.retry_after',
|
||||
'queue.connections.database.after_commit','queue.connections.beanstalkd.driver','queue.connections.beanstalkd.host','queue.connections.beanstalkd.queue','queue.connections.beanstalkd.retry_after',
|
||||
'queue.connections.beanstalkd.block_for','queue.connections.beanstalkd.after_commit','queue.connections.sqs.driver','queue.connections.sqs.key','queue.connections.sqs.secret',
|
||||
'queue.connections.sqs.prefix','queue.connections.sqs.queue','queue.connections.sqs.suffix','queue.connections.sqs.region','queue.connections.sqs.after_commit',
|
||||
'queue.connections.redis.driver','queue.connections.redis.connection','queue.connections.redis.queue','queue.connections.redis.retry_after','queue.connections.redis.block_for',
|
||||
'queue.connections.redis.after_commit','queue.connections.deferred.driver','queue.connections.failover.driver','queue.connections.failover.connections','queue.connections.background.driver',
|
||||
'queue.batching.database','queue.batching.table','queue.failed.driver','queue.failed.database','queue.failed.table',
|
||||
'services.postmark.key','services.resend.key','services.ses.key','services.ses.secret','services.ses.region',
|
||||
'services.slack.notifications.bot_user_oauth_token','services.slack.notifications.channel','session.driver','session.lifetime','session.expire_on_close',
|
||||
'session.encrypt','session.files','session.connection','session.table','session.store',
|
||||
'session.lottery','session.cookie','session.path','session.domain','session.secure',
|
||||
'session.http_only','session.same_site','session.partitioned','boost.enabled','boost.browser_logs_watcher',
|
||||
'boost.executable_paths.php','boost.executable_paths.composer','boost.executable_paths.npm','boost.executable_paths.vendor_bin','mcp.redirect_domains',
|
||||
'ide-helper.filename','ide-helper.models_filename','ide-helper.meta_filename','ide-helper.include_fluent','ide-helper.include_factory_builders',
|
||||
'ide-helper.write_model_magic_where','ide-helper.write_model_external_builder_methods','ide-helper.write_model_relation_count_properties','ide-helper.write_model_relation_exists_properties','ide-helper.write_eloquent_model_mixins',
|
||||
'ide-helper.include_helpers','ide-helper.helper_files','ide-helper.model_locations','ide-helper.ignored_models','ide-helper.model_hooks',
|
||||
'ide-helper.extra.Eloquent','ide-helper.extra.Session','ide-helper.magic','ide-helper.interfaces','ide-helper.model_camel_case_properties',
|
||||
'ide-helper.type_overrides.integer','ide-helper.type_overrides.boolean','ide-helper.include_class_docblocks','ide-helper.force_fqn','ide-helper.use_generics_annotations',
|
||||
'ide-helper.macro_default_return_types.Illuminate\\Http\\Client\\Factory','ide-helper.additional_relation_types','ide-helper.additional_relation_return_types','ide-helper.enforce_nullable_relationships','ide-helper.post_migrate',
|
||||
'tinker.commands','tinker.alias','tinker.dont_alias','tinker.trust_project',);
|
||||
'mail.from.name','mail.markdown.theme','mail.markdown.paths','mail.markdown.extensions','queue.default',
|
||||
'queue.connections.sync.driver','queue.connections.database.driver','queue.connections.database.connection','queue.connections.database.table','queue.connections.database.queue',
|
||||
'queue.connections.database.retry_after','queue.connections.database.after_commit','queue.connections.beanstalkd.driver','queue.connections.beanstalkd.host','queue.connections.beanstalkd.queue',
|
||||
'queue.connections.beanstalkd.retry_after','queue.connections.beanstalkd.block_for','queue.connections.beanstalkd.after_commit','queue.connections.sqs.driver','queue.connections.sqs.key',
|
||||
'queue.connections.sqs.secret','queue.connections.sqs.prefix','queue.connections.sqs.queue','queue.connections.sqs.suffix','queue.connections.sqs.region',
|
||||
'queue.connections.sqs.after_commit','queue.connections.redis.driver','queue.connections.redis.connection','queue.connections.redis.queue','queue.connections.redis.retry_after',
|
||||
'queue.connections.redis.block_for','queue.connections.redis.after_commit','queue.connections.deferred.driver','queue.connections.failover.driver','queue.connections.failover.connections',
|
||||
'queue.connections.background.driver','queue.batching.database','queue.batching.table','queue.failed.driver','queue.failed.database',
|
||||
'queue.failed.table','services.postmark.key','services.resend.key','services.ses.key','services.ses.secret',
|
||||
'services.ses.region','services.slack.notifications.bot_user_oauth_token','services.slack.notifications.channel','session.driver','session.lifetime',
|
||||
'session.expire_on_close','session.encrypt','session.files','session.connection','session.table',
|
||||
'session.store','session.lottery','session.cookie','session.path','session.domain',
|
||||
'session.secure','session.http_only','session.same_site','session.partitioned','boost.enabled',
|
||||
'boost.browser_logs_watcher','boost.executable_paths.php','boost.executable_paths.composer','boost.executable_paths.npm','boost.executable_paths.vendor_bin',
|
||||
'boost.executable_paths.current_directory','mcp.redirect_domains','ide-helper.filename','ide-helper.models_filename','ide-helper.meta_filename',
|
||||
'ide-helper.include_fluent','ide-helper.write_query_methods','ide-helper.write_model_magic_where','ide-helper.write_model_external_builder_methods','ide-helper.write_model_relation_count_properties',
|
||||
'ide-helper.write_model_relation_exists_properties','ide-helper.write_eloquent_model_mixins','ide-helper.include_helpers','ide-helper.helper_files','ide-helper.model_locations',
|
||||
'ide-helper.ignored_models','ide-helper.model_hooks','ide-helper.extra.Eloquent','ide-helper.extra.Session','ide-helper.magic',
|
||||
'ide-helper.interfaces','ide-helper.model_camel_case_properties','ide-helper.type_overrides.integer','ide-helper.type_overrides.boolean','ide-helper.include_class_docblocks',
|
||||
'ide-helper.force_fqn','ide-helper.use_generics_annotations','ide-helper.macro_default_return_types.Illuminate\\Http\\Client\\Factory','ide-helper.additional_relation_types','ide-helper.additional_relation_return_types',
|
||||
'ide-helper.enforce_nullable_relationships','ide-helper.soft_deletes_force_nullable','ide-helper.post_migrate','tinker.commands','tinker.alias',
|
||||
'tinker.dont_alias','tinker.trust_project',);
|
||||
registerArgumentsSet('middleware',
|
||||
'web','api',);
|
||||
'web','api','auth','auth.basic','auth.session',
|
||||
'cache.headers','can','guest','password.confirm','precognitive',
|
||||
'signed','throttle','verified',);
|
||||
registerArgumentsSet('routes',
|
||||
'boost.browser-logs','storage.local','storage.local.upload',);
|
||||
'boost.browser-logs','fivem','docs.index',
|
||||
'docs.quickstart','docs.documentation','storage.local','storage.local.upload',);
|
||||
registerArgumentsSet('views',
|
||||
'welcome','laravel-exceptions-renderer::components.badge','laravel-exceptions-renderer::components.empty-state','laravel-exceptions-renderer::components.file-with-line','laravel-exceptions-renderer::components.formatted-source',
|
||||
'docs.documentation','docs.index','docs.quickstart','layouts.app','layouts.partials.footer',
|
||||
'layouts.partials.navigation','mcp.cox.cox-event-list','mcp.cox.cox-event-reference','mcp.cox.cox-function','mcp.fivem.event-list',
|
||||
'mcp.fivem.event-reference','mcp.fivem.native-function','mcp.prodigy.prodigy-event-list','mcp.prodigy.prodigy-event-reference','mcp.prodigy.prodigy-export-list',
|
||||
'mcp.prodigy.prodigy-export-reference','mcp.qbcore.qbcore-config','mcp.qbcore.qbcore-event-list','mcp.qbcore.qbcore-event-reference','mcp.qbcore.qbcore-function',
|
||||
'mcp.qbcore.qbcore-players','mcp.qbcore.qbcore-resource-list','mcp.qbcore.qbcore-resource-reference','mcp.qbcore.qbcore-shared-data','mcp.resources.best-practices',
|
||||
'mcp.resources.code-snippets','mcp.resources.database-queries','mcp.resources.framework-comparison','mcp.shared.resource-boilerplate','mcp.shared.search-results',
|
||||
'mcp.templates.client-js','mcp.templates.client-lua','mcp.templates.config-js','mcp.templates.config-lua','mcp.templates.fxmanifest',
|
||||
'mcp.templates.nui-css','mcp.templates.nui-html','mcp.templates.nui-script-js','mcp.templates.nui-script-lua','mcp.templates.server-js',
|
||||
'mcp.templates.server-lua','laravel-exceptions-renderer::components.badge','laravel-exceptions-renderer::components.empty-state','laravel-exceptions-renderer::components.file-with-line','laravel-exceptions-renderer::components.formatted-source',
|
||||
'laravel-exceptions-renderer::components.frame','laravel-exceptions-renderer::components.frame-code','laravel-exceptions-renderer::components.header','laravel-exceptions-renderer::components.http-method','laravel-exceptions-renderer::components.icons.alert',
|
||||
'laravel-exceptions-renderer::components.icons.check','laravel-exceptions-renderer::components.icons.chevron-left','laravel-exceptions-renderer::components.icons.chevron-right','laravel-exceptions-renderer::components.icons.chevrons-down-up','laravel-exceptions-renderer::components.icons.chevrons-left',
|
||||
'laravel-exceptions-renderer::components.icons.chevrons-right','laravel-exceptions-renderer::components.icons.chevrons-up-down','laravel-exceptions-renderer::components.icons.copy','laravel-exceptions-renderer::components.icons.database','laravel-exceptions-renderer::components.icons.folder',
|
||||
|
||||
@@ -9,7 +9,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
|
||||
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.4.18
|
||||
- php - 8.4
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
@@ -23,11 +23,7 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
- `mcp-development` — Develops MCP servers, tools, resources, and prompts. Activates when creating MCP tools, resources, or prompts; setting up AI integrations; debugging MCP connections; working with routes/ai.php; or when the user mentions MCP, Model Context Protocol, AI tools, AI server, or building tools for AI assistants.
|
||||
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
|
||||
- `tailwindcss-development` — Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components, working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors, typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle, hero section, cards, buttons, or any visual/UI changes.
|
||||
This project has domain-specific skills available in `**/skills/**`. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -60,124 +56,85 @@ This project has domain-specific skills available. You MUST activate the relevan
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
## Tools
|
||||
|
||||
- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads.
|
||||
- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker.
|
||||
- Use `database-schema` to inspect table structure before writing migrations or models.
|
||||
- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user.
|
||||
- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries.
|
||||
|
||||
## Searching Documentation (IMPORTANT)
|
||||
|
||||
- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically.
|
||||
- Pass a `packages` array to scope results when you know which packages are relevant.
|
||||
- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first.
|
||||
- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Search Syntax
|
||||
|
||||
1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit".
|
||||
2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order.
|
||||
3. Combine words and phrases for mixed queries: `middleware "rate limit"`.
|
||||
4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`.
|
||||
|
||||
## Artisan
|
||||
|
||||
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters.
|
||||
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
||||
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
|
||||
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
|
||||
- To check environment variables, read the `.env` file directly.
|
||||
|
||||
## URLs
|
||||
## Tinker
|
||||
|
||||
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
|
||||
|
||||
## Tinker / Debugging
|
||||
|
||||
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
|
||||
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
|
||||
- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
|
||||
- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'`
|
||||
- Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`
|
||||
|
||||
=== php rules ===
|
||||
|
||||
# PHP
|
||||
|
||||
- Always use curly braces for control structures, even for single-line bodies.
|
||||
- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private.
|
||||
- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool`
|
||||
- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
|
||||
- Use array shape type definitions in PHPDoc blocks.
|
||||
|
||||
## Constructors
|
||||
=== deployments rules ===
|
||||
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- `public function __construct(public GitHub $github) { }`
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
# Deployment
|
||||
|
||||
## Type Declarations
|
||||
- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.
|
||||
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
=== tests rules ===
|
||||
|
||||
<!-- Explicit Return Types and Method Params -->
|
||||
```php
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
# Test Enforcement
|
||||
|
||||
## Enums
|
||||
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
## Comments
|
||||
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
|
||||
|
||||
## PHPDoc Blocks
|
||||
|
||||
- Add useful array shape type definitions when appropriate.
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
# Do Things the Laravel Way
|
||||
|
||||
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
|
||||
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
|
||||
- If you're creating a generic PHP class, use `php artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
## Database
|
||||
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries.
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
## APIs & Eloquent Resources
|
||||
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
## Controllers & Validation
|
||||
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
## URL Generation
|
||||
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
## Queues
|
||||
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
## Testing
|
||||
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
@@ -201,7 +158,7 @@ protected function isAccessible(User $user, ?string $path = null): bool
|
||||
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- The `app/Console/Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
## Database
|
||||
@@ -213,14 +170,6 @@ protected function isAccessible(User $user, ?string $path = null): bool
|
||||
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
=== mcp/core rules ===
|
||||
|
||||
# Laravel MCP
|
||||
|
||||
- Laravel MCP allows you to rapidly build MCP servers for your Laravel applications.
|
||||
- IMPORTANT: laravel/mcp is very new. Always use the `search-docs` tool for authoritative documentation on writing and testing Laravel MCP servers, tools, resources, and prompts.
|
||||
- IMPORTANT: Activate `mcp-development` every time you're working with an MCP-related task.
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
# Laravel Pint Code Formatter
|
||||
@@ -233,17 +182,8 @@ protected function isAccessible(User $user, ?string $path = null): bool
|
||||
## Pest
|
||||
|
||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||
- The `{name}` argument should not include the test suite directory. Use `php artisan make:test --pest SomeFeatureTest` instead of `php artisan make:test --pest Feature/SomeFeatureTest`.
|
||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||
- Do NOT delete tests without approval.
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
||||
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
||||
|
||||
=== tailwindcss/core rules ===
|
||||
|
||||
# Tailwind CSS
|
||||
|
||||
- Always use existing Tailwind conventions; check project patterns before adding new ones.
|
||||
- IMPORTANT: Always use `search-docs` tool for version-specific Tailwind CSS documentation and updated code examples. Never rely on training data.
|
||||
- IMPORTANT: Activate `tailwindcss-development` every time you're working with a Tailwind CSS or styling-related task.
|
||||
|
||||
</laravel-boost-guidelines>
|
||||
@@ -9,7 +9,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
|
||||
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.4.18
|
||||
- php - 8.4
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
@@ -23,11 +23,7 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
- `mcp-development` — Develops MCP servers, tools, resources, and prompts. Activates when creating MCP tools, resources, or prompts; setting up AI integrations; debugging MCP connections; working with routes/ai.php; or when the user mentions MCP, Model Context Protocol, AI tools, AI server, or building tools for AI assistants.
|
||||
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
|
||||
- `tailwindcss-development` — Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components, working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors, typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle, hero section, cards, buttons, or any visual/UI changes.
|
||||
This project has domain-specific skills available in `**/skills/**`. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -60,124 +56,85 @@ This project has domain-specific skills available. You MUST activate the relevan
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
## Tools
|
||||
|
||||
- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads.
|
||||
- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker.
|
||||
- Use `database-schema` to inspect table structure before writing migrations or models.
|
||||
- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user.
|
||||
- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries.
|
||||
|
||||
## Searching Documentation (IMPORTANT)
|
||||
|
||||
- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically.
|
||||
- Pass a `packages` array to scope results when you know which packages are relevant.
|
||||
- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first.
|
||||
- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Search Syntax
|
||||
|
||||
1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit".
|
||||
2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order.
|
||||
3. Combine words and phrases for mixed queries: `middleware "rate limit"`.
|
||||
4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`.
|
||||
|
||||
## Artisan
|
||||
|
||||
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters.
|
||||
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
||||
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
|
||||
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
|
||||
- To check environment variables, read the `.env` file directly.
|
||||
|
||||
## URLs
|
||||
## Tinker
|
||||
|
||||
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
|
||||
|
||||
## Tinker / Debugging
|
||||
|
||||
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
|
||||
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
|
||||
- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
|
||||
- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'`
|
||||
- Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`
|
||||
|
||||
=== php rules ===
|
||||
|
||||
# PHP
|
||||
|
||||
- Always use curly braces for control structures, even for single-line bodies.
|
||||
- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private.
|
||||
- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool`
|
||||
- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
|
||||
- Use array shape type definitions in PHPDoc blocks.
|
||||
|
||||
## Constructors
|
||||
=== deployments rules ===
|
||||
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- `public function __construct(public GitHub $github) { }`
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
# Deployment
|
||||
|
||||
## Type Declarations
|
||||
- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.
|
||||
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
=== tests rules ===
|
||||
|
||||
<!-- Explicit Return Types and Method Params -->
|
||||
```php
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
# Test Enforcement
|
||||
|
||||
## Enums
|
||||
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
## Comments
|
||||
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
|
||||
|
||||
## PHPDoc Blocks
|
||||
|
||||
- Add useful array shape type definitions when appropriate.
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
# Do Things the Laravel Way
|
||||
|
||||
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
|
||||
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
|
||||
- If you're creating a generic PHP class, use `php artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
## Database
|
||||
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries.
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
## APIs & Eloquent Resources
|
||||
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
## Controllers & Validation
|
||||
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
## URL Generation
|
||||
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
## Queues
|
||||
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
## Testing
|
||||
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
@@ -201,7 +158,7 @@ protected function isAccessible(User $user, ?string $path = null): bool
|
||||
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- The `app/Console/Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
## Database
|
||||
@@ -213,14 +170,6 @@ protected function isAccessible(User $user, ?string $path = null): bool
|
||||
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
=== mcp/core rules ===
|
||||
|
||||
# Laravel MCP
|
||||
|
||||
- Laravel MCP allows you to rapidly build MCP servers for your Laravel applications.
|
||||
- IMPORTANT: laravel/mcp is very new. Always use the `search-docs` tool for authoritative documentation on writing and testing Laravel MCP servers, tools, resources, and prompts.
|
||||
- IMPORTANT: Activate `mcp-development` every time you're working with an MCP-related task.
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
# Laravel Pint Code Formatter
|
||||
@@ -233,17 +182,8 @@ protected function isAccessible(User $user, ?string $path = null): bool
|
||||
## Pest
|
||||
|
||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||
- The `{name}` argument should not include the test suite directory. Use `php artisan make:test --pest SomeFeatureTest` instead of `php artisan make:test --pest Feature/SomeFeatureTest`.
|
||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||
- Do NOT delete tests without approval.
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
||||
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
||||
|
||||
=== tailwindcss/core rules ===
|
||||
|
||||
# Tailwind CSS
|
||||
|
||||
- Always use existing Tailwind conventions; check project patterns before adding new ones.
|
||||
- IMPORTANT: Always use `search-docs` tool for version-specific Tailwind CSS documentation and updated code examples. Never rely on training data.
|
||||
- IMPORTANT: Activate `tailwindcss-development` every time you're working with a Tailwind CSS or styling-related task.
|
||||
|
||||
</laravel-boost-guidelines>
|
||||
@@ -7,7 +7,7 @@ A comprehensive Model Context Protocol (MCP) server for FiveM (GTA5) development
|
||||
The FiveM MCP Server enables seamless integration with AI coding assistants (Claude, ChatGPT, etc.) to provide:
|
||||
- **Native function lookups** with client/server separation
|
||||
- **Event documentation** for all frameworks
|
||||
- **Framework support** for Standalone, ESX, QBCore, and COX
|
||||
- **Framework support** for Standalone, ESX, QBCore, COX, and Prodigy Scripts (prp-bridge)
|
||||
- **Resource generation** with customizable boilerplate
|
||||
- **Documentation search** across FiveM, QBCore, and COX MySQL
|
||||
- **Code snippets and best practices** for common patterns
|
||||
@@ -81,32 +81,45 @@ Configure in your AI client settings:
|
||||
|
||||
## Tools
|
||||
|
||||
The MCP server includes 15 powerful tools organized by framework and execution side:
|
||||
The MCP server includes 30 powerful tools organized by framework and execution side:
|
||||
|
||||
### FiveM Core (5 tools)
|
||||
### FiveM Core (7 tools)
|
||||
- **SearchFiveMDocs** - Search FiveM, QBCore, and COX documentation
|
||||
- **GetNativeClientFunction** - Look up client-side native functions (10 natives)
|
||||
- **GetNativeServerFunction** - Look up server-side native functions (12 natives)
|
||||
- **GetEventClientReference** - Client-side event reference (5 events)
|
||||
- **GetEventServerReference** - Server-side event reference (4+ events)
|
||||
- **GenerateManifest** - Generate fxmanifest.lua with framework support
|
||||
- **GenerateResourceBoilerplate** - Complete resource scaffolding
|
||||
|
||||
### QBCore Framework (4 tools)
|
||||
### QBCore Framework (9 tools)
|
||||
- **GetQBCoreServerFunction** - QBCore server functions (13+ functions)
|
||||
- **GetQBCoreClientFunction** - QBCore client functions (6 functions)
|
||||
- **GetQBCoreServerEventReference** - QBCore server events (6 events)
|
||||
- **GetQBCoreClientEventReference** - QBCore client events (6 events)
|
||||
- **GetQBCoreConfig** - QBCore configuration reference
|
||||
- **GetQBCorePlayers** - Player management functions
|
||||
- **GetQBCoreResourceList** - Available QBCore resources
|
||||
- **GetQBCoreResourceReference** - Resource-specific documentation
|
||||
- **GetQBCoreSharedData** - Shared data (jobs, items, vehicles)
|
||||
|
||||
### COX MySQL (2 tools)
|
||||
### COX / ox_libs (10 tools)
|
||||
- **GetCOXFunction** - OxMySQL database functions (8 functions)
|
||||
- **GetCOXEventReference** - COX-specific events
|
||||
- **GetInventoryServerFunction** - ox_inventory server functions (21 functions)
|
||||
- **GetInventoryClientFunction** - ox_inventory client functions (8 functions)
|
||||
- **GetOxDoorlockClientFunction** - ox_doorlock client API
|
||||
- **GetOxDoorlockServerFunction** - ox_doorlock server API
|
||||
- **GetOxDoorlockServerEvent** - ox_doorlock server events
|
||||
- **GetOxFuelClientFunction** - ox_fuel client API
|
||||
- **GetOxFuelServerFunction** - ox_fuel server API
|
||||
- **GetOxTargetClientFunction** - ox_target client API
|
||||
|
||||
### ox_inventory (2 tools)
|
||||
- **GetInventoryServerFunction** - Server-side inventory management (21 functions)
|
||||
- **GetInventoryClientFunction** - Client-side inventory functions (8 functions)
|
||||
|
||||
### Resource Generation (2 tools)
|
||||
- **GenerateManifest** - Generate fxmanifest.lua with framework support
|
||||
- **GenerateResourceBoilerplate** - Complete resource scaffolding
|
||||
### Prodigy Scripts / prp-bridge (4 tools)
|
||||
- **GetProdigyClientEventReference** - prp-bridge client-side events (11 events across notifications, callbacks, allowlist, medical)
|
||||
- **GetProdigyServerEventReference** - prp-bridge server-side events (8 events across framework, groups, medical, UniQueue)
|
||||
- **GetProdigyClientExport** - prp-bridge client exports (allowlist, prop placer, ped interactions)
|
||||
- **GetProdigyServerExport** - prp-bridge server exports (24 exports: cooldowns, allowlist, groups, UniQueue, sell shops, cases)
|
||||
|
||||
## Project Structure
|
||||
|
||||
@@ -114,12 +127,22 @@ The MCP server includes 15 powerful tools organized by framework and execution s
|
||||
app/Mcp/
|
||||
├── Tools/
|
||||
│ ├── COX/
|
||||
│ │ ├── Doorlock/
|
||||
│ │ │ ├── Client/GetOxDoorlockClientFunction.php
|
||||
│ │ │ └── Server/
|
||||
│ │ │ ├── GetOxDoorlockServerFunction.php
|
||||
│ │ │ └── GetOxDoorlockServerEvent.php
|
||||
│ │ ├── Fuel/
|
||||
│ │ │ ├── Client/GetOxFuelClientFunction.php
|
||||
│ │ │ └── Server/GetOxFuelServerFunction.php
|
||||
│ │ ├── Inventory/
|
||||
│ │ │ ├── Server/GetInventoryServerFunction.php
|
||||
│ │ │ └── Client/GetInventoryClientFunction.php
|
||||
│ │ ├── MySQL/
|
||||
│ │ │ ├── GetCOXFunction.php
|
||||
│ │ │ └── GetCOXEventReference.php
|
||||
│ │ └── Inventory/
|
||||
│ │ ├── Server/GetInventoryServerFunction.php
|
||||
│ │ └── Client/GetInventoryClientFunction.php
|
||||
│ │ └── Target/
|
||||
│ │ └── Client/GetOxTargetClientFunction.php
|
||||
│ ├── FiveM/
|
||||
│ │ ├── Client/
|
||||
│ │ │ ├── GetNativeClientFunction.php
|
||||
@@ -130,13 +153,25 @@ app/Mcp/
|
||||
│ │ ├── GenerateManifest.php
|
||||
│ │ ├── GenerateResourceBoilerplate.php
|
||||
│ │ └── SearchFiveMDocs.php
|
||||
│ ├── Prodigy/
|
||||
│ │ ├── Client/
|
||||
│ │ │ ├── GetProdigyClientEventReference.php
|
||||
│ │ │ └── GetProdigyClientExport.php
|
||||
│ │ └── Server/
|
||||
│ │ ├── GetProdigyServerEventReference.php
|
||||
│ │ └── GetProdigyServerExport.php
|
||||
│ └── QBCore/
|
||||
│ ├── Client/
|
||||
│ │ ├── GetQBCoreClientFunction.php
|
||||
│ │ └── GetQBCoreClientEventReference.php
|
||||
│ ├── Server/
|
||||
│ │ ├── GetQBCoreServerFunction.php
|
||||
│ │ └── GetQBCoreServerEventReference.php
|
||||
│ └── Client/
|
||||
│ ├── GetQBCoreClientFunction.php
|
||||
│ └── GetQBCoreClientEventReference.php
|
||||
│ ├── GetQBCoreConfig.php
|
||||
│ ├── GetQBCorePlayers.php
|
||||
│ ├── GetQBCoreResourceList.php
|
||||
│ ├── GetQBCoreResourceReference.php
|
||||
│ └── GetQBCoreSharedData.php
|
||||
├── Resources/
|
||||
│ ├── CodeSnippets.php
|
||||
│ ├── BestPractices.php
|
||||
@@ -186,7 +221,7 @@ php artisan test tests/Feature/GetNativeClientFunctionToolTest.php
|
||||
php artisan test --coverage
|
||||
```
|
||||
|
||||
**Current Test Status:** 82 tests passing ✅
|
||||
**Current Test Status:** 181 tests passing ✅
|
||||
|
||||
## Code Quality
|
||||
|
||||
@@ -206,6 +241,7 @@ vendor/bin/pint --test
|
||||
- [FiveM Natives Reference](https://docs.fivem.net/natives/)
|
||||
- [QBCore Documentation](https://docs.qbcore.org/)
|
||||
- [COX MySQL Documentation](https://coxdocs.dev/)
|
||||
- [Prodigy Scripts Documentation](https://docs.prodigyrp.net/)
|
||||
- [Laravel MCP Documentation](https://laravel.com/docs/mcp)
|
||||
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
||||
|
||||
|
||||
+125
-71
@@ -5,7 +5,7 @@
|
||||
|
||||
/**
|
||||
* A helper file for Laravel, to provide autocomplete information to your IDE
|
||||
* Generated for Laravel 12.53.0.
|
||||
* Generated for Laravel 12.58.0.
|
||||
*
|
||||
* This file should not be included in your code, only analyzed by your IDE!
|
||||
*
|
||||
@@ -353,7 +353,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Get the path to the views directory.
|
||||
*
|
||||
*
|
||||
* This method returns the first configured path in the array of view paths.
|
||||
*
|
||||
* @param string $path
|
||||
@@ -1229,9 +1229,9 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns true if the container can return an entry for the given identifier.
|
||||
*
|
||||
*
|
||||
* Returns false otherwise.
|
||||
*
|
||||
*
|
||||
* `has($id)` returning true does not mean that `get($id)` will not throw an exception.
|
||||
* It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
|
||||
*
|
||||
@@ -2607,7 +2607,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Log the user out of the application on their current device only.
|
||||
*
|
||||
*
|
||||
* This method does not cycle the "remember" token.
|
||||
*
|
||||
* @return void
|
||||
@@ -2621,7 +2621,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Invalidate other sessions for the current user.
|
||||
*
|
||||
*
|
||||
* The application must be using the AuthenticateSession middleware.
|
||||
*
|
||||
* @param string $password
|
||||
@@ -3585,7 +3585,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Register the routes for handling broadcast authentication and sockets.
|
||||
*
|
||||
*
|
||||
* Alias of "routes" method.
|
||||
*
|
||||
* @param array|null $attributes
|
||||
@@ -3832,7 +3832,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Dispatch a command to its appropriate handler in the current process.
|
||||
*
|
||||
*
|
||||
* Queueable jobs will be dispatched to the "sync" queue.
|
||||
*
|
||||
* @param mixed $command
|
||||
@@ -4031,7 +4031,6 @@ namespace Illuminate\Support\Facades {
|
||||
* Assert if a job was pushed exactly once.
|
||||
*
|
||||
* @param string|\Closure $command
|
||||
* @param int $times
|
||||
* @return void
|
||||
* @static
|
||||
*/
|
||||
@@ -4632,7 +4631,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Retrieve multiple items from the cache by key.
|
||||
*
|
||||
*
|
||||
* Items not found in the cache will have a null value.
|
||||
*
|
||||
* @param array $keys
|
||||
@@ -6264,7 +6263,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* @template TReturn of mixed
|
||||
*
|
||||
*
|
||||
* Run the callback function with the given context values and restore the original context state when complete.
|
||||
* @param (callable(): TReturn) $callback
|
||||
* @param array<string, mixed> $data
|
||||
@@ -8382,7 +8381,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* @template TReturn of mixed
|
||||
*
|
||||
*
|
||||
* Execute a Closure within a transaction.
|
||||
* @param (\Closure(static): TReturn) $callback
|
||||
* @param int $attempts
|
||||
@@ -8813,7 +8812,6 @@ namespace Illuminate\Support\Facades {
|
||||
* Assert if an event was dispatched exactly once.
|
||||
*
|
||||
* @param string $event
|
||||
* @param int $times
|
||||
* @return void
|
||||
* @static
|
||||
*/
|
||||
@@ -9263,7 +9261,7 @@ namespace Illuminate\Support\Facades {
|
||||
* Get the file type of a given file.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
* @return string|false
|
||||
* @static
|
||||
*/
|
||||
public static function type($path)
|
||||
@@ -9521,7 +9519,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Recursively delete a directory.
|
||||
*
|
||||
*
|
||||
* The directory itself may be optionally preserved.
|
||||
*
|
||||
* @param string $directory
|
||||
@@ -11298,7 +11296,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Action must be taken immediately.
|
||||
*
|
||||
*
|
||||
* Example: Entire website down, database unavailable, etc. This should
|
||||
* trigger the SMS alerts and wake you up.
|
||||
*
|
||||
@@ -11315,7 +11313,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Critical conditions.
|
||||
*
|
||||
*
|
||||
* Example: Application component unavailable, unexpected exception.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
@@ -11346,7 +11344,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Exceptional occurrences that are not errors.
|
||||
*
|
||||
*
|
||||
* Example: Use of deprecated APIs, poor use of an API, undesirable things
|
||||
* that are not necessarily wrong.
|
||||
*
|
||||
@@ -11377,7 +11375,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Interesting events.
|
||||
*
|
||||
*
|
||||
* Example: User logs in, SQL logs.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
@@ -12897,7 +12895,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Indicate that queue workers should not poll for restart or pause signals.
|
||||
*
|
||||
*
|
||||
* This prevents the workers from hitting the application cache to determine if they need to pause or restart.
|
||||
*
|
||||
* @return void
|
||||
@@ -13659,7 +13657,7 @@ namespace Illuminate\Support\Facades {
|
||||
*/
|
||||
class RateLimiter {
|
||||
/**
|
||||
* Register a named limiter configuration.
|
||||
* Register a named rate limiter configuration.
|
||||
*
|
||||
* @param \UnitEnum|string $name
|
||||
* @param \Closure $callback
|
||||
@@ -14508,7 +14506,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* This method belongs to Symfony HttpFoundation and is not usually needed when using Laravel.
|
||||
*
|
||||
*
|
||||
* Instead, you may use the "input" method.
|
||||
*
|
||||
* @param string $key
|
||||
@@ -14582,7 +14580,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Whether the request contains a Session object.
|
||||
*
|
||||
*
|
||||
* This method does not give any information about the state of the session object,
|
||||
* like whether the session is started or not. It is just a way to check if this Request
|
||||
* is associated with a Session instance.
|
||||
@@ -14831,7 +14829,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Sets the parameters for this request.
|
||||
*
|
||||
*
|
||||
* This method also re-initializes all properties.
|
||||
*
|
||||
* @param array $query The GET parameters
|
||||
@@ -14863,7 +14861,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Creates a Request based on a given URI and configuration.
|
||||
*
|
||||
*
|
||||
* The information contained in the URI always take precedence
|
||||
* over the other information (server and parameters).
|
||||
*
|
||||
@@ -14885,7 +14883,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Sets a callable able to create a Request instance.
|
||||
*
|
||||
*
|
||||
* This is mainly useful when you need to override the Request class
|
||||
* to keep BC with an existing system. It should not be used for any
|
||||
* other purpose.
|
||||
@@ -14900,7 +14898,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Overrides the PHP global variables according to this request instance.
|
||||
*
|
||||
*
|
||||
* It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE.
|
||||
* $_FILES is never overridden, see rfc1867
|
||||
*
|
||||
@@ -14915,7 +14913,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Sets a list of trusted proxies.
|
||||
*
|
||||
*
|
||||
* You should only list the reverse proxies that you manage directly.
|
||||
*
|
||||
* @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR'] and 'PRIVATE_SUBNETS' by IpUtils::PRIVATE_SUBNETS
|
||||
@@ -14954,7 +14952,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Sets a list of trusted host patterns.
|
||||
*
|
||||
*
|
||||
* You should only list the hosts you manage using regexs.
|
||||
*
|
||||
* @param array $hostPatterns A list of trusted host patterns
|
||||
@@ -14980,7 +14978,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Normalizes a query string.
|
||||
*
|
||||
*
|
||||
* It builds a normalized query string, where keys/value pairs are alphabetized,
|
||||
* have consistent escaping and unneeded delimiters are removed.
|
||||
*
|
||||
@@ -14994,13 +14992,13 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Enables support for the _method request parameter to determine the intended HTTP method.
|
||||
*
|
||||
*
|
||||
* Be warned that enabling this feature might lead to CSRF issues in your code.
|
||||
* Check that you are using CSRF tokens when required.
|
||||
* If the HTTP method parameter override is enabled, an html-form with method "POST" can be altered
|
||||
* and used to send a "PUT" or "DELETE" request via the _method request parameter.
|
||||
* If these methods are not protected against CSRF, this presents a possible vulnerability.
|
||||
*
|
||||
*
|
||||
* The HTTP method can only be overridden when the real HTTP method is POST.
|
||||
*
|
||||
* @static
|
||||
@@ -15024,7 +15022,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Sets the list of HTTP methods that can be overridden.
|
||||
*
|
||||
*
|
||||
* Set to null to allow all methods to be overridden (default). Set to an
|
||||
* empty array to disallow overrides entirely. Otherwise, provide the list
|
||||
* of uppercased method names that are allowed.
|
||||
@@ -15087,11 +15085,11 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns the client IP addresses.
|
||||
*
|
||||
*
|
||||
* In the returned array the most trusted IP address is first, and the
|
||||
* least trusted one last. The "real" client IP address is the last one,
|
||||
* but this is also the least trusted one. Trusted proxies are stripped.
|
||||
*
|
||||
*
|
||||
* Use this method carefully; you should use getClientIp() instead.
|
||||
*
|
||||
* @see getClientIp()
|
||||
@@ -15106,13 +15104,13 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns the client IP address.
|
||||
*
|
||||
*
|
||||
* This method can read the client IP address from the "X-Forwarded-For" header
|
||||
* when trusted proxies were set via "setTrustedProxies()". The "X-Forwarded-For"
|
||||
* header value is a comma+space separated list of IP addresses, the left-most
|
||||
* being the original client, and each successive proxy that passed the request
|
||||
* adding the IP address where it received the request from.
|
||||
*
|
||||
*
|
||||
* If your reverse proxy uses a different header name than "X-Forwarded-For",
|
||||
* ("Client-Ip" for instance), configure it via the $trustedHeaderSet
|
||||
* argument of the Request::setTrustedProxies() method instead.
|
||||
@@ -15142,11 +15140,11 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns the path being requested relative to the executed script.
|
||||
*
|
||||
*
|
||||
* The path info always starts with a /.
|
||||
*
|
||||
*
|
||||
* Suppose this request is instantiated from /mysite on localhost:
|
||||
*
|
||||
*
|
||||
* * http://localhost/mysite returns '/'
|
||||
* * http://localhost/mysite/about returns '/about'
|
||||
* * http://localhost/mysite/enco%20ded returns '/enco%20ded'
|
||||
@@ -15164,9 +15162,9 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns the root path from which this request is executed.
|
||||
*
|
||||
*
|
||||
* Suppose that an index.php file instantiates this request object:
|
||||
*
|
||||
*
|
||||
* * http://localhost/index.php returns an empty string
|
||||
* * http://localhost/index.php/page returns an empty string
|
||||
* * http://localhost/web/index.php returns '/web'
|
||||
@@ -15184,9 +15182,9 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns the root URL from which this request is executed.
|
||||
*
|
||||
*
|
||||
* The base URL never ends with a /.
|
||||
*
|
||||
*
|
||||
* This is similar to getBasePath(), except that it also includes the
|
||||
* script filename (e.g. index.php) if one exists.
|
||||
*
|
||||
@@ -15214,10 +15212,10 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns the port on which the request is made.
|
||||
*
|
||||
*
|
||||
* This method can read the client port from the "X-Forwarded-Port" header
|
||||
* when trusted proxies were set via "setTrustedProxies()".
|
||||
*
|
||||
*
|
||||
* The "X-Forwarded-Port" header must contain the client port.
|
||||
*
|
||||
* @return int|string|null Can be a string if fetched from the server bag
|
||||
@@ -15269,7 +15267,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns the HTTP host being requested.
|
||||
*
|
||||
*
|
||||
* The port name will be appended to the host if it's non-standard.
|
||||
*
|
||||
* @static
|
||||
@@ -15296,7 +15294,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Gets the scheme and HTTP host.
|
||||
*
|
||||
*
|
||||
* If the URL was called with basic authentication, the user
|
||||
* and the password are not added to the generated string.
|
||||
*
|
||||
@@ -15337,12 +15335,12 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns the path as relative reference from the current Request path.
|
||||
*
|
||||
*
|
||||
* Only the URIs path component (no schema, host etc.) is relevant and must be given.
|
||||
* Both paths must be absolute and not contain relative parts.
|
||||
* Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
|
||||
* Furthermore, they can be used to reduce the link size in documents.
|
||||
*
|
||||
*
|
||||
* Example target paths, given a base path of "/a/b/c/d":
|
||||
* - "/a/b/c/d" -> ""
|
||||
* - "/a/b/c/" -> "./"
|
||||
@@ -15361,7 +15359,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Generates the normalized query string for the Request.
|
||||
*
|
||||
*
|
||||
* It builds a normalized query string, where keys/value pairs are alphabetized
|
||||
* and have consistent escaping.
|
||||
*
|
||||
@@ -15376,10 +15374,10 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Checks whether the request is secure or not.
|
||||
*
|
||||
*
|
||||
* This method can read the client protocol from the "X-Forwarded-Proto" header
|
||||
* when trusted proxies were set via "setTrustedProxies()".
|
||||
*
|
||||
*
|
||||
* The "X-Forwarded-Proto" header must contain the protocol: "https" or "http".
|
||||
*
|
||||
* @static
|
||||
@@ -15393,10 +15391,10 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns the host name.
|
||||
*
|
||||
*
|
||||
* This method can read the client host name from the "X-Forwarded-Host" header
|
||||
* when trusted proxies were set via "setTrustedProxies()".
|
||||
*
|
||||
*
|
||||
* The "X-Forwarded-Host" header must contain the client host name.
|
||||
*
|
||||
* @throws SuspiciousOperationException when the host name is invalid or not trusted
|
||||
@@ -15423,13 +15421,13 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Gets the request "intended" method.
|
||||
*
|
||||
*
|
||||
* If the X-HTTP-Method-Override header is set, and if the method is a POST,
|
||||
* then it is used to determine the "real" intended HTTP method.
|
||||
*
|
||||
*
|
||||
* The _method request parameter can also be used to determine the HTTP method,
|
||||
* but only if enableHttpMethodParameterOverride() has been called.
|
||||
*
|
||||
*
|
||||
* The method is always an uppercased string.
|
||||
*
|
||||
* @see getRealMethod()
|
||||
@@ -15481,7 +15479,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Gets the format associated with the mime type.
|
||||
*
|
||||
*
|
||||
* Resolution order:
|
||||
* 1) Exact match on the full MIME type (e.g. "application/json").
|
||||
* 2) Match on the canonical MIME type (i.e. before the first ";" parameter).
|
||||
@@ -15518,9 +15516,9 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Gets the request format.
|
||||
*
|
||||
*
|
||||
* Here is the process to determine the format:
|
||||
*
|
||||
*
|
||||
* * format defined by the user (with setRequestFormat())
|
||||
* * _format request attribute
|
||||
* * $default
|
||||
@@ -15661,7 +15659,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns the protocol version.
|
||||
*
|
||||
*
|
||||
* If the application is behind a proxy, the protocol version used in the
|
||||
* requests between the client and the proxy and between the proxy and the
|
||||
* server might be different. This returns the former (from the "Via" header)
|
||||
@@ -15731,7 +15729,7 @@ namespace Illuminate\Support\Facades {
|
||||
* Gets the preferred format for the response by inspecting, in the following order:
|
||||
* * the request format set using setRequestFormat;
|
||||
* * the values of the Accept HTTP header.
|
||||
*
|
||||
*
|
||||
* Note that if you use this method, you should send the "Vary: Accept" header
|
||||
* in the response to prevent any issues with intermediary HTTP caches.
|
||||
*
|
||||
@@ -15798,7 +15796,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Returns true if the request is an XMLHttpRequest.
|
||||
*
|
||||
*
|
||||
* It works if your JavaScript library sets an X-Requested-With HTTP header.
|
||||
* It is known to work with common JavaScript frameworks:
|
||||
*
|
||||
@@ -15827,7 +15825,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Indicates whether this request originated from a trusted proxy.
|
||||
*
|
||||
*
|
||||
* This can be useful to determine whether or not to trust the
|
||||
* contents of a proxy-specific header.
|
||||
*
|
||||
@@ -15913,6 +15911,18 @@ namespace Illuminate\Support\Facades {
|
||||
return $instance->wantsJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current request is asking for Markdown.
|
||||
*
|
||||
* @return bool
|
||||
* @static
|
||||
*/
|
||||
public static function wantsMarkdown()
|
||||
{
|
||||
/** @var \Illuminate\Http\Request $instance */
|
||||
return $instance->wantsMarkdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current requests accepts a given content type.
|
||||
*
|
||||
@@ -15963,6 +15973,18 @@ namespace Illuminate\Support\Facades {
|
||||
return $instance->acceptsJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a request accepts Markdown.
|
||||
*
|
||||
* @return bool
|
||||
* @static
|
||||
*/
|
||||
public static function acceptsMarkdown()
|
||||
{
|
||||
/** @var \Illuminate\Http\Request $instance */
|
||||
return $instance->acceptsMarkdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a request accepts HTML.
|
||||
*
|
||||
@@ -16457,7 +16479,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Retrieve data as a boolean value.
|
||||
*
|
||||
*
|
||||
* Returns true when value is "1", "true", "on", and "yes". Otherwise, returns false.
|
||||
*
|
||||
* @param string|null $key
|
||||
@@ -16531,6 +16553,20 @@ namespace Illuminate\Support\Facades {
|
||||
return $instance->date($key, $format, $tz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve data from the instance as a CarbonInterval instance.
|
||||
*
|
||||
* @param string $key
|
||||
* @param \Carbon\Unit|string|null $unit
|
||||
* @return \Carbon\CarbonInterval|null
|
||||
* @static
|
||||
*/
|
||||
public static function interval($key, $unit = null)
|
||||
{
|
||||
/** @var \Illuminate\Http\Request $instance */
|
||||
return $instance->interval($key, $unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve data from the instance as an enum.
|
||||
*
|
||||
@@ -17683,7 +17719,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Add a middleware to the beginning of a middleware group.
|
||||
*
|
||||
*
|
||||
* If the middleware is already in the group, it will not be added again.
|
||||
*
|
||||
* @param string $group
|
||||
@@ -17699,7 +17735,7 @@ namespace Illuminate\Support\Facades {
|
||||
|
||||
/**
|
||||
* Add a middleware to the end of a middleware group.
|
||||
*
|
||||
*
|
||||
* If the middleware is already in the group, it will not be added again.
|
||||
*
|
||||
* @param string $group
|
||||
@@ -18292,7 +18328,7 @@ namespace Illuminate\Support\Facades {
|
||||
/**
|
||||
* Create new schedule group.
|
||||
*
|
||||
* @param \Illuminate\Console\Scheduling\Event $event
|
||||
* @param \Closure $events
|
||||
* @return void
|
||||
* @throws \RuntimeException
|
||||
* @static
|
||||
@@ -20556,6 +20592,7 @@ namespace Illuminate\Support\Facades {
|
||||
* @param array $headers
|
||||
* @param string|null $disposition
|
||||
* @return \Symfony\Component\HttpFoundation\StreamedResponse
|
||||
* @throws UnableToRetrieveMetadata
|
||||
* @static
|
||||
*/
|
||||
public static function response($path, $name = null, $headers = [], $disposition = 'inline')
|
||||
@@ -20573,6 +20610,7 @@ namespace Illuminate\Support\Facades {
|
||||
* @param string|null $name
|
||||
* @param array $headers
|
||||
* @return \Symfony\Component\HttpFoundation\StreamedResponse
|
||||
* @throws UnableToRetrieveMetadata
|
||||
* @static
|
||||
*/
|
||||
public static function serve($request, $path, $name = null, $headers = [])
|
||||
@@ -20589,6 +20627,7 @@ namespace Illuminate\Support\Facades {
|
||||
* @param string|null $name
|
||||
* @param array $headers
|
||||
* @return \Symfony\Component\HttpFoundation\StreamedResponse
|
||||
* @throws UnableToRetrieveMetadata
|
||||
* @static
|
||||
*/
|
||||
public static function download($path, $name = null, $headers = [])
|
||||
@@ -20787,6 +20826,7 @@ namespace Illuminate\Support\Facades {
|
||||
*
|
||||
* @param string $path
|
||||
* @return string|false
|
||||
* @throws UnableToRetrieveMetadata
|
||||
* @static
|
||||
*/
|
||||
public static function mimeType($path)
|
||||
@@ -24309,7 +24349,7 @@ namespace {
|
||||
|
||||
/**
|
||||
* Specify attributes that should be added to any new models created by this builder.
|
||||
*
|
||||
*
|
||||
* The given key / value pairs will also be added as where conditions to the query.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|array|string $attributes
|
||||
@@ -24890,7 +24930,7 @@ namespace {
|
||||
|
||||
/**
|
||||
* Add a relationship count / exists condition to the query with where clauses.
|
||||
*
|
||||
*
|
||||
* Also load the relationship with the same condition.
|
||||
*
|
||||
* @param string $relation
|
||||
@@ -27345,6 +27385,20 @@ namespace {
|
||||
return $instance->inRandomOrder($seed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an "order by" clause to order results by a given sequence of values.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @param \Illuminate\Contracts\Support\Arrayable|array $values
|
||||
* @return \Illuminate\Database\Eloquent\Builder<static>
|
||||
* @static
|
||||
*/
|
||||
public static function inOrderOf($column, $values)
|
||||
{
|
||||
/** @var \Illuminate\Database\Query\Builder $instance */
|
||||
return $instance->inOrderOf($column, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a raw "order by" clause to the query.
|
||||
*
|
||||
|
||||
@@ -28,6 +28,10 @@ use App\Mcp\Tools\FiveM\GenerateResourceBoilerplate;
|
||||
use App\Mcp\Tools\FiveM\SearchFiveMDocs;
|
||||
use App\Mcp\Tools\FiveM\Server\GetEventServerReference;
|
||||
use App\Mcp\Tools\FiveM\Server\GetNativeServerFunction;
|
||||
use App\Mcp\Tools\Prodigy\Client\GetProdigyClientEventReference;
|
||||
use App\Mcp\Tools\Prodigy\Client\GetProdigyClientExport;
|
||||
use App\Mcp\Tools\Prodigy\Server\GetProdigyServerEventReference;
|
||||
use App\Mcp\Tools\Prodigy\Server\GetProdigyServerExport;
|
||||
use App\Mcp\Tools\QBCore\Client\GetQBCoreClientEventReference;
|
||||
use App\Mcp\Tools\QBCore\Client\GetQBCoreClientFunction;
|
||||
use App\Mcp\Tools\QBCore\GetQBCoreConfig;
|
||||
@@ -86,6 +90,10 @@ class FiveMServer extends Server
|
||||
GetOxDoorlockClientFunction::class,
|
||||
GetOxDoorlockServerFunction::class,
|
||||
GetOxDoorlockServerEvent::class,
|
||||
GetProdigyClientEventReference::class,
|
||||
GetProdigyServerEventReference::class,
|
||||
GetProdigyClientExport::class,
|
||||
GetProdigyServerExport::class,
|
||||
];
|
||||
|
||||
protected array $resources = [
|
||||
|
||||
@@ -90,7 +90,7 @@ class GetOxDoorlockClientFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -75,7 +75,7 @@ class GetOxDoorlockServerEvent extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -111,7 +111,7 @@ class GetOxDoorlockServerFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -74,7 +74,7 @@ class GetOxFuelClientFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -74,7 +74,7 @@ class GetOxFuelServerFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -159,7 +159,7 @@ class GetInventoryClientFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -318,7 +318,7 @@ class GetInventoryServerFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -190,7 +190,7 @@ class GetCOXEventReference extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -172,7 +172,7 @@ class GetCOXFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -241,7 +241,7 @@ class GetOxTargetClientFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -158,7 +158,7 @@ class GetEventClientReference extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -202,7 +202,7 @@ class GetNativeClientFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ class GenerateManifest extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -126,7 +126,7 @@ class GenerateResourceBoilerplate extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -262,7 +262,7 @@ class SearchFiveMDocs extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -164,7 +164,7 @@ class GetEventServerReference extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -191,7 +191,7 @@ class GetNativeServerFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools\Prodigy\Client;
|
||||
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Description('prp-bridge event reference for client-side events fired by the Prodigy Studios bridge resource. Includes notifications, sound, callbacks, allowlist, and medical events.')]
|
||||
class GetProdigyClientEventReference extends Tool
|
||||
{
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$eventName = $request->get('event_name', '');
|
||||
|
||||
$events = $this->getClientEvents();
|
||||
|
||||
if ($eventName) {
|
||||
$event = collect($events)->firstWhere('name', $eventName);
|
||||
if ($event) {
|
||||
return Response::text($this->formatEvent($event));
|
||||
}
|
||||
|
||||
return Response::text("prp-bridge client-side event '$eventName' not found. See https://docs.prodigyrp.net/prp-bridge/events.html for the full reference.");
|
||||
}
|
||||
|
||||
return Response::text($this->formatEventList($events));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all prp-bridge client-side events.
|
||||
*/
|
||||
protected function getClientEvents(): array
|
||||
{
|
||||
return [
|
||||
// Core — Notifications & Sound
|
||||
[
|
||||
'name' => 'prp-bridge:notify',
|
||||
'side' => 'client',
|
||||
'category' => 'Core — Notifications & Sound',
|
||||
'description' => 'Sends a notification to the player. Trigger from server via bridge.fw.notify. Type: success, error, inform.',
|
||||
'parameters' => [
|
||||
'type' => 'Notification type: "success", "error", or "inform"',
|
||||
'message' => 'The notification body text',
|
||||
'title' => 'Optional title text',
|
||||
'duration' => 'Duration in milliseconds (optional)',
|
||||
],
|
||||
'lua_example' => "-- Trigger from client\nTriggerEvent('prp-bridge:notify', 'success', 'You sold the item!', 'Shop', 5000)\n\n-- Trigger from server\nTriggerClientEvent('prp-bridge:notify', source, 'success', 'You sold the item!', 'Shop', 5000)",
|
||||
'js_example' => "// Trigger from client\nemit('prp-bridge:notify', 'success', 'You sold the item!', 'Shop', 5000);\n\n// Trigger from server\nemitNet('prp-bridge:notify', source, 'success', 'You sold the item!', 'Shop', 5000);",
|
||||
],
|
||||
[
|
||||
'name' => 'prp-bridge:sound:play',
|
||||
'side' => 'client',
|
||||
'category' => 'Core — Notifications & Sound',
|
||||
'description' => 'Plays a sound on the client.',
|
||||
'parameters' => [
|
||||
'fileName' => 'Sound file name',
|
||||
'volume' => 'Volume level (0.0 – 1.0)',
|
||||
'noAutoPath' => 'Set true to use a fully qualified path instead of the automatic one',
|
||||
'resourceName' => 'Resource that owns the sound file',
|
||||
],
|
||||
'lua_example' => "TriggerEvent('prp-bridge:sound:play', 'cash_register', 0.5, false, 'my-resource')",
|
||||
'js_example' => "emit('prp-bridge:sound:play', 'cash_register', 0.5, false, 'my-resource');",
|
||||
],
|
||||
[
|
||||
'name' => 'prp-bridge:sound:playSpatial',
|
||||
'side' => 'client',
|
||||
'category' => 'Core — Notifications & Sound',
|
||||
'description' => 'Plays a 3D spatial sound at world coordinates.',
|
||||
'parameters' => [
|
||||
'fileName' => 'Sound file name',
|
||||
'volume' => 'Volume level (0.0 – 1.0)',
|
||||
'coords' => 'vector3 world position',
|
||||
'distance' => 'Max audible distance',
|
||||
'shouldMuffle' => 'Whether to apply interior muffling',
|
||||
'noAutoPath' => 'Set true to use a fully qualified path',
|
||||
'resourceName' => 'Resource that owns the sound file',
|
||||
],
|
||||
'lua_example' => "TriggerEvent('prp-bridge:sound:playSpatial', 'explosion', 1.0, vector3(100.0, 200.0, 30.0), 50.0, false, false, 'my-resource')",
|
||||
'js_example' => "emit('prp-bridge:sound:playSpatial', 'explosion', 1.0, [100.0, 200.0, 30.0], 50.0, false, false, 'my-resource');",
|
||||
],
|
||||
|
||||
// Core — Callbacks
|
||||
[
|
||||
'name' => 'prp-bridge:progress',
|
||||
'side' => 'client',
|
||||
'category' => 'Core — Callbacks',
|
||||
'description' => 'Progress bar callback. Invoked via lib.callback from the server.',
|
||||
'parameters' => [],
|
||||
'lua_example' => "-- Invoked internally via lib.callback from the server\n-- Use lib.callback.await on the server to trigger it\nlib.callback('prp-bridge:progress', source, function(result)\n print('Progress result:', result)\nend, { duration = 5000, label = 'Processing...' })",
|
||||
'js_example' => '// Invoked internally via lib.callback from the server',
|
||||
],
|
||||
[
|
||||
'name' => 'prp-bridge:minigame',
|
||||
'side' => 'client',
|
||||
'category' => 'Core — Callbacks',
|
||||
'description' => 'Minigame callback. Invoked via lib.callback from the server.',
|
||||
'parameters' => [],
|
||||
'lua_example' => "-- Invoked internally via lib.callback from the server\nlib.callback('prp-bridge:minigame', source, function(success)\n if success then\n print('Minigame passed!')\n end\nend, { type = 'circle', difficulty = 'easy' })",
|
||||
'js_example' => '// Invoked internally via lib.callback from the server',
|
||||
],
|
||||
[
|
||||
'name' => 'prp-bridge:confirmDialog',
|
||||
'side' => 'client',
|
||||
'category' => 'Core — Callbacks',
|
||||
'description' => 'Confirm dialog callback. Invoked via lib.callback from the server.',
|
||||
'parameters' => [],
|
||||
'lua_example' => "-- Invoked internally via lib.callback from the server\nlib.callback('prp-bridge:confirmDialog', source, function(confirmed)\n if confirmed then\n print('Player confirmed!')\n end\nend, { title = 'Are you sure?', content = 'This cannot be undone.' })",
|
||||
'js_example' => '// Invoked internally via lib.callback from the server',
|
||||
],
|
||||
[
|
||||
'name' => 'prp-bridge:inputDialog',
|
||||
'side' => 'client',
|
||||
'category' => 'Core — Callbacks',
|
||||
'description' => 'Input dialog callback. Invoked via lib.callback from the server.',
|
||||
'parameters' => [],
|
||||
'lua_example' => "-- Invoked internally via lib.callback from the server\nlib.callback('prp-bridge:inputDialog', source, function(input)\n print('Player entered:', input)\nend, { title = 'Enter value', inputs = { { type = 'input', label = 'Name' } } })",
|
||||
'js_example' => '// Invoked internally via lib.callback from the server',
|
||||
],
|
||||
[
|
||||
'name' => 'prp-bridge:placeProp',
|
||||
'side' => 'client',
|
||||
'category' => 'Core — Callbacks',
|
||||
'description' => 'Prop placer callback. Opens the prop placement UI. Invoked via lib.callback from the server.',
|
||||
'parameters' => [],
|
||||
'lua_example' => "-- Invoked internally via lib.callback from the server\nlib.callback('prp-bridge:placeProp', source, function(coords)\n if coords then\n print('Prop placed at:', coords)\n end\nend, { model = 'prop_barrier_work05a' })",
|
||||
'js_example' => '// Invoked internally via lib.callback from the server',
|
||||
],
|
||||
|
||||
// Allowlist
|
||||
[
|
||||
'name' => 'prp-bridge:client:updateAllowlist',
|
||||
'side' => 'client',
|
||||
'category' => 'Allowlist',
|
||||
'description' => 'Fired when the player\'s allowlists are updated or loaded.',
|
||||
'parameters' => [
|
||||
'allowlists' => 'table<string, boolean> — map of allowlist name to whether the player has it',
|
||||
],
|
||||
'lua_example' => "AddEventHandler('prp-bridge:client:updateAllowlist', function(allowlists)\n if allowlists['my_allowlist'] then\n print('Player has my_allowlist!')\n end\nend)",
|
||||
'js_example' => "on('prp-bridge:client:updateAllowlist', (allowlists) => {\n if (allowlists['my_allowlist']) {\n console.log('Player has my_allowlist!');\n }\n});",
|
||||
],
|
||||
|
||||
// Medical
|
||||
[
|
||||
'name' => 'prp-bridge:client:revived',
|
||||
'side' => 'client',
|
||||
'category' => 'Medical',
|
||||
'description' => 'Fired when the local player is revived.',
|
||||
'parameters' => [],
|
||||
'lua_example' => "AddEventHandler('prp-bridge:client:revived', function()\n print('Player was revived!')\nend)",
|
||||
'js_example' => "on('prp-bridge:client:revived', () => {\n console.log('Player was revived!');\n});",
|
||||
],
|
||||
[
|
||||
'name' => 'prp-bridge:client:died',
|
||||
'side' => 'client',
|
||||
'category' => 'Medical',
|
||||
'description' => 'Fired when the local player dies.',
|
||||
'parameters' => [],
|
||||
'lua_example' => "AddEventHandler('prp-bridge:client:died', function()\n print('Player died!')\nend)",
|
||||
'js_example' => "on('prp-bridge:client:died', () => {\n console.log('Player died!');\n});",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single event for output.
|
||||
*/
|
||||
protected function formatEvent(array $event): string
|
||||
{
|
||||
return view('mcp.prodigy.prodigy-event-reference', [
|
||||
'event' => $event,
|
||||
])->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format full event listing grouped by category.
|
||||
*/
|
||||
protected function formatEventList(array $events): string
|
||||
{
|
||||
$byCategory = collect($events)->groupBy('category')->toArray();
|
||||
|
||||
return view('mcp.prodigy.prodigy-event-list', [
|
||||
'eventsByCategory' => $byCategory,
|
||||
])->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'event_name' => $schema
|
||||
->string()
|
||||
->description('The prp-bridge client event name to look up (e.g. "prp-bridge:notify"). Omit to list all client events.')
|
||||
->default(''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools\Prodigy\Client;
|
||||
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Description('prp-bridge client-side exports reference. Covers the IsAllowlisted allowlist check, the PropPlacer UI, and the Ped Interactions system (AddPedInteraction, RemovePedInteraction).')]
|
||||
class GetProdigyClientExport extends Tool
|
||||
{
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$exportName = $request->get('export_name', '');
|
||||
|
||||
$exports = $this->getClientExports();
|
||||
|
||||
if ($exportName) {
|
||||
$export = collect($exports)->firstWhere('name', $exportName);
|
||||
if ($export) {
|
||||
return Response::text($this->formatExport($export));
|
||||
}
|
||||
|
||||
return Response::text("prp-bridge client export '$exportName' not found. See https://docs.prodigyrp.net/prp-bridge/exports.html for the full reference.");
|
||||
}
|
||||
|
||||
return Response::text($this->formatExportList($exports));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all prp-bridge client-side exports.
|
||||
*/
|
||||
protected function getClientExports(): array
|
||||
{
|
||||
return [
|
||||
// Allowlist
|
||||
[
|
||||
'name' => 'IsAllowlisted',
|
||||
'category' => 'Allowlist',
|
||||
'side' => 'client',
|
||||
'description' => 'Returns whether the current player character has a specific allowlist.',
|
||||
'parameters' => [
|
||||
['name' => 'allowlist', 'type' => 'string', 'description' => 'The allowlist key to check (e.g. "police_allowlist")'],
|
||||
],
|
||||
'returns' => ['type' => 'boolean', 'description' => 'true if the character has the allowlist, false otherwise'],
|
||||
'lua_example' => "local isAllowed = exports['prp-bridge']:IsAllowlisted('police_allowlist')\nif isAllowed then\n print('Player has police_allowlist')\nend",
|
||||
],
|
||||
|
||||
// Prop Placer
|
||||
[
|
||||
'name' => 'PropPlacer',
|
||||
'category' => 'Prop Placer',
|
||||
'side' => 'client',
|
||||
'description' => 'Opens the interactive prop placement UI. Returns the final position and heading, or nil if the player cancelled.',
|
||||
'parameters' => [
|
||||
['name' => 'model', 'type' => 'number', 'description' => 'Model hash of the prop to place'],
|
||||
['name' => 'forceGround', 'type' => 'boolean?', 'description' => 'Force the prop to snap to the ground (optional)'],
|
||||
['name' => 'allowedMaterials', 'type' => 'table?', 'description' => 'Table of allowed surface material types (optional)'],
|
||||
['name' => 'maxDistance', 'type' => 'number?', 'description' => 'Maximum placement distance from the player (optional)'],
|
||||
],
|
||||
'returns' => ['type' => 'vector4 | nil', 'description' => 'vector4(x, y, z, heading) of the placed prop, or nil if cancelled'],
|
||||
'lua_example' => "local result = exports['prp-bridge']:PropPlacer(\n GetHashKey('prop_barrier_work05a'),\n true,\n nil,\n 10.0\n)\nif result then\n print('Placed at:', result.x, result.y, result.z, 'heading:', result.w)\nelse\n print('Placement cancelled')\nend",
|
||||
],
|
||||
|
||||
// Ped Interactions
|
||||
[
|
||||
'name' => 'AddPedInteraction',
|
||||
'category' => 'Ped Interactions',
|
||||
'side' => 'client',
|
||||
'description' => 'Registers a ped interaction. The ped spawns when a player enters the radius and is removed when they leave. Returns true on success, or false with an error string.',
|
||||
'parameters' => [
|
||||
['name' => 'id', 'type' => 'string', 'description' => 'Unique identifier for this ped interaction'],
|
||||
['name' => 'payload', 'type' => 'table', 'description' => 'Configuration table (model, coords, heading, radius, options, scenario/anim, component, weapon)'],
|
||||
],
|
||||
'returns' => ['type' => 'boolean, string?', 'description' => 'true on success; false, "missing" if id/payload is nil; false, "exists" if id is already registered'],
|
||||
'lua_example' => "exports['prp-bridge']:AddPedInteraction('my_dealer', {\n model = 's_m_y_dealer_01',\n coords = vector3(100.0, 200.0, 30.0),\n heading = 180.0,\n radius = 50.0,\n scenario = 'WORLD_HUMAN_STAND_IMPATIENT',\n options = {\n {\n icon = 'fas fa-comments',\n label = 'Talk to Dealer',\n distance = 2.0,\n onSelect = function()\n openDealerMenu()\n end\n }\n }\n})",
|
||||
],
|
||||
[
|
||||
'name' => 'RemovePedInteraction',
|
||||
'category' => 'Ped Interactions',
|
||||
'side' => 'client',
|
||||
'description' => 'Removes a registered ped interaction and deletes the ped entity if it currently exists.',
|
||||
'parameters' => [
|
||||
['name' => 'id', 'type' => 'string', 'description' => 'The ID used when the ped was registered with AddPedInteraction'],
|
||||
],
|
||||
'returns' => ['type' => 'boolean', 'description' => 'true if the ped was removed; false if the ID was not found'],
|
||||
'lua_example' => "local removed = exports['prp-bridge']:RemovePedInteraction('my_dealer')\nif not removed then\n print('Ped not found')\nend\n\n-- Auto-cleanup on resource stop is handled automatically.\n-- Use manual removal only when you need to remove the ped while the resource is still running.",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single export for output.
|
||||
*/
|
||||
protected function formatExport(array $export): string
|
||||
{
|
||||
return view('mcp.prodigy.prodigy-export-reference', [
|
||||
'export' => $export,
|
||||
])->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format full export listing grouped by category.
|
||||
*/
|
||||
protected function formatExportList(array $exports): string
|
||||
{
|
||||
$byCategory = collect($exports)->groupBy('category')->toArray();
|
||||
|
||||
return view('mcp.prodigy.prodigy-export-list', [
|
||||
'exportsByCategory' => $byCategory,
|
||||
])->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'export_name' => $schema
|
||||
->string()
|
||||
->description('The prp-bridge client export name to look up (e.g. "IsAllowlisted", "AddPedInteraction"). Omit to list all client exports.')
|
||||
->default(''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools\Prodigy\Server;
|
||||
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Description('prp-bridge event reference for server-side events fired by the Prodigy Studios bridge resource. Includes player load/unload, group lifecycle, medical, and UniQueue events.')]
|
||||
class GetProdigyServerEventReference extends Tool
|
||||
{
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$eventName = $request->get('event_name', '');
|
||||
|
||||
$events = $this->getServerEvents();
|
||||
|
||||
if ($eventName) {
|
||||
$event = collect($events)->firstWhere('name', $eventName);
|
||||
if ($event) {
|
||||
return Response::text($this->formatEvent($event));
|
||||
}
|
||||
|
||||
return Response::text("prp-bridge server-side event '$eventName' not found. See https://docs.prodigyrp.net/prp-bridge/events.html for the full reference.");
|
||||
}
|
||||
|
||||
return Response::text($this->formatEventList($events));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all prp-bridge server-side events.
|
||||
*/
|
||||
protected function getServerEvents(): array
|
||||
{
|
||||
return [
|
||||
// Framework
|
||||
[
|
||||
'name' => 'prp-bridge:server:playerLoad',
|
||||
'side' => 'server',
|
||||
'category' => 'Framework',
|
||||
'description' => 'Fired when a player character loads on the server.',
|
||||
'parameters' => [
|
||||
'src' => 'number — the player server ID',
|
||||
],
|
||||
'lua_example' => "AddEventHandler('prp-bridge:server:playerLoad', function(src)\n print('Player ' .. src .. ' character loaded')\nend)",
|
||||
'js_example' => "on('prp-bridge:server:playerLoad', (src) => {\n console.log(`Player \${src} character loaded`);\n});",
|
||||
],
|
||||
[
|
||||
'name' => 'prp-bridge:server:playerUnload',
|
||||
'side' => 'server',
|
||||
'category' => 'Framework',
|
||||
'description' => 'Fired when a player character unloads on the server.',
|
||||
'parameters' => [
|
||||
'src' => 'number — the player server ID',
|
||||
],
|
||||
'lua_example' => "AddEventHandler('prp-bridge:server:playerUnload', function(src)\n print('Player ' .. src .. ' character unloaded')\nend)",
|
||||
'js_example' => "on('prp-bridge:server:playerUnload', (src) => {\n console.log(`Player \${src} character unloaded`);\n});",
|
||||
],
|
||||
|
||||
// Groups
|
||||
[
|
||||
'name' => 'prp-bridge:server:groupMemberAdded',
|
||||
'side' => 'server',
|
||||
'category' => 'Groups',
|
||||
'description' => 'Fired when a member joins a group.',
|
||||
'parameters' => [
|
||||
'src' => 'number — the player server ID who joined',
|
||||
'groupUuid' => 'string — unique identifier of the group',
|
||||
],
|
||||
'lua_example' => "AddEventHandler('prp-bridge:server:groupMemberAdded', function(src, groupUuid)\n print('Player ' .. src .. ' joined group ' .. groupUuid)\nend)",
|
||||
'js_example' => "on('prp-bridge:server:groupMemberAdded', (src, groupUuid) => {\n console.log(`Player \${src} joined group \${groupUuid}`);\n});",
|
||||
],
|
||||
[
|
||||
'name' => 'prp-bridge:server:groupMemberRemoved',
|
||||
'side' => 'server',
|
||||
'category' => 'Groups',
|
||||
'description' => 'Fired when a member leaves or is kicked from a group.',
|
||||
'parameters' => [
|
||||
'src' => 'number — the player server ID who left',
|
||||
'groupUuid' => 'string — unique identifier of the group',
|
||||
],
|
||||
'lua_example' => "AddEventHandler('prp-bridge:server:groupMemberRemoved', function(src, groupUuid)\n print('Player ' .. src .. ' left group ' .. groupUuid)\nend)",
|
||||
'js_example' => "on('prp-bridge:server:groupMemberRemoved', (src, groupUuid) => {\n console.log(`Player \${src} left group \${groupUuid}`);\n});",
|
||||
],
|
||||
[
|
||||
'name' => 'prp-bridge:server:groupDisbanded',
|
||||
'side' => 'server',
|
||||
'category' => 'Groups',
|
||||
'description' => 'Fired when a group is disbanded.',
|
||||
'parameters' => [
|
||||
'groupUuid' => 'string — unique identifier of the disbanded group',
|
||||
],
|
||||
'lua_example' => "AddEventHandler('prp-bridge:server:groupDisbanded', function(groupUuid)\n print('Group ' .. groupUuid .. ' was disbanded')\nend)",
|
||||
'js_example' => "on('prp-bridge:server:groupDisbanded', (groupUuid) => {\n console.log(`Group \${groupUuid} was disbanded`);\n});",
|
||||
],
|
||||
|
||||
// Medical
|
||||
[
|
||||
'name' => 'prp-bridge:server:revived',
|
||||
'side' => 'server',
|
||||
'category' => 'Medical',
|
||||
'description' => 'Fired when a player is revived. Source is implicit (use source/global.source).',
|
||||
'parameters' => [],
|
||||
'lua_example' => "AddEventHandler('prp-bridge:server:revived', function()\n local src = source\n print('Player ' .. src .. ' was revived')\nend)",
|
||||
'js_example' => "on('prp-bridge:server:revived', () => {\n const src = global.source;\n console.log(`Player \${src} was revived`);\n});",
|
||||
],
|
||||
[
|
||||
'name' => 'prp-bridge:server:died',
|
||||
'side' => 'server',
|
||||
'category' => 'Medical',
|
||||
'description' => 'Fired when a player dies. Source is implicit (use source/global.source).',
|
||||
'parameters' => [],
|
||||
'lua_example' => "AddEventHandler('prp-bridge:server:died', function()\n local src = source\n print('Player ' .. src .. ' died')\nend)",
|
||||
'js_example' => "on('prp-bridge:server:died', () => {\n const src = global.source;\n console.log(`Player \${src} died`);\n});",
|
||||
],
|
||||
|
||||
// UniQueue
|
||||
[
|
||||
'name' => 'prp-bridge:uniqueue:partyDestroyed',
|
||||
'side' => 'server',
|
||||
'category' => 'UniQueue',
|
||||
'description' => 'Fired when a UniQueue party is destroyed.',
|
||||
'parameters' => [
|
||||
'partyUuid' => 'string — unique identifier of the destroyed party',
|
||||
],
|
||||
'lua_example' => "AddEventHandler('prp-bridge:uniqueue:partyDestroyed', function(partyUuid)\n print('Party ' .. partyUuid .. ' was destroyed')\nend)",
|
||||
'js_example' => "on('prp-bridge:uniqueue:partyDestroyed', (partyUuid) => {\n console.log(`Party \${partyUuid} was destroyed`);\n});",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single event for output.
|
||||
*/
|
||||
protected function formatEvent(array $event): string
|
||||
{
|
||||
return view('mcp.prodigy.prodigy-event-reference', [
|
||||
'event' => $event,
|
||||
])->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format full event listing grouped by category.
|
||||
*/
|
||||
protected function formatEventList(array $events): string
|
||||
{
|
||||
$byCategory = collect($events)->groupBy('category')->toArray();
|
||||
|
||||
return view('mcp.prodigy.prodigy-event-list', [
|
||||
'eventsByCategory' => $byCategory,
|
||||
])->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'event_name' => $schema
|
||||
->string()
|
||||
->description('The prp-bridge server event name to look up (e.g. "prp-bridge:server:playerLoad"). Omit to list all server events.')
|
||||
->default(''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools\Prodigy\Server;
|
||||
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Description('prp-bridge server-side exports reference. Covers Cooldowns, Allowlist, Groups, UniQueue, Sell Shops, and Cases exports provided by the Prodigy Studios bridge resource.')]
|
||||
class GetProdigyServerExport extends Tool
|
||||
{
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$exportName = $request->get('export_name', '');
|
||||
|
||||
$exports = $this->getServerExports();
|
||||
|
||||
if ($exportName) {
|
||||
$export = collect($exports)->firstWhere('name', $exportName);
|
||||
if ($export) {
|
||||
return Response::text($this->formatExport($export));
|
||||
}
|
||||
|
||||
return Response::text("prp-bridge server export '$exportName' not found. See https://docs.prodigyrp.net/prp-bridge/exports.html for the full reference.");
|
||||
}
|
||||
|
||||
return Response::text($this->formatExportList($exports));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all prp-bridge server-side exports.
|
||||
*/
|
||||
protected function getServerExports(): array
|
||||
{
|
||||
return [
|
||||
// Cooldowns
|
||||
[
|
||||
'name' => 'startGlobalCooldown',
|
||||
'category' => 'Cooldowns',
|
||||
'side' => 'server',
|
||||
'description' => 'Starts a global cooldown for all players for the given duration.',
|
||||
'parameters' => [
|
||||
['name' => 'cooldownKey', 'type' => 'string', 'description' => 'Unique key identifying this cooldown'],
|
||||
['name' => 'minutes', 'type' => 'number', 'description' => 'Duration of the cooldown in minutes'],
|
||||
],
|
||||
'returns' => ['type' => 'void', 'description' => 'No return value'],
|
||||
'lua_example' => "exports['prp-bridge']:startGlobalCooldown('bank_robbery', 30)",
|
||||
],
|
||||
[
|
||||
'name' => 'startCooldownByIdentifier',
|
||||
'category' => 'Cooldowns',
|
||||
'side' => 'server',
|
||||
'description' => 'Starts a cooldown for a specific player identifier.',
|
||||
'parameters' => [
|
||||
['name' => 'identifier', 'type' => 'string', 'description' => 'The player state ID / identifier'],
|
||||
['name' => 'cooldownKey', 'type' => 'string', 'description' => 'Unique key identifying this cooldown'],
|
||||
['name' => 'minutes', 'type' => 'number', 'description' => 'Duration of the cooldown in minutes'],
|
||||
['name' => 'applyToAllCharacters', 'type' => 'boolean?', 'description' => 'Apply to all characters of the same player (optional)'],
|
||||
],
|
||||
'returns' => ['type' => 'void', 'description' => 'No return value'],
|
||||
'lua_example' => "local identifier = GetPlayerIdentifier(source, 0)\nexports['prp-bridge']:startCooldownByIdentifier(identifier, 'drug_run', 15)",
|
||||
],
|
||||
[
|
||||
'name' => 'startCooldownByPlayerId',
|
||||
'category' => 'Cooldowns',
|
||||
'side' => 'server',
|
||||
'description' => 'Starts a cooldown for a player by their server ID.',
|
||||
'parameters' => [
|
||||
['name' => 'playerId', 'type' => 'number', 'description' => 'The player server ID'],
|
||||
['name' => 'cooldownKey', 'type' => 'string', 'description' => 'Unique key identifying this cooldown'],
|
||||
['name' => 'minutes', 'type' => 'number', 'description' => 'Duration of the cooldown in minutes'],
|
||||
['name' => 'applyToAllCharacters', 'type' => 'boolean?', 'description' => 'Apply to all characters of the same player (optional)'],
|
||||
],
|
||||
'returns' => ['type' => 'void', 'description' => 'No return value'],
|
||||
'lua_example' => "exports['prp-bridge']:startCooldownByPlayerId(source, 'drug_run', 15)",
|
||||
],
|
||||
[
|
||||
'name' => 'isCooldownActive',
|
||||
'category' => 'Cooldowns',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns whether a global cooldown is currently active.',
|
||||
'parameters' => [
|
||||
['name' => 'cooldownKey', 'type' => 'string', 'description' => 'The cooldown key to check'],
|
||||
],
|
||||
'returns' => ['type' => 'boolean', 'description' => 'true if the global cooldown is active, false otherwise'],
|
||||
'lua_example' => "if exports['prp-bridge']:isCooldownActive('bank_robbery') then\n -- notify player to wait\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'isCooldownActiveForIdentifier',
|
||||
'category' => 'Cooldowns',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns whether a cooldown is active for a specific player identifier.',
|
||||
'parameters' => [
|
||||
['name' => 'identifier', 'type' => 'string', 'description' => 'The player state ID / identifier'],
|
||||
['name' => 'cooldownKey', 'type' => 'string', 'description' => 'The cooldown key to check'],
|
||||
],
|
||||
'returns' => ['type' => 'boolean', 'description' => 'true if the cooldown is active for that identifier, false otherwise'],
|
||||
'lua_example' => "local identifier = GetPlayerIdentifier(source, 0)\nif exports['prp-bridge']:isCooldownActiveForIdentifier(identifier, 'drug_run') then\n -- notify player they must wait\nend",
|
||||
],
|
||||
|
||||
// Allowlist
|
||||
[
|
||||
'name' => 'GetAllowlist',
|
||||
'category' => 'Allowlist',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns all allowlists for a character by their state ID.',
|
||||
'parameters' => [
|
||||
['name' => 'stateId', 'type' => 'string', 'description' => 'The character state ID'],
|
||||
],
|
||||
'returns' => ['type' => 'table<string, boolean>', 'description' => 'Map of allowlist name to whether the character has it'],
|
||||
'lua_example' => "local allowlists = exports['prp-bridge']:GetAllowlist(stateId)\nfor name, has in pairs(allowlists) do\n print(name, has)\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'HasAllowlist',
|
||||
'category' => 'Allowlist',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns whether a character has a specific allowlist.',
|
||||
'parameters' => [
|
||||
['name' => 'stateId', 'type' => 'string', 'description' => 'The character state ID'],
|
||||
['name' => 'allowlist', 'type' => 'string', 'description' => 'The allowlist key to check'],
|
||||
],
|
||||
'returns' => ['type' => 'boolean', 'description' => 'true if the character has the allowlist'],
|
||||
'lua_example' => "if exports['prp-bridge']:HasAllowlist(stateId, 'police_allowlist') then\n print('Has police allowlist')\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'AddAllowlist',
|
||||
'category' => 'Allowlist',
|
||||
'side' => 'server',
|
||||
'description' => 'Adds an allowlist to a character.',
|
||||
'parameters' => [
|
||||
['name' => 'source', 'type' => 'number', 'description' => 'The player server ID'],
|
||||
['name' => 'stateId', 'type' => 'string', 'description' => 'The character state ID'],
|
||||
['name' => 'allowlist', 'type' => 'string', 'description' => 'The allowlist key to add'],
|
||||
],
|
||||
'returns' => ['type' => 'boolean', 'description' => 'true if successfully added'],
|
||||
'lua_example' => "local success = exports['prp-bridge']:AddAllowlist(source, stateId, 'police_allowlist')\nif not success then\n print('Failed to add allowlist')\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'RemoveAllowlist',
|
||||
'category' => 'Allowlist',
|
||||
'side' => 'server',
|
||||
'description' => 'Removes an allowlist from a character.',
|
||||
'parameters' => [
|
||||
['name' => 'source', 'type' => 'number', 'description' => 'The player server ID'],
|
||||
['name' => 'stateId', 'type' => 'string', 'description' => 'The character state ID'],
|
||||
['name' => 'allowlist', 'type' => 'string', 'description' => 'The allowlist key to remove'],
|
||||
],
|
||||
'returns' => ['type' => 'boolean', 'description' => 'true if successfully removed'],
|
||||
'lua_example' => "local success = exports['prp-bridge']:RemoveAllowlist(source, stateId, 'police_allowlist')\nif not success then\n print('Failed to remove allowlist')\nend",
|
||||
],
|
||||
|
||||
// Groups
|
||||
[
|
||||
'name' => 'CreateGroup',
|
||||
'category' => 'Groups',
|
||||
'side' => 'server',
|
||||
'description' => 'Creates a new group with the specified player as the leader.',
|
||||
'parameters' => [
|
||||
['name' => 'leaderSrc', 'type' => 'number', 'description' => 'Server ID of the player who will be the group leader'],
|
||||
],
|
||||
'returns' => ['type' => 'table<{ success, error?, group? }>', 'description' => 'Result table with success boolean, optional error string, and optional group object'],
|
||||
'lua_example' => "local result = exports['prp-bridge']:CreateGroup(source)\nif result.success then\n local group = result.group\n print('Group created:', group.getUUID())\nelse\n print('Error:', result.error)\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetGroupFromMember',
|
||||
'category' => 'Groups',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns the group a player is currently in, or nil.',
|
||||
'parameters' => [
|
||||
['name' => 'src', 'type' => 'number', 'description' => 'The player server ID'],
|
||||
],
|
||||
'returns' => ['type' => 'Group?', 'description' => 'The Group object if the player is in a group, nil otherwise'],
|
||||
'lua_example' => "local group = exports['prp-bridge']:GetGroupFromMember(source)\nif group then\n print('Player is in group:', group.getUUID())\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetGroupFromMemberByIdentifier',
|
||||
'category' => 'Groups',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns the group a player identifier is currently in, or nil.',
|
||||
'parameters' => [
|
||||
['name' => 'identifier', 'type' => 'string', 'description' => 'The player state ID / identifier'],
|
||||
],
|
||||
'returns' => ['type' => 'Group?', 'description' => 'The Group object if found, nil otherwise'],
|
||||
'lua_example' => "local identifier = GetPlayerIdentifier(source, 0)\nlocal group = exports['prp-bridge']:GetGroupFromMemberByIdentifier(identifier)\nif group then\n print('Group UUID:', group.getUUID())\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetGroupByUuid',
|
||||
'category' => 'Groups',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns a group by its unique identifier.',
|
||||
'parameters' => [
|
||||
['name' => 'uuid', 'type' => 'string', 'description' => 'The group UUID'],
|
||||
],
|
||||
'returns' => ['type' => 'Group?', 'description' => 'The Group object, or nil if not found'],
|
||||
'lua_example' => "local group = exports['prp-bridge']:GetGroupByUuid('some-group-uuid')\nif group then\n print('Found group with', #group.getMembers(), 'members')\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetGroupByPartyUuid',
|
||||
'category' => 'Groups',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns a group by its linked UniQueue party UUID.',
|
||||
'parameters' => [
|
||||
['name' => 'uuid', 'type' => 'string', 'description' => 'The UniQueue party UUID linked to the group'],
|
||||
],
|
||||
'returns' => ['type' => 'Group?', 'description' => 'The Group object, or nil if not found'],
|
||||
'lua_example' => "local group = exports['prp-bridge']:GetGroupByPartyUuid(partyUuid)\nif group then\n print('Found group for party:', group.getUUID())\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetGroupIdFromMember',
|
||||
'category' => 'Groups',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns the group UUID for a player if they are in a group.',
|
||||
'parameters' => [
|
||||
['name' => 'src', 'type' => 'number', 'description' => 'The player server ID'],
|
||||
],
|
||||
'returns' => ['type' => 'string?', 'description' => 'The group UUID string, or nil if not in a group'],
|
||||
'lua_example' => "local groupId = exports['prp-bridge']:GetGroupIdFromMember(source)\nif groupId then\n print('Group ID:', groupId)\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetGroupIdFromMemberByIdentifier',
|
||||
'category' => 'Groups',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns the group UUID for a player identifier if they are in a group.',
|
||||
'parameters' => [
|
||||
['name' => 'identifier', 'type' => 'string', 'description' => 'The player state ID / identifier'],
|
||||
],
|
||||
'returns' => ['type' => 'string?', 'description' => 'The group UUID string, or nil if not in a group'],
|
||||
'lua_example' => "local identifier = GetPlayerIdentifier(source, 0)\nlocal groupId = exports['prp-bridge']:GetGroupIdFromMemberByIdentifier(identifier)\nif groupId then\n print('Group ID:', groupId)\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetGroupPlayerIds',
|
||||
'category' => 'Groups',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns all player server IDs currently in the specified group.',
|
||||
'parameters' => [
|
||||
['name' => 'uuid', 'type' => 'string', 'description' => 'The group UUID'],
|
||||
],
|
||||
'returns' => ['type' => 'number[]', 'description' => 'Array of player server IDs in the group'],
|
||||
'lua_example' => "local playerIds = exports['prp-bridge']:GetGroupPlayerIds(groupUuid)\nfor _, playerId in ipairs(playerIds) do\n TriggerClientEvent('myresource:start', playerId)\nend",
|
||||
],
|
||||
|
||||
// UniQueue
|
||||
[
|
||||
'name' => 'CreateQueue',
|
||||
'category' => 'UniQueue',
|
||||
'side' => 'server',
|
||||
'description' => 'Creates a new mission queue. Most Prodigy resources create their own queues — use this only when building custom missions.',
|
||||
'parameters' => [
|
||||
['name' => 'name', 'type' => 'string', 'description' => 'Unique queue identifier'],
|
||||
['name' => 'type', 'type' => 'string', 'description' => 'Queue type: "civ" (civilian missions) or "crime" (crime missions)'],
|
||||
['name' => 'requiredPolicePower', 'type' => 'number', 'description' => 'Police power required to run the mission'],
|
||||
['name' => 'maxConcurrentTasks', 'type' => 'number', 'description' => 'Max missions from this queue running simultaneously'],
|
||||
['name' => 'cooldown', 'type' => 'number', 'description' => 'Seconds after a task ends before the next mission can run'],
|
||||
],
|
||||
'returns' => ['type' => 'UniQueue', 'description' => 'The created queue object'],
|
||||
'lua_example' => "local queue = exports['prp-bridge']:CreateQueue(\n 'my_mission',\n 'crime',\n 4, -- requires 4 police power\n 1, -- max 1 concurrent task\n 60 -- 60 second cooldown\n)\n\nqueue.setExecFunction(function(queueName, partyUuid, partyMembers, taskId)\n startMission(partyMembers, taskId)\nend)",
|
||||
],
|
||||
[
|
||||
'name' => 'GetQueue',
|
||||
'category' => 'UniQueue',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns a queue by its name, or nil if not found.',
|
||||
'parameters' => [
|
||||
['name' => 'name', 'type' => 'string', 'description' => 'The queue name used in CreateQueue'],
|
||||
],
|
||||
'returns' => ['type' => 'UniQueue?', 'description' => 'The queue object, or nil if not found'],
|
||||
'lua_example' => "local queue = exports['prp-bridge']:GetQueue('my_mission')\nif queue then\n print('Queue position count:', queue.getNumOfExecTasks())\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetQueuesByType',
|
||||
'category' => 'UniQueue',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns all queue names of a given type.',
|
||||
'parameters' => [
|
||||
['name' => 'type', 'type' => 'string', 'description' => 'Queue type: "civ" or "crime"'],
|
||||
],
|
||||
'returns' => ['type' => 'table<string, boolean>', 'description' => 'Map of queue names of that type'],
|
||||
'lua_example' => "local crimeQueues = exports['prp-bridge']:GetQueuesByType('crime')\nfor queueName in pairs(crimeQueues) do\n print('Crime queue:', queueName)\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'CreateParty',
|
||||
'category' => 'UniQueue',
|
||||
'side' => 'server',
|
||||
'description' => 'Creates a new UniQueue party of the given type.',
|
||||
'parameters' => [
|
||||
['name' => 'type', 'type' => 'string', 'description' => 'Party type: "civ" or "crime"'],
|
||||
],
|
||||
'returns' => ['type' => 'UniQueueParty', 'description' => 'The created party object'],
|
||||
'lua_example' => "local party = exports['prp-bridge']:CreateParty('crime')\n\n-- Add members by identifier\nlocal identifier = GetPlayerIdentifier(source, 0)\nlocal result = party.addMember(identifier)\nif not result.success then\n print('Error:', result.error)\nend\n\n-- Join a queue\nlocal queue = exports['prp-bridge']:GetQueue('my_mission')\nqueue.add(party)",
|
||||
],
|
||||
[
|
||||
'name' => 'GetParty',
|
||||
'category' => 'UniQueue',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns a party by its UUID, or nil if not found.',
|
||||
'parameters' => [
|
||||
['name' => 'uuid', 'type' => 'string', 'description' => 'The party UUID'],
|
||||
],
|
||||
'returns' => ['type' => 'UniQueueParty?', 'description' => 'The party object, or nil if not found'],
|
||||
'lua_example' => "local party = exports['prp-bridge']:GetParty(partyUuid)\nif party then\n local members = party.getMembersAsArray()\n print('Party has', #members, 'members')\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetPartiesByType',
|
||||
'category' => 'UniQueue',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns all party UUIDs of a given type.',
|
||||
'parameters' => [
|
||||
['name' => 'queueType', 'type' => 'string', 'description' => 'Party type: "civ" or "crime"'],
|
||||
],
|
||||
'returns' => ['type' => 'table<string, boolean>', 'description' => 'Map of party UUIDs of that type'],
|
||||
'lua_example' => "local crimeParties = exports['prp-bridge']:GetPartiesByType('crime')\nfor partyUuid in pairs(crimeParties) do\n print('Crime party:', partyUuid)\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetPartiesFromPlayer',
|
||||
'category' => 'UniQueue',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns all party UUIDs the player identifier is currently in.',
|
||||
'parameters' => [
|
||||
['name' => 'identifier', 'type' => 'string', 'description' => 'The player state ID / identifier'],
|
||||
],
|
||||
'returns' => ['type' => 'table<string, boolean>', 'description' => 'Map of party UUIDs the player is in'],
|
||||
'lua_example' => "local identifier = GetPlayerIdentifier(source, 0)\nlocal parties = exports['prp-bridge']:GetPartiesFromPlayer(identifier)\nfor partyUuid in pairs(parties) do\n print('In party:', partyUuid)\nend",
|
||||
],
|
||||
|
||||
// Sell Shops
|
||||
[
|
||||
'name' => 'RegisterSellShop',
|
||||
'category' => 'Sell Shops',
|
||||
'side' => 'server',
|
||||
'description' => 'Registers a sell shop with accepted items and prices. Safe to call repeatedly — re-registering the same ID does nothing.',
|
||||
'parameters' => [
|
||||
['name' => 'id', 'type' => 'string', 'description' => 'Unique shop identifier'],
|
||||
['name' => 'payload', 'type' => 'table', 'description' => 'Config table with label, items (keyed by item name with label and price), optional reason and coords'],
|
||||
],
|
||||
'returns' => ['type' => 'void', 'description' => 'No return value'],
|
||||
'lua_example' => "exports['prp-bridge']:RegisterSellShop('pawn_shop', {\n label = 'Pawn Shop',\n items = {\n ['gold_bar'] = { label = 'Gold Bar', price = 500 },\n ['silver_bar'] = { label = 'Silver Bar', price = 200 },\n ['diamond'] = { label = 'Diamond', price = 1000 },\n }\n})",
|
||||
],
|
||||
[
|
||||
'name' => 'OpenSellShop',
|
||||
'category' => 'Sell Shops',
|
||||
'side' => 'server',
|
||||
'description' => 'Opens a registered sell shop for a player. The player sees a stash UI and items are consumed for cash on move.',
|
||||
'parameters' => [
|
||||
['name' => 'source', 'type' => 'number', 'description' => 'The player server ID'],
|
||||
['name' => 'id', 'type' => 'string', 'description' => 'The shop ID registered with RegisterSellShop'],
|
||||
],
|
||||
'returns' => ['type' => 'void', 'description' => 'No return value'],
|
||||
'lua_example' => "RegisterNetEvent('myresource:openShop', function()\n local src = source\n -- Register before opening (safe to call every time)\n exports['prp-bridge']:RegisterSellShop('pawn_shop', {\n label = 'Pawn Shop',\n items = {\n ['gold_bar'] = { label = 'Gold Bar', price = 500 },\n }\n })\n exports['prp-bridge']:OpenSellShop(src, 'pawn_shop')\nend)",
|
||||
],
|
||||
|
||||
// Cases
|
||||
[
|
||||
'name' => 'CreateCase',
|
||||
'category' => 'Cases',
|
||||
'side' => 'server',
|
||||
'description' => 'Registers a new loot case type. All item chances must sum to exactly 100.',
|
||||
'parameters' => [
|
||||
['name' => 'caseId', 'type' => 'string', 'description' => 'Unique case identifier (e.g. "CRIME_CASE")'],
|
||||
['name' => 'payload', 'type' => 'table', 'description' => 'Config table with optional label and required items array (each with name, chance, optional min/max/metadata/image)'],
|
||||
],
|
||||
'returns' => ['type' => 'boolean', 'description' => 'true on success; false if item chances do not sum to 100'],
|
||||
'lua_example' => "local success = exports['prp-bridge']:CreateCase('ROBBERY_CASE', {\n label = 'Robbery Loot',\n items = {\n { name = 'money', chance = 50, min = 200, max = 800 },\n { name = 'lockpick', chance = 30 },\n { name = 'weapon_pistol', chance = 20 },\n }\n})\nif not success then\n print('Item chances must sum to 100!')\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetCase',
|
||||
'category' => 'Cases',
|
||||
'side' => 'server',
|
||||
'description' => 'Gets case data by its unique identifier.',
|
||||
'parameters' => [
|
||||
['name' => 'caseId', 'type' => 'string', 'description' => 'The case ID used in CreateCase'],
|
||||
],
|
||||
'returns' => ['type' => 'CaseDataPayload?', 'description' => 'The case data table, or nil if not found'],
|
||||
'lua_example' => "local caseData = exports['prp-bridge']:GetCase('ROBBERY_CASE')\nif caseData then\n print('Case label:', caseData.label)\n print('Item count:', #caseData.items)\nend",
|
||||
],
|
||||
[
|
||||
'name' => 'GetAllCases',
|
||||
'category' => 'Cases',
|
||||
'side' => 'server',
|
||||
'description' => 'Returns all registered case types and their item data.',
|
||||
'parameters' => [],
|
||||
'returns' => ['type' => 'CaseDataPayload[]', 'description' => 'Array of all registered case data tables'],
|
||||
'lua_example' => "local allCases = exports['prp-bridge']:GetAllCases()\nfor _, caseData in ipairs(allCases) do\n print('Case:', caseData.label or 'unlabelled', '- Items:', #caseData.items)\nend",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single export for output.
|
||||
*/
|
||||
protected function formatExport(array $export): string
|
||||
{
|
||||
return view('mcp.prodigy.prodigy-export-reference', [
|
||||
'export' => $export,
|
||||
])->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format full export listing grouped by category.
|
||||
*/
|
||||
protected function formatExportList(array $exports): string
|
||||
{
|
||||
$byCategory = collect($exports)->groupBy('category')->toArray();
|
||||
|
||||
return view('mcp.prodigy.prodigy-export-list', [
|
||||
'exportsByCategory' => $byCategory,
|
||||
])->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'export_name' => $schema
|
||||
->string()
|
||||
->description('The prp-bridge server export name to look up (e.g. "startGlobalCooldown", "CreateGroup", "RegisterSellShop"). Omit to list all server exports.')
|
||||
->default(''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class GetQBCoreClientEventReference extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -235,7 +235,7 @@ class GetQBCoreClientFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -139,7 +139,7 @@ class GetQBCoreConfig extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -150,7 +150,7 @@ class GetQBCorePlayers extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -475,7 +475,7 @@ class GetQBCoreResourceList extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -311,7 +311,7 @@ class GetQBCoreResourceReference extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -163,7 +163,7 @@ class GetQBCoreSharedData extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -129,7 +129,7 @@ class GetQBCoreServerEventReference extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
@@ -392,7 +392,7 @@ class GetQBCoreServerFunction extends Tool
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\JsonSchema\JsonSchema>
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
|
||||
+32
-1
@@ -3,13 +3,44 @@
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\DatabaseNotification;
|
||||
use Illuminate\Notifications\DatabaseNotificationCollection;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property Carbon|null $email_verified_at
|
||||
* @property string $password
|
||||
* @property string|null $remember_token
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read DatabaseNotificationCollection<int, DatabaseNotification> $notifications
|
||||
* @property-read int|null $notifications_count
|
||||
*
|
||||
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmailVerifiedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePassword($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRememberToken($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereUpdatedAt($value)
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
"claude_code",
|
||||
"gemini"
|
||||
],
|
||||
"cloud": false,
|
||||
"guidelines": true,
|
||||
"herd_mcp": false,
|
||||
"mcp": true,
|
||||
"nightwatch_mcp": false,
|
||||
"sail": false,
|
||||
"skills": [
|
||||
"laravel-best-practices",
|
||||
"mcp-development",
|
||||
"pest-testing",
|
||||
"tailwindcss-development"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
AppServiceProvider::class,
|
||||
];
|
||||
@@ -55,6 +55,11 @@
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"ide-helper": [
|
||||
"@php artisan ide-helper:generate",
|
||||
"@php artisan ide-helper:models -RW",
|
||||
"@php artisan ide-helper:meta"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
@@ -63,6 +68,7 @@
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
|
||||
"@php artisan boost:update --ansi",
|
||||
"@php artisan ide-helper:generate",
|
||||
"@php artisan ide-helper:models -RW",
|
||||
"@php artisan ide-helper:meta"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
|
||||
Generated
+491
-428
File diff suppressed because it is too large.
Load diff
+3
-1
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@@ -62,7 +64,7 @@ return [
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pdo\Mysql;
|
||||
|
||||
return [
|
||||
|
||||
@@ -59,7 +60,7 @@ return [
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
@@ -79,7 +80,7 @@ return [
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
|
||||
Generated
+439
-373
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -12,6 +12,6 @@
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^7.0.7"
|
||||
"vite": "^7.3.2"
|
||||
}
|
||||
}
|
||||
@@ -3,364 +3,368 @@
|
||||
@section('title', 'Full Documentation - FiveM MCP Server')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-5xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-8">
|
||||
<h1 class="text-3xl font-extrabold text-gray-900 dark:text-white mb-6">FiveM Development MCP Server</h1>
|
||||
<div class="max-w-5xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-8">
|
||||
<h1 class="text-3xl font-extrabold text-gray-900 dark:text-white mb-6">FiveM Development MCP Server</h1>
|
||||
|
||||
<p class="text-lg text-gray-700 dark:text-gray-300 mb-8">
|
||||
A comprehensive Model Context Protocol (MCP) server for FiveM (GTA5) development, built with Laravel MCP.
|
||||
This server provides AI assistants with powerful tools to help you develop FiveM resources more efficiently.
|
||||
</p>
|
||||
<p class="text-lg text-gray-700 dark:text-gray-300 mb-8">
|
||||
A comprehensive Model Context Protocol (MCP) server for FiveM (GTA5) development, built with Laravel MCP.
|
||||
This server provides AI assistants with powerful tools to help you develop FiveM resources more efficiently.
|
||||
</p>
|
||||
|
||||
<!-- Installation -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Installation & Connection</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">Choose how you want to connect to the FiveM MCP server:</p>
|
||||
<!-- Installation -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Installation & Connection</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">Choose how you want to connect to the FiveM MCP server:</p>
|
||||
|
||||
<!-- Tab Buttons -->
|
||||
<div class="border-b border-gray-200 dark:border-gray-700 mb-6">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
<button data-tab="remote"
|
||||
class="transition-all duration-200 ease-in-out active border-b-2 border-gta-orange py-4 px-1 text-center text-sm font-medium text-gta-orange">
|
||||
<span class="flex items-center">
|
||||
<span class="mr-2">🌐</span>
|
||||
Remote Server
|
||||
<span
|
||||
class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 border border-gta-orange text-gta-orange">
|
||||
Recommended
|
||||
<!-- Tab Buttons -->
|
||||
<div class="border-b border-gray-200 dark:border-gray-700 mb-6">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
<button data-tab="remote"
|
||||
class="transition-all duration-200 ease-in-out active border-b-2 border-gta-orange py-4 px-1 text-center text-sm font-medium text-gta-orange">
|
||||
<span class="flex items-center">
|
||||
<span class="mr-2">🌐</span>
|
||||
Remote Server
|
||||
<span
|
||||
class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 border border-gta-orange text-gta-orange">
|
||||
Recommended
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button data-tab="local"
|
||||
class="transition-all duration-200 ease-in-out border-b-2 border-transparent py-4 px-1 text-center text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span class="flex items-center">
|
||||
<span class="mr-2">💻</span>
|
||||
Local Installation
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</button>
|
||||
<button data-tab="local"
|
||||
class="transition-all duration-200 ease-in-out border-b-2 border-transparent py-4 px-1 text-center text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span class="flex items-center">
|
||||
<span class="mr-2">💻</span>
|
||||
Local Installation
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content Container -->
|
||||
<div class="tab-content-container">
|
||||
<!-- Remote Server Option -->
|
||||
<div data-tab-content="remote" class="animate-fade-in active">
|
||||
<div class="bg-gray-100 dark:bg-gray-700/30 border-l-4 border-gta-orange p-6">
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||
<strong>Easy setup!</strong> Connect to a hosted MCP server. This is the easiest option and
|
||||
works from anywhere without any local setup.
|
||||
</p>
|
||||
<!-- Tab Content Container -->
|
||||
<div class="tab-content-container">
|
||||
<!-- Remote Server Option -->
|
||||
<div data-tab-content="remote" class="animate-fade-in active">
|
||||
<div class="bg-gray-100 dark:bg-gray-700/30 border-l-4 border-gta-orange p-6">
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||
<strong>Easy setup!</strong> Connect to a hosted MCP server. This is the easiest option and
|
||||
works from anywhere without any local setup.
|
||||
</p>
|
||||
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">Claude Desktop Configuration:</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">Edit your configuration file:</p>
|
||||
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 mb-3 ml-4">
|
||||
<li><strong>macOS:</strong> <code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">~/Library/Application Support/Claude/claude_desktop_config.json</code>
|
||||
</li>
|
||||
<li><strong>Windows:</strong> <code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">%APPDATA%\Claude\claude_desktop_config.json</code>
|
||||
</li>
|
||||
</ul>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">Claude Desktop Configuration:</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">Edit your configuration file:</p>
|
||||
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 mb-3 ml-4">
|
||||
<li><strong>macOS:</strong> <code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">~/Library/Application Support/Claude/claude_desktop_config.json</code>
|
||||
</li>
|
||||
<li><strong>Windows:</strong> <code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">%APPDATA%\Claude\claude_desktop_config.json</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">Add this configuration (replace with your
|
||||
actual
|
||||
server URL):</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>@verbatim{
|
||||
"mcpServers": {
|
||||
"fivem": {
|
||||
"url": "https://your-domain.com/fivem"
|
||||
}
|
||||
}
|
||||
}@endverbatim</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">VSCode Configuration (Cline
|
||||
Extension):
|
||||
</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">If using VSCode with the Cline extension, edit
|
||||
your
|
||||
settings:</p>
|
||||
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 mb-3 ml-4">
|
||||
<li>Open VSCode Settings (JSON)</li>
|
||||
<li>Add to your global or workspace <code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">settings.json</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>@verbatim{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">Add this configuration (replace with your
|
||||
actual
|
||||
server URL):</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>@verbatim{
|
||||
"mcpServers": {
|
||||
"fivem": {
|
||||
"url": "https://your-domain.com/fivem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}@endverbatim</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
}@endverbatim</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- Local Installation Option -->
|
||||
<div data-tab-content="local" class="animate-fade-in hidden">
|
||||
<div class="bg-gray-50 dark:bg-gray-700/20 border-l-4 border-gray-400 dark:border-gray-500 p-6">
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||
<strong>Advanced setup.</strong> Run the MCP server on your own machine. Requires PHP and
|
||||
Laravel setup.
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">VSCode Configuration (Cline
|
||||
Extension):
|
||||
</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">If using VSCode with the Cline extension, edit
|
||||
your
|
||||
settings:</p>
|
||||
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 mb-3 ml-4">
|
||||
<li>Open VSCode Settings (JSON)</li>
|
||||
<li>Add to your global or workspace <code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">settings.json</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">Claude Desktop Configuration:</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">Edit your configuration file and add:</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto mb-4"><code>@verbatim{
|
||||
"mcpServers": {
|
||||
"fivem": {
|
||||
"command": "/path/to/php",
|
||||
"args": [
|
||||
"/path/to/fivem-mcp/artisan",
|
||||
"mcp:start",
|
||||
"fivem"
|
||||
]
|
||||
}
|
||||
}
|
||||
}@endverbatim</code></pre>
|
||||
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">Command Line Usage:</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">For any MCP-compatible client:</p>
|
||||
<pre
|
||||
class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>php artisan mcp:start fivem</code></pre>
|
||||
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2 mt-6">VSCode Configuration (Cline
|
||||
Extension):</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">If using VSCode with the Cline extension, add
|
||||
to
|
||||
<code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">settings.json</code>:
|
||||
</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>@verbatim{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"fivem": {
|
||||
"command": "php",
|
||||
"args": ["artisan", "mcp:start", "fivem"],
|
||||
"cwd": "/path/to/fivem-mcp"
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>@verbatim{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"fivem": {
|
||||
"url": "https://your-domain.com/fivem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}@endverbatim</code></pre>
|
||||
}@endverbatim</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3 mt-4"><strong>Or use the full PHP path:</strong>
|
||||
</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>@verbatim{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
<!-- Local Installation Option -->
|
||||
<div data-tab-content="local" class="animate-fade-in hidden">
|
||||
<div class="bg-gray-50 dark:bg-gray-700/20 border-l-4 border-gray-400 dark:border-gray-500 p-6">
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||
<strong>Advanced setup.</strong> Run the MCP server on your own machine. Requires PHP and
|
||||
Laravel setup.
|
||||
</p>
|
||||
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">Claude Desktop Configuration:</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">Edit your configuration file and add:</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto mb-4"><code>@verbatim{
|
||||
"mcpServers": {
|
||||
"fivem": {
|
||||
"command": "/path/to/php",
|
||||
"args": ["/path/to/fivem-mcp/artisan", "mcp:start", "fivem"]
|
||||
"args": [
|
||||
"/path/to/fivem-mcp/artisan",
|
||||
"mcp:start",
|
||||
"fivem"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}@endverbatim</code></pre>
|
||||
}@endverbatim</code></pre>
|
||||
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">Command Line Usage:</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">For any MCP-compatible client:</p>
|
||||
<pre
|
||||
class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>php artisan mcp:start fivem</code></pre>
|
||||
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2 mt-6">VSCode Configuration (Cline
|
||||
Extension):</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3">If using VSCode with the Cline extension, add
|
||||
to
|
||||
<code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">settings.json</code>:
|
||||
</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>@verbatim{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"fivem": {
|
||||
"command": "php",
|
||||
"args": ["artisan", "mcp:start", "fivem"],
|
||||
"cwd": "/path/to/fivem-mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}@endverbatim</code></pre>
|
||||
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-3 mt-4"><strong>Or use the full PHP path:</strong>
|
||||
</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>@verbatim{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"fivem": {
|
||||
"command": "/path/to/php",
|
||||
"args": ["/path/to/fivem-mcp/artisan", "mcp:start", "fivem"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}@endverbatim</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-500 p-4 mt-6">
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||
<strong>💡 Tip:</strong> After configuring Claude Desktop, completely quit and restart the
|
||||
application
|
||||
for
|
||||
changes to take effect.
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-500 p-4 mt-6">
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||
<strong>💡 Tip:</strong> After configuring Claude Desktop, completely quit and restart the
|
||||
application
|
||||
for
|
||||
changes to take effect.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Available Tools -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Available Tools</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
The FiveM MCP Server provides {{ count($tools) }} tools organized by category.
|
||||
These tools allow AI assistants to search documentation, generate code, and lookup framework functions.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Available Tools -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Available Tools</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
The FiveM MCP Server provides {{ count($tools) }} tools organized by category.
|
||||
These tools allow AI assistants to search documentation, generate code, and lookup framework functions.
|
||||
</p>
|
||||
@php
|
||||
$groupedTools = collect($tools)->groupBy('category');
|
||||
@endphp
|
||||
|
||||
@php
|
||||
$groupedTools = collect($tools)->groupBy('category');
|
||||
@endphp
|
||||
@foreach ($groupedTools as $category => $categoryTools)
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-bold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
||||
<span class="inline-block w-1 h-6 bg-gta-orange mr-3"></span>
|
||||
{{ $category }} Tools
|
||||
<span
|
||||
class="ml-3 text-sm font-normal text-gray-500 dark:text-gray-400">({{ count($categoryTools) }})</span>
|
||||
</h3>
|
||||
|
||||
@foreach ($groupedTools as $category => $categoryTools)
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-bold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
||||
<span class="inline-block w-1 h-6 bg-gta-orange mr-3"></span>
|
||||
{{ $category }} Tools
|
||||
<span
|
||||
class="ml-3 text-sm font-normal text-gray-500 dark:text-gray-400">({{ count($categoryTools) }})</span>
|
||||
</h3>
|
||||
@foreach ($categoryTools as $index => $tool)
|
||||
<div class="mb-6 border-l-4 border-gta-orange pl-6">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">{{ $tool['name'] }}</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300">{{ $tool['description'] }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
</section>
|
||||
|
||||
@foreach ($categoryTools as $index => $tool)
|
||||
<div class="mb-6 border-l-4 border-gta-orange pl-6">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">{{ $tool['name'] }}</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300">{{ $tool['description'] }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
</section>
|
||||
<!-- Available Resources -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Available Resources</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
MCP Resources provide read-only reference documentation that AI assistants can access.
|
||||
Currently available: {{ count($resources) }} resources.
|
||||
</p>
|
||||
|
||||
<!-- Available Resources -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Available Resources</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
MCP Resources provide read-only reference documentation that AI assistants can access.
|
||||
Currently available: {{ count($resources) }} resources.
|
||||
</p>
|
||||
@forelse ($resources as $index => $resource)
|
||||
<div class="mb-6 border-l-4 border-green-500 pl-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">{{ $index + 1 }}.
|
||||
{{ $resource['name'] }}
|
||||
</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300">{{ $resource['description'] }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-500 dark:text-gray-400 italic">No resources configured.</p>
|
||||
@endforelse
|
||||
</section>
|
||||
|
||||
@forelse ($resources as $index => $resource)
|
||||
<div class="mb-6 border-l-4 border-green-500 pl-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">{{ $index + 1 }}.
|
||||
{{ $resource['name'] }}
|
||||
</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300">{{ $resource['description'] }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-500 dark:text-gray-400 italic">No resources configured.</p>
|
||||
@endforelse
|
||||
</section>
|
||||
<!-- Available Prompts -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Available Prompts</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
MCP Prompts are pre-built prompt templates for common tasks.
|
||||
Currently available: {{ count($prompts) }} prompts.
|
||||
</p>
|
||||
|
||||
<!-- Available Prompts -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Available Prompts</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
MCP Prompts are pre-built prompt templates for common tasks.
|
||||
Currently available: {{ count($prompts) }} prompts.
|
||||
</p>
|
||||
@forelse ($prompts as $index => $prompt)
|
||||
<div class="mb-6 border-l-4 border-purple-500 pl-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">{{ $index + 1 }}.
|
||||
{{ $prompt['name'] }}
|
||||
</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300">{{ $prompt['description'] }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-500 dark:text-gray-400 italic">No prompts configured.</p>
|
||||
@endforelse
|
||||
</section>
|
||||
|
||||
@forelse ($prompts as $index => $prompt)
|
||||
<div class="mb-6 border-l-4 border-purple-500 pl-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">{{ $index + 1 }}.
|
||||
{{ $prompt['name'] }}
|
||||
</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300">{{ $prompt['description'] }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-500 dark:text-gray-400 italic">No prompts configured.</p>
|
||||
@endforelse
|
||||
</section>
|
||||
<!-- Testing -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Testing the Server</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">You can test the MCP server locally using the inspector:
|
||||
</p>
|
||||
<pre
|
||||
class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>php artisan mcp:inspector fivem</code></pre>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm mt-2">This will open the MCP Inspector in your browser
|
||||
where
|
||||
you
|
||||
can test all the tools interactively.</p>
|
||||
</section>
|
||||
|
||||
<!-- Testing -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Testing the Server</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">You can test the MCP server locally using the inspector:
|
||||
</p>
|
||||
<pre
|
||||
class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>php artisan mcp:inspector fivem</code></pre>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm mt-2">This will open the MCP Inspector in your browser
|
||||
where
|
||||
you
|
||||
can test all the tools interactively.</p>
|
||||
</section>
|
||||
<!-- Development -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Development</h2>
|
||||
|
||||
<!-- Development -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Development</h2>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">Adding New Tools</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">1. Create a new tool class in <code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">app/Mcp/Tools/</code>:
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">Adding New Tools</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">1. Create a new tool class in <code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">app/Mcp/Tools/</code>:
|
||||
</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto mb-4"><code>@verbatim<?php
|
||||
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto mb-4"><code>@verbatim<?php
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Description('Your tool description')]
|
||||
class YourTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
#[Description('Your tool description')]
|
||||
class YourTool extends Tool
|
||||
{
|
||||
// Tool logic here
|
||||
return Response::text('Result');
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
// Tool logic here
|
||||
return Response::text('Result');
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'param' => $schema->string()->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@endverbatim</code></pre>
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'param' => $schema->string()->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@endverbatim</code></pre>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">2. Register the tool in <code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">app/Mcp/Servers/FiveMServer.php</code>:
|
||||
</p>
|
||||
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">2. Register the tool in <code
|
||||
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1 rounded text-sm">app/Mcp/Servers/FiveMServer.php</code>:
|
||||
</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto mb-4"><code>@verbatimprotected array $tools = [
|
||||
// ... existing tools
|
||||
\App\Mcp\Tools\YourTool::class,
|
||||
];@endverbatim</code></pre>
|
||||
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto mb-4"><code>@verbatimprotected array $tools = [
|
||||
// ... existing tools
|
||||
\App\Mcp\Tools\YourTool::class,
|
||||
];@endverbatim</code></pre>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-3 mt-6">Code Formatting</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">Format code with Laravel Pint:</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>vendor/bin/pint</code></pre>
|
||||
</section>
|
||||
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-3 mt-6">Code Formatting</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">Format code with Laravel Pint:</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-4 overflow-x-auto"><code>vendor/bin/pint</code></pre>
|
||||
</section>
|
||||
<!-- Frameworks & Languages -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Supported Frameworks</h2>
|
||||
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-2 ml-4">
|
||||
<li><strong>Standalone:</strong> Basic FiveM resources without framework dependencies</li>
|
||||
<li><strong>ESX:</strong> ESX Framework (es_extended)</li>
|
||||
<li><strong>QBCore:</strong> QBCore Framework (qb-core / qbx_core)</li>
|
||||
<li><strong>Prodigy Studios (prp-bridge):</strong> Unified bridge supporting QBox, QB-Core, ESX, and ND Core with
|
||||
events, exports, UniQueue, groups, allowlists, sell shops, cases, and ped interactions</li>
|
||||
</ul>
|
||||
|
||||
<!-- Frameworks & Languages -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Supported Frameworks</h2>
|
||||
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-2 ml-4">
|
||||
<li><strong>Standalone:</strong> Basic FiveM resources without framework dependencies</li>
|
||||
<li><strong>ESX:</strong> ESX Framework (es_extended)</li>
|
||||
<li><strong>QBCore:</strong> QBCore Framework (qb-core)</li>
|
||||
</ul>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4 mt-6">Scripting Languages</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300">Both Lua and JavaScript are fully supported across all tools and
|
||||
generators.</p>
|
||||
</section>
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4 mt-6">Scripting Languages</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300">Both Lua and JavaScript are fully supported across all tools and
|
||||
generators.</p>
|
||||
</section>
|
||||
<!-- Resources -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Resources</h2>
|
||||
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-2 ml-4">
|
||||
<li><a href="https://docs.fivem.net/docs/" class="link-gta">FiveM
|
||||
Documentation</a></li>
|
||||
<li><a href="https://docs.fivem.net/natives/" class="link-gta">FiveM
|
||||
Natives Reference</a></li>
|
||||
<li><a href="https://coxdocs.dev/" class="link-gta">COX
|
||||
MySQL
|
||||
Documentation</a> - Required
|
||||
for database operations</li>
|
||||
<li><a href="https://docs.qbcore.org/" class="link-gta">QBCore
|
||||
Documentation</a></li>
|
||||
<li><a href="https://docs.prodigyrp.net/" class="link-gta">Prodigy
|
||||
Studios Documentation</a></li>
|
||||
<li><a href="https://laravel.com/docs/mcp" class="link-gta">Laravel
|
||||
MCP Documentation</a></li>
|
||||
<li><a href="https://modelcontextprotocol.io/" class="link-gta">Model
|
||||
Context Protocol</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Resources -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Resources</h2>
|
||||
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-2 ml-4">
|
||||
<li><a href="https://docs.fivem.net/docs/" class="link-gta">FiveM
|
||||
Documentation</a></li>
|
||||
<li><a href="https://docs.fivem.net/natives/" class="link-gta">FiveM
|
||||
Natives Reference</a></li>
|
||||
<li><a href="https://coxdocs.dev/" class="link-gta">COX
|
||||
MySQL
|
||||
Documentation</a> - Required
|
||||
for database operations</li>
|
||||
<li><a href="https://docs.qbcore.org/" class="link-gta">QBCore
|
||||
Documentation</a></li>
|
||||
<li><a href="https://laravel.com/docs/mcp" class="link-gta">Laravel
|
||||
MCP Documentation</a></li>
|
||||
<li><a href="https://modelcontextprotocol.io/" class="link-gta">Model
|
||||
Context Protocol</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<!-- Contributing -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Contributing</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">Contributions are welcome! Areas for expansion:</p>
|
||||
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-2 ml-4">
|
||||
<li>More native functions in the database</li>
|
||||
<li>Additional framework support (vRP, etc.)</li>
|
||||
<li>More event types</li>
|
||||
<li>Advanced code generators</li>
|
||||
<li>Database query builders</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Contributing -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Contributing</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">Contributions are welcome! Areas for expansion:</p>
|
||||
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-2 ml-4">
|
||||
<li>More native functions in the database</li>
|
||||
<li>Additional framework support (vRP, etc.)</li>
|
||||
<li>More event types</li>
|
||||
<li>Advanced code generators</li>
|
||||
<li>Database query builders</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- License -->
|
||||
<section>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">License</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300">MIT License</p>
|
||||
</section>
|
||||
<!-- License -->
|
||||
<section>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">License</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300">MIT License</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -10,218 +10,220 @@
|
||||
FiveM Development MCP Server
|
||||
</h1>
|
||||
<p class="mt-5 max-w-xl mx-auto text-xl text-gray-500 dark:text-gray-400">
|
||||
A comprehensive Model Context Protocol server for FiveM development with 5 tools, 3 resources, and 5
|
||||
prompts.
|
||||
</p>
|
||||
</div>
|
||||
A comprehensive Model Context Protocol server for FiveM development. Supports FiveM natives, QBCore,
|
||||
COX MySQL, Prodigy Studios (prp-bridge), and more.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Features Grid -->
|
||||
<div class="mt-12">
|
||||
<h2 class="text-3xl font-extrabold text-gray-900 dark:text-white mb-8">Features</h2>
|
||||
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Documentation Search -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Documentation Search</h3>
|
||||
<!-- Features Grid -->
|
||||
<div class="mt-12">
|
||||
<h2 class="text-3xl font-extrabold text-gray-900 dark:text-white mb-8">Features</h2>
|
||||
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Documentation Search -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Documentation Search</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Search FiveM documentation by topic and category. Get direct links to relevant
|
||||
documentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Search FiveM documentation by topic and category. Get direct links to relevant
|
||||
documentation.
|
||||
|
||||
<!-- Native Functions -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Native Functions</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Look up GTA5/FiveM native functions with signatures, parameters, and code examples.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manifest Generator -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Manifest Generator</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Generate fxmanifest.lua files automatically for any framework and scripting language.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Reference -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Event Reference</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Complete database of FiveM events including core, QBCore, and Prodigy Studios (prp-bridge)
|
||||
events with examples.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boilerplate Generator -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Boilerplate Generator</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Generate complete resource structures with client, server, config, and optional NUI files.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Framework Support -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Framework Support</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Full support for Standalone, ESX, QBCore, and Prodigy Studios (prp-bridge) in both Lua and
|
||||
JavaScript.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="mt-16 bg-gray-900 dark:bg-black border border-gta-orange rounded-lg shadow-xl overflow-hidden">
|
||||
<div class="px-6 py-12 sm:px-12">
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl font-extrabold text-white">Ready to get started?</h2>
|
||||
<p class="mt-4 text-lg text-gray-200 dark:text-gray-300">
|
||||
Set up the MCP server in just 5 minutes with our quick start guide.
|
||||
</p>
|
||||
<div class="mt-8 flex justify-center space-x-4">
|
||||
<a href="{{ route('docs.quickstart') }}"
|
||||
class="btn-gta-secondary inline-flex items-center px-6 py-3 text-base">
|
||||
Quick Start Guide
|
||||
</a>
|
||||
<a href="{{ route('docs.documentation') }}"
|
||||
class="btn-gta-primary inline-flex items-center px-6 py-3 text-base">
|
||||
Full Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Native Functions -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Native Functions</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Look up GTA5/FiveM native functions with signatures, parameters, and code examples.
|
||||
<!-- Resources -->
|
||||
<div class="mt-16">
|
||||
<h2 class="text-3xl font-extrabold text-gray-900 dark:text-white mb-8">Resources</h2>
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">MCP Server Configuration</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
To connect to the FiveM MCP server, use the following configuration in your MCP client:
|
||||
</p>
|
||||
<pre
|
||||
class="bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded p-4 text-sm overflow-x-auto"><code>{
|
||||
"servers": {
|
||||
"fivem": {
|
||||
"url": "{{ route('fivem') }}",
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manifest Generator -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Manifest Generator</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Generate fxmanifest.lua files automatically for any framework and scripting language.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Reference -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Event Reference</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Complete database of FiveM events including core, ESX, and QBCore events with examples.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boilerplate Generator -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Boilerplate Generator</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Generate complete resource structures with client, server, config, and optional NUI files.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Framework Support -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 bg-gta-orange rounded-md p-3">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Framework Support</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Full support for Standalone, ESX, and QBCore frameworks in both Lua and JavaScript.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="mt-16 bg-gray-900 dark:bg-black border border-gta-orange rounded-lg shadow-xl overflow-hidden">
|
||||
<div class="px-6 py-12 sm:px-12">
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl font-extrabold text-white">Ready to get started?</h2>
|
||||
<p class="mt-4 text-lg text-gray-200 dark:text-gray-300">
|
||||
Set up the MCP server in just 5 minutes with our quick start guide.
|
||||
</p>
|
||||
<div class="mt-8 flex justify-center space-x-4">
|
||||
<a href="{{ route('docs.quickstart') }}"
|
||||
class="btn-gta-secondary inline-flex items-center px-6 py-3 text-base">
|
||||
Quick Start Guide
|
||||
</a>
|
||||
<a href="{{ route('docs.documentation') }}"
|
||||
class="btn-gta-primary inline-flex items-center px-6 py-3 text-base">
|
||||
Full Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resources -->
|
||||
<div class="mt-16">
|
||||
<h2 class="text-3xl font-extrabold text-gray-900 dark:text-white mb-8">Resources</h2>
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">MCP Server Configuration</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
To connect to the FiveM MCP server, use the following configuration in your MCP client:
|
||||
</p>
|
||||
<pre
|
||||
class="bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded p-4 text-sm overflow-x-auto"><code>{
|
||||
"servers": {
|
||||
"fivem": {
|
||||
"url": "{{ route('fivem') }}",
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">External Links</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li>
|
||||
<a href="https://docs.fivem.net/docs/"
|
||||
class="link-gta">FiveM
|
||||
Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docs.fivem.net/natives/"
|
||||
class="link-gta">FiveM
|
||||
Natives Reference</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docs.qbcore.org/"
|
||||
class="link-gta">QBCore
|
||||
Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://coxdocs.dev/"
|
||||
class="link-gta">COX
|
||||
MySQL Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">External Links</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li>
|
||||
<a href="https://docs.fivem.net/docs/" class="link-gta">FiveM
|
||||
Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docs.fivem.net/natives/" class="link-gta">FiveM
|
||||
Natives Reference</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docs.qbcore.org/" class="link-gta">QBCore
|
||||
Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://coxdocs.dev/" class="link-gta">COX
|
||||
MySQL Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docs.prodigyrp.net/" class="link-gta">Prodigy
|
||||
Studios Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://laravel.com/docs/mcp"
|
||||
class="link-gta">Laravel
|
||||
MCP Documentation</a>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# prp-bridge Events
|
||||
|
||||
@foreach($eventsByCategory as $category => $events)
|
||||
## {{ $category }}
|
||||
|
||||
@foreach($events as $event)
|
||||
- **{{ $event['name'] }}** ({{ ucfirst($event['side']) }}) - {{ $event['description'] }}
|
||||
@endforeach
|
||||
|
||||
@endforeach
|
||||
@@ -0,0 +1,30 @@
|
||||
# prp-bridge Event: {{ $event['name'] }}
|
||||
|
||||
**Side:** {{ ucfirst($event['side']) }}
|
||||
|
||||
### Description
|
||||
{{ $event['description'] }}
|
||||
|
||||
### Parameters
|
||||
@if(count($event['parameters']) > 0)
|
||||
@foreach($event['parameters'] as $name => $description)
|
||||
- `{{ $name }}`: {{ $description }}
|
||||
@endforeach
|
||||
@else
|
||||
This event takes no parameters.
|
||||
@endif
|
||||
|
||||
### Code Examples
|
||||
|
||||
**Lua:**
|
||||
```lua
|
||||
{{ $event['lua_example'] }}
|
||||
```
|
||||
|
||||
**JavaScript:**
|
||||
```javascript
|
||||
{{ $event['js_example'] }}
|
||||
```
|
||||
|
||||
### Documentation
|
||||
[View at docs.prodigyrp.net](https://docs.prodigyrp.net/prp-bridge/events.html)
|
||||
@@ -0,0 +1,10 @@
|
||||
# prp-bridge Exports
|
||||
|
||||
@foreach($exportsByCategory as $category => $exports)
|
||||
## {{ $category }}
|
||||
|
||||
@foreach($exports as $export)
|
||||
- **{{ $export['name'] }}** ({{ ucfirst($export['side']) }}) - {{ $export['description'] }}
|
||||
@endforeach
|
||||
|
||||
@endforeach
|
||||
@@ -0,0 +1,28 @@
|
||||
# prp-bridge Export: {{ $export['name'] }}
|
||||
|
||||
**Category:** {{ $export['category'] }}
|
||||
**Side:** {{ ucfirst($export['side']) }}
|
||||
|
||||
### Description
|
||||
{{ $export['description'] }}
|
||||
|
||||
### Parameters
|
||||
@if(count($export['parameters']) > 0)
|
||||
@foreach($export['parameters'] as $param)
|
||||
- **{{ $param['name'] }}** ({{ $param['type'] }}): {{ $param['description'] }}
|
||||
@endforeach
|
||||
@else
|
||||
This export takes no parameters.
|
||||
@endif
|
||||
|
||||
### Returns
|
||||
**Type:** {{ $export['returns']['type'] }}
|
||||
{{ $export['returns']['description'] }}
|
||||
|
||||
### Usage Example
|
||||
```lua
|
||||
{{ $export['lua_example'] }}
|
||||
```
|
||||
|
||||
### Documentation
|
||||
[View at docs.prodigyrp.net](https://docs.prodigyrp.net/prp-bridge/exports.html)
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Mcp\Resources\BestPractices;
|
||||
use App\Mcp\Resources\CodeSnippets;
|
||||
use App\Mcp\Resources\FrameworkComparison;
|
||||
use Laravel\Mcp\Request;
|
||||
|
||||
it('code snippets resource returns response with content', function () {
|
||||
@@ -24,7 +26,7 @@ it('code snippets response contains content', function () {
|
||||
});
|
||||
|
||||
it('best practices resource returns response with content', function () {
|
||||
$resource = new \App\Mcp\Resources\BestPractices;
|
||||
$resource = new BestPractices;
|
||||
$request = new Request([]);
|
||||
|
||||
$response = $resource->handle($request);
|
||||
@@ -34,7 +36,7 @@ it('best practices resource returns response with content', function () {
|
||||
});
|
||||
|
||||
it('best practices response contains content', function () {
|
||||
$resource = new \App\Mcp\Resources\BestPractices;
|
||||
$resource = new BestPractices;
|
||||
$request = new Request([]);
|
||||
|
||||
$response = $resource->handle($request);
|
||||
@@ -44,7 +46,7 @@ it('best practices response contains content', function () {
|
||||
});
|
||||
|
||||
it('framework comparison resource returns response with content', function () {
|
||||
$resource = new \App\Mcp\Resources\FrameworkComparison;
|
||||
$resource = new FrameworkComparison;
|
||||
$request = new Request([]);
|
||||
|
||||
$response = $resource->handle($request);
|
||||
@@ -54,7 +56,7 @@ it('framework comparison resource returns response with content', function () {
|
||||
});
|
||||
|
||||
it('framework comparison response contains content', function () {
|
||||
$resource = new \App\Mcp\Resources\FrameworkComparison;
|
||||
$resource = new FrameworkComparison;
|
||||
$request = new Request([]);
|
||||
|
||||
$response = $resource->handle($request);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use App\Mcp\Tools\Prodigy\Client\GetProdigyClientEventReference;
|
||||
use Laravel\Mcp\Request;
|
||||
|
||||
it('lists all client events when no event name given', function () {
|
||||
$tool = new GetProdigyClientEventReference;
|
||||
$request = new Request(['event_name' => '']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->not()->toBeEmpty();
|
||||
});
|
||||
|
||||
it('returns client event reference for prp-bridge:notify', function () {
|
||||
$tool = new GetProdigyClientEventReference;
|
||||
$request = new Request(['event_name' => 'prp-bridge:notify']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('prp-bridge:notify');
|
||||
});
|
||||
|
||||
it('returns client event reference for sound play event', function () {
|
||||
$tool = new GetProdigyClientEventReference;
|
||||
$request = new Request(['event_name' => 'prp-bridge:sound:play']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('prp-bridge:sound:play');
|
||||
});
|
||||
|
||||
it('returns client event reference for allowlist update event', function () {
|
||||
$tool = new GetProdigyClientEventReference;
|
||||
$request = new Request(['event_name' => 'prp-bridge:client:updateAllowlist']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('updateAllowlist');
|
||||
});
|
||||
|
||||
it('returns client event reference for revived event', function () {
|
||||
$tool = new GetProdigyClientEventReference;
|
||||
$request = new Request(['event_name' => 'prp-bridge:client:revived']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('revived');
|
||||
});
|
||||
|
||||
it('returns not found for unknown client event', function () {
|
||||
$tool = new GetProdigyClientEventReference;
|
||||
$request = new Request(['event_name' => 'prp-bridge:nonexistent:event']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('not found');
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use App\Mcp\Tools\Prodigy\Client\GetProdigyClientExport;
|
||||
use Laravel\Mcp\Request;
|
||||
|
||||
it('lists all client exports when no export name given', function () {
|
||||
$tool = new GetProdigyClientExport;
|
||||
$request = new Request(['export_name' => '']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->not()->toBeEmpty();
|
||||
});
|
||||
|
||||
it('returns client export reference for IsAllowlisted', function () {
|
||||
$tool = new GetProdigyClientExport;
|
||||
$request = new Request(['export_name' => 'IsAllowlisted']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('IsAllowlisted');
|
||||
});
|
||||
|
||||
it('returns client export reference for PropPlacer', function () {
|
||||
$tool = new GetProdigyClientExport;
|
||||
$request = new Request(['export_name' => 'PropPlacer']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('PropPlacer');
|
||||
});
|
||||
|
||||
it('returns client export reference for AddPedInteraction', function () {
|
||||
$tool = new GetProdigyClientExport;
|
||||
$request = new Request(['export_name' => 'AddPedInteraction']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('AddPedInteraction');
|
||||
});
|
||||
|
||||
it('returns client export reference for RemovePedInteraction', function () {
|
||||
$tool = new GetProdigyClientExport;
|
||||
$request = new Request(['export_name' => 'RemovePedInteraction']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('RemovePedInteraction');
|
||||
});
|
||||
|
||||
it('returns not found for unknown client export', function () {
|
||||
$tool = new GetProdigyClientExport;
|
||||
$request = new Request(['export_name' => 'NonExistentExport']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('not found');
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use App\Mcp\Tools\Prodigy\Server\GetProdigyServerEventReference;
|
||||
use Laravel\Mcp\Request;
|
||||
|
||||
it('lists all server events when no event name given', function () {
|
||||
$tool = new GetProdigyServerEventReference;
|
||||
$request = new Request(['event_name' => '']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->not()->toBeEmpty();
|
||||
});
|
||||
|
||||
it('returns server event reference for playerLoad', function () {
|
||||
$tool = new GetProdigyServerEventReference;
|
||||
$request = new Request(['event_name' => 'prp-bridge:server:playerLoad']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('playerLoad');
|
||||
});
|
||||
|
||||
it('returns server event reference for playerUnload', function () {
|
||||
$tool = new GetProdigyServerEventReference;
|
||||
$request = new Request(['event_name' => 'prp-bridge:server:playerUnload']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('playerUnload');
|
||||
});
|
||||
|
||||
it('returns server event reference for groupMemberAdded', function () {
|
||||
$tool = new GetProdigyServerEventReference;
|
||||
$request = new Request(['event_name' => 'prp-bridge:server:groupMemberAdded']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('groupMemberAdded');
|
||||
});
|
||||
|
||||
it('returns server event reference for uniqueue party destroyed', function () {
|
||||
$tool = new GetProdigyServerEventReference;
|
||||
$request = new Request(['event_name' => 'prp-bridge:uniqueue:partyDestroyed']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('partyDestroyed');
|
||||
});
|
||||
|
||||
it('returns not found for unknown server event', function () {
|
||||
$tool = new GetProdigyServerEventReference;
|
||||
$request = new Request(['event_name' => 'prp-bridge:server:nonexistentEvent']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('not found');
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
use App\Mcp\Tools\Prodigy\Server\GetProdigyServerExport;
|
||||
use Laravel\Mcp\Request;
|
||||
|
||||
it('lists all server exports when no export name given', function () {
|
||||
$tool = new GetProdigyServerExport;
|
||||
$request = new Request(['export_name' => '']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->not()->toBeEmpty();
|
||||
});
|
||||
|
||||
it('returns server export reference for startGlobalCooldown', function () {
|
||||
$tool = new GetProdigyServerExport;
|
||||
$request = new Request(['export_name' => 'startGlobalCooldown']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('startGlobalCooldown');
|
||||
});
|
||||
|
||||
it('returns server export reference for isCooldownActive', function () {
|
||||
$tool = new GetProdigyServerExport;
|
||||
$request = new Request(['export_name' => 'isCooldownActive']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('isCooldownActive');
|
||||
});
|
||||
|
||||
it('returns server export reference for CreateGroup', function () {
|
||||
$tool = new GetProdigyServerExport;
|
||||
$request = new Request(['export_name' => 'CreateGroup']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('CreateGroup');
|
||||
});
|
||||
|
||||
it('returns server export reference for GetGroupPlayerIds', function () {
|
||||
$tool = new GetProdigyServerExport;
|
||||
$request = new Request(['export_name' => 'GetGroupPlayerIds']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('GetGroupPlayerIds');
|
||||
});
|
||||
|
||||
it('returns server export reference for UniQueue CreateQueue', function () {
|
||||
$tool = new GetProdigyServerExport;
|
||||
$request = new Request(['export_name' => 'CreateQueue']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('CreateQueue');
|
||||
});
|
||||
|
||||
it('returns server export reference for RegisterSellShop', function () {
|
||||
$tool = new GetProdigyServerExport;
|
||||
$request = new Request(['export_name' => 'RegisterSellShop']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('RegisterSellShop');
|
||||
});
|
||||
|
||||
it('returns server export reference for CreateCase', function () {
|
||||
$tool = new GetProdigyServerExport;
|
||||
$request = new Request(['export_name' => 'CreateCase']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('CreateCase');
|
||||
});
|
||||
|
||||
it('returns server export reference for HasAllowlist', function () {
|
||||
$tool = new GetProdigyServerExport;
|
||||
$request = new Request(['export_name' => 'HasAllowlist']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('HasAllowlist');
|
||||
});
|
||||
|
||||
it('returns not found for unknown server export', function () {
|
||||
$tool = new GetProdigyServerExport;
|
||||
$request = new Request(['export_name' => 'NonExistentExport']);
|
||||
|
||||
$response = $tool->handle($request);
|
||||
|
||||
expect((string) $response->content())->toContain('not found');
|
||||
});
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
@@ -11,7 +13,7 @@
|
||||
|
|
||||
*/
|
||||
|
||||
pest()->extend(Tests\TestCase::class)
|
||||
pest()->extend(TestCase::class)
|
||||
// ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
->in('Feature');
|
||||
|
||||
|
||||
Reference in new issue
Block a user