How to make composite/multiple primary key in Laravel >= 5.6? - php

I have a many-to-many relationship created between USERS and COURSES. The models and migrations are made as follows.
User Model:
public function courses()
{
return $this->belongsToMany(Course::class)->withTimestamps();
}
Course Model:
public function users()
{
return $this->belongsToMany(User::class, 'course_user', 'course_id', 'user_id')->withTimeStamps();
}
The problem is I cannot find a way to implement a composite primary key into my migration file or pivot table in Laravel like this,
Schema::create('course_user', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->integer('course_id')->unsigned()->index()->primary();
$table->foreign('course_id')->references('id')->on('courses');
$table->integer('user_id')->unsigned()->index()->primary();
$table->foreign('user_id')->references('id')->on('users');
});
After coding the above, when I call php artisan migrate:refresh an error appears showing multiple primary keys performed in the linked table. So I did a research on this matter and found out that ELOQUENT does not support composite primary keys as stated in this forum: link to the forum
Is there any other way I can go around this?
I have somehow managed to write a piece of code to detect a existing entries in the pivot table or the many-to-many relationship, inside the controller and avoid new duplicate entries.

To counter attack this problem I wrote a piece code in the controller to detect an existing tuple in the pivot table as follows. NOTE: I have used Resources to retrieve data, you can avoid the Resource functions and use the normal method also.
The userId and courseId are passed as parameters in the request:
public function index(Request $request) {
$target = new AcademicPaymentResource(User::with('courses')->findOrFail($request['userId']));
if ($target != null)
{
foreach ($target as $query){
// search for existing tuples
foreach ($query->courses as $subQuery => $key){
// Check if courseId in existing tuple is equal to the courseId of request
if ( $query->courses[$subQuery]->pivot->course_id == $request['courseId']){
// Do anything here..
// display data to test if it works
return $query->courses[$subQuery];
}
}
// update the pivot table if the tuple doesnt exist
$user = User::findOrFail($request['userId']);
$user->courses()->attach($request['courseId']);
// read tuple data again to display if it works
$target = new AcademicPaymentResource(User::with('courses')->findOrFail($request['userId']));
return $target;
}
}
}
This method actually works fine, since I have used it flawlessly so far.
But if there is any other proper method please don't hesitate to answer..

Related

I'm getting an error when trying to save a new model with 2 fields referencing different ids in the same table

I'm new to laravel, and I've picked up the basic workflow of creating, updating and deleting database entries using migrations, models and controllers. But now I'm trying to do the same with a subscriptions table that has a subscriberId and a followeeId in it. Both of these fields reference different ids of the same table (users). This kind of task seem to require some finetuning. And I'm stuck.
Here's my code with some comments.
Subscriptions Table
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('subscriberId');
$table->unsignedBigInteger('followeeId');
$table->foreign('subscriberId')->references('id')->on('users');
$table->foreign('followeeId')->references('id')->on('users');
});
Previously, I've used another approach to foreign ids, namely the one with the $table->foreignId('user_id')->constrained() pattern, but in this particular case I need to make sure that the two foreign ids reference different users, so I went for a more verbose option.
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
Here I've added the second parameter. This seems to work.
Subscription Model
class Subscription extends Model
{
use HasFactory;
protected $fillable = [
'subscriberId',
'followeeId'
];
public function subscriberId()
{
return $this->belongsTo(User::class, 'id', 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'id', 'followeeId');
}
}
Here I pass additional parameters, too, although in this case I'm not so sure if these are the correct ones. But this is my best guess. If I'm not mistaken, the second parameter of the belongsTo relation is inferred from the model that is being passed in, not the model of the parent class as is the case with the hasMany relation. So in this case that would be 'id' of the users table, which would be the default here anyway, but I need the third parameter, so I explicitly state the second parameter as well. Again, I'm not sure about this combination, but that's what I was able to make of the docs. I've also used other combinations of additional parameters, and even tried getting rid of these two public functions altogether, but that won't work either.
Now, here's the controller. If I do this:
$user->subscriptions()->get();
I do get the subscriptions I want. But if I do this instead:
$user->subscriptions()->create([
'subscriberId' => 1,
'followeeId' => 2
]);
I get the 500 error. I've also tried another approach:
$newSub = new Subscription;
$newSub->subscriberId = 1;
$newSub->followeeId = 2;
$newSub->save();
return $newSub;
But still no success. I still get the 500 error when I try to save()
Please help me out.
Solution
I should have used
public $timestamps = false
in the Subscription model, and I also misunderstood the docs. The correct combo is
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
and
Subscription Model
public function subscriberId()
{
return $this->belongsTo(User::class, 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'followeeId');
}

Laravel: matches array (Tinder clone) to my user returns empty

I'm making a Tinder clone to practice, I want to retrieve a user matches (when both users like eachother) but when I do:
$user = User::with('matches')->findOrFail(1);
The matches array returns empty, despite all users liking eachothers,
I created a Users_Users_liked pivot table:
Schema::create('users_users_liked', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id')->index();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade');
$table->unsignedInteger('user_liked_id')->nullable()->index();
$table->foreign('user_liked_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade');
$table->timestamps();
});
I set up the User model with relationship as follows (notice matches realtionship, which always returns an empty array)
public function likesToUsers()
{
return $this->belongsToMany(self::class, 'Users_Users_liked', 'user_id', 'user_liked_id');
}
public function likesFromUsers()
{
return $this->belongsToMany(self::class, 'Users_Users_liked', 'user_liked_id', 'user_id');
}
public function matches()
{
return $this->likesFromUsers()->whereIn('user_id', $this->likesToUsers->keyBy('id'));
}
In my UsersSeeder I give likes to user 1 from all users
$users = json_decode($json, true);
foreach ($users as $data)
{
$user = new User();
$user->name = $data['name'];
$user->save();
if($user->id > 1)
{
$user->likesToUsers()->attach([1]);
$user->save();
}
}
$user = User::findOrFail(1);
$user->likesToUsers()->attach([2,3,4,5,6,7,8,9,10,11]);
$user->save();
There are a couple of issues in your current matches relationship:
whereIn takes an array of ids (where the values of each item is the id), whereas, using keyBy will mean that the values are the instances of the model. pluck would have been the right method to use i.e. $this->likesToUsers->pluck('id').
The above would only have worked if you already had the model loaded so eager loading wouldn't have worked. There are a couple of reasons for this:
The models wouldn't have yet been loaded so you couldn't then call the likesToUsers relationship to actually retrieve the ids i.e. you can't use the value of one relationship in another relationship in this way.
Even if you were lazily eager loading the relationships, if you were loading multiple models, Laravel would actually only use the relationship values from the first model (I think it's the first) so the rests of the relationships would be wrong.
What you can do though is use the query side of a relationship to retrieve the data. The long and the short of it is don't try and use the value of one relationship in another when trying to eager load.
All that being said, one option would potentially be to join the pivot table on the relationship (again) and compared the opposite columns:
public function matches()
{
return $this->likesFromUsers()
->join('users_users_liked as alt_users_users_liked', function (JoinClause $join) {
$join
->whereColumn('users_users_liked.user_liked_id', 'alt_users_users_liked.user_id')
->whereColumn('users_users_liked.user_id', 'alt_users_users_liked.user_liked_id');
});
}
Some general notes/tips:
You can simplify your users_users_liked migration by using the foreignId() method instead:
$table->increments('id');
$table->foreignId('user_id')->constrained()->cascadeOnDelete()->cascadeOnUpdate();
$table->foreignId('user_liked_id')->constrained('users')->cascadeOnDelete()->cascadeOnUpdate();
$table->timestamps();
It's also usually a good idea to add a unique constraint to your pivot tables (unless you potentially want duplicates):
$table->unique(['user_id', 'user_liked_id']);
Finally, it might be worth taking a look at Model Factories.

Laravel Create Method does not return primary key values that were inserted by Observer

Let me preface this post with, I cannot change the method of inserting the primary key. This is being developed on a legacy system and I have no control over the method of retrieving the primary key, I just have to deal with it.
I have found that Laravel will not update the collection primary key when using the create method, with an observer that inserts the primary key value.
Here is my scenario (I have shrunk the models and files for space):
migration file:
Schema::create('forms_maps', function (Blueprint $table) {
$table->integer('id')->unsigned();
$table->string('name');
});
ModelObserver.php:
public function creating(Model $model)
{
$countername = strtolower(class_basename(get_class($model))).'s_id';
$model->id = tap(\App\Models\OCounter::where('countername',$countername)->first())->increment('counterval')->fresh()->counterval;
Log::debug("Creating ". strtolower(class_basename(get_class($model))) . ": " . $model);
}
DatabaseSeeder.php:
$accountApp = \App\Models\FormsMap::create(['name' => 'Account Application']);
Log::debug("Created formsmap: " . $accountApp);
The output log:
Creating formsmap: {"name":"Account Application","id":84}
Created formsmap: {"name":"Account Application","id":0}
As you can see from the log, when the record is created using the create method, inside of the observer, I am getting the proper id; however, that value is not being passed back to the collection in the DatabaseSeeder. Am I looking at this incorrectly? Should I be using something else to insert the values into the tables? I do not want to insert this value manually/inline because every model has to have this information injected.
Thanks!
GRRR!!!! I do this every time! The answer is:
The model needs to have the incrementing turned off.
class FormsMap extends Model
{
public $timestamps = false;
public $incrementing = false;
...
}
Heh!

Laravel delete by BelongsToMany relationship

I have a Task class. And each record of this table can have one or many childs and can be one or many parents of another task.
Table tasks
id | name
Table tasks_links
parent_id | child_id
Task model
<?php
namespace App;
class Task extends Model
{
public function childs()
{
return $this->belongsToMany(Task::class, 'tasks_links', 'parent_id','child_id');
}
public function parents()
{
return $this->belongsToMany(Task::class, 'tasks_links' , 'child_id' , 'parent_id');
}
}
And in my controller and views
I can use the relationship like
Task::find($id)->parents(); //Return an array
When I delete a task, I would also to delete the links with other related tasks
So the following is working :
\DB::table('tasks_links')->where('parent_id' , $task->id)->delete();
\DB::table('tasks_links')->where('child_id' , $task->id)->delete();
But I the following is not working
foreach ($task->parents() as $parent) {
$parent->delete();
}
Is there any way to delete the links by using the parents and childs functions instead of searching directly in the database ?
$task =Task::find($id);
$task->childs()->detach();
$task->parents()->detach();
$task->delete();
The fastest way of doing this.. if every time you delete a task you will want to delete all relations you should just include this in your task migrations
$table->foreign('the_foreign_key')->references('related_table_primary_key')->on('table_name_related')->onDelete('cascade');
Every time you delete a task on relationships will be deleted.
What I usually do on these situations is, when designing the database, I write a migration and set what columns I want to be affected by cascade.
Example
php artisan make:migration cascade_implementation
and on the Up() I set whatever I wish to be affected, on Down() I set the inverse, in case of a rollback needed. In your case, something like (or the other way around, depending on your needs):
public function up()
{
Schema::table('tasks', function (Blueprint $table) {
$table->foreign('parent_id')
->references('id')->on('tasks_links')
->onDelete('cascade');
$table->foreign('child_id')
->references('id')->on('tasks_links')
->onDelete('cascade');
});
}
Read more at https://laravel.com/docs/5.5/migrations

Dealing with Eloquent relationships in Laravel 5.3

I have the following tables created using Schemas:
Schema::create('typen', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->unique();
});
Schema::create('auktionen', function (Blueprint $table) {
$table->increments('id');
$table->integer('typ')->unsigned();
$table->foreign('typ')->references('id')->on('typen');
});
The table typen is only created once at contains fixed values:
Typen
id| name
1 | Typ 1
2 | Typ 2
...
Those values are constant, no more will be added during the application lifetime. So every Auktion I create should be associated with one Typ.
I created the model for Auktion and thought of it as a one-to-one relationship (is this correct?).
I would like to create an Auktion with a given Typ using Eloquent queries like this:
$thetyp = App\Typ::where("id", "=", 1)->first();
$auktion->typ()->associate($thetyp);
and fetch the name of the Typ of my Auktion:
$auktion->typ->name;
Currently my model looks like this for Auktion:
public function typ()
{
return $this->hasOne('App\Typ', 'typ');
}
which is not working. I already tried setting different relationships but I just end in different error codes ranging from undefined methods (associate() to an error where an SQL statement failed trying to update my typen table (when using save() - which I really do not want to).
Can someone clarify this problem for me and explain which relationship I have to use?
EDIT1: As mentioned in a comment I already tried using belongsTo in Auktion model
public function typ()
{
return $this->belongsTo('App\Typ', 'typ');
}
which results in Undefined property: App\Auktion::$typ when calling
$thetyp = App\Typ::where("id", "=", 1)->first();
$auktion->typ()->save($thetyp);
and when calling $auktion->typ->name; in
Trying to get property of non-object
EDIT2:
I just figured that
echo $auktion->typ()->first()->name;
is indeed working. But referring to this answer this should be the same as
echo $auktion->typ->name;
What exactly am i doing wrong?
EDIT3:
I tried using suggested code:
$thetyp = App\Typ::find($typ);
$auktion->typ->save($thetyp);
After I navigated to the view ehere I run the code I got this:
I got this the second time today, somwhow out of nowhere
Here is some code enhancement:
$thetyp = App\Typ::where("id", "=", 1)->first();
$auktion->typ()->save($thetyp);
To:
//Will return null if Model is not found
$thetyp = App\Typ::find(1);
//You actually have to retrieve the relationship, because calling typ()
//will only retrieve the Relationship query
$auktion->typ()->get()->save($thetyp);
The problem is that the relationship is defined backwards. You need to make Auction belongTo Type, and change Type to hasMany Auctions. The statement would read:
"A Type has many Auctions. An Auction has one Type".
Here are the classes (in English, sorry, my German is bad :( so I just did it in English) with the migrations:
-Auction class:
class Auction extends Model
{
protected $table = 'auction';
public function type()
{
return $this->belongsTo('App\Type');
}
}
-Auction migration:
Schema::create('auction', function(Blueprint $table){
$table->increments('id');
$table->integer('type_id')->references('id')->on('type')->nullable();
$table->string('title')->nullable();
$table->string('description')->nullable();
$table->timestamps();
});
-Type class:
class Type extends Model
{
protected $table = 'type';
public function auction()
{
return $this->hasMany('App\Auction');
}
}
-Type migration:
Schema::create('type', function(Blueprint $table){
$table->increments('id');
$table->string('name');
$table->timestamps();
});
First, you can create a Type object (or insert it with a query) so we can have a Type row that we can relate to an Auction object/entry, and do the following:
//Example of Type obj creation
$type = new Type();
$type->name = 'Type #1';
//Don't forget to save
$type->save();
//OR find/retrieve a Type obj
//Where $id is the id of the Type obj, else the obj will be null
$type = Type::find($id);
$auction = new Auction();
$auction->title = 'Title #1';
$auction->description = 'Test description';
$auction->type_id = $type->id;
//Don't forget to save
$auction->save();
Now later in your code, whenever you are using an Auction object and you want to retrieve the associated type (if any), you can use:
$type = $auction->type()->get();
Which will return the instance of Type, and you will be able to retrieve the property name like so:
$type->name
I hope this helps! Let me know if you have any more questions!
Your second edit makes a lot of sense. Your method name is typ but that's also the name of the column. So when you use $auktion->typ() it's actually loading the relationship. When you use $auktion->typ it's grabbing the value of that column.
You need to either continue working this way using the parenthesis to load the relation and no parenthesis to grab the value of the column, or you can change the name of the column to something better such as typ_id which is ultimately what Laravel expects it to be and should save you from more similar headaches down the road.
You can try this in your Auktion model
$thetyp = App\Typ::find(1);
$auktion->typ->save($thetyp);
Now fetch
$auktion->typ->name

Categories