Code review · PHP
Review a Laravel Orders Controller
Medium Code review Most asked
This controller is in a pull request for an e-commerce admin panel. It lists a customer's orders with their items, and lets staff update an order. The code works in the developer's local environment with a handful of rows.
Review it as you would in a real pull request: list every problem you see, say why it matters in production, and suggest a fix. Look at correctness, security, performance and reliability.
The code to review
There are 5 issues to find.
<?php
namespace App\Http\Controllers;
use App\Models\Customer;
use App\Models\Order;
use Illuminate\Http\Request;
class CustomerOrdersController extends Controller
{
public function index(Customer $customer)
{
$orders = Order::where('customer_id', $customer->id)->get();
$result = [];
foreach ($orders as $order) {
$result[] = [
'id' => $order->id,
'total' => $order->total,
'items' => $order->items->map(fn ($i) => $i->product->name),
];
}
return response()->json($result);
}
public function update(Request $request, Order $order)
{
$order->update($request->all());
return response()->json($order);
}
}
Write your review
Loading the editor… (JavaScript is required to run code in your browser.)
The issues and how to fix them
Show the issues
1. Mass assignment from the raw request
Critical, line 29
$order->update($request->all()) writes whatever fields the client sends. Unless the model's $fillable is very strict, a caller can change customer_id, total, status or paid_at. Even with $fillable set, the controller has no validation, so bad types and values reach the database.
Fix: Validate with a Form Request (or $request->validate([...])) and pass only the validated fields: $order->update($request->validated()). Keep $fillable explicit on the model as a second line of defence.
2. No authorization check
Critical, line 11, 27
Route-model binding loads any Customer or Order by ID, and nothing checks that the signed-in user may see or change it. Changing the ID in the URL exposes other customers' orders (an IDOR, OWASP's "broken access control").
Fix: Add a policy and call $this->authorize('view', $customer) / $this->authorize('update', $order), or use ->can() middleware on the routes.
3. N+1 queries in the loop
Major, line 20
$order->items runs one query per order, and $i->product runs one more per item. A customer with 200 orders of 5 items makes about 1,200 queries for one page. It is invisible locally and slow in production.
Fix: Eager load: $customer->orders()->with('items.product')->get(). Enable Model::preventLazyLoading() in non-production environments so the next N+1 fails in tests.
4. Unbounded query
Major, line 13
->get() loads every order the customer ever placed into memory, along with all items. Large B2B customers can have tens of thousands of orders, so response time and memory grow without limit.
Fix: Paginate (->paginate(50) or ->cursorPaginate(50)) and select only the needed columns.
5. Hand-built JSON instead of an API Resource
Minor, line 15-24
Building arrays by hand in the controller spreads the response format across controllers, makes it easy to leak fields later (the update action already returns the whole model, including any internal columns), and skips consistent date and money formatting.
Fix: Return OrderResource::collection($orders) and new OrderResource($order), with the exposed fields listed in one place.
Interviewers use review exercises to see what you notice first and how you rank it. A strong answer leads with the two critical issues (anyone can read or rewrite any order), then the performance problems that only show up with real data (N+1, unbounded queries), and ends with maintainability. For each one, say what breaks in production and the smallest safe fix. A list of style nits with the security holes missed is the most common way to fail this exercise.
Fixed version
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UpdateOrderRequest;
use App\Http\Resources\OrderResource;
use App\Models\Customer;
use App\Models\Order;
class CustomerOrdersController extends Controller
{
public function index(Customer $customer)
{
$this->authorize('view', $customer);
$orders = $customer->orders()
->with('items.product:id,name')
->latest()
->cursorPaginate(50, ['id', 'customer_id', 'total', 'created_at']);
return OrderResource::collection($orders);
}
public function update(UpdateOrderRequest $request, Order $order)
{
$this->authorize('update', $order);
$order->update($request->validated());
return new OrderResource($order);
}
}
Follow-up questions
- How would you catch N+1 queries automatically in CI?
- The update must also adjust stock levels: what changes (transactions, locking)?
More Code Clinic: Code Review problems
- Review an Express User Search Endpoint · Easy
- Review a Payment Webhook Handler · Medium
- Review a Sign-Up Function · Medium
- Review a CSV Export Endpoint · Medium
- Review a React Search Component · Medium
- Review a Vue Comment List · Medium