Save HasMany and asscoiate BelongsTo in Eloquent - laravel

There is following table structure:
Users
HasMany Pages
Pages
BelongsTo User
HasMany Notes
Notes
BelongsTo User
To create a note I do:
$note = new Note([
'page_id': $this->page->id,
'user_id': auth()->id(),
'title': 'Foobar'
]);
$note->save();
Easy. Because of mass assignment vulnerability I want to avoid having foreign keys inside fillable of the note model. The code changes to:
$note = $this->page->notes()->create([
'title': 'Foobar'
]);
$note->user()->associate(auth()->user());
$note->save();
Fuzzy. For this to work I have to make the user_id of note nullable in the migration. This feels wrong because a note should never exist without a user. How would you solve this problem?

You can do this instead:
$note = new Note([
'title' => 'your_title_here' //works if title is in the fillable attributes
]);
$note->page()->associate($this->page);
$note->user()->associate(auth()->user());
$note->save();
Here, you don't need any nullable column and there are no risks about mass assignment.
Another advantage, there is only one database query instead of two when you are doing create first then another save.

Related

firstOrNew with "or" statement

I have a table with columns like "name", "surname", "user_id", and I need to check if entry exists first by id, and then by name and surname together, if there is none, create it. How do I do it neatly, instead of making two update statements, and if both return 0 just create a new one (which seems too bulky)
I thought of using firstOrNew, but it seems that it only can work while matching all of the parameters.
Is there any method I've missed that would apply well to my situation?
You could try something like this (assuming you want to create a model [saved to the database]):
$attributes = [
'name' => ...,
'surname' => ...,
];
$model = Model::where('id', $id)
->orWhere(fn ($q) => $q->where($attributes))
->firstOr(fn () => Model::create($attributes));
This would search for a record by id OR name and surname. If it doesn't find one it will create a new record with the name and surname (assuming those attributes are fillable on the Model).
Laravel 8.x Docs - Eloquent - Retrieving Single Models / Aggregates firstOr

Why last id is lost while creating process?

I have a create method that works fine, but it fails trying to create its relation:
public function create(array $article, string $note)
{
$art = $this->article->create($article);
print($art->id) // this line is for helping me to know if id is successfully generated. It works!
$this->article->note()->create([
'article_id' => $art->id, // the id desapears
'note' => $note
])
}
Console logs:
Integrity constraint violation: 1048 Column 'article_id' cannot be null (SQL: insert into notes (article_id, note, updated_at, created_at) values (?, TESTING, 2019-11-13 13:03:07, 2019-11-13 13:03:07))"
The field article_id is probably not a fillable attribute on the Note model.
You could also create the note like this, without adding article_id as a fillable attribute:
$art->note()->create([
'note' => $note
]);
Also make sure that the relations are defined correctly on the models. If an article can have many notes, there might be something wrong with your definitions, since note is currently singular. If it is a one to one relation everything might be fine.
You need to save your article. $art->save()

Column Shows 'id' Number Instead of Foreign Key Attribute

When using the select2 field/column type in Laravel Backpack, the list view displays the 'id' of the foreign entity instead of the foreign key required (in this case the 'name' of the Session).
Laravel 5.8.4, Backpack 3.4. I asked in GitHub and the response was that my relationships were incorrect in my models. I don't think that's the problem as the name loads in the edit view.
GradeCrudController
$this->crud->addColumn([
'label' => "Session",
'type' => 'select2',
'name' => 'session_id', // the db column for the foreign key
'entity' => 'session', // the method that defines the relationship in your Model
'attribute' => 'name', // foreign key attribute that is shown to user
'model' => "App\Models\Session" // foreign key model
]);
Grade (Model)
public function session()
{
return $this->belongsTo('App\Models\Session');
}
Session (Model)
public function grades()
{
return $this->hasMany('App\Models\Grade');
}
As it's been a few days and nobody has responded, I thought I'd post the answer I came up with. Note that I highly doubt that this is the correct solution, but for my project it will do.
I added a Laravel Observer for the Grade Model. Once a user adds a new record, the observer visits the session table, pulls the name of the session using the key and adds it as a column to the Grades table.
Then in backpack I just display the 'name' column.
There has to be a better way than this... But for now it will do.
I see you're using a "select2" column type. That's not something Backpack provides by default - it only has a "select" column.
Most likely what happened is that Backpack loaded the "text" column, since it couldn't find a "select2" column. Hence, the ID.
Try changing "select2" to "select". It should work for you without any observers/anything else.
I was having a similar issue. I could not get the foreign key attribute to show up no matter what. I finally got it working by doing to following.
Add the foreign key to the belongsTo method. It should be the name of the column in that model that has the ID that is associated with in the belongsTo model.
public function session()
{
return $this->belongsTo('App\Models\Session','name');
}
One other item that I suggest is to make sure all columns that have foreign keys are set to the same data types in the database.

User::all show relationship items

I Laravel 5.5 I am returning users information like this...
$users = User::all();
return Response::json(array(
'error' => false,
'response' => $users,
));
I have a belongs to many categories relationship setup and would like to also show all of the categories each user belongs to.
Anyone have an example I can see?
Use the with() method to load categories for each user:
$users = User::with('categories')->get();
If you don't need to load all the columns from the categories table, use select() inside the with() closure. Also, since you're using Laravel 5.5 you could use Resource classes for formatting JSON.

Laravel 5: How to use firstOrNew with additional relationship fields

I have a CMS that allows the user to save and create bike tours. Each bike tour also has categories, which are definined using Laravel's Many to Many relationship utilising an intermediary pivot table. At the point of saving a tour, we don't know if the tour is an existing one being edited, or a new one.
I think I should be using Laravel's firstOrNew method for saving the tour, and the sync method for saving categories. However, all the tutorials very simplistically just give the example of passing a single object to the function like so:
$tour = Tour::firstOrNew($attributes);
But what happens when my $attributes also contains extra stuff, like the categories which are linked to a relationship table, and which I will need to save in the next step? For example this very good tutorial gives the following example:
$categories = [7, 12, 52, 77];
$tour = Tour::find(2);
$tour->categories()->sync($categories);
But what happens if the category data is bundled with the data for the rest of the tour, and instead of using find I need to use firstOrNew to create the tour? Should I keep the categories in the $attributes while I instantiate the tour, then run the sync, then unset them before saving the tour, or...? Is there a better way to achieve this?
EDIT: To be clear, the $attributes variable in my example here is essentially the tour object data bundled together- just as the Laravel/Eloquent system would return it from the transaction using the belongsToMany method- with subequent modifications from the user). ie: here is a snapshot of what it contains:
array (
'id' => 1,
'uid' => '03ecc797-f47e-493a-a85d-b5c3eb4b9247',
'active' => 1,
'code' => '2-0',
'title' => 'Tour Title',
'url_title' => 'tour_title',
'distance_from' => 20,
'distance_to' => 45,
'price_from' => '135.00',
'price_to' => '425.00',
'created_at' => '2013-12-31 15:23:19',
'updated_at' => '2015-07-24 16:02:50',
'cats' => // This is not a column name!
array (
0 => 1,
1 => 7
),
)
All of these attributes are column names in my tours table, other than cats, which references another table via a hasMany relationship. Do I need to unset it manually before I can set this object class and save it with $tour->save?
I am looking for the cleanest most Laravel way to do it?
EDIT2: Here is the relationship defined in the Tours model:
class Tour extends Model
{
protected $guarded = [];
public function cats(){
return $this->belongsToMany('App\TourCategory', 'tour_cat_assignments', 'tour_id', 'cat_id');
}
}
you need to define $fillable property of your Tour model to tell eloquent which attributes to consider when using mass assignment so it will ignore categories related attributes silently. for ex.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tour extends Model {
protected $fillable = ['name'] //... other attributes which are part of this model only and laravel will consider only these attributes and ignore category related attributes which you can consider later use.
}
You can use firstOrCreate. The data actually gets persisted using this method.
$categories = [7, 12, 52, 77];
$tour = Tour::firstOrCreate($attributes)->cats()->sync($categories);
Got to make sure the fields are mass-assignable to be able to use the firstOrCreate method though. So either set the fieldnames in the $fillable property or put this in the Tour model:
protected $guarded = [];
Since you have mentioned "CMS" and "subsequent modifications from user", I guess that you are getting your attributes from a Form which means you are getting a Request object/collection.
If that is the case then you can try
$tour = Tour::firstOrCreate($request->except('cats'));
$categories = [];
foreach($request->get('cats') as $key=>$value){
$categories[] = $value;
}
$tour->cats()->sync($categories);
However, if your $attributes us constructed as an array (probably with some manipulations on form data) as per your EDIT then in that case you may try:
$tour = Tour::firstOrCreate(array_except($attributes, ['cats']);
$categories = [];
foreach($attributes['cats'] as $key=>$value){
$categories[] = $value;
}
$tour->cats()->sync($categories);
In any case, you must have the mass assignable fields declared in $fillable property in your model i.e. Tour.
Hope this helps.

Resources