Laravel: How should I update this relationship? - php

Suppose I have the following tables:
User:
-userID
-userName
...
Exercises:
-exerciseID
...
User model:
<?php
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableInterface;
class User extends Eloquent implements UserInterface, RemindableInterface {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'users';
protected $primaryKey = 'userID';
...
public function hasPassedExercises() {
return $this->hasMany('Exercise', 'exerciseID');
}
}
I want to say that a User has many completedExercises, so when the user completes an exercise, I update the model like so:
Route::post('dbm/userPassedExercise', function () {
$user = User::with('hasPassedExercises')->find($_POST['userID']);
$exercise = Exercise::find($_POST['exerciseID']);
$user->hasPassedExercises->save($exercise);
});
However, this has no effect on any underlying table, as far as I have understood. I'm trying to make sense of the documentation and see how it applies to my problem. So my question is what is the right course of action to do here.
Should I create a table users_completed_exercises that has userID and exerciseID as foreign keys, and if so, how do I link them to my user when I do the update? Or is there a more elegant solution?

Indeed, you have to use a relationship table (called pivot table).
In the laravel documentation, you have to name your pivot table with your tables name ordered by their name (you have not to, but it's prefered).
We'll take your naming convention so : users_completed_exercises
So here we shoud have this :
users:
- userId // Unsigned Int PRIMARY KEY AUTO_INCREMENT
Exercises:
- exerciseId // Unsigned Int PRIMARY KEY AUTO_INCREMENT
users_completed_exercises:
- id // Unsigned Int PRIMARY KEY AUTO_INCREMENT
- exerciseId // Unsigned Int FOREIGN KEY REFERECES EXERCICES ON ID
- userId // Unsigned Int FOREIGN KEY REFERECES USERS ON ID
On the user model, you should have :
public function passedExercises()
{
// Alphabetical order of your id's are here, very important because laravel
// retreives the good ID with your table name.
return $this->belongsToMany('Exercise', 'users_completed_exercises', 'exerciseId', 'userId');
}
And the inverse on Excercise Model
public function usersWhoPassed()
{
// Alphabetical order of your id's are here, very important because laravel
// retreives the good ID with your table name.
return $this->belongsToMany('User', 'users_completed_exercises', 'exerciseId', 'userId');
}
Retreiving infos are now, so easy.
Route::post('dbm/userPassedExercise', function () {
// Don't use $_POST with laravel, they are exceptions indeed, but avoid as much as
// possible.
$user = User::find(Input::get('userId'));
$exercise = Exercise::find(Input::get('exerciseId'));
// Very important, use () on relationships only if you want to continue the query
// Without () you will get an Exercises Collection. Use ->get() or ->first() to end
// the query and get the result(s)
$exercise->usersWhoPassed()->save($user);
});
You can easly check if user has passed an exercise too
Route::get('/exercises/{id}/passed_users', function($id)
{
$exercise = Exercise::find($id);
if ($exercise->usersWhoPassed()
->where('userId', '=', Input::get('userId'))->count()) {
return 'User has passed';
}
return 'User has failed';
});

Related

Laravel - Set data to null before deleting column from other table

I have 2 tables. (1) being users, and (2) being foods.
In the [users] table, there is a food_id bracket that is linked as a foreign key to an item/column's id in the other table [foods].
I am able to make a reservation of a [food] column, although I want to be able to press a 'confirm' button on the item once reserved, which will delete the item's targetted column in the Database.
Although since both tables are linked with a foreign key, I know that I need to set the parent key to null in order to be able to fully delete the target column. Otherwise it throws me an error that I can't delete/update a parent item with children objects.
(my food's object primary ID being linked to the authenticated user's food_id foreign key.)
This current code I tried only throws me the following error: "Call to a member function onDelete() on null"
$foodsId = User::find(auth()->user()->foods_id);
$foodsId->onDelete('set null');
$foodsId->save();
$foodDel = Foods::find($id);
$foodDel->delete();
Don't exactly know what to think here.
You could edit the foreign key constraint to do this automatically, but what you're trying to do can be done with these lines.
$food = Food::findOrFail(auth()->user()->food_id);
auth()->user()->fill(['food_id' => null])->save(); // use forceFill() if food_id is not in the User's fillable fields.
$food->delete();
To have this happen automatically, you could make a migration with the command
php artisan make:migration changeFoodIdForeignKeyConstraint --table=users
function up()
{
Schema::table('users', function (Blueprint $table) {
$table->dropForeign(['food_id']);
$table->foreign('food_id')
->references('id')->on('foods')
->onUpdate('cascade')
->onDelete('set null');
});
}
Another option is to use a model event on the Food model
class Food extends Model
{
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::deleting(function ($food) {
User::where('food_id', $food->id)->update(['food_id' => null]);
});
}
Actually have found my own solution. I went to directly target my first foreign key, to then asign a null parameter to it, THEN fetch the targetted ID of my item and delete its column.
$user = auth()->user();
$user->food_id = null;
$foodDel = Foods::find($id);
$foodDel->delete();

Laravel Eloquent belongsTo relationship is not working

I'm trying to create a relationship between two tables using Eloquent belongsTo but it doesn't seem to work.
the two tables are documents and departments , each document belongs to one department.
documents
id INT
department INT
departments
id INT
name varchar(255)
this is the function that defines the relationship
public function department(){
// department: foreign key
// id : departments table primary key
return $this->belongsTo('\App\Department' , 'department' , 'id');
}
and this is the accessor function
public function getDepartmentAttribute(){
return $this->department()->first()->name;
}
it returns the following error message: Undefined property: App\AjaxSearch::$department
In documents table add
department_id INT Foreign
In your documents migration
$table->integer('department')->unsigned();
Also edit the relationship
public function department() {
return $this->belongsTo('App\Department', 'department');
}
Update
Ok according to your updates, you can get the department name like this
$doc = Document::find(1);
$name = $doc->department->name;
You need to check whether the related record exists
public function department()
{
return $this->belongsTo('App\Department', 'department');
}
$document is your current document record.
$name = (empty($document->department->id) === false) ? ($document->department->name) : '';

Laravel updateExistingPivot with multiple primary keys

Problem
I want to update a row in a pivot table that have 2 primary keys. But updateExistingPivot want only a single primary key.
$user = App\User::find(1);
$user->roles()->updateExistingPivot($roleId, $attributes);
My DB-tables
Campaign
User
Campaign_user (primary keys are user_id and campaign_id)
My Question
Should I change my pivot table so it only have 1 primary key called id. Or can I keep it with 2 primary keys, and still update it, with Eloquent?
I think for best practice you should add a key id in your Campaign_user table, structure should:
Campaign_user
id|user_id|campaign_id
In User Model
public function campaign()
{
return $this->belongsToMany('Campaign', 'Campaign_user','user_id','campaign_id')->withPivot('extra attribute if any');
}
In Campaign Model
public function users()
{
return $this->belongsToMany('User', 'Campaign_user','campaign_id','user_id')->withPivot('extra attribute if any');
}
Now your code is:
$user = App\User::find($userId);
$user->campaign()->updateExistingPivot($campaignId, array('any attribute'=>$value));

One-To-Many relationship deletes foreign key in Doctrine

This is driving me crazy.
A Client can have many Vehicles.
This is a one to many relationship. When trying to save the entities I get an error saying that the foreign key is null. When I remove the Doctrine relation and store the Vehicle separately everything is working fine.
This is how I created the relation:
class Vehicle {
...
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="vehicles")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id")
*/
public $client;
}
class Client {
...
public function __construct()
{
parent::__construct();
$this->vehicles = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #ORM\OneToMany(targetEntity="Vehicle", mappedBy="client", cascade={"persist"})
*/
private $vehicles;
}
I try to save the entities like this:
$client = new Client();
$vehicle = new Vehicle();
$client->getVehicles()->add($vehicle);
$em->persist($client);
$em->flush();
Next I get a PDO exception saying that client_id can't be null on the Vehicle table.
It seems like Doctrine is not copying the foreign key correctly.
What am I doing wrong?
According to their docs:
It is not possible to use join columns pointing to non-primary keys.
Doctrine will think these are the primary keys and create lazy-loading proxies with the data, which can lead to unexpected results. Doctrine can for performance reasons not validate the correctness of this settings at runtime but only through the Validate Schema command.
Obviously you need a different approach.
A solution is given in their example:
CREATE TABLE product (
id INTEGER,
name VARCHAR,
PRIMARY KEY(id)
);
CREATE TABLE product_attributes (
product_id INTEGER,
attribute_name VARCHAR,
attribute_value VARCHAR,
PRIMARY KEY (product_id, attribute_name)
);
This schema should be mapped to a Product Entity as follows:
class Product
{
private $id;
private $name;
private $attributes = array();
}
Where the attribute_name column contains the key and attribute_value contains the value of each array element in $attributes.

Laravel belongsToMany relationship defining local key on both tables

So the belongsToMany relationship is a many-to-many relationship so a pivot table is required
Example we have a users table and a roles table and a user_roles pivot table.
The pivot table has two columns, user_id, foo_id... foo_id referring to the id in roles table.
So to do this we write the following in the user eloquent model:
return $this->belongsToMany('Role', 'user_roles', 'user_id', 'foo_id');
Now this looks for an id field in users table and joins it with the user_id field in the user_roles table.
Issue is I want to specify a different field, other than id to join on in the users table. For example I have bar_id in the users table that I want to use as the local key to join with user_id
From laravel's documentation, it is not clear on how to do this. In other relationships like hasMany and belongsTo we can specify local key and foriegn key but not in here for some reason.
I want the local key on the users table to be bar_id instead of just id.
How can I do this?
Update:
as of Laravel 5.5 onwards it is possible with generic relation method, as mentioned by #cyberfly below:
public function categories()
{
return $this->belongsToMany(
Category::class,
'service_categories',
'service_id',
'category_id',
'uuid', // new in 5.5
'uuid' // new in 5.5
);
}
for reference, previous method:
I assume id is the primary key on your User model, so there is no way to do this with Eloquent methods, because belongsToMany uses $model->getKey() to get that key.
So you need to create custom relation extending belongsToMany that will do what you need.
A quick guess you could try: (not tested, but won't work with eager loading for sure)
// User model
protected function setPrimaryKey($key)
{
$this->primaryKey = $key;
}
public function roles()
{
$this->setPrimaryKey('desiredUserColumn');
$relation = $this->belongsToMany('Role', 'user_roles', 'user_id', 'foo_id');
$this->setPrimaryKey('id');
return $relation;
}
On Laravel 5.5 and above,
public function categories()
{
return $this->belongsToMany(Category::class,'service_categories','service_id','category_id', 'uuid', 'uuid');
}
From the source code:
public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null,
$parentKey = null, $relatedKey = null, $relation = null)
{}
This is a recently added feature. I had to upgrade to 4.1 because I was also looking for this.
From the API documentation:
public BelongsToMany belongsToMany(string $related, string $table = null, string $foreignKey = null, string $otherKey = null, string $relation = null)
The $otherKey and $relation parameters were added in 4.1. Using the $foreignKey and $otherKey parameters allows you to specify the keys on both sides of the relation.
The best way is set the primary key.
class Table extends Eloquent {
protected $table = 'table_name';
protected $primaryKey = 'local_key';
belongsToMany allows to define the name of the fields that are going to store che keys in the pivot table but the method insert always the primary key values into these fields.
You have to:
define in the method belongsToMany the table and the columns;
then using protected $primaryKey = 'local_key'; you can choose which value store.
I recently went through the same problem where I needed to have an associated table that used ID's to link two tables together that were not Primary Keys. Basically what I did was create a copy of my model that models the pivot table and set the Primary Key to the value that I wanted it to use. I tried creating a model instance, settings the primary key and then passing that to the relation but Laravel was not respecting the primary key I had set ( using the ->setPrimaryKey() method above ).
Making a copy of the model and setting the primary key feels a little bit 'hackish' but in the end it works as it should and since Pivot table models are generally very small I don't see it causing any problems in the future.
Would love to see a third key option available in the next release of Laravel that lets you get more specific with your linking.

Categories