I have a problem using Laravel's implicit model binding using a nested route and a custom key (not ID). I have these example models:
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('name')->unique();
});
}
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->nullable()->constrained();
$table->timestamps();
$table->string('text');
});
}
The relationship is One-To-Many:
A Post has one category. A category can have multiple Posts (Posts that have the same category).
The model classes like:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* The relationships that should always be loaded.
*
* @var array
*/
protected $with = ['category'];
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'category_id',
];
use HasFactory;
public function category() {
return $this->belongsTo(Category::class);
}
}
and
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
public function posts()
{
return $this->hasMany(Post::class);
}
}
And I am using these routes to create categories/posts and attach a post to a category:
api.php:
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PostController;
use App\Http\Controllers\CategoryController;
Route::apiResource('posts', PostController::class);
Route::apiResource('categories', CategoryController::class);
Route::get('posts/{post}/category', [PostController::class, 'getCategory']);
Route::post('posts/{post}/category/{category:name}', [PostController::class, 'addCategory']);
The PostController:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Post;
use App\Models\Category;
use Illuminate\Database\QueryException;
class PostController extends Controller
{
/**
* Other functions for other routes
*
*
*/
public function addCategory(Request $request, Post $post, Category $category)
{
$category->posts()->save($post);
$post->category()->associate($category);
$post->save();
return $post;
}
public function removeCategory(Request $request, Post $post, Category $category)
{
$post->category()->dissociate($post);
$post->save();
return response(['message'=>'Category removed.'], 200);
}
public function getCategory(Request $request, Post $post, Category $category)
{
return $post->category;
}
}
Now I create a Post and a category (works fine) and want to associate a post with a category using:
POST-Request to: http://localhost/api/posts/5/category/life
Results in:
Expected response status code [200] but received 500.
The following exception occurred during the request:
BadMethodCallException: Call to undefined method App\Models\Post::categories() in /var/www/html/vendor/laravel/framework/src/Illuminate/Support/Traits/ForwardsCalls.php:71
Why is Laravel trying to call that method on a one-to-many relationship?
A Post with ID 5 and a category with name attribute with string value "life" exists in the database.
But I always get a 404 error using the route. If I use the category-ID instead it is working fine. So the implicit model binding is not working for the nested route if using a custom key?
If I rewrite the 'addCategory'-method it is working fine:
public function addCategory(Request $request, Post $post, $name)
{
$category = Category::where('name', '=', $name)->firstOrFail();
$category->posts()->save($post);
$post->category()->associate($category);
$post->save();
return $post;
}
But I think Laravel's implicit binding should exactly do this automatically?
Anyone knows what is going wrong here?
The way laravel works with nested model bindings is when you have more than one wildcard it will assume the last model wildcard belongs to the second last model wildcard, and this one belongs to the third to last place model wildcard and so on... to the second model wildcard belongs to the first (that's why it calls a has many method), so the first has many seconds, that has many thirds, I hope you understood, the thing is, the relation has to already exist in order to do that, so you can't put post before category, because a category has many posts. You just have to remember always put the one model that has many before the one that belongs to, even if the instances are not related they will be found. So just swap the endpoint route like this Route::post('category/{category:name}/posts/{post}', [PostController::class, 'addCategory']);
, and then swap the addCategory method arguments. I'd recommend you doing that with your request, since you're not doing anything with it, it is weird seeing you have a post method with no request body