How to compare two objects and get different columns in Laravel - 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.

Related

Laravel firstOrCreate without Eloquent

Eloquent has a firstOrCreate method which gets a model based on a condition, or creates it if it doesn't exist.
Is there any equivalent method in Laravel's query builder (i.e. NOT in Eloquent)? For example:
$row = DB::table('users')->where('user_id', 5)->firstOrCreate('name' => 'Peter', 'last_name' => 'Pan');
That would try to get a row from users with 'user_id'==5. If it doesn't exist, it would insert a row with that id number, plus the other mentioned fields.
EDIT: I'm not trying to apply my question with users. I used users as an example to make as clear as possible what I'm looking for.
updateOrInsert function with empty values give me the result like firstOrCreate
Nope, Laravel firstOrCreate is function, that says next:
public function firstOrCreate(array $attributes, array $values = [])
{
if (! is_null($instance = $this->where($attributes)->first())) {
return $instance;
}
return tap($this->newModelInstance($attributes + $values), function ($instance) {
$instance->save();
});
}
But you can add it with query micro:
DB::query()->macro('firstOrCreate', function (array $attributes, array $values = [])
{
if ($record = $this->first()) {
// return model instance
}
// create model instance
});
So than you will be able to call it same way you do with Eloquent.
$record= DB::table('records')->where('alias', $alias)->firstOrFail();
Yeah of course! Just use normal SQL and ->selectRaw( your conditions ) and look for if there is a entry where your specifications are.
https://laravel.com/docs/5.7/queries#raw-expressions

Additional attributes in Laravel all request

Good day.
For example, I have a model People with fields/attributes:
name
surname
and the model also has this method:
public function FullName()
{
return "{$this->name} {$this->surname}";
}
if I make the next request:
$p = $people->all();
I'll get collection with names and surnames as attributes
how i can make function execution for each in all() request?
What is the best practice?
Well, depends on what kind of result do you want.
OPTION A: Have name, surname and full_name in all the items of the array.
Eleazar's answer is correct, but a little bit incomplete.
1. Define a new accessor in your model.
This will define a new attribute in your model, just like name or surname. When the new attribute is defined, you can just do $user->full_name to get the attribute.
As the documentation says, to define an accessor you need to add a method in your model:
// The function name will need to start with `get`and ends with `Attribute`
// with the attribute field in-between in camel case.
public function getFullNameAttribute() // notice that the attribute name is in CamelCase.
{
return $this->name . ' ' . $this->surname;
}
2. Append the attribute to the model
This will make the attribute to be considered just like any other attribute, so whenever a record of the table is called, this attribute will be added to the record.
To accomplish this you'll need to add this new value in the protected $appends configuration property of the model, as you can see in the documentation:
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The accessors to append to the model's array form.
*
* #var array
*/
// notice that here the attribute name is in snake_case
protected $appends = ['full_name'];
}
3. Make sure that this attribute is visible
Notice this important part of the docs:
Once the attribute has been added to the appends list, it will be
included in both the model's array and JSON representations.
Attributes in the appends array will also respect the visible and
hidden settings configured on the model.
4. Query your data.
When doing the following:
$p = $people->all();
The $p array should have name, surname and also the new full_name attribute for each item.
OPTION B: Just get the full_name for specific purposes.
You can do the following when querying, iterate each result to get the attribute.
Now to do this you can iterate the collection with a foreach sentence, but given that whenever querying data, the array returned is always a Collection instance, so you simply use the map function:
$full_names = $p->map(function ($person) {
// This will only return the person full name,
// if you want additional information just custom this part.
return $person->fullname;
});
Using collection higher order messages it can be even shorter:
$full_names = $p->map->fullname;
I use the following:
public function getFullNameAttribute()
{
return "{$this['name']} {$this['lastname']}";
}
and then, I add it in appends:
class User extends Authenticatable {
protected $appends = ['fullname'];
}
What do you think?
In your model write a function to concatenate the name
public function getFullNameAttribute() {
return ucfirst($this->first_name) . ' ' . ucfirst($this->last_name);
}
now you can call it this way
$user = User::find(1);
echo $user->full_name;
or
Auth::user()->full_name;

eloquent use ->with() with only retrieving one column?

e.g.
i want to retrieve all users with the relation roles, but only the field of role name.
something like this:
User::with('user_role', 'user_role.name')
does something like this exist? i have looked around and don't seem to find something related. The performance might be better if you can filter down the returned columns
Yes, you can use something like this:
$user = User::with('user_role:foreign_key,name')->find(1);
In this case, the foreign_key should be the name of the foreign key that is used to build the relation and it's required here and then you may pass other field names to select them by separating with comma.
This is not documented so be careful, it could be removed in the newer versions. It exists there and below is the code sample, taken from Laravel-5.3 (Illuminate\Database\Eloquent\Builder), it works tho (This is how I've used it: User::with('messages:recipient_id,body')->get()):
/**
* Parse a list of relations into individuals.
*
* #param array $relations
* #return array
*/
protected function parseWithRelations(array $relations)
{
$results = [];
foreach ($relations as $name => $constraints) {
// If the "relation" value is actually a numeric key, we can assume that no
// constraints have been specified for the eager load and we'll just put
// an empty Closure with the loader so that we can treat all the same.
if (is_numeric($name)) {
if (Str::contains($constraints, ':')) {
list($constraints, $columns) = explode(':', $constraints);
$f = function ($q) use ($columns) {
$q->select(explode(',', $columns));
};
} else {
$f = function () {
//
};
}
list($name, $constraints) = [$constraints, $f];
}
// We need to separate out any nested includes. Which allows the developers
// to load deep relationships using "dots" without stating each level of
// the relationship with its own key in the array of eager load names.
$results = $this->parseNestedWith($name, $results);
$results[$name] = $constraints;
}
return $results;
}
You can add constraints to eager loaded relations by supplying a with array with closure as the value with the relation as the key.
$user = User::with(['user_role' => function($query) {
return $query->select('name');
}]);

Eloquent casting attributes to Collections unexpected behaviour

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.

Eloquent Return Non Associative Array?

I am trying to get an array list of IDs to pass onto another model query.
$companies = $user->companies->pluck('id');
But it keeps returning an associative array as such:
[ 0 => 2, 1 => 9]
So when I pass it to the find method on my Company model, like this
$company = Company::find($companies);
I get the following error:
Trying to get property of non-object
I need to be able to pass a non-associative array to the call like such:
Company::find([2,9]);
Try that:
$companies = $user->companies->pluck('id')->toArray();
Just did a test in tinker and the result is a flat array, the "find" will work for sure!
As you can see in Laravels source code
src/Illuminate/Database/Eloquent/Builder.php
public function find($id, $columns = ['*'])
{
if (is_array($id)) {
return $this->findMany($id, $columns);
}
$this->query->where($this->model->getQualifiedKeyName(), '=', $id);
return $this->first($columns);
}
find() will internally call findMany() if the first parameter is an array. But $user->companies->pluck('id') returns a Collection and the Builder creates a wrong query. So your options are:
Use findMany():
$company = Company::findMany($companies);
Convert the Collection to an array:
$company = Company::find($companies->toArray());
Use whereIn():
$company = Company::whereIn('id', $companies)->get();
But actually that all doesn't seem to make any sense, because $user->companies probably already contains the collection you want to fetch from DB. So you could also write:
$company = $user->companies;
However - the fact that you are using singular naming (company) for a set of companies, let me think that you are trying to achieve something completely different.
You can try with the whereIn method
Company::whereIn('id', $companies)->get();

Resources