Is there a way to mass assign relations in laravel 5? - php

I have two Model classes with a oneToMany relation like this
App\Car
class Car extends Model
{
public $timestamps = true;
protected $fillable = [
'name',
'price'
];
public function parts ()
{
return $this->hasMany('App\Part');
}
}
App\Part
class Part extends Model
{
public $timestamps = false;
protected $fillable = [
'name',
'price'
];
public function car ()
{
return $this->belongsTo('App\Car');
}
}
The client makes a POST request with a JSON representing a Car with a nested Array of Parts
{
"name": "Fiat Punto",
"price": 15000,
"parts": [
{
"name": "wheel",
"price": 300
},
{
"name": "engine",
"price": 5000
}
]
}
Is there a way to save the Car model and create the relations in just one hit?
I tried to do this in my controller but it doesn't work:
...
public function store (Request $request) {
$input = $request->all();
Car::create($input);
}
...
PS: I already know how to do the task with a foreach or array_reduce, just wondering if laravel could do that for me
-- Edit --
Here is how i implemented the controller right now:
...
public function store (Request $request) {
$input = $request->all();
$car = Car::create($input);
$parts = array_reduce(
$input['parts'],
function ($carry, $item) {
array_push($carry, new Part($item));
return $carry;
},
[]
);
$car->parts()->saveMany($parts);
}
...
Any improvement is welcome

I donĀ“t think there is a way of getting around the fact that you need to create each of the App\Part model instances that are defined in the request object. So, at some point you have to iterate over those items in the request object, meaning you have (at least) two options. Those are both described here.
On a side note I think in this case it is better to use a foreach loop for the first option:
foreach ($request->input('parts') as $item) {
$car->parts()->save(new Part($item));
}
If you'd go for the second option of storing them in an array first, then I think array_map is the more appropriate method to use (because there is no actual "reducing" going on):
$parts = array_map(function($part) {
return new App\Part($part);
}, $request->input('parts'));
$car->parts()-saveMany($parts);
Edit: As per suggestion from #TomasKim, with only 2 queries - one for the Car-model and another for the Part-models:
$parts = array_map(function($part) {
return [...$part, 'car_id' => $car->id];
}, $input['parts'])
DB::table('parts')->insert($parts);

While there isn't a way to technically do this entirely automatically, you can make the eloquent api for your model act this way by just tweaking the fillable property and adding a method:
class Car extends Model
{
public $timestamps = true;
protected $fillable = [
'name',
'price',
'parts',
];
public function parts ()
{
return $this->hasMany('App\Part');
}
public function setPartsAttribute($parts)
{
foreach ($parts as $part) {
Part::updateOrCreate(
['id' => array_get($parts, 'id')],
array_merge($part, ['car_id' => $this->id])
);
}
// ... delete parts not in the list if you want
}
}
Notice we just added parts to the fillable property, and then used Eloquent's attribute setting hook set[Field]Attribute() to perform the assignment. This makes it possible to have the Car api look about right:
Car::find(1)->update($carWithParts);
Note: This won't work when creating the Car model since the there is no id. If you want to automate that too, you can queue these updates and then use the model's event hooks to perform this action.

Related

Laravel access to column object

I have a table, table name is bookings and here have a column e_provider. this column i direct fetch data by id and save all data in e_provider field
how can i access this e_provider data like $data->e_provider->name
here is code
[
{
"id": 2,
"e_provider": "{"id":11,"name":"Architect O'Reilly, Ratke and Miller","phone_number":"661.425.3559","mobile_number":"307.607.7472"}",
}
]
in laravel 8 and less try this
Defining A Accessors & Mutators
public function getEProviderAttribute(){
return json_decode($this->getAttributeValue('e_provider'));
}
public function setEProviderAttribute($value){
return json_encode($value);
}
For update value
$e_provider = $object->e_provider;
$e_provider->name = "new name";
$object->e_provider = $e_provider;
$object->save();
https://laravel.com/docs/8.x/eloquent-mutators#accessors-and-mutators
You can use $casts in your model
class BookingModel extends Model
{
protected $casts = [
'e_provider' => AsCollection::class,
];
}
Now you will be able to get the data by the following:
$bookingModel->e_provider->id
https://laravel.com/docs/9.x/eloquent-mutators#array-object-and-collection-casting
Another way if you don't want to use casts, you can define an accessor:
protected function eProvider(): Attribute
{
return Attribute::make(
get: fn ($value) => json_decode($value),
set: fn ($value) => json_encode($value),
);
}
https://laravel.com/docs/9.x/eloquent-mutators#defining-an-accessor

Laravel PUT relationship

Fiddling with Laravel and coming from Symfony, I'm trying to replicate some code.
I'm trying to PUT a Suggestion model (overwritting anything, even relationships) and wanted to know the proper way to overwrite the model.
Since tags attribute in fillable doesn't exist, I certainly get an error (Undefined column: 7 ERROR: column "tags" of relation "suggestions" does not exist).
Suggestions and tags both have their own tables and a pivot table that contains two foreign keys to both tables id.
Request & Response :
{
"id":2,
"content":"Magni.",
"tags":[{"id":13,"name":"MediumAquaMarine"}]
}
{
"id":2,
"content":"Magni.",
"tags":[{"id":10,"name":"Navy"},{"id":13,"name":"MediumAquaMarine"}]
}
public function update(Request $request, Suggestion $suggestion)
{
$validator = Validator::make($request->all(), [
'content' => 'required',
'tags.id' => 'numeric',
]);
if ($validator->fails()) {
return response()->json($validator->messages(), Response::HTTP_BAD_REQUEST);
}
$suggestion->fill($request->only($suggestion->getFillable()))->save();
return new SuggestionResource($suggestion);
}
class Suggestion extends Model
{
use HasFactory;
protected $fillable = ['content', 'tags'];
protected $with = ['tags'];
public function tags()
{
return $this->belongsToMany(Tag::class, 'suggestions_tags')->withTimestamps();
}
}
class Tag extends Model
{
use HasFactory;
protected $hidden = ['pivot'];
public function suggestions()
{
return $this->belongsToMany(Suggestion::class, 'suggestions_tags')->withTimestamps();
}
}
You could just pass an array of IDs for tags instead of the whole object.
Do:
"tags":[10, 13]
Instead of:
"tags":[{"id":10,"name":"Navy"},{"id":13,"name":"MediumAquaMarine"}]
Change the validation rules accordingly and then you can remove tags from $fillable and do something like:
$suggestion->update($request->validated());
$suggestion->tags()->sync($request->tags);

Laravel query builder get custom attribute

I'm trying to get a custom attribute (https://laravel.com/docs/5.5/eloquent-mutators#defining-an-accessor)
from a query.
Right now I have:
User.php
public function getViewUrlAttribute()
{
return route('admin.users.view', ['id' => $this->id]);
}
public function role()
{
return $this->belongsTo('App\Role')->withDefault([
'name' => 'User'
]);
}
UserController.php
public function dataTable(Request $request)
{
$length = $request->has('length') ? $request->input('length') : 10;
$orderDirection = $request->input('orderDirection');
$searchValue = $request->input('search');
$users = User::select('id', 'name', 'email', 'created_at')->with('role:name')->limit($length);
if ($request->has('orderBy')) {
if ($request->has('orderDirection')) {
$users = $users->orderBy($request->input('orderBy'), $request->input('orderDirection') > 0 ? 'asc' : 'desc');
} else {
$users = $users->orderBy($request->input('orderBy'), 'desc');
}
}
return $users->get();
}
Returns
[
{
"id": 1,
"name": "User",
"email": "user#test.com",
"created_at": "2018-04-24 14:14:12",
"role": {
"name": "User"
}
}
]
So the thing is: there's any way to also get the view_url attribute? (I tried inside the with() but it fails)
Also can I return only the role name and not the whole object as you can see in the "Return" code? (I would like something like: "role": "User").
(Of course I'm trying to avoid running raw sql)
Thanks!
You're almost done...
1- To add a custom attribute you need to append it on the Model with $appends attribute:
protected $appends = ['view_url'];
And define your attribute method:
public function getViewUrlAttribute()
{
return route('admin.users.view', ['id' => $this->id]);
}
2- To add attribute to a model from another related model, I think you should try :
// to add them as attribute automatically
protected $appends = ['view_url', 'role_name'];
// to hide attributes or relations from json/array
protected $hidden = ['role']; // hide the role relation
public function getRoleNameAttribute()
{
// if relation is not loaded yet, load it first in case you don't use eager loading
if ( ! array_key_exists('role', $this->relations))
$this->load('role');
$role = $this->getRelation('role');
// then return the name directly
return $role->name;
}
Then you might not event need ->with('role') eager loading.

Laravel save many-to-many relationship in Eloquent mutators

I've got 2 models with a many-to-many relationship. I want to be able to set a specific attribute with an array of ids and make the relationship in the mutator like this:
<?php
class Profile extends Eloquent {
protected $fillable = [ 'name', 'photo', 'tags' ];
protected $appends = [ 'tags' ];
public function getTagsAttribute()
{
$tag_ids = [];
$tags = $this->tags()->get([ 'tag_id' ]);
foreach ($tags as $tag) {
$tag_ids[] = $tag->tag_id;
}
return $tag_ids;
}
public function setTagsAttribute($tag_ids)
{
foreach ($tag_ids as $tag_id) {
$this->tags()->attach($tag_id);
}
}
public function tags()
{
return $this->belongsToMany('Tag');
}
}
<?php
class Tag extends Eloquent {
protected $fillable = [ 'title' ];
protected $appends = [ 'profiles' ];
public function getProfilesAttribute()
{
$profile_ids = [];
$profiles = $this->profiles()->get([ 'profile_id' ]);
foreach ($profiles as $profile) {
$profile_ids[] = $profile->profile_id;
}
return $profile_ids;
}
public function profiles()
{
return $this->belongsToMany('Profile');
}
}
However the setTagsAttribute function isn't working as expected. I'm getting the following error: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'profile_id' cannot be null (SQL: insert intoprofile_tag(profile_id,tag_id) values (?, ?)) (Bindings: array ( 0 => NULL, 1 => 1, ))
You can't attach many-to-many relations until you've saved the model. Call save() on the model before setting $model->tags and you should be OK. The reason for this is that the model needs to have an ID that Laravel can put in the pivot table, which needs the ID of both models.
It looks like you're calling the function incorrectly or from an uninitialized model. The error says that profile_id is NULL. So if you're calling the function as $profile->setTagsAttribute() you need to make sure that $profile is initialized in the database with an ID.
$profile = new Profile;
//will fail because $profile->id is NULL
//INSERT: profile->save() or Profile::Create();
$profile->setTagsAttribute(array(1,2,3));
Additionally, you can pass an array to the attach function to attach multiple models at once, like so:
$this->tags()->attach($tag_ids);
You can also pass it the model instead of the ID (but pretty sure array of models won't work)
Try using the sync method:
class Profile extends Eloquent {
protected $fillable = [ 'name', 'photo', 'tags' ];
protected $appends = [ 'tags' ];
public function getTagsAttribute()
{
return $this->tags()->lists('tag_id');
}
public function setTagsAttribute($tag_ids)
{
$this->tags()->sync($tagIds, false);
// false tells sync not to remove tags whose id's you don't pass.
// remove it all together if that is desired.
}
public function tags()
{
return $this->belongsToMany('Tag');
}
}
Don't access the tags through the tags() function, rather use the tags property. Use the function name if you want to pop additional parameters onto the relationship query and the property if you just want to grab the tags. tags() works in your getter because you're using get() on the end.
public function setTagsAttribute($tagIds)
{
foreach ($tagIds as $tagId)
{
$this->tags->attach($tagId);
}
}

Laravel 4 Eloquent get ids of relations

I'm building a client application that needs to have the ids of related models in my server API response.
In my example I've got two models, a Post and a Tag model. The relationship between them is many-to-many, so a pivot table is required.
class Post extends Eloquent {
protected $fillable = [ 'title', 'body' ];
public function tags()
{
return $this->hasMany('Tag');
}
}
class Tag extends Eloquent {
protected $fillable = [ 'title' ];
public function posts()
{
return $this->belongsToMany('Post');
}
}
I've got a resourcefull controller set up on the /api/posts route like this:
class PostsController extends \BaseController {
public function index()
{
$posts = Post::all();
return Response::json([ 'posts' => $posts->toArray() ]);
}
}
This will return a response much like this one:
{
"posts": [
{
"title": "Laravel is awesome",
"body": "Lorem Ipsum..."
},
{
"title": "Did I mention how awesome Laravel is?",
"body": "Lorem Ipsum..."
}
]
}
What I'm looking for is an easy way to include the ids of the related Tags model in the response like this:
{
"posts": [
{
"title": "Laravel is awesome",
"body": "Lorem Ipsum...",
"tags": [ 1, 2, 3 ]
},
{
"title": "Did I mention how awesome Laravel is?",
"body": "Lorem Ipsum...",
"tags": [ 1, 2, 4 ]
}
]
}
This isn't the most elegant solution, but it may work like you want (code not tested)
public function index()
{
$posts = Post::all();
$postsArray = array();
foreach ($posts as $post)
{
$postArray = $post->toArray();
$postArray['tags'] = array_values($post->tags->lists('id'));
$postsArray[] = $postArray;
}
return Response::json([ 'posts' => $postsArray]);
}
Add the following code to your Model/BaseModel:
/**
* Set additional attributes as hidden on the current Model
*
* #return instanceof Model
*/
public function addHidden($attribute)
{
$hidden = $this->getHidden();
array_push($hidden, $attribute);
$this->setHidden($hidden);
// Make method chainable
return $this;
}
/**
* Convert appended collections into a list of attributes
*
* #param object $data Model OR Collection
* #param string|array $levels Levels to iterate over
* #param string $attribute The attribute we want to get listified
* #param boolean $hideOrigin Hide the original relationship data from the result set
* #return Model
*/
public function listAttributes($data, $levels, $attribute = 'id', $hideOrigin = true)
{
// Set some defaults on first call of this function (because this function is recursive)
if (! is_array($levels))
$levels = explode('.', $levels);
if ($data instanceof Illuminate\Database\Eloquent\Collection) // Collection of Model objects
{
// We are dealing with an array here, so iterate over its contents and use recursion to look deeper:
foreach ($data as $row)
{
$this->listAttributes($row, $levels, $attribute, $hideOrigin);
}
}
else
{
// Fetch the name of the current level we are looking at
$curLevel = array_shift($levels);
if (is_object($data->{$curLevel}))
{
if (! empty($levels))
{
// We are traversing the right section, but are not at the level of the list yet... Let's use recursion to look deeper:
$this->listAttributes($data->{$curLevel}, $levels, $attribute, $hideOrigin);
}
else
{
// Hide the appended collection itself from the result set, if the user didn't request it
if ($hideOrigin)
$data->addHidden($curLevel);
// Convert Collection to Eloquent lists()
if (is_array($attribute)) // Use specific attributes as key and value
$data->{$curLevel . '_' . $attribute[0]} = $data->{$curLevel}->lists($attribute[0], $attribute[1]);
else // Use specific attribute as value (= numeric keys)
$data->{$curLevel . '_' . $attribute} = $data->{$curLevel}->lists($attribute);
}
}
}
return $data;
}
You can use it on your Model/Collection Object like this:
// Fetch posts data
$data = Post::with('tags')->get(); // or use ->first()
// Convert relationship data to list of id's
$data->listAttributes($data, 'tags');
$data will now contain the following object store:
{
"posts": [
{
"title": "Laravel is awesome",
"body": "Lorem Ipsum...",
"tags_id": [ 1, 2, 3 ]
},
{
"title": "Did I mention how awesome Laravel is?",
"body": "Lorem Ipsum...",
"tags_id": [ 1, 2, 4 ]
}
]
}
It also supports nested relationships:
// Fetch posts data
$data = Post::with('comments', 'comments.tags')->get(); // or use ->first()
// Convert relationship data to list of id's
$data->listAttributes($data, 'comments.tags');
Eloquent relationship returns a collection object which you can filter or even modify, say if you need only an array of the id's you can do this:
$posts->pluck('id');
$posts->pluck('id')->toArray();
I think you need to use the $appends property. Take a look at the docs here http://laravel.com/docs/eloquent#converting-to-arrays-or-json.
This question also is relevant as he encountered the same issue as you (see the edit on the accepted answer).
Add a custom attribute to a Laravel / Eloquent model on load?

Categories