Laravel4 - Track changes to be saved by Eloquent - php

In Laravel 4, I have a class extending Eloquent and I need to record changes (to keep the history) at the time of saving.
Saving event in boot function is called as expected. The question is: how do I know which fields were changed and are about to be saved? Also, can I access existing values without loading the record again?
I know, one way could be to load the record again and compare all fields one by one. Is there any better optimized way to do that?
class Record extends Eloquent {
protected static function boot()
{
parent::boot();
static::saving(
function($record)
{
// It runs properly. This is where changes should be compared
return true;
}
);
}
}
Thank you.

Eloquent's getDirty() and getOriginal() did the trick:
static::saving(
function($record)
{
$dirty = $record->getDirty();
foreach ($dirty as $field => $newdata)
{
$olddata = $record->getOriginal($field);
if ($olddata != $newdata)
{
// save changes from $olddata to $newdata
}
}
return true;
}
);

I don't think accepted answer is the best way to do that. Why would you loop through all changed fields to figure out what has changed? Laravel has this two Model methods that can help you better,
on save function/route/controller whichever you use
$model::find(X);
...
'''
$model->fill(Input::all());
if( !empty($model->getDirty() )
// Get status of model. if nothing has changed there is no need to get in here
{
if($model->isDirty( { mixed data (array,string,null) } ))
// isDirty() will help you to find out if the field is different than original
{
.....
enter code here
....
}
}

You can use events:
Event::listen('saving', function($model)
{
// Do whatever you need to do with your $model before saving or not:
return true;
});
You can also hook that saving method to any controller action:
Event::listen('saving', 'LogThings#saving');

I have something like this in my project.
Record::updating(function($record){
$original = $record->getOriginal();
foreach($original as $index => $value){
if($index != 'updated_at' AND $index != 'created_at'){
if($value != $record[$index]){
RecordUpdate::create(array('record_id' => $record->id, 'fieldname' => $index, 'old_value' => $value, 'new_value' => $record[$index], 'created_at' => date('Y-m-d H:i:s')));
}
}
}
});

The Eloquent class that you're extending has getOriginal() and getAttributes() methods to get the originally-hydrated properties and the currently set properties, respectively.
So, in your saving event you can do something like...
if ( ! empty( array_diff( $record->getOriginal(), $record->getAttributes() ) ) )
{
//log changes
}
If you want to do this for all/most of your Models, I suggest abstracting this out to a generic Model Observer:
////
/* Subclassable, generic Model Observer */
////
class ModelObserver {
public function saving($model)
{
if ( ! empty( array_diff( $record->getOriginal(), $record->getAttributes() ) ) )
{
//log changes
}
}
}
////
/* Set up observers, for example in start/global.php */
////
$modelObserver = new ModelObserver;
Comment::observe($modelObserver);
Post::observe($modelObserver);
User::observe($modelObserver);

Related

If Class exists && If Method exists PHP / Laravel

PHP/Laravel
Hey, I'm moving into abstraction in php and am attempting to validate and store values based on whatever has been submitted, where I expect that the methods should neither know what to validate against and/or which class and method to use to do so -
What I've got works but I can see that there would be issues where classes/methods do not exist. Here lays my question.
If I were to call a method in the following format, which way would be best to 'check' if class_exists() or the method exists()?
public function store(Request $request)
{
$dataSet = $request->all();
$inputs = $this->findTemplate();
$errors = [];
$inputValidators = [];
foreach ($inputs as $input) {
$attributes = json_decode($input->attributes);
if (isset($attributes->validate)) {
$inputValidators[$input->name] = $input->name;
}
}
foreach ($dataSet as $dataKey => $data) {
if (array_key_exists($dataKey, $inputValidators)) {
$validate = "validate" . ucfirst($dataKey);
$validated = $this->caseValidator::{$validate}($data);
if ($validated == true) {
$inputValidators[$dataKey] = $data;
} else {
$errors[$dataKey] = $data;
}
} else {
$inputValidators[$dataKey] = $data;
}
}
if (empty($errors)) {
$this->mapCase($dataSet);
} else {
return redirect()->back()->with(['errors' => $errors]);
}
}
public function mapCase($dataSet)
{
foreach($dataSet as $dataKey => $data) {
$model = 'case' . ucfirst($dataKey);
$method = 'new' . ucfirst($dataKey);
$attribute = $this->{$model}::{$method}($dataKey);
if($attribute == false) {
return redirect()->back()->with(['issue' => 'error msg here']);
}
}
return redirect()->back->with(['success' => 'success msg here'])'
}
For some additional context, an input form will consist of a set of inputs, this can be changed at any time. Therefore I am storing all values as a json 'payload'.
When a user submits said form firstly the active template is found, which provides details on what should be validated $input->attributes, once this has been defined I am able to call functions from caseValidator model as $this->caseValidator::{$validate}($data);.
I do not think that any checks for existence will be needed here as the validation parameters are defined against an input, thus if none exist this check will be skipped using if (array_key_exists($dataKey, $inputValidators))
However, I am dispersing some data to other tables within the second block of code using mapCase(). This is literally iterating over all array keys regardless of if a method for it exists and thus the initial check cannot be made as seen in the first block. I've attempted to make use of class_exists() and method_exists but logically it does not fit and I cannot expect them to work as I'd like, perhaps my approach in mapCase is not correct? I guess if I'm defining a class for each key I should instead use one class and have methods exist there, which would remove the need to check for the class existing. Please advise
Reference:
$attribute = $this->{$model}::{$method}($dataKey);
Solved the potential issue by using class_exists(), considering I know the method names as they are the same as the $dataKey.
public function mapCase($dataSet)
{
foreach($dataSet as $dataKey => $data) {
$model = 'case' . ucfirst($dataKey);
if (class_exists("App\Models\CaseRepository\\" . $model)) {
$method = 'new' . ucfirst($dataKey);
$attribute = $this->{$model}::{$method}($dataKey);
}
if($attribute == false) {
return redirect()->back()->with(['issue' => 'error msg here']);
}
}
return redirect()->back->with(['success' => 'success msg here'])'
}

Laravel Model Dynamic Attribute

I would like to ask how it's possible to create a dynamic attribute on the model class. Let's suppose I have a table structure like below code.
Schema::create('materials', function (Blueprint $table) {
$table->increments('id');
$table->string('sp_number');
$table->string('factory');
$table->text('dynamic_fields')->comment('All description of the material will saved as json');
$table->timestamps();
});
I have a column in my table structure named "dynamic_fields" that will hold a JSON string for the fields. An example of JSON structure below.
[
{
"name":"COLOR WAY",
"value":"ASDFF12"
},
{
"name":"DESCRIPTION",
"value":"agg2sd12"
},
{
"name":"REF NUM",
"value":"121312"
}
]
I want to access a field from my dynamic fields, like for example "COLOR WAY".
In my model I want to access the "COLOR WAY" field on the dynamic field like this way
$material->color_way;
Can anybody show me how to do it?
If you know that there will only be certain dynamic fields ahead of time, you could opt to create accessor methods for them. For example, you could add this to your model:
// Dynamic fields must be cast as an array to iterate through them as shown below
protected $casts = [
'dynamic_fields' => 'array'
];
// ...
public function getColorWayAttribute()
{
foreach ($this->dynamic_fields as $field) {
if ($field['name'] === 'COLOR WAY') {
return $field['value'];
}
}
return null;
}
This will allow you to do:
$colorWay = $material->color_way;
Alternatively, if the combinations your dynamic_fields are not limited, there could be a large number of them or you want there to be more flexibility to be able to add more and have them accessible, you could override the getAttribute method of Laravel's model class.
// Dynamic fields must be cast as an array to iterate through them as shown below
protected $casts = [
'dynamic_fields' => 'array'
];
// ...
public function getAttribute($key)
{
$attribute = parent::getAttribute($key);
if ($attribute === null && array_key_exists('dynamic_fields', $this->attributes)) {
foreach ($this->dynamic_fields as $dynamicField) {
$name = $dynamicField['name'];
if (str_replace(' ', '_', mb_strtolower($name)) === $key) {
return $dynamicField['value'];
}
}
}
return $attribute;
}
This approach calls Laravel's implementation of getAttribute which first checks if you have an actual attribute defined, or if you have an accessor defined for the attribute (like in my first suggestion), then checks if a method exists with that name on the base model class and then finally attempts to load a relation if you have one defined.
When each of those approaches fails (null is returned), we then check to see if there's a dynamic_fields attribute in the model. If there is, we loop through each of the dynamic fields (assuming your dynamic_fields is cast as an array), we then convert the name of the defined dynamic field to lowercase and replace spaces with underscores. We then finally check to see if the name we have just derived matches the key provided and if it does, we return the value. If it doesn't, the original $attribute will be returned, which will be null.
This would allow you to get any of your dynamic fields as if they were defined as attributes in the class.
$colorWay = $material->color_way;
$description = $material->description;
$refNum = $material->ref_num;
Please note: I have not tested this code, there could well be an issue or two present. Give it a try and see if it works for you. Also note that this will only work for getting dynamic fields, setting them will require overriding another method.
Try to use this code in your model:
protected $casts = [
'dynamic_fields' => 'array',
];
public function setAttribute($key, $value)
{
if (!$this->getOriginal($key)) {
$this->dynamic_fields[$key] = $value;
}
parent::setAttribute($key, $value);
}
public function getAttribute($key)
{
if (!$this->getOriginal($key)) {
return $this->dynamic_fields[$key]
}
parent::getAttribute($key);
}
In this example, you can get Dynamic Column form Dynamic Model. as well as its Models Relation too
1) first you have to define a table Scope in Model.
private $dynamicTable='';
public function scopeDefineTable($query,$tableName)
{
if( $tableName )
{
$this->dynamicTable= $tableName;
}
else
{
$this->dynamicTable= "deviceLogs_".date('n')."_".date('Y');
}
$query->from( $this->dynamicTable );
$this->table=$this->dynamicTable; # give dynamic table nam to this model.
}
public function scopeCustomSelect( $query ,$items=[])
{
$stu_class_col=['id as stu_class_id','std_id']; // Required else retional model will not retun data. here id and std_id is primary key and foreign key.
$stu_doc_col=['id as stu_doc_id','std_id'];// Required else retional model will not retun data. here id and std_id is primary key and foreign key.
foreach ( $items as $col)
{
if( Schema::hasColumn('student_information', $col ))
{
$stu_info_col[]= $col ;
}
elseif ( Schema::hasColumn('student_class',$col))
{
$stu_class_col[]= $col ;
}
elseif ( Schema::hasColumn('student_image',$col))
{
$stu_doc_col[]= $col ;
}
}
// converting array to string for bind column into with relation...
$stu_class_col_string = implode(',',$stu_class_col);
$stu_doc_col_string = implode(',',$stu_doc_col);
return $colQuery = $query->select($stu_info_col)
->with(["student_class:$stu_class_col_string", "studentImage:$stu_doc_col_string"]);
}
using this you can get data from Rational Model too...
from Controller
$studentInfo = Student::whereHas("student_class",function($q) use($req){
$q->where("std_session",$req->session_code);
$q ->where("std_class",$req->class_code);
$q ->where("std_section",$req->std_section); })
->customSelect($fields['dataList'])
->get();
here I am not using dynamic Model Scope. only Dynamic SustomSelect scope..

CakePHP 3.5 Always apply asText() MySQL function to Spatial field

I have a custom PolygonType which represents a POLYGON() field in a MySQL table.
class PolygonType extends BaseType implements ExpressionTypeInterface
{
public function toPHP($value, Driver $d)
{
// $value is binary, requires unpack()
}
}
I can use $query->func()->astext() on every find, but I would like to know if it's possible to always apply MySQL's AsText() function when selecting this field instead (similar to how toExpression() can be used when inserting data).
AFAIK there is no such functionality, type classes and select clause contents never touch.
If you wanted to apply this to all finds, then you could for example use the Model.beforeFind() event, traverse the select clause and transform the fields to expressions. Here's a quick and dirty example, where field is the name of the POLYGON type column:
// in the respective table class
use Cake\Event\Event;
use Cake\ORM\Query;
// ...
public function beforeFind(Event $event, Query $query, \ArrayObject $options, $primary)
{
$query->traverse(
function (&$value) use ($query) {
if (empty($value)) {
$value = $query->aliasFields($this->getSchema()->columns());
}
foreach ($value as $key => $field) {
if (is_string($field) &&
$this->aliasField($field) === $this->aliasField('field')
) {
unset($value[$key]);
$value[key($query->aliasField($field))] = $query->func()->AsText([
$this->aliasField('field') => 'identifier'
]);
}
}
},
['select']
);
}
You may have to account for $field as expressions too, in case the field might be used in one and needs to be converted there too.
Another way would be to convert the data on PHP level in the type class' toPHP() method, as already indicated in your code example.
See also
Cookbook > Database Access & ORM > Table Objects > Lifecycle Callbacks > beforeFind
API > \Cake\Database\Query::traverse()
Based on ndp's answer, it's possible to inspect the field types via $query->getDefaultTypes() and apply a SQL function as required. However, $value is empty if no fields are initially stated (e.g. when using Table::get() so there's also a check for this.
public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary)
{
$query->traverse(
function (&$value) use ($query) {
if (is_array($value) && empty($value)) {
$query->all();
}
$defaultTypes = $query->getDefaultTypes();
foreach ($value as $key => $field) {
if (in_array($defaultTypes[$field], ['point', 'polygon'])) {
$value[$key] = $query->func()->astext([
$this->aliasField($field) => 'identifier'
]);
}
}
$query->select($value);
},
['select']
);
}

How to get values that doesn't pass from FilterIterator

I'm using FilterIterator to filter out the values and implemented the accept() method successfully. However I was wondering how would it be possible to get the values that returned false from my accept method in single iteration. Let's take the code below as an example (taken from php.net);
class UserFilter extends FilterIterator
{
private $userFilter;
public function __construct(Iterator $iterator , $filter )
{
parent::__construct($iterator);
$this->userFilter = $filter;
}
public function accept()
{
$user = $this->getInnerIterator()->current();
if( strcasecmp($user['name'],$this->userFilter) == 0) {
return false;
}
return true;
}
}
On the code above, it directly filters out the values and returns the values that pass from the filteriterator. Implemented as;
$array = array(
array('name' => 'Jonathan','id' => '5'),
array('name' => 'Abdul' ,'id' => '22')
);
$object = new ArrayObject($array);
$iterator = new UserFilter($object->getIterator(),'abdul');
It will contain only the array with name Jonathan. However I was wondering would it be possible to store the object with name Abdul in another variable using the same filter with a slight addition instead of reimplementing the entire filter to do the opposite?. One way I was thinking would exactly copy paste the FilterIterator and basically change values of true and false. However are there any neat ways of doing it, since it will require another traversal on the list.
I think you must rewrite the accept() mechanic. Instead of returning true or false, you may want to break down the array to
$result = array(
'passed' => array(...),
'not_passed' => array(...)
);
Your code may look like this
if (strcasecmp($user['name'], $this->userFilter) == 0) {
$result['not_passed'][] = $user;
} else {
$result['passed'][] = $user;
}
return $result;

PHP Object Validation

I'm currently working on an OO PHP application. I have a class called validation which I would like to use to check all of the data submitted is valid, however I obviously need somewhere to define the rules for each property to be checked. At the moment, I'm using arrays during the construction of a new object. eg:
$this->name = array(
'maxlength' => 10,
'minlength' => 2,
'required' => true,
'value' => $namefromparameter
)
One array for each property.
I would then call a static method from the validation class which would carry out various checks depending on the values defined in each array.
Is there a more efficient way of doing this?
Any advice appreciated.
Thanks.
I know the associative array is used commonly to configure things in PHP (it's called magic container pattern and is considered bad practice, btw), but why don't you create multiple validator classes instead, each of which able to handle one rule? Something like this:
interface IValidator {
public function validate($value);
}
$validators[] = new StringLengthValidator(2, 10);
$validators[] = new NotNollValidator();
$validators[] = new UsernameDoesNotExistValidator();
This has multiple advantages over the implementation using arrays:
You can document them (very important), phpdoc cannot parse comments for array keys.
Your code becomes typo-safe (array('reqiured' => true))
It is fully OO and does not introduce new concepts
It is more readable (although much more verbose)
The implementation of each constraint can be found intuitively (it's not in a 400-line function, but in the proper class)
EDIT: Here is a link to an answer I gave to a different question, but that is mostly applicable to this one as well.
Since using OO it would be cleaner if you used classes for validating properties. E.g.
class StringProperty
{
public $maxLength;
public $minlength;
public $required;
public $value;
function __construct($value,$maxLength,$minLength,$required)
{
$this->value = $value;
$this-> maxLength = $maxLength;
$this-> minLength = $minLength;
$this-> required = $required;
}
function isValidat()
{
// Check if it is valid
}
function getValidationErrorMessage()
{
}
}
$this->name = new StringProperty($namefromparameter,10,2,true);
if(!$this->name->isValid())
{
$validationMessage = $this->name-getValidationErrorMessage();
}
Using a class has the advantage of encapsulating logic inside of it that the array (basically a structure) does not have.
Maybe get inspired by Zend-Framework Validation.
So define a master:
class BaseValidator {
protected $msgs = array();
protected $params = array();
abstract function isValid($value);
public function __CONSTRUCT($_params) {
$this->params = $_params;
}
public function getMessages() {
// returns errors-messages
return $this->msgs;
}
}
And then build your custom validators:
class EmailValidator extends BaseValidator {
public function isValid($val=null) {
// if no value set use the params['value']
if ($val==null) {
$val = $this->params['value'];
}
// validate the value
if (strlen($val) < $this->params['maxlength']) {
$this->msgs[] = 'Length too short';
}
return count($this->msgs) > 0 ? false : true;
}
}
Finally your inital array could become something like:
$this->name = new EmailValidator(
array(
'maxlength' => 10,
'minlength' => 2,
'required' => true,
'value' => $namefromparameter,
),
),
);
validation could then be done like this:
if ($this->name->isValid()) {
echo 'everything fine';
} else {
echo 'Error: '.implode('<br/>', $this->name->getMessages());
}

Categories