backend-stack-laravel
A Laravel backend (11 to 13): API routes and resource controllers, Form Requests, mass assignment, API Resources, Eloquent without N+1, chunking, transactions and race-free updates, policies and gates, Sanctum, exception rendering to consistent JSON, queues with Horizon, the scheduler, events and listeners, caching, config caching and Octane in production, tests with Pest or PHPUnit, Larastan and Pint. Read when building or changing a Laravel app.
- License 许可
- Apache-2.0
- 所属仓库
- enowdev/enowxcli
- 最近更新
- 2026-10-06
- Stars
- ★ 67
- Forks
- 21
01 前言 Front Matter
- name
- backend-stack-laravel
- description
- A Laravel backend (11 to 13): API routes and resource controllers, Form Requests, mass assignment, API Resources, Eloquent without N+1, chunking, transactions and race-free updates, policies and gates, Sanctum, exception rendering to consistent JSON, queues with Horizon, the scheduler, events and listeners, caching, config caching and Octane in production, tests with Pest or PHPUnit, Larastan and Pint. Read when building or changing a Laravel app.
02 资源文件 33
- 参考文档references/agentic-rft.md3.2 KB
- 参考文档references/dataset-formats.md3.7 KB
- 参考文档references/deployment.md3.4 KB
- 参考文档references/evaluation.md5.8 KB
- 参考文档references/grader-design.md3.4 KB
- 参考文档references/hyperparameters.md3.2 KB
- 参考文档references/large-file-uploads.md927 B
- 参考文档references/platform-gotchas.md2.0 KB
- 参考文档references/reward-hacking.md2.9 KB
- 参考文档references/training-curves.md4.1 KB
- 参考文档references/training-types.md3.1 KB
- 参考文档references/vision-fine-tuning.md4.1 KB
- 脚本scripts/calibrate_grader.py9.3 KB
- 脚本scripts/check_training.py7.8 KB
- 脚本scripts/cleanup.py9.5 KB
- 脚本scripts/common.py7.6 KB
- 脚本scripts/convert_dataset.py10.7 KB
- 脚本scripts/deploy_model.py9.4 KB
- 脚本scripts/evaluate_model.py11.4 KB
- 脚本scripts/generate_distillation_data.py10.1 KB
- 脚本scripts/monitor_training.py5.7 KB
- 脚本scripts/score_dataset.py7.7 KB
- 脚本scripts/submit_training.py10.8 KB
- 脚本scripts/validate/__init__.py305 B
- 脚本scripts/validate/data_stats.py6.2 KB
- 脚本scripts/validate/validate_dpo.py3.7 KB
- 脚本scripts/validate/validate_rft.py8.6 KB
- 脚本scripts/validate/validate_sft.py4.5 KB
- 参考文档workflows/dataset-creation.md3.1 KB
- 参考文档workflows/diagnose-poor-results.md2.3 KB
- 参考文档workflows/full-pipeline.md3.1 KB
- 参考文档workflows/iterative-training.md3.1 KB
- 参考文档workflows/quickstart.md4.5 KB
03 SKILL.md 正文
Laravel
The generated Laravel app: $request->all() into Order::create(),
$guarded = [], a model returned as JSON with hidden columns and a lazy load
per row, authorisation only in the Vue page, env() called in a controller
(it returns null once config is cached), a job dispatched inside a
transaction that runs before the commit, and a scheduled command running on
every server. The backend rules are in the backend skill; these are
Laravel's own. Keep the project's version, structure and packages; the notes
below assume Laravel 11 or later (bootstrap/app.php for middleware and
exceptions). Laravel 13 (current, PHP 8.3+) adds PHP attributes for much of
the configuration; use them when the project does.
1. Routes and controllers
- API routes in
routes/api.php(php artisan install:apiadds it), grouped with a prefix and middleware (auth:sanctum); resource controllers (Route::apiResource('orders', OrderController::class)) and route model binding. - Thin controllers: a Form Request in, one call to an action or service
class (
app/Actions/PayOrder.php) that holds the rules, an API Resource out. - Nested resources with
->scoped(), so/orders/1/items/5is a404when item 5 is not order 1's. - The base controller has no traits since Laravel 11:
$this->authorize()needsAuthorizesRequestsadded;Gate::authorize('refund', $order)works anywhere (Laravel 13 also has the#[Authorize]controller attribute). - Rate limits defined in
AppServiceProvider::boot(), applied asthrottle:api:RateLimiter::for('api', fn (Request $r) => Limit::perMinute(60)->by($r->user()?->id ?: $r->ip())); tighter ones (Limit::perMinute(5)) on sign-in and anything that mails.
2. Input
- A Form Request per write (
StoreOrderRequest) withrules()andauthorize(); the controller uses$request->validated(), never$request->all(). - Models list
$fillableexplicitly; never$guarded = []. Fields a user must not set (role,price,user_id) are assigned in code. Laravel 13 writes the list as#[Fillable([...])]on the class. - Unknown fields refused (
backend-api): Laravel 13 has#[FailOnUnknownFields]per request, orFormRequest::failOnUnknownFields()once in a service provider.
public function rules(): array
{
return [
'lines' => ['required', 'array', 'min:1', 'max:50'],
'lines.*.product_id' => ['required', 'uuid', Rule::exists('products', 'id')],
'lines.*.quantity' => ['required', 'integer', 'between:1,100'],
'note' => ['nullable', 'string', 'max:500'],
];
}
3. Output
- API Resources (
OrderResource,OrderResource::collection) decide the fields; never return a model or->toArray()directly (hidden columns and future ones leak). - Lists with
->paginate(25)or->cursorPaginate(25)in the query,with()for the relations the resource shows. - Relations in a resource through
$this->whenLoaded('items')and counts throughwhenCounted, so a resource never triggers a query of its own.JsonResource::withoutWrapping()inAppServiceProviderreturns a record as itself rather than insidedata. - Errors in the format of
backend-errors, rendered inbootstrap/app.phpwith->withExceptions(...)for requests that expect JSON;abort(404)and domain exceptions with arendermethod rather than hand-built responses.
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(fn (Request $r) => $r->is('api/*') || $r->expectsJson());
$exceptions->render(function (ValidationException $e, Request $request) {
if (! $request->is('api/*')) {
return null; // web forms keep the redirect back with errors
}
$errors = [];
foreach ($e->validator->failed() as $field => $rules) { // ['lines.0.quantity' => ['Between' => [...]]]
foreach (array_keys($rules) as $rule) {
$errors[] = ['field' => preg_replace('/\.(\d+)/', '[$1]', $field), 'code' => Str::snake($rule)];
}
}
return response()->json(['type' => 'https://example.com/errors/validation-failed', 'status' => 422,
'code' => 'validation_failed', 'errors' => $errors], 422, ['Content-Type' => 'application/problem+json']);
});
})
- Laravel answers validation with
422; keep it when clients rely on it (backend-apiallows either).
4. Who may do what
- Policies per model (
OrderPolicy) and$this->authorize('refund', $order)orGate, on every action; queries scoped to the user or tenant (a global scope, or$user->orders()), so another id in the URL gives404. - A policy can answer
Response::denyAsNotFound()where even a403would reveal that the record exists;before()for the admin shortcut.
class OrderPolicy
{
public function refund(User $user, Order $order): Response
{
return $order->user_id === $user->id ? Response::allow() : Response::denyAsNotFound();
}
}
// The controller: a Form Request in, the check, one action, a resource out.
public function refund(RefundOrderRequest $request, Order $order, RefundOrder $refund): OrderResource
{
Gate::authorize('refund', $order);
return new OrderResource($refund($order, $request->validated('reason')));
}
- Sanctum: cookie sessions for the app's own frontend (with
statefulApi()and CSRF), personal access tokens with abilities for mobile and other clients. Starter kits (Breeze, or the React, Vue and Livewire kits) and Fortify for sign-in, reset and verification rather than writing them. - Sanctum tokens never expire by default (
expirationisnullinconfig/sanctum.php): set it, and schedulesanctum:prune-expired --hours=24. Abilities are checked with$request->user()->tokenCan(...)or theabilitiesandabilitymiddleware, whose aliases are registered inbootstrap/app.php.
5. Eloquent and data
Model::preventLazyLoading(! app()->isProduction())inAppServiceProvider, so N+1 fails in development;with(),withCount(),load()to load relations.Model::shouldBeStrict()also makes a silently discarded or a missing attribute throw.DB::transaction(fn () => ...)around writes that belong together. Race-free stock with a conditional update that reports rows changed:Product::whereKey($id)->where('stock', '>=', $qty)->decrement('stock', $qty)returns 0 when there is not enough;lockForUpdate()inside the transaction when you must read then write.DB::transaction($callback, 3)retries a deadlocked transaction up to three times;createOrFirst()leans on a unique index wherefirstOrCreate()races;upsert()for bulk writes.- Casts for types (
'total' => 'integer', enums,'immutable_datetime') in thecasts()method, money as integers, timestamps in UTC ('timezone' => 'UTC'inconfig/app.php;APP_TIMEZONE=UTCin Laravel 11), converted for display. - Large sets in pieces:
chunkById(500, ...)(plainchunk()skips rows when the callback changes the filtered column),lazyById()to stream, never->get()on a whole table. - Public ids with
HasUuids(UUIDv7 since Laravel 12, so inserts stay in index order) orHasUlids;php artisan model:show Orderlists a model's casts, relations and columns. - Migrations from
php artisan make:migration, with foreign keys (foreignId('order_id')->constrained()->restrictOnDelete()) and unique indexes; factories and seeders for sample data.
6. Queues and the scheduler
- Jobs implementing
ShouldQueuewith$tries,backoff(),ShouldBeUniquewhere a duplicate would harm, andafterCommitso they never run before the transaction; the database driver to start, Redis with Horizon later. - Laravel 13 declares these with attributes (
#[Tries(5)],#[Backoff(10, 60, 300)],#[Timeout(120)]);ShouldQueueAfterCommitmakes every dispatch wait for the commit.
#[Tries(5), Backoff(10, 60, 300), Timeout(60)]
class SendReceipt implements ShouldQueueAfterCommit
{
use Queueable;
public function __construct(public int $orderId) {} // an id, fetched again in handle()
public function middleware(): array
{
return [new WithoutOverlapping($this->orderId)];
}
public function handle(ReceiptMailer $mailer): void
{
$mailer->sendOnce(Order::findOrFail($this->orderId)); // safe to run twice
}
}
- A job's timeout stays below the connection's
retry_after(90 seconds inconfig/queue.php), or a slow job is handed to a second worker while the first still runs. Job middleware for the rest:WithoutOverlapping,RateLimited,ThrottlesExceptions; afailed()method for the last failure (backend-jobs). - Horizon supervises Redis workers from
config/horizon.php; each deploy runsphp artisan horizon:terminate(plain workers:queue:restart, andqueue:work --max-time=3600), since long-lived workers keep the old code. - Scheduled work in
routes/console.phpwith->onOneServer()and->withoutOverlapping(); oneschedule:runcron orschedule:work.onOneServerneeds a shared cache store with locks (Redis, database, Memcached, DynamoDB). - Mail and notifications queued (
ShouldQueue), with texts through the lang files (thei18nskill).
7. Events and caching
- Events for side effects other parts react to; listeners in
app/Listenersare discovered by their type-hint, queued withShouldQueue, and wait for the commit withShouldHandleEventsAfterCommit. Cache::remember("product:{$id}", now()->addMinutes(10), fn () => ...);Cache::flexible('stats', [300, 600], fn () => ...)serves stale while it refreshes;Cache::lock("order:{$id}", 10)->block(5, fn () => ...)for one writer at a time; tags only on Redis and Memcached. More inbackend-caching.
8. Config, logs and production
env()only insideconfig/*.php; code readsconfig('services.x.key'), and production runsphp artisan config:cache.Log::withContext(['request_id' => ...])in a middleware; no request bodies with passwords or tokens in the log.Context::add('request_id', $id)(Laravel 11+) does more: it lands on every log record and travels into queued jobs.- Deploys run
php artisan optimize(config, events, routes and views cached), migrations as their own step, and the worker restart above.APP_DEBUG=false; JSON logs to stderr in containers (LOG_CHANNEL=stderr,LOG_STDERR_FORMATTER=Monolog\Formatter\JsonFormatter); the/uproute for health checks (backend-observability). - Octane (FrankenPHP, Swoole or RoadRunner) boots the app once for many
requests: no request or user state in singletons or static properties
(resolve the request where it is used),
octane:reloadon deploy, and--max-requests(500 by default) recycles workers that leak.
9. Tests
Pest or PHPUnit feature tests with RefreshDatabase, factories and
actingAs($user); postJson() with assertStatus, assertJsonPath and
assertJsonValidationErrors; Http::fake(), Queue::fake(), Mail::fake()
and $this->travelTo(...); the test database of the same kind as
production (backend-testing).
it("does not reveal another user's order", function () {
$order = Order::factory()->create(); // belongs to someone else
$this->actingAs(User::factory()->create())
->postJson("/api/orders/{$order->id}/pay")
->assertNotFound();
expect($order->fresh()->status)->toBe('open');
});
Http::preventStrayRequests()fails any outbound call without a fake;Sanctum::actingAs($user, ['orders:write'])for token abilities;Exceptions::fake()to assert what was reported;$this->freezeTime();php artisan test --parallelgives each process its own database.assertJsonValidationErrorsreads Laravel's defaulterrorsobject: once validation renders as problem details, assert withassertJsonPathon theerrorslist instead.- Pest 5 needs PHP 8.4; Pest 4 runs on PHP 8.3.
10. Check
php artisan test, ./vendor/bin/pint --test, Larastan or PHPStan when the
project has it (vendor/bin/phpstan analyse, with Larastan's
extension.neon included and the level raised over time), php artisan route:list for the routes, composer audit, then call them with curl.
Avoid
$request->all() into create(); $guarded = []; models returned as JSON;
authorisation only in the frontend; lazy loading in a loop; env() outside
config; a job that runs before its transaction commits; scheduled commands
that run on every server; rules written in controllers; a job timeout above
retry_after; workers left on old code after a deploy; Sanctum tokens that
never expire; chunk() over rows the callback updates; request state in a
singleton under Octane.
04 同仓库更多 Skills
demo-skill
Project-scope demo skill for tests.
writing
Writing words people read: the specific thing instead of claims, words to drop, sentences, interface copy (buttons, labels, errors, empty states, confirmations), page copy, notifications and emails, alt text and link text, numbers and dates, words that translate well, documentation and voice. Read before writing a page's copy, an interface's text or a document.
ui
Designing or restyling an interface: direction, layout, type, colour, icons, components, states, responsive behaviour. Read before building a page, a screen or a set of components.
ui-themes
Light and dark themes, and colour that holds in both: tokens with a value per theme, and what each kind of content needs to stay visible (icons, SVG drawings and line art, logos, photos, screenshots, charts, code, maps, borders, shadows, focus, form controls), switching without a flash, and checking both. Read before building a second theme or a theme toggle, and before any drawing, logo or chart on a page that has two themes.
ui-structure
Structure before styling: the blueprint written before any interface code (the screen's job, the regions its kind of page needs, their order and why, the layout skeleton in numbers, the components and their states), the anatomy and order of a homepage, a dashboard, a list, a detail page, settings, a form, sign-in, pricing, docs, an article and a checkout, and how to turn the blueprint into code (tokens, layout primitives, components, sections, page). Read first, before building or rebuilding any page or screen.
ui-stack-vue
Building interfaces in Vue 3 and Nuxt: project layout, script setup with typed defineProps, defineEmits and defineModel, attribute fallthrough, slots and scoped slots, template refs, ref, reactive and computed, watch versus watchEffect, composables and VueUse, provide and inject, Pinia, keyed v-for, transitions, Nuxt 4 (app directory, pages, layouts, useFetch and useAsyncData keys, $fetch in handlers, server routes, runtime config, useSeoMeta, NuxtImg, middleware), VeeValidate forms, Reka UI and Nuxt UI, testing, vue-tsc, and the traps. Read when the project uses Vue or Nuxt.