ARTICLE AD BOX
I am working on a full-stack application where the frontend is a plain HTML page using JavaScript fetch and Bootstrap 5, and the backend is a Laravel 11 API. I have defined a standard Route::apiResource('invoice_details', InvoiceController::class); route, which handles endpoints for invoice line items. The GET request to fetch data functions correctly, but my POST, PATCH, and DELETE requests fail with unexpected behaviors. For instance, when attempting a DELETE request, the controller fails to receive the model, resulting in a custom 404 error block I implemented because the object arrives empty. Additionally, during a POST request, the InvoiceRequest validation class triggers, but it returns error responses or fails silently without hitting the database correctly.
Looking at my Eloquent models, Invoice has a hasMany relation called invoice_detailes, and InvoiceDetail belongs to Invoice. In my InvoiceController, the methods are type-hinted with InvoiceDetail $invoiceDetail based on typical resource controller structures. However, since the resource route is named invoice_details (plural with an underscore), I suspect Laravel might be expecting a different variable name for implicit binding. Furthermore, my frontend sends JSON data exactly matching the database fields, yet validation fails to process properly under certain HTTP methods. What is causing this route parameter disconnect between my JavaScript fetch requests and Laravel's route-model binding? How can I correct the resource setup or controller parameters to ensure models are resolved and stored correctly?
Teszt HTML:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css"> <title>Document</title> <style> body { max-width: 100vw; } @media (max-width: 992px) { h3 { font-size: 1.2rem !important; } p { font-size: 0.9rem !important; } th, td { font-size: 0.8rem !important; } } </style> </head> <body> <nav> <div class="nav nav-tabs" id="nav-tab" role="tablist"> <button class="nav-link active" id="nav-home-tab" data-bs-toggle="tab" data-bs-target="#nav-home" type="button" role="tab" aria-controls="nav-home" aria-selected="true">Főoldal</button> <button class="nav-link" id="nav-invoice-tab" data-bs-toggle="tab" data-bs-target="#nav-invoice" type="button" role="tab" aria-controls="nav-invoice" aria-selected="false">Számlák</button> <button class="nav-link" id="nav-new-tab" data-bs-toggle="tab" data-bs-target="#nav-new" type="button" role="tab" aria-controls="nav-new" aria-selected="false">Új tétel</button> </div> </nav> <div class="tab-content" id="nav-tabContent"> <div class="tab-pane fade show active p-3 px-5" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab"> <h1>Főoldal</h1> </div> <div class="tab-pane fade p-1 px-2 px-sm-3 py-sm-2 px-lg-5 py-lg-3" id="nav-invoice" role="tabpanel" aria-labelledby="nav-invoice-tab"> <h1>Számlák</h1> <button class="btn btn-outline-dark mb-5" id="getData">Adatok lekérése</button> <div id="data"></div> </div> <div class="tab-pane fade p-3 px-5" id="nav-new" role="tabpanel" aria-labelledby="nav-new-tab"> <h1 class="h1 mb-5">Új hozzáadása</h1> <div class="container"> <label class="form-label" for="invoice_id">Invoice Id:</label> <input class="form-control" type="number" name="invoice_id" id="invoice_id"> <label class="form-label" for="product_name">Product Name</label> <input class="form-control" type="text" name="product_name" id="product_name"> <label class="form-label" for="unit_price">Unit Price</label> <input class="form-control" type="number" name="unit_price" id="unit_price"> <label class="form-label" for="quantity">Quantity</label> <input class="form-control" type="number" name="quantity" id="quantity"> <label class="form-label" for="line_total">Line Total</label> <input class="form-control" type="number" name="line_total" id="line_total"> <button class="btn btn-outline-dark mt-5" type="button" id="submit">Létrehozás</button> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> <script> document.getElementById("getData").addEventListener("click", async (e) => { e.preventDefault(); try { const dataDiv = document.getElementById("data"); dataDiv.innerHTML = "<div class=\"spinner-border\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div>"; const response = await fetch("http://localhost:8000/api/invoice_details", { method: 'GET', headers: { 'Content-Type': 'application/json' }, }); if (!response.ok) { console.log("Hiba a request során: ", response.status); return; } const jsonData = await response.json(); let htmlContent = ""; jsonData.data.forEach(element => { htmlContent += `<div class=\"row g-0\"><div class=\"col-12 col-md-4 col-sm-12\"><h3 class=\"responsive-h3\">${element.number}</h3><p>Created at: ${element.created_at}</p><p>Updated at: ${element.updated_at}</p></div>`; htmlContent += "<div class=\"col-12 col-md-8 col-sm-12 overflow-auto\"><table class=\"table table-striped table-hover responsive-table\"><thead><tr><th>Id</th><th>Product Name</th><th>Unit Price</th><th>Quantity</th><th>Line Total</th><th>Created At</th><th>Updated At</th><th>Actions</th></tr></thead><tbody>"; element.invoice_detailes.forEach(detail => { htmlContent += `<tr><th>${detail.id}</th><td>${detail.product_name}</td><td>${detail.unit_price}</td><td>${detail.quantity}</td><td>${detail.line_total}</td><td>${detail.created_at}</td><td>${detail.updated_at}</td><td><button id="${detail.id}" class="btn btn-outline-dark delete-btn"><i class="bi bi-trash3-fill"></i></button></td></tr>`; }); htmlContent += "</tbody></table></div><hr class=\"hr my-4\"></div>"; }); dataDiv.innerHTML = htmlContent; const delete_buttons = document.querySelectorAll(".delete-btn"); delete_buttons.forEach((button) => { button.addEventListener("click", async (e) => { if (confirm(`Biztosan törli a(z) ${button.id} id-jű elemet?`)) { try { const response_del = await fetch(`http://localhost:8000/api/invoice_details/${button.id}`, { method: "DELETE", headers: { "Content-type": "application/json", "Accept": "application/json" }, }); if (!response_del.ok) { console.log("Response failed: ", response_del.status); return; } console.log(await response_del.json()); document.getElementById("getData").click(); } catch (err) { console.log("Something went wrong: ", err); } } }); }); } catch (err) { console.log("Error: ", err) } }); document.getElementById("submit").addEventListener("click", async (e) => { e.preventDefault(); let invoice_id = document.getElementById("invoice_id").value; let product_name = document.getElementById("product_name").value; let unit_price = document.getElementById("unit_price").value; let quantity = document.getElementById("quantity").value; let line_total = document.getElementById("line_total").value; if (invoice_id == "" || product_name == "" || unit_price == "" || quantity == "" || line_total == "") { alert("Nem adott meg minden adatot!"); return; } const inputJson = { "invoice_id": invoice_id, "product_name": product_name, "unit_price": unit_price, "quantity": quantity, "line_total": line_total }; try { const response = await fetch("http://localhost:8000/api/invoice_details", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify(inputJson), }); if (!response.ok) { console.log("A válasz sikertelen: ", response.status); alert(response_json.message); return; } const response_json = await response.text(); console.log(response_json); alert("Sikeresen létrehozva!"); } catch (err) { console.log("An error occured: ", err); } }); </script> </body> </html>Invoice Migration:
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. */ public function up(): void { Schema::create('invoices', function (Blueprint $table) { $table->id(); $table->string('number'); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { // } };Invoice Detail migration:
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('invoice_details', function (Blueprint $table) { $table->id(); $table->foreignId('invoice_id')->constrained('invoices')->onDelete('cascade'); $table->string('product_name'); $table->integer('unit_price'); $table->integer('quantity'); $table->integer('line_total'); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('invoice_details'); } };Invoice Seeder:
<?php namespace Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use Carbon\Carbon; class InvoiceSeeder extends Seeder { public function run(): void { DB::table('invoices')->insert([ ['number' => 'SZLA-001/2026', 'created_at' => Carbon::now()->format('Y-m-d H:i:s'), 'updated_at' => Carbon::now()->format('Y-m-d H:i:s')], ['number' => 'SZLA-002/2026', 'created_at' => Carbon::now()->format('Y-m-d H:i:s'), 'updated_at' => Carbon::now()->format('Y-m-d H:i:s')], ['number' => 'SZLA-003/2026', 'created_at' => Carbon::now()->format('Y-m-d H:i:s'), 'updated_at' => Carbon::now()->format('Y-m-d H:i:s')], ['number' => 'SZLA-004/2026', 'created_at' => Carbon::now()->format('Y-m-d H:i:s'), 'updated_at' => Carbon::now()->format('Y-m-d H:i:s')], ]); } }Database Seeder:
<?php namespace Database\Seeders; use App\Models\User; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { use WithoutModelEvents; /** * Seed the application's database. */ public function run(): void { // User::factory(10)->create(); User::factory()->create([ 'name' => 'Test User', 'email' => '[email protected]', ]); $this->call([ InvoiceSeeder::class, InvoiceDetailsSeeder::class, ]); } }Invoice Detail Seeder:
<?php namespace Database\Seeders; use App\Models\InvoiceDetail; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; use Carbon\Carbon; use Illuminate\Support\Facades\Log; class InvoiceDetailsSeeder extends Seeder { /** * Run the database seeds. */ public function run(): void { $data = fopen(base_path("database/seeders/invoice_details.csv"), "r"); while (!feof($data)) { $line = fgets($data); $line_arr = explode(";", $line); InvoiceDetail::create([ 'id' => $line_arr[0], 'invoice_id' => (int)$line_arr[1], 'product_name' => $line_arr[2], 'unit_price' => (int)$line_arr[3], 'quantity' => (int)$line_arr[4], 'line_total' => (int)$line_arr[5], 'created_at' => Carbon::now()->format('Y-m-d H:i:s'), 'updated_at' => Carbon::now()->format('Y-m-d H:i:s') ]); } fclose($data); } }Invoice Model:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Invoice extends Model { public $timestamps = true; protected $fillable = [ 'number' ]; public function invoice_detailes(){ return $this->hasMany(InvoiceDetail::class, 'invoice_id', 'id'); } }Invoice Detail Model:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class InvoiceDetail extends Model { public $timestamps = true; protected $fillable = [ 'id', 'invoice_id', 'product_name', 'unit_price', 'quantity', 'line_total' ]; public function invoice_detailes(){ return $this->belongsTo(Invoice::class, 'id', 'invoice_id'); } }Invoice Controller:
<?php namespace App\Http\Controllers; use App\Http\Requests\InvoiceRequest; use App\Models\Invoice; use App\Models\InvoiceDetail; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; class InvoiceController extends Controller { /** * Display a listing of the resource. */ public function index() { $invoices = Invoice::with('invoice_detailes')->get(); return response()->json([ "data" => $invoices ], 200); } /** * Store a newly created resource in storage. */ public function store(InvoiceRequest $request) { Log::error("eljutottam idaig\n\n\n\n\n"); $invoice = InvoiceDetail::create($request->validated()); return response()->json([ "message" => "Resource created successfully", "invoice" => $invoice, ], 201); } /** * Update the specified resource in storage. */ public function update(InvoiceRequest $request, InvoiceDetail $invoiceDetail) { if ($invoiceDetail == null || $invoiceDetail == []) return response()->json(["message" => "Nincs ilyen id-n adat!"], 404); $invoiceDetail->update($request->validated()); return response()->json([ "Módosított adat" => $invoiceDetail, ], 200); } /** * Remove the specified resource from storage. */ public function destroy(InvoiceDetail $invoiceDetail) { if ($invoiceDetail == null || $invoiceDetail == []) return response()->json(["message" => "Nincs ilyen id-n adat!"], 404); $invoiceDetail->delete(); return response()->json([ "message" => "Adat sikeresen törölve!", "id" => $invoiceDetail->id, ]); } }Invoice Request:
<?php namespace App\Http\Requests; use Illuminate\Support\Facades\Log; use Illuminate\Foundation\Http\FormRequest; class InvoiceRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; } /** * Get the validation rules that apply to the request. * * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> */ public function rules(): array { if ($this->method() == "PATCH") { return [ 'invoice_id' => ["sometimes", "required", "integer", "exists:invoices,id"], 'product_name' => ["sometimes", "required", "string", "max:255"], 'unit_price' => ["sometimes", "required", "integer", "min:0"], 'quantity' => ["sometimes", "required", "integer", "min:0"], 'line_total' => ["sometimes", "required", "integer", "min:0"], ]; } return [ 'invoice_id' => ["required", "integer", "exists:invoices,id"], 'product_name' => ["required", "string", "max:255"], 'unit_price' => ["required", "integer", "min:0"], 'quantity' => ["required", "integer", "min:0"], 'line_total' => ["required", "integer", "min:0"], ]; } public function messages(): array { return [ "invoice_id.exists" => "Nincs a megadott id-vel található invoice az adatbázisban!", "invoice_id.integer" => "Az invoice_id nem szám!", ]; } }API.php:
<?php use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use App\Http\Controllers\InvoiceController; Route::get('/user', function (Request $request) { return $request->user(); })->middleware('auth:sanctum'); Route::apiResource('invoice_details', InvoiceController::class);