We picked the worst controller we had.
Not the messiest for shock value, just the one we’d been avoiding for months. The kind every team has, the file nobody wants to touch because it works, technically, and touching it feels risky.
So we pointed Claude Code at the file and let it try. This is our honest, unfiltered Claude Code Laravel refactor session, mistakes included.
The Controller We Started With
This one handled order creation. Validation, pricing logic, inventory checks, and a notification email, all crammed into a single method.
public function store(Request $request)
{
$validated = $request->validate([
'product_id' => 'required|exists:products,id',
'quantity' => 'required|integer|min:1',
'customer_email' => 'required|email',
]);
$product = Product::find($validated['product_id']);
if ($product->stock < $validated['quantity']) {
return response()->json(['error' => 'Insufficient stock'], 400);
}
$price = $product->price;
if ($validated['quantity'] >= 10) {
$price = $price * 0.9;
}
$total = $price * $validated['quantity'];
$order = new Order();
$order->product_id = $product->id;
$order->quantity = $validated['quantity'];
$order->total = $total;
$order->customer_email = $validated['customer_email'];
$order->status = 'pending';
$order->save();
$product->stock = $product->stock - $validated['quantity'];
$product->save();
Mail::to($validated['customer_email'])->send(new OrderConfirmation($order));
DB::table('order_logs')->insert([
'order_id' => $order->id,
'action' => 'created',
'created_at' => now(),
]);
return response()->json($order, 201);
}Nothing about this is broken. It runs fine. It’s just doing five different jobs in one place, business logic, persistence, notifications, and raw logging, all tangled together.
Exactly the kind of thing that’s tedious to refactor by hand and easy to get subtly wrong while doing it. In other words, the perfect first candidate for a Claude Code Laravel refactor: mechanical, risky to touch manually, and easy to verify once it’s done.
Setting Up the Claude Code Laravel Session
Before touching anything, we opened a terminal in the project root and started a session, pointing it at the controller file directly rather than describing the problem from memory.
This matters more than it sounds like it should. A Claude Code Laravel workflow works best when it can actually read the surrounding codebase, not just the one file you’re complaining about. It checks existing conventions, naming patterns, and related classes before making changes, which is a big part of why the output ends up fitting the project instead of looking bolted on.
We gave it read access to the whole app/ directory, not just the controller, specifically so it could match our existing patterns instead of inventing new ones.
The First Prompt
We opened a terminal session and kept the ask simple on purpose. A good Claude Code Laravel prompt names the target file and the constraint, then gets out of the way.
This OrderController@store method is doing too much. Extract the business
logic into a service class, keep the controller thin, and make sure
validation still happens the same way. Don't change the actual behavior.Claude Code read through the file, then the surrounding directory structure, checking whether we already had a services folder or a pattern it should follow.
It didn’t. So it asked.
I don't see an existing app/Services directory or a service class pattern
elsewhere in this codebase. Should I create app/Services/OrderService.php,
or is there a naming convention you'd prefer?Small thing, but it mattered. It didn’t just guess and generate a structure that clashed with how we actually organize things. It checked first.
We told it to go with app/Services, which is what we’d have picked anyway.
What It Actually Changed
A few minutes later, the controller looked like this:
public function store(StoreOrderRequest $request, OrderService $orderService)
{
$order = $orderService->createOrder($request->validated());
return response()->json($order, 201);
}And the logic moved into a dedicated service:
class OrderService
{
public function createOrder(array $data): Order
{
$product = Product::findOrFail($data['product_id']);
if ($product->stock < $data['quantity']) {
throw new InsufficientStockException;
}
$price = $this->calculatePrice($product, $data['quantity']);
$order = Order::create([
'product_id' => $product->id,
'quantity' => $data['quantity'],
'total' => $price * $data['quantity'],
'customer_email' => $data['customer_email'],
'status' => 'pending',
]);
$product->decrement('stock', $data['quantity']);
Mail::to($data['customer_email'])->send(new OrderConfirmation($order));
OrderLog::create([
'order_id' => $order->id,
'action' => 'created',
]);
return $order;
}
private function calculatePrice(Product $product, int $quantity): float
{
return $quantity >= 10 ? $product->price * 0.9 : $product->price;
}
}It also pulled validation into a StoreOrderRequest form request class, and swapped the raw DB::table('order_logs')->insert() call for a proper OrderLog model, since one already existed elsewhere in the codebase and just wasn’t being used here.
That last part we didn’t ask for directly. It noticed the inconsistency on its own and flagged it before making the change, which is where a Claude Code Laravel refactor pulls ahead of a plain find-and-replace.
Where It Got Something Wrong
Here’s the part most write-ups of an AI refactor on a Laravel controller quietly skip.
It initially wrapped the stock check and order creation in a database transaction, which sounds like a good instinct, except it used DB::transaction() around the Mail::to()->send() call as well.
Sending an email inside a database transaction is a real problem. If the mail service hangs or times out, you’re holding a database lock the entire time for no good reason, and if it throws, you roll back a legitimate order over a notification failure.
We caught it in review, not because it flagged it as risky. It didn’t. It just wrote a decent test suite that happened to expose the mock timing issue when a fake mail delay was added.
The fix was simple:
Move the Mail::to()->send() call outside the DB::transaction() block.
Emails failing shouldn't roll back a valid order.It made the change immediately, and explained the reasoning back to us correctly once corrected, which was reassuring. But it hadn’t caught it on its own, and that’s worth saying plainly instead of glossing over.
Being Honest About the Limitations
A few other things worth flagging, since credibility matters more here than hype.
- It’s very good at pattern-matching to your existing codebase conventions, but it won’t always catch domain-specific risk, like the transaction-and-email issue above, unless you’re specific about it
- It asks clarifying questions when structure is ambiguous, which we appreciated, but it will confidently generate a plausible-looking structure if you don’t push back, so review matters more than it might feel like it should
- Test generation was solid but not exhaustive, it covered the obvious paths well and missed a couple of edge cases around zero-quantity orders that we had to prompt for separately
None of this makes the tool less useful. It just means treating it like a genuinely capable pair programmer, not an autopilot.
The Verdict: Is a Claude Code Laravel Refactor Worth It?
For this kind of work, yes, with a caveat.
Extracting business logic out of a bloated controller, restructuring into services and form requests, updating call sites consistently, that’s exactly the kind of mechanical-but-careful work where a Claude Code Laravel workflow genuinely saves real time. It didn’t just move code around, it understood what the code was actually doing well enough to preserve behavior while restructuring it.
Where it’s worth being careful is anything touching side effects with real consequences, payments, emails, external API calls inside transactions. Not because it can’t handle that logic, it clearly can once you point it at the specific risk, the same way other AI-driven features in a Laravel app need explicit guardrails, but because it won’t always flag that risk unprompted.
If you’re maintaining a Laravel codebase with the same kind of controller we started with, a Claude Code Laravel session like this one is worth trying. Just review it the way you’d review a capable junior developer’s pull request, not the way you’d rubber-stamp your own code.
For more, browse our Laravel guides.



