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:
- Categories (migration):
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('name')->unique();
});
}
- Posts (migration):
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?