System prompts for Laravel-aware context, few-shot examples that produce idiomatic PHP, chain-of-thought for complex business logic, structured output with JSON schemas, and the prompt templates that turn a generic LLM into a tool that understands your specific codebase — practical patterns, not theory.
Ask an LLM to “write a Laravel controller method that updates a post” with no other context, and it produces something that works — and looks nothing like the rest of the codebase. Array-based validation instead of a Form Request. DB::table() instead of Eloquent. No policy check. A raw return response()->json(...) instead of an Inertia response. None of it is wrong, exactly. It’s just generic PHP wearing a Laravel-shaped costume, because the model has no idea this project uses Form Requests, policies, and Inertia as its actual conventions — it’s pattern-matching against the entire internet’s Laravel code, most of which doesn’t look like this codebase at all.
This is a prompting problem, not a model capability problem. The same model, given the right context, produces code that’s often indistinguishable from what a senior developer on the project would write. This post covers the five patterns that make that difference reliably: system prompts that establish Laravel-aware context once instead of repeating it every message, few-shot examples that anchor output to your actual conventions rather than generic ones, chain-of-thought prompting for business logic complex enough that skipping straight to code produces subtly wrong results, structured output via JSON schemas for anything downstream code needs to parse reliably, and reusable prompt templates that turn “write me a controller” into a repeatable, codebase-aware operation instead of a one-off request you re-explain every time.
System Prompts — Establishing Convention Once, Not Every Message
The single highest-leverage prompting pattern for Laravel work is a system prompt that encodes the project’s actual conventions once, so every subsequent request inherits them instead of needing to restate “use Form Requests” and “we use Inertia, not a JSON API” every single time.
You are a senior Laravel developer working on [Project Name], a [brief description]
built with:
- Laravel 12, PHP 8.4
- Inertia.js + Vue 3 (no separate REST API — controllers return Inertia::render())
- Eloquent as the only data access layer — no raw DB::table() queries in
application code
- Form Requests for all validation, never inline $request->validate()
- Policies for all authorization — $this->authorize() in controllers,
never manual role checks
- Pest for tests, written as it()/expect() blocks, not PHPUnit classes
Conventions to follow without being asked:
- Controllers stay thin — business logic lives in Actions
(app/Actions/{Domain}/{ActionName}.php), single __invoke method
- All Eloquent relationships are typed with return type declarations
- Money is always stored as integer cents, never floats
- Every model that can be soft-deleted uses SoftDeletes with a documented
pruning schedule, not left to accumulate indefinitely
When asked to write code, match these conventions exactly, even if a
more "generic" Laravel approach is common elsewhere. Ask before introducing
a new pattern not listed here.
This is not a one-time nicety — it’s the difference between every generated snippet needing a conventions pass before it’s usable, and most generated snippets being close to committable as-is. The “ask before introducing a new pattern not listed here” line matters more than it looks: without it, a model asked for something the system prompt doesn’t cover will confidently invent a plausible-looking convention rather than flagging the gap, and a confidently-invented convention that doesn’t match the rest of the codebase is often harder to catch in review than an obviously wrong answer would have been.
The trap: writing this once and never updating it. A system prompt describing “Laravel 10, Blade only” six months after the project migrated to Inertia produces code for a stack the project no longer uses, and it’ll do so confidently, because nothing about the prompt signals it’s stale. This file is documentation with the same maintenance burden as any other piece of documentation — it needs an owner and a reason to get updated when a real architectural decision changes.
Few-Shot Examples — Anchoring to Idiomatic, Not Generic
A system prompt describes conventions in prose. Few-shot examples show the model actual code that follows them — and for anything with a specific, repeatable shape (a Form Request, an Action class, a Pest test), showing two or three real examples produces measurably more consistent output than describing the pattern in words alone.
Here are two examples of Action classes in this codebase. Match this exact
structure — constructor-injected dependencies, single __invoke method,
DB transaction only when multiple models are written together, explicit
return type.
Example 1:
<?php
namespace App\Actions\Posts;
class PublishPost
{
public function __construct(
private NotifySubscribers $notifySubscribers,
) {}
public function __invoke(Post $post): Post
{
$post->update(['published_at' => now()]);
$this->notifySubscribers->handle($post);
return $post->fresh();
}
}
Example 2:
<?php
namespace App\Actions\Teams;
class InviteTeamMember
{
public function __invoke(Team $team, string $email, string $role): TeamInvitation
{
return DB::transaction(function () use ($team, $email, $role) {
$invitation = $team->invitations()->create([
'email' => $email,
'role' => $role,
'token' => Str::random(32),
]);
Mail::to($email)->send(new TeamInvitationMail($invitation));
return $invitation;
});
}
}
Now write an Action class: CancelSubscription, which cancels a tenant's
Stripe subscription via Cashier and records an audit log entry.
Two examples is usually enough to lock in structure — constructor injection style, whether a transaction wraps multi-model writes, naming conventions, return type discipline. A single example risks the model overfitting to that one example’s specific quirks (mistaking “this action happened to not need a transaction” for “actions never use transactions”). Three or more rarely buys meaningfully more consistency than two, and costs more in prompt length, especially inside a system prompt that gets sent on every single message.
The pattern that separates “generic PHP” output from “our codebase’s” output is showing, not just telling. A system prompt saying “we use constructor injection” is a rule. A concrete example of constructor injection in this exact codebase’s style is a template — and models follow templates more reliably than they follow rules stated in isolation, especially for structural conventions (where a return type goes, how a transaction is wrapped, what a class is named) rather than purely semantic ones.
Chain-of-Thought for Business Logic — When Skipping Straight to Code Produces Subtly Wrong Results
For a simple CRUD operation, asking for code directly works fine. For business logic with real conditional complexity — a subscription proration calculation, a multi-tier discount stack, a permission check with several interacting conditions — asking for code directly tends to produce something that compiles, looks plausible, and gets an edge case wrong in a way that’s hard to spot in review, because the code reads as confident even where the underlying logic is incomplete.
Before writing any code, think through this step by step:
We need to calculate a subscription's prorated refund amount when a tenant
downgrades mid-billing-cycle from Pro ($99/mo) to Starter ($29/mo).
1. What information do we need to calculate this correctly? (days remaining
in the cycle, days already used, the per-day rate for both plans)
2. What edge cases exist? (downgrade on the very first day, downgrade on
the very last day, a billing cycle that isn't exactly 30 days, a tenant
who has an active discount/coupon applied)
3. Walk through the calculation with a concrete example: a 31-day cycle,
downgrade on day 20.
4. Only after that — write the PHP implementation as an Action class
following this codebase's conventions.
Asking the model to reason through the problem before generating code surfaces exactly the edge cases that a direct “write the code” request tends to silently miss — because working through “what if the downgrade happens on day one” as a distinct reasoning step forces an explicit answer, where jumping straight to code lets that case slip through unconsidered. The value here isn’t that chain-of-thought makes the model “smarter” in some abstract sense — it’s that the intermediate reasoning step is available for you to actually read and check before code gets generated from it, and a wrong assumption caught in the reasoning step is far cheaper to fix than the same wrong assumption already baked into forty lines of PHP.
This is worth reserving for logic that’s actually complex — not the default for every request. Prorated billing calculations, multi-step approval workflows, permission logic with several interacting roles: yes. A standard CRUD controller method: no, because forcing a reasoning step on trivial logic just adds latency and verbosity without surfacing anything a direct request wouldn’t have gotten right anyway. The judgment call is whether the logic has enough real branching complexity that an edge case could plausibly be missed — if yes, make the model show its reasoning before it shows code.
Structured Output With JSON Schemas — When Downstream Code Needs to Parse the Answer
Any time an LLM’s output is going to be consumed by code rather than read by a person — extracting structured data from unstructured text, classifying a support ticket, generating a set of test cases as data rather than prose — asking for “JSON” in prose and hoping the model’s output parses cleanly is a reliability problem waiting to surface. A model asked loosely for JSON will occasionally wrap it in a markdown code fence, add an explanatory sentence before it, or vary field names across calls in ways that break a naive json_decode().
// Using the OpenAI API's structured output mode — the response is
// constrained to match the schema, not just prompted to resemble it
$response = $client->chat()->create([
'model' => 'gpt-5',
'messages' => [
['role' => 'system', 'content' => 'Extract structured ticket data from support emails.'],
['role' => 'user', 'content' => $emailBody],
],
'response_format' => [
'type' => 'json_schema',
'json_schema' => [
'name' => 'support_ticket',
'strict' => true,
'schema' => [
'type' => 'object',
'properties' => [
'category' => [
'type' => 'string',
'enum' => ['billing', 'technical', 'account', 'other'],
],
'urgency' => [
'type' => 'string',
'enum' => ['low', 'medium', 'high'],
],
'summary' => ['type' => 'string'],
'requires_human_review' => ['type' => 'boolean'],
],
'required' => ['category', 'urgency', 'summary', 'requires_human_review'],
'additionalProperties' => false,
],
],
],
]);
$ticket = json_decode($response->choices[0]->message->content, associative: true);
The difference between this and prompting “please respond with JSON containing category, urgency, and summary” isn’t stylistic — strict: true structured output mode constrains the model’s actual token generation to match the schema, rather than relying on the model to voluntarily produce well-formed JSON because it was asked nicely. This is the difference between a parsing failure that happens occasionally in production, at a rate just low enough to be hard to catch in testing, and a parsing failure that structurally can’t happen because the output is constrained to conform.
// The Laravel side — validate what comes back even with strict mode,
// because "the shape is guaranteed" isn't the same as "the values are sensible"
class SupportTicketExtraction extends Data
{
public function __construct(
public string $category,
public string $urgency,
public string $summary,
public bool $requires_human_review,
) {}
}
$extraction = SupportTicketExtraction::from($ticket);
Even with a strict schema guaranteeing shape, validating the actual values on the Laravel side is worth keeping — a schema constrains structure, not semantic correctness, and a category field that’s technically a valid enum value but wrong for the actual email content is still possible. The schema eliminates the parsing failure category of bug; it doesn’t eliminate the classification-accuracy category, which still needs the normal validation and human-review-threshold logic a non-AI pipeline would have.
Reusable Prompt Templates — Turning a One-Off Request Into a Repeatable Operation
The pattern that ties the previous four together in daily use: instead of re-explaining context every time a request is made, build small, parameterized templates for the request shapes that come up repeatedly, so the system prompt, the relevant few-shot examples, and the reasoning-step instruction are assembled consistently instead of reconstructed from memory each time.
// A lightweight template registry — not tied to any specific AI SDK,
// just a pattern for consistent prompt assembly
class PromptTemplate
{
public static function generateAction(string $actionName, string $description): string
{
return view('prompts.action-template', [
'system' => file_get_contents(base_path('prompts/system.md')),
'examples' => file_get_contents(base_path('prompts/examples/actions.md')),
'actionName' => $actionName,
'description' => $description,
])->render();
}
public static function generateFormRequest(string $requestName, array $fields): string
{
return view('prompts.form-request-template', [
'system' => file_get_contents(base_path('prompts/system.md')),
'examples' => file_get_contents(base_path('prompts/examples/form-requests.md')),
'requestName' => $requestName,
'fields' => $fields,
])->render();
}
}
Storing the system prompt and few-shot examples as versioned files in the repository — not copy-pasted into a chat window from memory each time — means the whole team’s AI-assisted output stays anchored to the same conventions, and a convention change (switching form validation patterns, adopting a new Action class shape) is a one-file edit that immediately improves every subsequent generation, rather than a change that has to be manually remembered and re-explained by every developer separately.
The templates worth building first are the request shapes that recur most, not an exhaustive library up front. A Laravel-heavy team typically gets the most value from templates for: Action classes, Form Requests, Pest feature tests for a given controller action, and Eloquent model + migration + factory triplets. Building out a template for every conceivable request type before any of them have been used in practice is solving a problem that hasn’t been observed yet — start with the two or three shapes that come up daily, and only add a new template once a request pattern has actually repeated enough times to be worth the maintenance.
Putting It Together
1. System prompt — Laravel/PHP version, stack (Inertia? API?), core
conventions (Form Requests, Policies, Action classes), updated when
the stack actually changes, not left stale.
2. Few-shot examples — 2, occasionally 3, real examples from this
codebase for any structurally repeatable pattern (Actions, Form
Requests, Pest tests). Show the shape, don't just describe it.
3. Chain-of-thought — reserved for logic with real branching complexity
(billing calculations, multi-condition permissions, approval
workflows). Skipped for standard CRUD, where it adds verbosity
without surfacing anything a direct request would've missed.
4. Structured output — JSON schema with strict mode for anything
downstream code parses. Still validate values on the Laravel side —
schema guarantees shape, not correctness.
5. Templates — versioned in the repo, parameterized, built for the 2-3
request shapes that actually recur daily. Expanded only once a new
pattern has repeated enough to justify it.
The One Rule
An LLM with no context about a specific codebase writes generic, working PHP. An LLM with the right system prompt, real examples from the actual project, a reasoning step for genuinely complex logic, and a schema constraining its output where correctness matters writes code that’s frequently close to what a senior developer on the project would have written themselves. The gap between those two isn’t model capability — it’s how much of the codebase’s actual convention was ever communicated, and whether it was communicated once, reliably, in a versioned prompt template, or re-explained imperfectly from memory in every single chat message.
