How to make Laravel Resources work with polymorphic relation - laravel

I have a polymorphic relation in a Laravel application. I want a user of the website to be able to give a rating to both a User model as well as Product model.
I have following models and relations
class Rating extends Model
{
public function ratable()
{
return $this->morphTo();
}
}
class User extends Authenticatable
{
public function ratings()
{
return $this->morphMany('App\Rating', 'ratable');
}
}
class Product extends Model
{
public function ratings()
{
return $this->morphMany('App\Rating', 'ratable');
}
}
and the following database migration:
class CreateRatingsTable extends Migration
{
public function up()
{
Schema::create('ratings', function (Blueprint $table) {
$table->bigIncrements('id');
$table->integer('ratable_id');
$table->string('ratable_type');
$table->double('rating');
$table->text('comment');
$table->timestamps();
});
}
}
I have defined two routes:
1) Route::post('products/{product}/rating', 'ProductController#setRating')->name('products.rating');
2) Route::post('users/{user}/rating', 'UserController#setRating')->name('users.rating');
I have the following code in the controller (will only show the Product example)
public function setRating(Request $request, Product $product)
{
$rating = new Rating();
$rating->rating = $request->rating;
$rating->comment = $request->comment;
$product->ratings()->save($rating);
}
The above works perfectly and the correct records get inserted in the database depending on whether the Product route or the User route is called.
Now, all the rest of my code is using Laravel Resources, and for consistency reasons, I have also defined a resource for Rating:
class RatingResource extends JsonResource
{
public function toArray($request)
{
return [
'ratable_id' => $this->ratable_id,
'ratable_type' => $this->ratable_type,
'rating' => $this->rating,
'comment' => $this->comment
];
}
}
I'm also changing the ProductController code to use this resource
public function setRating(Request $request, Product $product)
{
return new RatingResource(Rating::create([
'ratable_id' => $product->id,
'ratable_type' => $product,
'rating' => $request->rating,
'comment' => $request->comment,
]));
}
In postman, I'm calling the REST API:
http://{{url}}/api/products/1/rating with body:
rating: 4
comment: "Test"
Yet, I always get following error message
"SQLSTATE[HY000]: General error: 1364 Field 'ratable_id'
doesn't have a default value (SQL: insert into ratings (rating,
comment, updated_at, created_at) values (4, test, 2019-09-07
13:44:22, 2019-09-07 13:44:22))"
I'm not passing the ratable_id and ratable_typeas I'm filling these in already in the controller code.
I somehow need to pass the resource that it's a Productor a UserI'm giving a rating for.
How can I make this work?

The problem probably is that ratable_id is missing from $fillable.
Try $product->ratings()->create([...data...]) so you don't have to set ratable_id and ratable_type yourself:
public function setRating(Request $request, Product $product)
{
return new RatingResource(
$product->ratings()->create([
'rating' => $request->rating,
'comment' => $request->comment,
])
);
}

Related

laravel 8 store request with foreign key user_id not working

I would like to store the corresponding logged in user when adding a new School data. What I'm trying to do is store the logged in user_id in the schools table, in order to know on who added the school data. I have a users table already, which will establish the relation in the schools table.
My goal is when an admin is logged in, he/she can see all of the School records, otherwise if it's a user, then only fetch the records he/she added. The problem is that I can't figure out on when and where to insert the user_id data during the store request as I'm getting an error "user id field is required". Here's what I've tried so far:
Migration:
class CreateSchoolsTable extends Migration
{
public function up()
{
Schema::create('schools', function (Blueprint $table) {
$table->id();
$table->string('school_name');
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
}
}
School Model:
class School extends Model
{
use HasFactory;
protected $fillable = ['school_name', 'user_id'];
public function User() {
return $this->belongsTo(User::class);
}
}
Store Request:
class StoreSchoolRequest extends FormRequest
{
public function rules(): array
{
return [
'school_name' => 'required|string|max:255',
'user_id' => 'required|exists:users,id'
];
}
}
Controller:
class SchoolController extends Controller
{
public function store(StoreSchoolRequest $request) {
$school_data = $request->validated();
$user_id = \Auth::user()->id;
$school_data['user_id'] = $user_id;
School::create($school_data );
return Redirect::route('schools.index');
}
}
Any inputs will be of big help! Thanks.
Laravel has elegant way to bind authenticated user_id. Remove user_id from request class and chaining method. Also setup relationship from User model to School Model
Form Request Class
class StoreSchoolRequest extends FormRequest
{
public function rules(): array
{
return [
'school_name' => 'required|string|max:255',
];
}
}
User Model
protected $fillable = ['school_name', 'user_id'];
...
// new line
public function schools() {
return $this->hasMany(School::class);
}
Your Controller
class SchoolController extends Controller
{
public function store(StoreSchoolRequest $request) {
auth()->user()->schools()->create($request->validated());
return Redirect::route('schools.index');
}
}
UPDATE ANSWER
Since user_id value is school name (based on image link from comment), probably there's something wrong either in User or School model. Here the quick fix
Your Controller
class SchoolController extends Controller
{
public function store(StoreSchoolRequest $request) {
auth()->user()->schools()->create(
array_merge(
$request->validated(),
['user_id' => auth()->id()]
)
);
return Redirect::route('schools.index');
}
}
You can add 'created_by' and 'updated_by' fields to your table. so you can register in these fields when additions or updates are made.
Then you can see who has added or updated from these fields.
class School extends Model
{
use HasFactory;
protected $fillable = ['school_name', 'user_id', 'created_by', 'updated_by'];
public function User() {
return $this->belongsTo(User::class);
}
}
Your controller part is correct but since you get the logged in user, you wont be having user_id in the request. So you should remove the rules about user_id from your StoreSchoolRequest.
class StoreSchoolRequest extends FormRequest
{
public function rules(): array
{
return [
'school_name' => 'required|string|max:255'
];
}
}
Problem is here ..
$school_data = $request->validated();
Since you are using $request->validated()..
You have to safe()->merge user_id into it , here Docs : .
$validated = $request->safe()->merge(['user_id' => Auth::user()->id]);
Then put this $validated into create query , Thanks. –

View is not returning attributes stored in database

I am attempting to return which user posted a comment, along with the time they posted the comment.
I have a model for comments
use Illuminate\Database\Eloquent\Model;
class comments extends Model
{
protected $guarded = [];
public function adjustments()
{
return $this->belongsToMany(User::class, 'adjustments')
->withTimestamps();
}
}
A pivot table which tracks which users posted which comments
public function up()
{
Schema::create('adjustments', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id')->index();
$table->unsignedInteger('comments_id')->index();
$table->timestamps();
});
}
An empty adjustments model
use Illuminate\Database\Eloquent\Model;
class adjustment extends Model
{
//
}
In php artisan tinker when $comment = App\comment::first(); and $user = App\user::first(); I'm able to successfully attach a user_id to a comment_id using $comment->adjustments()->attach($user->id) and calling App\adjustments::all(); will correctly return
=> Illuminate\Database\Eloquent\Collection {#2935
all: [
App\adjustment {#2941
id: 1,
user_id: 1,
comments_id: 1,
created_at: "2019-11-16 17:05:10",
updated_at: "2019-11-16 17:05:10",
},
],
}
When I'm trying to show the adjustments in my view, I get an empty list.
#foreach ($comment->adjustments as $user)
<li>{{$user->name}} on {{$user->pivot->updated_at}}</li>
#endforeach
In my products controller (a user makes comments on products) I have the following code in my show function
public function show(products $product, comments $comment, User $user)
{
return view ('products.show', compact('product'), compact('comment'));
}
Here you don't need a pivot table. Becasue here you has one to many relation. User can create many comment. & one comment is belongs one user.In user model add this
public function comments()
{
return $this->hasMany(Comment::class);
}
& comments table u have a foreign key user_id.
in comment model
public function user()
{
return $this->belongsTo(User::class,'user_id','id')
}
public function show(products $product, comments $comment, User $user)
{
$comments=Comment::with('user')->all();
return view ('products.show', compact(['product','comments']));
}
#foreach ($comments as $comment)
<li>{{$comment->user->name}} on {{$comment->updated_at}}</li>
#endforeach
Then you can acces all comments table data with a user

Create new Post with default Category belongsToMany

I have a Post/Category manyToMany relations and would like to be able to attach a default category named "Uncategorised" to each new post that is created. How can I do that? A BelongsToMany method only works on the Details page, not on Create page.
BelongsToMany::make(__('Categories'), 'categories', Category::class),
You can also set default value to your database field so that you can omit passing category and will be taken default to Uncategorised like if you are using MySQL you can do it this way by creating migration
$table->text('category')->default(0);
Because the BelongsToMany not show on mode create in Post Nova model. So we have to make our custom Select, by add this code to your fields:
public function fields(Request $request)
{
if($request->editMode=="create"){
$categories = \App\Category::get(['id','name']);
$options = [];
foreach($categories as $value){
$options[$value->id] = $value->name;
}
return [
ID::make()->sortable(),
Text::make('Title'),
Text::make('Summary'),
Textarea::make('Content'),
Select::make('Categories', 'category_id')
->options($options)
->displayUsingLabels()
->withMeta(['value' => 1]) // 1 = id of Uncategorised in categories table
];
}
return [
ID::make()->sortable(),
Text::make('Title'),
Text::make('Summary'),
Textarea::make('Content'),
BelongsToMany::make('Categories','categories')->display('name'),
];
}
Don’t forget relationship function in both, Post and Category model:
class Post extends Model
{
public function categories(){
return $this->belongsToMany(Category::class, 'category_post', 'post_id', 'category_id');
}
}
And:
class Category extends Model
{
public function posts(){
return $this->belongsToMany(Post::class,'category_post', 'category_id', 'post_id');
}
}
Then, custom the function process the data on mode Create of Post resource page, it’s at nova\src\Http\Controllers\ResourceStoreController.php, change function handle to this:
public function handle(CreateResourceRequest $request)
{
$resource = $request->resource();
$resource::authorizeToCreate($request);
$resource::validateForCreation($request);
$model = DB::transaction(function () use ($request, $resource) {
[$model, $callbacks] = $resource::fill(
$request, $resource::newModel()
);
if ($request->viaRelationship()) {
$request->findParentModelOrFail()
->{$request->viaRelationship}()
->save($model);
} else {
$model->save();
// your code to save to pivot category_post here
if(isset($request->category_id)&&($resource=='App\Nova\Post')){
$category_id = $request->category_id;
$post_id = $model->id;
\App\Post::find($post_id)->categories()->attach($category_id);
}
}
ActionEvent::forResourceCreate($request->user(), $model)->save();
collect($callbacks)->each->__invoke();
return $model;
});
return response()->json([
'id' => $model->getKey(),
'resource' => $model->attributesToArray(),
'redirect' => $resource::redirectAfterCreate($request, $request->newResourceWith($model)),
], 201);
}
}
All runs well on my computer. A fun question with me! Hope best to you, and ask me if you need!
What I ended up doing was saving the data on Post Model in boot().
public static function boot()
{
parent::boot();
static::created(function (Post $post) {
$post->categories()->attach([1]);
});
}

Laravel: One to Many Poly relation not updating automatically

3 types of posts: Personal, Business & Nature.
Below is the Post Model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'Personal' => 'App\Personal',
'Business' => 'App\Business',
'Nature' => 'App\Nature',
]);
class Post extends Model
{
public function postable()
{
return $this->morphTo();
}
}
Below is the Personal Model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Personal extends Model
{
public function posts()
{
return $this->morphMany(Post::class,'postable');
}
}
Likewise Busines & Nature models.
When I manually enter the data in phpMyAdmin, the tinker shows the result as required, but when I create a post from front-end (via form), the posts table remains unchanged.
I tried googling, but nothing works! :(
Below is the PersonalController
public function create()
{
if(Auth::guest()){
return redirect()->route('login');
}
$sectors = Sector::all();
$cities = City::all();
$ranges = Range::all();
return view('front.personal-register',compact('sectors','cities','ranges'));
}
public function store(Request $request)
{
$this->validate($request,[
"sectors" => "required",
"cities" => "required",
"ranges" => "required",
"g-recaptcha-response" => "required|captcha"
]);
$franchisee = new Personal;
$franchisee->user_id = Auth::user()->id;
$franchisee->save();
$franchisee->sectors()->sync($request->sectors);
$franchisee->cities()->sync($request->cities);
$franchisee->ranges()->sync($request->ranges);
return redirect(route('personal.index'))->with('message','Thank You! Your post has been added');
}

Eloquent Injected Multiple Model Relationships

So I learned in JeffreyWay's screencasts that I can use Eloquent to get the associated id from a model injected to another model.
I'm following his series about Laravel 5.4.
Here, I have a one-to-many relationships of user to posts.
App/Post
public function user()
{
return $this->belongsTo(User::class);
}
In my User Model, I have a publish method where the Post Model is injected. The publish method is used to create a post entry into the database.
App/User
public function posts()
{
return $this->hasMany(Post::class);
}
public function publish(Post $post)
{
$this->posts()->save($post);
}
I then have a store method in my PostsController that calls the publish method inside my User Model.
PostsController
class PostsController extends Controller
{
public function __construct()
{
$this->middleware('auth')->except(['index', 'show']);
}
public function store()
{
auth()->user()->publish(
new Post(request(['title', 'body']))
);
}
}
When the publish method is called, the injected Post class automatically sets the user_id to the save method.
My question is, how do I make a relationship like this in a situation where for every posts, there are comments. These comments are associated to the Post and the User that created the comment.
In short, I should have both user_id and post_id when I call the addComment method.
User Model:
public function posts(){
return $this->hasMany(Post::class);
}
public function comments(){
return $this->hasMany(Comments::class);
}
Posts Model
public function user(){
return $this->belongsTo(User::class);
}
public function comments(){
return $this->hasMany(Comments::class);
}
Comments Model
public function post(){
return $this->belongsTo(Post::class);
}
public function user(){
return $this->belongsTo(User::class);
}
Example Problems:
1) Get user comments:
Solution: auth()->user()->comments()->get(); <- collection of user
comments .
2) Get user from the given comment:
Solution: Comment::find($someCommentId)->user()->first()->name; <-
User name from a specific comment.
3) Get all comments for a specific post .
Solution: Post::first()->comments()->get(); or eager load
Post::with('comments')->first(); <- A collection that contains post
information within it u can find a collection of comments for that
post.
4) Load user when loading a comment:
Solution: Comment::with('user')->first(); <- single collection
containing a collection with user info and comment info.
5) Load a specific user post and comments for that post at the same time:
Solution: User::with('posts.comments')->first(); <- Contains a
collection with user info and collection of all user posts with each
post containing comments.
In your question you wrote:
In short, I should have both user_id and post_id when I call the addComment method.
Which is absolutely correct and no problem. You don't have to set these properties through a method like $user->posts()->save($post) - this is just a convenience method that does the job for you (see save($model) and related setForeignAttributesForCreate($model) in the framework code; these methods just set the foreign key property for you).
In fact, the following three ways to create a new post are interchangeable:
// what you did
$user->posts->save(
new Post([
'title' => 'Hello',
'body' => 'World!',
])
);
// equivalent
Post::create([
'user_id' => \Auth::user()->id, // or \Auth::id()
'title' => 'Hello',
'body' => 'World!',
]);
// also equivalent
$post = new Post([
'user_id' => \Auth::user()->id, // or \Auth::id()
'title' => 'Hello',
'body' => 'World!',
]);
$post->save();
When storing a new comment, you will most likely have a controller like this, because a comment always belongs to a post and you therefore will need a reference of the post:
class CommentsController extends Controller
{
public function __construct()
{
$this->middleware('auth')->except(['index', 'show']);
}
public function store(Post $post)
{
$comment = new Comment(request(['body']));
$comment->user_id = \Auth::user()->id;
$comment->post_id = $post->id;
$comment->save();
}
}
You could also abbreviate it and write:
Comment::create(
array_merge(request(['body']), ['user_id' => \Auth::id(), 'post_id' => $post->id])
);

Resources