Eloquent casting attributes to Collections unexpected behaviour - laravel

Q1. I have an Eloquent model that casts an attribute to a Collection.
Calling Collection's method on this attribute doesn't affect the model values. Eg: put()
When using Collections , iam able to do this :
$var = collect();
$var->put('ip', '127.0.0.1');
var_dump($var);
Output as expected :
object(Illuminate\Support\Collection)[191]
protected 'items' =>
array (size=1)
'ip' => string '127.0.0.1' (length=4)
But when i use with a casted attribute on a Eloquent model, this doesn't work as expected
$user = App\User::create(['email'=>'Name', 'email'=>'mail#example.com', 'password'=>bcrypt('1234')]);
$user->properties = collect();
$user->properties->put('ip', '127.0.0.1');
var_dump($user->properties);
object(Illuminate\Support\Collection)[201]
protected 'items' =>
array (size=0)
empty
This doesn't populate the field.
I think that another collection is created, so to work as expected i must assign this new collection to my field.
Like so :
$user->properties = $user->properties->put('ip', '127.0.0.1');
Q2. Is there a proper way to initialize collection of the field by default (create an empty collection if the field is null), without having to call $user->properties = collect(); "manually" every time?
User.php
class User extends Authenticatable
{
protected $casts = [
'properties' => 'collection',
];
...
}
Migration file
Schema::table('users', function($table) {
$table->text('properties')->nullable();
});

Q1: an attribute casted to collection has a getter that returns, each time, a new BaseCollection that is constructed on the value of the attribute.
As already supposed the getter returns another collection instance and every direct change on it does not change the value of the attribute but instead the newly created collection object.
As also pointed by you the only way to set a a collection casted attribute is to assign it his own original value merged with new ones.
So instead of put() you have to use:
$user->properties = $user->properties->put('ip', '127.0.0.1');
// or
$user->properties = $user->properties ->merge(['ip'=>'127.0.0.1'])
Q2: We have to think that the database representation is a text; so IMHO the proper way to initialize a Model in the migration is to give it a default empty json, i.e.:
$table->text('properties')->default('{}');
But this works only for models created without setting the property field and retrieved after.
For a newly created Model my advice is to pass a default void array, i.e.:
App\User::create([
'name'=>'Name',
'email'=>'mail#example.com',
'password'=>bcrypt('1234'),
'properties' => []
]);

In addition to dparoli's outstanding answer, it is also possible to add a default value through Laravel's boot method, which is available on every Model.
Something like the following example code
protected static function boot()
{
parent::boot(); //because we want the parent boot to be run as well
static::creating(function($model){
$model->propertyName = 'propertyValue';
});
}
You can play with this approach if you like as well.

Related

Laravel updateOrCreate ignores 1 field

I have the following updateOrCreate:
protected $fillable = ['type', 'token', 'expires_on'];
$access = Ebaytoken::updateOrCreate(
['type' => 'access_token'],
['token' => $request->access_token, 'expires_on' => $request->access_token_expires_on]
);
However it gives this error:
SQLSTATE[23000]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Cannot insert the value NULL into column 'type', table 'toolkit.dbo._ebaytokens'; column does not allow nulls. INSERT fails. (SQL: insert into [_ebaytokens] ([token], [expires_on], [updated_at], [created_at]) values (access token goes here, 2020-03-09 09:01:02, 2021-07-12 14:18:31.031, 2021-07-12 14:18:31.031))
It's ignoring the type column in my query.
What have I done wrong here?
You have defined your own constructor on your model which is not its default constructor it gets from the Model class. The constructor on Model takes the attributes as an array and fills them on the instance. This way you can create a new instance of a Model with attributes. This is the important part of that constructor:
public function __construct(array $attributes = [])
{
...
$this->fill($attributes);
}
Without that you wouldn't be able to create a new Model instance with attributes filled.
This is what updateOrCreate is doing:
public function updateOrCreate(array $attributes, array $values = [])
{
return tap($this->firstOrNew($attributes), function ($instance) use ($values) {
$instance->fill($values)->save();
});
}
It is retrieving by the attributes or creating a new instance with the attributes (first argument). Eventually down those method calls you get to Model::newInstance which is doing this:
$model = new static((array) $attributes);
So without that constructor it can't create that new instance of the Model with those attributes, which in this case because of calling updateOrCreate is the first array you pass to updateOrCreate. It can fill the instance with the second array though as that is just a call to fill on the Model instance.
I would suggest to not override the constructor on a Model but if you really need to you could show what you are trying to do and perhaps you could get some advice or ideas on how to go about it.

How to compare two objects and get different columns in Laravel

I have two objects of the same record which I am getting from the database. One is before the update, and the other is after the update. I want to know the column values which are changed during this update query.
$before_update = DeliveryRun::find($id);
$before_update->name = $request->input('name');
$before_update->save();
$after_update = DeliveryRun::find($id);
compare($before_update, $after_update)
I would define a method on your DeliveryRun model which can be used to compare objects of the same type.
Lets say we want to be able to do something like $deliveryRun->compareTo($otherDeliveryRun). That seems like a nice fluid syntax and reads well in my opinion.
What we want to do is get the attributes and their values for the DeliveryRun we're calling compareTo on and then compare them against the attributes and values for the DeliveryRun we provide as an arguement to the compareTo method.
class DeliveryRun extends Model
{
public function compareTo(DeliveryRun $other)
{
$attributes = collect($this->getAttributes())
->map(function ($attribute, $key) use ($other) {
if ($attribute != $other->$key) {
return $key = $attribute;
}
})->reject(function ($attribute, $key) {
return !$attribute || in_array($key, ['id', 'created_at', 'updated_at']);
});
return $attributes;
}
}
The above gets the attributes for the current ($this) DeliveryRun, converts the array returned from getAttributes() to a collection so we can use the map() function and then loops over each attribute on the DeliveryRun model comparing the key and value of each against the $other DeliveryRun model provided.
The reject() call is used to remove attributes which are the same and some attribute keys which you might not be interested in leaving you just the attributes that have changed.
Update
I am saving object in other variable before update $before_update = $delivery_run; but after update $before_update variable I also gets updated
If I am understanding you correctly, you're still comparing the same object to itself. Try something like the following.
$before = clone $delivery_run; // use clone to force a copy
$delivery_run->name = 'something';
$delivery_run->save();
$difference = $before->compareTo($delivery_run);
I would consider using getChanges() as suggested by #Clément Baconnier if all you're doing is looking to get the changes of an object straight after the object has been saved/updated.

Laravel, how cast object to new Eloquent Model?

I get via Request a Json Object.
I clearly parse this object in order to check if it may fit the destination model.
Instead of assigning property by property. Is there a quick way to populate the model with the incoming object?
If you have an array of arrays, then you can use the hydrate() method to cast it to a collection of the specified model:
$records = json_decode($apiResult, true);
SomeModel::hydrate($records);
If you just have a single record, then you can just pass that array to the model’s constructor:
$model = new SomeModel($record);
Just pass your object casted to array as Model constructor argument
$model = new Model((array) $object);
Internally this uses fill() method, so you may first need to add incoming attributes to $fillable property or first create model and then use forceFill().
You should convert that object to array and use fill($attributes) method.
As method name says, it will fill object with provided values. Keep in mind that it will not persist to database, You have to fire save() method after that.
Or if You want to fill and persist in one method - there is create($attributes) which runs fill($attributes) and save() under the hood.
You can use Mass Assignment feature of Laravel,
You model would look like this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['name', 'email', 'phone'];
}
And the process of populating the Model would be like this:
// This would be your received json data converted to array
// use 'json_decode($json, true)' to convert json data to array
$json_arr = [
'name' => 'User Name',
'email' => 'email#example.com',
'phone' => '9999999999'
];
$user = new \App\User($json_arr);
Hope this helps!
Castings may fail due to several reasons. A safe way is to add a static function to the model to generate from both array or object. feels like an extension to the model.
public static function generateFromObject($object)
{
$myModel = new MyModel();
foreach($object as $k => $v)
$myModel->{$k} = $v; //for arrays $myModel[$k] = $v;
return $myModel;
}
and you can use anywhere like,
$myModel = MyModel::generateFromObject($myObjectOrArray)->save();

Can you create a new Model instance without saving it to the database

I want to create a whole bunch of instances of a model object in Laravel, then pick the optimal instance and save it to the database. I know that I can create an instance with Model::create([]), but that saves to the database. If possible I'd like to create a bunch of models, then only "create" the one that is best.
Is this possible?
I am using Laravel 5.0
You create a new model simply by instantiating it:
$model = new Model;
You can then save it to the database at a later stage:
$model->save();
You can create instances with Model::make(). It works the same way as create but it doesn't save it.
Whether or not this is best practice is another matter entirely.
Yes, it is possible different ways: you can use the mass assignment without saving.
Please remember to set first the $fillable property in your model.
WAY #1: using the method fill
$model = new YourModel;
$model->fill([
'field' => 'value',
'another_field' => 'another_value'
]);
$model->save();
WAY #2: using the constructor
$model = new YourModel([
'field' => 'value',
'another_field' => 'another_value'
]);
$model->save();
In YourModel set the $fillable property with the fileds allowed for mass assignment:
class YourModel extends Model
{
protected $fillable = ['field', 'another_field'];
// ...
}
Laravel documentation: https://laravel.com/docs/9.x/eloquent#mass-assignment
there is also a method you can call it statically to get new instance:
$modelInstance = $modelName::newModelInstance();
it takes array $attributes = [] as a parameter

Return Eloquent model with different name from database tables

I want to return a JSON of an Eloquent model, but I'd like to change the array keys. By default they are set as the table field names, but I want to change them.
For example if I have a users table with two fields : id and user_name
When I return User::all(); I'll have a JSON with "[{"id" => 1, "user_name" => "bob}] etc.
I'd like to be able to change user_name to username. I haven't found the way to do it without an ugly foreach loop on the model.
I'm not sure why you would want to do this in the first place and would warn you first about the structure if your app/would it be better to make things uniform throughout.. but if you really want to do it.. you could do:
$user = User::find($id);
return Response::json(array('id' => $user->id, 'username' => $user->user_name));
That will return a JSON object with what you want.
You can also change the name of the key with:
$arr[$newkey] = $arr[$oldkey];
unset($arr[$oldkey]);
Just have a look at robclancy's presenter package, this ServiceProvider handles those things you want to achieve.
GITHUB LINK
Just set the $hidden static for you model to the keys you want to hide:
class User extends Eloquent
{
public static $hidden = 'id';
}
and name them the way you like with get and set functons.

Resources