As there is firstOrCreate()
in Laravel Eloquent, I was wondering if there was a function that could either create a record or if it exists, update the current one?
Or would I have to write my own one?
I wasn't able to find anything in the documentation, but it's not the first time, I've found stuff about Eloquent elsewhere than the docs.
You nearly named it. :)
$instance = Model::updateOrCreate(['id' => $id], $newAttributes);
If $id is null then a new instance will be created and saved, else it will be updated.
You need to find model before updating it, right? You cannot just call Model::firstOrUpdate($newAttributes) simply because there is no model in database with such (new) attributes.
I. e. you must know some model's unique attribute, for example, an id. After this, you can fetch it and call update method with new attributes: Model::firstOrNew(['id' => $id])->update($newAttributes). $id here can be null, in this case new model will be instantiated (but not saved).
As you can see, this code is pretty short, but of course, you might put it into method if you wish.
More straight forward and DRY would it be to add the following method to your BaseModel:
public function write($input, $key = 'id')
{
// Instantiate new OR existing object
if (! empty($input[$key]))
$resource = $this->findOrFail($input[$key]);
else
$resource = $this; // Use a clone to prevent overwriting the same object in case of recursion
// Fill object with user input using Mass Assignment
$resource->fill($input);
// Save data to db
if (! $resource->save())
App::abort(500, 'Could not save resource');
return $resource;
}
Related
I know that data can be inserted into the database in
Method 1:
public function store(Request $request)
{
// Validate the request...
$flight = new Flight;
$flight->name = $request->name;
$flight->save();
}
Method 2:
$flight = Flight::create([
'name' => 'London to Paris',
]);
What is the best way to use when inserting 1 value? What is the best way to use when you need to insert, say, 10 values? And are there any other better ways to insert values?
Technically speaking, there isn't much difference, but the main thing about create is that it has mass assignment related issues.
What it means is that, for example if you have a model User which has certain fields including a role field which can be user or admin.
Then if you create a new record using create method like this:
$user = User::create($request->all());
then the member can pass the parameter role in request inputs and change his/her own role and get admin privileges, simple as that! You can prevent this with the property $fillable inside your models, but if it is not taken care of properly it will lead to these kind of issues.
One another point is that, the create method uses the save() itself, if you look at its implementation you can see this:
public function create(array $attributes = [])
{
return tap($this->newModelInstance($attributes), function ($instance) {
$instance->save();
});
}
Just wondering if it is possible that some kind of findOrNew for relationships exist in Eloquent (in case if relationship do not exist attach new model instance)?
What that mean:
Lets say that we have devices and specifications tables. Device belongs to specification. Specification_id is an FK (Know that is not best approach, but I have something like this left by previous programmer). Under id 11 we have device that do not have specification but we have to display that for user anyway.
$device = Device::find(11);
echo $device->specification->cpu;
In this case it will throw an error because specification will be null - it do not exist for device with id 11.
Know that I could check first if it exist but there a a lot of similar lines and app is pretty big. I need to move it from Kohana to Laravel. It works in Kohana because empty object is loaded then and 2nd line just return null. For Laravel I can just check if relationship exist and load new model then but I am curios if maybe there is any other and better way?
I would go for creating extra method in Device model this way:
public function getSpecification()
{
if ($device->specification) {
return $device->specification;
}
return Specification::find(20); // some default specification
// or
// return new Specification(['cpu' => 'Not provided']);
}
And now you could use it this way:
$device = Device::find(11);
$device->getSpecification()->cpu;
Of course it depends how would you need to use it. If you have many properties, you should run this method just once for object to not run multiple queries and in case you would use it for big collections you should also rethink improvements to lower database queries.
This doesn't quite create the related object as you requested, but for the purposes of outputting the data or replicating Kohana's null output in the absence of a related model, I tend to use the data_get() or object_get() helpers for this purpose.
$device = Device::find(11);
echo object_get($device->specification, 'cpu');
// You could probably do this too (untested)
echo object_get($device, 'specification.cpu');
Having had a bit of a look, you can override the getRelationshipFromMethod() method in Illuminate\Database\Eloquent\Model
protected function getRelationshipFromMethod($method)
{
// Different relationships return different types of data so
// tweak this as necessary. In theory you only care if the relationship
// type is a single entity rather than a collection.
$results = parent::getRelationshipFromMethod($method);
if ($results instanceOf Illuminate\Database\Eloquent\Collection) {
return $results;
}
// Generate a null value for any missing attributes
// PHP7 anonymous class. Return a real class < 7.0
return $this->relations[$method] = new class {
public function __get($attribute) {
return null;
}
};
// Or perhaps actually create a relationship with a specification
$this->relations[$method] = Specification::where('default', true)->first();
$this->specification()->associate($this->relations[$method]);
return $this->relations[$method];
}
We are trying to detect the changes in Laravel related models at attribute level, as we have to keep audit trail of all the changes which are made via the application.
We can track the changes via isDirty method on the Eloquent model for single model that is not related to any other model, but there is no way that we can track the changes on the related eloquent models. isDirty doesn't work on related models attributes. Can some one please help us on this?
Update to original question:
Actually we are trying to track changes on the pivot table that has extra attributes as well defined on it. IsDirty method doesn't work on those extra attributes which are defined in the pivot table.
Thanks
As much I understand your question, It's can achieve through Model Event and some sort of extra code with current and relation model.
Laravel Model Events
If you dont want to use any additional stuff, you can just use the Laravel Model Events (that in fact Ardent is wrapping in the hooks). Look into the docs http://laravel.com/docs/5.1/eloquent#events
Eloquent models fire several events, allowing you to hook into various
points in the model's lifecycle using the following methods: creating,
created, updating, updated, saving, saved, deleting, deleted,
restoring, restored.
Whenever a new item is saved for the first time, the creating and
created events will fire. If an item is not new and the save method is
called, the updating / updated events will fire. In both cases, the
saving / saved events will fire.
If false is returned from the creating, updating, saving, or deleting
events, the action will be cancelled:
Finally, reffering to your question you can utilize the above approaches in numerous ways but most obviously you can combine it (or not) with the Eloquent Models' getDirty() api docs here method and getRelation() api docs here method
It will work for example with the saving event.
Model::saving(function($model){
foreach($model->getDirty() as $attribute => $value){
$original= $model->getOriginal($attribute);
echo "Changed";
}
$relations = $model->getRelations();
foreach($relations as $relation){
$relation_model = getRelation($relation);
foreach($relation_model->getDirty() as $attribute => $value){
$original= $relation_model->getOriginal($attribute);
echo "Relation Changed";
}
}
return true; //if false the model wont save!
});
Another Thought might help you. when you saving
save() will check if something in the model has changed. If it hasn't it won't run a db query.
Here's the relevant part of code in Illuminate\Database\Eloquent\Model#performUpdate:
protected function performUpdate(Builder $query, array $options = [])
{
$dirty = $this->getDirty();
if (count($dirty) > 0)
{
// runs update query
}
return true;
}
The getDirty() method simply compares the current attributes with a copy saved in original when the model is created. This is done in the syncOriginal() method:
public function __construct(array $attributes = array())
{
$this->bootIfNotBooted();
$this->syncOriginal();
$this->fill($attributes);
}
public function syncOriginal()
{
$this->original = $this->attributes;
return $this;
}
check model is dirty isDirty():
if($user->isDirty()){
// changes have been made
}
Or check certain attribute:
if($user->isDirty('price')){
// price has changed
}
I did not check this code but hopeful to use as your answer by thoughts, if you have any confusion to deal such requirement or something need to optimize or change please let me know.
Please be gentle with me - I'm a Laravel noob.
So currently, I loop through a load of users deciding whether I need to update a related model (UserLocation).
I've got as far as creating a UserLocation if it needs creating, and after a bit of fumbling, I've come up with the following;
$coords = $json->features[0]->geometry->coordinates;
$location = new UserLocation(['lat'=>$coords[1],'lng'=>$coords[0]]);
$user->location()->save($location);
My issue is that one the second time around, the Location may want updating and a row will already exist for that user.
Is this handled automatically, or do I need to do something different?
The code reads like it's creating a new row, so wouldn't handle the case of needing to update it?
Update - solution:
Thanks to Matthew, I've come up with the following solution;
$location = UserLocation::firstOrNew(['user_id'=>$user->id]);
$location->user_id = $user->id;
$location->lat = $coords[1];
$location->lng = $coords[0];
$location->save();
You should reference the Laravel API Docs. I don't think they mention these methods in the "regular docs" though so I understand why you may have not seen it.
You can use the models firstOrNew or firstOrCreate methods.
firstOrNew: Get the first record matching the attributes or instantiate
it.
firstOrCreate: Get the first record matching the attributes or create it.
For Example:
$model = SomeModel::firstOrNew(['model_id' => 4]);
In the above example, if a model with a model_id of 4 isn't found then it creates a new instance of SomeModel. Which you can then manipulate and later ->save(). If it is found, it is returned.
You can also use firstOrCreate, which instead of creating a new Model instance would insert the new model into the table immediately.
So in your instance:
$location = UserLocation::firstOrNew(['lat'=>$coords[1],'lng'=>$coords[0]]);
$location will either contain the existing model from the DB or a new instance with the attributes lat and lng set to $coords[1] and $coords[0] respectively, which you can then save or set more attribute values if needed.
Another example:
$location = UserLocation::firstOrCreate(['lat'=>$coords[1],'lng'=>$coords[0]]);
$location will either contain the existing model from the DB or a new model with the attributes set again, except this time the model will have already been written to the table if not found.
Let's say I make a call to my User and return the following:
$user = User::with('permissions')->find(1);
I expect to get a user with $user->permissions being the permission of the user.
Next, I create and assign some new permissions to this user by:
// Say Input::all() contains an array of new permissions I want to add
// I am using Underscore PHP here
$newPermissions = Arrays::each(Input::all(), function($permission) {
$new = new Permission($permission);
$user->associate($new);
return $new;
});
Now I want to update the $user and return them back:
// This does NOT work (it returns the original $user->permissions)
return $user->permissions = $newPermissions;
// But this DOES work
unset($user->permissions);
return $user->permissions = $newPermissions;
Is this a PHP thing or Laravel thing? And what can I do? (btw, even if I say $user->permissions ='anything, text, string, or object doesnt work!' nothing happens).
It looks like permissions is a relationship, not a simple value that you can assign values to. I'd recommend reading the documentation on relationships, eager loading, and on inserting related models.
That last one, in particular, is what you're trying to do. Instead of assigning a value to $user->permissions, you need to update the relationship with either the save, associate, or attach methods, depending on the type of relationship.