How to save Laravel Multi Polymorphic Relationship? - laravel

I have a scenario that needs two polymorphic relationship in one model. The main model is Calls which includes a Caller and Callee. Caller and Callee can be a User, Buyer or Seller. So I designed my table like following.
class Call extends Model
{
public function caller()
{
return $this->morphTo();
}
public function callee()
{
return $this->morphTo();
}
}
I created my User model like this
class User extends Authenticatable
{
public function buyers()
{
return $this->hasMany(Buyer::class);
}
public function outgoingCalls()
{
return $this->morphMany(self::class, 'caller');
}
public function incomingCalls()
{
return $this->morphMany(self::class, 'callee');
}
}
and created my Buyer and Seller models like,
class Buyer extends Model
{
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function outgoingCalls()
{
return $this->morphMany(self::class, 'caller');
}
public function incomingCalls()
{
return $this->morphMany(self::class, 'callee');
}
}
So, I want save a call when a User calls a Buyer like this
Auth::user()->buyers()->find($args['buyer_id'])->outgoingCalls()->save()
and save a call when a Buyer or Seller calls to User
Buyer::find(2)->user()->outgoingCalls()->save()
my problem is I couldn't make it work like I wanted so I had to do it like this,
$userCall = new UserCall([
'caller_id' => Auth::user()->id,
'caller_type' => User::class,
'status' => 1,
'started_at' => '2020-06-24 01:00:00',
'ended_at' => '2020-06-24 02:00:00',
]);
if (Auth::user()->buyers()->find($args['buyer_id'])->incomingCalls()->save($userCall)) {
return [
'status' => 'SUCCESS',
'message' => 'User Call successfully saved',
];
}
could you please help me figure out this problem ? Thank you in advance !

Related

laravel relation data to search for other models

Client model has relations to Invoice. I need to get the amounts from the Invoice relationship and find the matching transactions from the Transaction model.
I do it like this:
class Client extends Model
{
public function invoices()
{
return $this->hasMany(Invoice::class);
}
public function priceInvoices()
{
return $this->hasMany(Invoice::class)->select('gross_price');
}
}
foreach (Client::find($id)->priceInvoices->toArray() as $item) {
$prices[] = $item['gross_price'];
}
$transactions_for_prices = Transaction::whereIn('price', $prices)->get();
Will it be possible to make it more elegant?
If your Invoice.php Model has a relationship with a Transaction.php Model,
Example:
class Invoice extends Model
{
public function trasnactions()
{
return $this->hasMany(Transaction::class);
}
}
You could do something like this in a controller. (example)
public function show($id) {
$client = Client::findOrFail($id);
return view('view_name', [
'client' => $client,
'transactions => $client->invoices->transactions
]);
}

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. –

Getting multiple related models together

In my Laravel app, I have three models, User, Course and CourseScore. I want to get all courses for an specific user with his score. something like:
{
id: 1,
name: first_name,
corses: [
{
id: 1,
name: 'course one',
score: 17, // this is what i need for every course,
},
{
id: 2,
name: 'course two',
score: 19, // this is what i need for every course,
},
]
}
Here are my Models:
User
<?php
class User extends Authenticatable
{
public function courseScores()
{
return $this->hasMany(CourseScore::class);
}
public function courses()
{
return $this->belongsToMany(Course::class);
}
}
Course
<?php
class Course extends Model
{
public function courseScores()
{
return $this->hasMany(CourseScore::class);
}
public function users()
{
return $this->belongsToMany(User::class);
}
}
CourseScore
<?php
class CourseScore extends Model
{
protected $table = 'course_scores';
public function course()
{
return $this->belongsTo(Course::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
And of course, please let me know if the relations are incorrect.
If I understand it correctly, you can do something like this,
in your User model
public function courses(){
return $this->hasMany(Course::class); // changed belongsToMany to hasMany
}
in your Course model
public function users(){
return $this->belongsTo(User::class); // changed belongsToMany to belongsTo
}
In your controller
$usersData = User::with('courses.courseScores')->get();
dd($usersData);
I dont fully understand your table structure so Im assuming you have multiple row of Scores for single Course (One to Many).
If the relationship is one is to one then add in your Course Model
public function courseScores(){
return $this->hasOne(CourseScore::class); //changed hasMany to hasOne
}
You can try using this
$val = DB::table('course as c')
->join('user as u', 'u.id','c.id')
-join('courseScore as cs','u.id','cs.id')
->get();
Then just dd($val) to see what value it shown

Testing BelongsToMany relationship with pivot table

I'm trying to figure out how to create all the data based on this relationship testing in Laravel.
Company Model
class Company
{
public function stores()
{
return $this->hasMany(Store::class, 'company_id');
}
public function employers()
{
return $this->belongsToMany(User::class, 'employers',
'company_id', 'user_id');
}
}
Store Model
class Store
{
public function company()
{
return $this->belongsTo(Company::class, 'company_id');
}
public function employers()
{
return $this->belongsToMany(User::class, 'employers',
'store_id', 'user_id');
}
}
User Model
class User
{
public function company()
{
return $this->belongsToMany(Company::class, 'employers',
'user_id', 'company_id');
}
public function store()
{
return $this->belongsToMany(Store::class, 'employers',
'user_id', 'store_id');
}
}
$company = Company::factory()->hasStores(
Store::factory()->hasEmployers(User::factory())
)->create();
dd($company) // App\Models\Company {#2470... Ok!
$store = $company->store()->first();
dd($store) // App\Models\Store {#2479... Ok!
$user = $store->employers()->first();
dd($user) // null (T-T)
Background: this is an application that allows a proprietor to own several companies. For that reason, I got many relationships, and even so, employees sometimes can only belong to a single company or store.
Try:
$store = $company->store->first();
dump($store);
$user = $store->employers->first();
dump($user);
Try something like this with DB Facade
private $employ;
public function setUp(): void
{
$this->employ = Employ::factory()->create([
'id' => 14,
'name' => 'Name Employ'
]);
}
public function test_pivote_table()
{
$user = User::factory()->create([
'name' => 'User test'
);
//here :)
DB::table('name_pivote_table')->insert([
'user_id' => $user->id,
'employ_id' => $this->employ->id
]);
}
Credits to Fguzman :)

Laravel: prevent changing other users' items

I have three models. I want to avoid that users can change the todo's from todolists belonging to other users.
class User extends Authenticatable
{
public function todolists()
{
return $this->hasMany('App\Todolist');
}
public function todos()
{
return $this->hasManyThrough('App\Todo', 'App\Todolist');
}
}
class Todolist extends Model
{
public function user()
{
return $this->belongsTo('App\User');
}
public function todos()
{
return $this->hasMany('App\Todo');
}
}
class Todo extends Model
{
protected $casts = [
'completed' => 'boolean',
];
public function todolist()
{
return $this->belongsTo('App\Todolist');
}
}
To avoid users can view other users' todolists and todo items, I have implemented the following:
public function getTodosForTodolist(Todolist $todolist)
{
if (Auth::user()->id == $todolist->user_id) {
$todos = Todo::where('todolist_id', $todolist->id )->get();
return view('todo/index', ['todos' => $todos);
}
else {
abort(403, 'Unauthorized action.');
}
}
Next step is to prevent that users can edit other users' todo items. Currently in the TodoController I have simply the following:
public function edit(Todo $todo)
{
if (Auth::user()->todos->id == $todo->todolist->id) {
return view('todo/edit', ['todo' => $todo]);
}
}
This gives the following error:
Property [id] does not exist on this collection instance.
The error is because the current user has multiple todos. So I changed my code as follows.
public function edit(Todo $todo)
{
if (Auth::user()->todos->first()->id == $todo->todolist->id) {
return view('todo/edit', ['todo' => $todo]);
}
abort('403', 'Unauthorized action.');
}
This works but it just feels very wrong to do this as such.
What would be a better way to accomplish that users' can view/edit/delete items belonging to other users?
I suggest that you use policies for your Todo and TodoList models and a scope to restrict todos to one user to prevent duplicated code within your app:
class ToDoListPolicy
{
public function view(User $user, TodoList $post)
{
return $user->id === $todolist->user_id;
}
}
class ToDoPolicy
{
public function edit(User $user, Todo $toDo)
{
$toDo->loadMissing('todolist');
return $user->id === $toDo->todolist->user_id;
}
}
Register them in your AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
TodoList::class => ToDoListPolicy::class,
Todo::class => ToDoPolicy::class
];
}
and then use them in your actions:
public function getTodosForTodolist(Todolist $toDoList)
{
$this->authorize('view', $toDoList);
$toDoList->loadMissing('todos');
return view('todo.index', ['todos' => $toDoList->todos);
}
class ToDoController extends Controller
{
public function edit(Todo $toDo)
{
$this->authorize('edit', $toDo);
return view('todo.edit', compact('toDo'));
}
}
And a scope to restrict the query to a specific user:
class Todo extends Model {
// ...
public function scopeByUser(Builder $query, ?User $user = null)
{
if (! $user) {
$user = Auth::user();
}
$query->whereHas('todolist', function (Builder $toDoListQuery) use ($user) {
$toDoListQuery->where('user_id', $user->id);
});
}
}
Answer to your questions in the comments.
Q1: I had to put Auth::user()->can('view', $todolist); in an if-else clause for it to work. Guess this is the way it works?
Q2: what is the difference between $this->authorize('edit', $todo) and Auth::user()->can('edit', $todo)?
Sorry, that was a mistake on my side. Auth::user()->can() returns a boolean whereas $this->authorize() (which is a method of the AuthorizesRequests trait usually included in the BaseController) throws an exception if the authorization failed.
If you want to let each user work only with his/her own Todos then adding a Global Scope is what you are looking for. This implementation will let your application feel that Todos ( of users other than the logged one ) does not exist.
Global Scopes can be used for many models which means it will reduce boiler plate code.
https://laravel.com/docs/7.x/eloquent#global-scopes

Resources