Eloquent - Compare attribute changes after save() model - php

I'm building an audit system in my application, and I want to compare an Eloquent Model attribute changes after save() method. Here's an example of what I need:
$person = Person::find(1); //Original data: $person->name -> 'Original name', $person->age -> 22
$person->name = 'A new name';
$person->age = 23;
$person->save();
//At this point, I need to get an array like this (only with the attributes that have changed):
[
'age' => ['old' => 22, 'new' => 23],
'name' => ['old' => 'Original name', 'new' => 'A new name']
]
I know Eloquent already has some functions like isDirty(), getDirty() and getChanges(), however this methods only return the new values, and I need to get the old and the new values to store in my audit table.
Is there any way to do this without need to "clone" my variables and then compare it to get the changes?

Before saving your model you can access the original (old) attribute values like:
$person->original.
Furthermore you can call: $person->getChanges() to get all attributes that changed.

Before the Save() function and before overwriting the variables on lines 2 and 3, you can get old data by doing this:
$oldName = $person->name;
$oldAge = $person->age;
And then, after saving, you can insert your values in an array, like this:
$values = array(
"oldName" => $oldName,
"newName" => "New Name",
"oldAge" => $oldAge,
"newAge" => "New Age",
);
So you can get values from the array by doing:
echo $values["oldName"];
echo $values["newAge"];
...

You can clone the newly retrieved model before making the changes.
Something along the line of
$person = Person::find(1);
$original_person = cone $person;
// update the person object
// ...
$person->save();
You can proceed to construct your array like so:
[
'age' => ['old' => $original_person->age, 'new' => $person->age],
'name' => ['old' => $original_person->name, 'new' => $person->name]
]

You can do this within updated() boot function in the model class
class Mymodel extends Model
{
public static function boot()
{
parent::boot();
self::updated(function ($model) {
var_dump($model->original);
var_dump($model->getChanges());
// Traverse the changed array and save with original values
});
}
}

Related

Populate database table with Laravel Faker from pre-populated table

I want to populate a table with listings of car offers with dummy data.
The specific is that I want the make and model to be from a table already created in the database. Also the listing's title to be Make and Model. My code is this, but I assume it is totally wrong:
public function definition()
{
$id = DB::table('cars')->select('id')->inRandomOrder()->first();
$make = DB::table('cars')->where('id', $id)->select('make')->first();
$model = DB::table('cars')->where('id', $id)->select('model')->first();
return [
'title' => $make . " " . $model,
'make' => $make,
'model' => $model,
'year' => $this->faker->numberBetween(1950, 2021),
'slug' => $this->faker->word(),
];
}
The query builder in Laravel returns an object representing the database model when you fetch with first()
You should access the attributes when you're setting them as other model attributes
return [
'title' => $make->make . " " . $model->model,
'make' => $make->make,
'model' => $model->model,
'year' => $this->faker->numberBetween(1950, 2021),
'slug' => $this->faker->word(),
];
Note
The answer above only fixes the issue in the current code
The correct implementation would be to create models (if don't exist already) and link them with relationships through foreign keys and then attach them together in a seeder
The factory should only contain a clear and clean definition of fake data

Return only certain data in from a collection - Laravel

I'm learning Laravel and have created a public endpoint where I want to output only certain information of some comments if a user is not authenticated from a GET request.
I have managed to filter out the comments based on whether or not they are approved. I now want to filter out the data that is returned. I have attached a screenshot of what is currently returned.
Ideally, I only want to return the id, name and the body in the json. How can I go about this? I tried the pluck() method which did not give the desired results. Any pointers would be greatly appreciated
public function index(Request $request)
{
if (Auth::guard('api')->check()) {
return Comment::all();
} else {
$comments = Comment::where('approved', 1)->get();
return $comments->pluck('id','name','body');
}
}
To select the particular columns, you can pass columns name to get as
$comments = Comment::where('approved', 1) -> get(['id','name','body']);
You can use a transformer to map the incoming data to a sensible output based on the auth state. The following example comes from the Fractal lib:
<?php
use Acme\Model\Book;
use League\Fractal;
$books = Book::all();
$resource = new Fractal\Resource\Collection($books, function(Book $book) {
return [
'id' => (int) $book->id,
'title' => $book->title,
'year' => $book->yr,
'author' => [
'name' => $book->author_name,
'email' => $book->author_email,
],
'links' => [
[
'rel' => 'self',
'uri' => '/books/'.$book->id,
]
]
];
});
Ideally, you would create 2 classes that extend from Transformer and pass the correct one to the output.
If you want to pass the result as json respose
$comments = Comment::where('approved', 1)->pluck('id','name','body')->toArray();
return Response::json($comments);
If you want to pass the result as to blade
$comments = Comment::where('approved', 1)->pluck('id','name','body')->toArray();
return view('your_blade_name')->with('comments',$comments);

Laravel faker - loop to add records to simulate version control

I am using faker to seed my DB.
$factory->define(App\Product::class, function (Faker\Generator $faker) {
$campaign = factory(App\Campaign::class)->create();
$size= $faker->randomElement($array = array ('728x90','300x250','315x315', '715x425', '750x650'));
return [
'campaign_id' => $campaign->campaign_name,
'size' => $size,
'product_id' => $campaign->campaign_name."_".$size,
'version' => $faker->randomElement($array = array ('1','2','3', '4', '5')),
];
});
The bit I am interested in is the version field. What I would like to do is generate a random number between 1 and 5 and then enter that number of records in the database,
So a product can have been 1 and 5 entries depending on the number of 'versions' which have bene created.
Is this possible?
I tried a simple for loop around the return array with no luck.
From what you say, you want to create multiple entries for the same product with different versions. The Model factory can be used to create a single model entry. You can use the faker directly in the seeder and achieve what you are expecting.
$campaign = factory(App\Campaign::class)->create();
$size= $faker->randomElement($array = array ('728x90','300x250','315x315', '715x425', '750x650'))
$max_version = $faker->randomElement($array = array ('1','2','3', '4', '5'));
for ($version=0; $version < $max_version; $version++) {
Product::create([
'campaign_id' => $campaign->campaign_name,
'size' => $size,
'product_id' => $campaign->campaign_name."_".$size,
'version' => $version,
]);
}
One of the simplest solutions is to create factory method (or trait) in your test class, something like this, you'll get the point :)
public function createProduct()
{
$product = factory(Product::class)->create();
foreach(range(0, $product->version) as $i) {
factory(Version::class)->create([
'product_id' => $product->id
]);
}
return $product;
}

Convert an array to eloquent model in Laravel

I have mysql table 'test' with three columns,
1.sno 2.name 4.country
this code is easily understandable
$person = \App\Test::find(1);
$person->country; //Defined in Test eloquent model
now i want to do something like this:
$p = ['sno' => 1, 'name' => 'Joe', 'country' => '1' ];
$p->country; //Return relevent column form countries table as defined in Model
The thing to remember is that the user i am trying to map is already present in the database table. How to i convert an array to eloquent model?
You could instantiate the model class with no attributes:
$dummy = new \App\Test;
Then you can call the newInstance() method:
$attributes = ['sno' => 1, 'name' => 'Joe', 'country' => '1' ];
$desiredResult = $dummy->newInstance($attributes, true);
The true flag in the method is telling eloquent that the instance already exists in database, so you can continue working with it normally. Now you can do:
$desiredResult->country //'1'

CakePHP changing virtual fields at runtime

I have a Product model for a multi site application.
Depending on the domain(site) I want to load different data.
For example instead of having a name and description fields in my database I have posh_name, cheap_name, posh_description, and cheap_description.
if I set something up like this:
class Product extends AppModel
{
var $virtualFields = array(
'name' => 'posh_name',
'description' => 'posh_description'
);
}
Then it always works, whether accessed directly from the model or via association.
But I need the virtual fields to be different depending on the domain. So first I creating my 2 sets:
var $poshVirtualFields = array(
'name' => 'posh_name',
'description' => 'posh_description'
);
var $cheapVirtualFields = array(
'name' => 'cheap_name',
'description' => 'cheap_description'
);
So these are my 2 sets, but how do I assign the correct one based on domain? I do have a global function called isCheap() that lets me know if I am on the lower end domain or not.
so I tried this:
var $virtualFields = isCheap() ? $this->cheapVirtualFields : $this->poshVirtualFields;
This gives me an error. Apparently you cannot assign variables in a Class definition like this.
So I put this in my Product model instead:
function beforeFind($queryData)
{
$this->virtualFields = isCheap() ? $this->cheapVirtualFields : $this->poshVirtualFields;
return $queryData;
}
This works ONLY when the data is accessed directly from the model, DOES NOT work when the data is accessed via model association.
There has got to be a way to get this to work right. How?
Well if I put it in the constructor instead of the beforeFind callback it seems to work:
class Product extends AppModel
{
var $poshVirtualFields = array(
'name' => 'posh_name',
'description' => 'posh_description'
);
var $cheapVirtualFields = array(
'name' => 'cheap_name',
'description' => 'cheap_description'
);
function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
$this->virtualFields = isCheap() ? $this->cheapVirtualFields : $this->poshVirtualFields;
}
}
However, I am not sure if this is a CakePHP no no that can come back to bite me?
seems like the issue could be that the model association is a model that is built on the fly. eg AppModel
try and do pr(get_class($this->Relation)); in the code and see what the output is, it should be your models name and not AppModel.
also try and use:
var $poshVirtualFields = array(
'name' => 'Model.posh_name',
'description' => 'Model.posh_description'
);
var $cheapVirtualFields = array(
'name' => 'Model.cheap_name',
'description' => 'Model.cheap_description'
);

Categories