cakephp 3 original data in afterSave - cakephp

Is there any way to access original data in afterSave?
I would like to log the changes on important data.
With $entity->isNew() I could check if it was an insert or an update, but how can I get what changed?

You can access the original values via Entity::getOriginal() or Entity::extractOriginal(). If you want to get all changed fields, combine the latter one with Entity::visibleProperties(), something like:
debug($entity->extractOriginal($entity->visibleProperties()));
This should return the original values of all changed fields.
See also
http://api.cakephp.org/3.0/class-Cake.Datasource.EntityTrait.html#_extractOriginal
http://api.cakephp.org/3.0/class-Cake.Datasource.EntityTrait.html#_getOriginal
http://api.cakephp.org/3.0/class-Cake.Datasource.EntityInterface.html#_visibleProperties

As of CakePHP 3.0.4, you can either use Entity::extractOriginal(), which will return the original value of any field, whether it has changed or not, or use Entity::extractOriginalChanged(), which will only return changed fields.
With this update, to reproduce the behaviour described in the accepted answer, you will thus need something like:
public function afterSave(Event $event, Entity $entity, $options)
{
debug($entity->extractOriginalChanged($entity->visibleProperties()));
}
See CakePHP 3.0.4 Release Notes, stating:
EntityTrait::extractOriginal() now behaves consistently with extract(). Both methods now include all named properties [...] A new method extractOriginalChanged() can be used to extract only the original values of changed attributes.

Related

CakePHP3 After removal of afterFind, where to check if query result empty?

Before i checked in the afterFind callback if the result of the find is empty.
Since the callback was removed in latest versions, where would be the place to check that from a behavior?
Im not realy sure if that is what i need. My use case is, i want to find out if a query has no result. Then i would create a default entry so it has 1 result.
https://book.cakephp.org/3.0/en/appendices/orm-migration.html#no-afterfind-event-or-virtual-fields
Once defined you can access your new property using $user->full_name. Using the Modifying Results with Map/Reduce features of the ORM allow you to build aggregated data from your results, which is another use case that the afterFind callback was often used for.
Call count() on the ResultSet object
https://api.cakephp.org/3.4/class-Cake.ORM.ResultSet.html#_count
No idea why you're not using that and want to do the count manually, but go on.
Using resultFormatter()
https://book.cakephp.org/3.0/en/orm/query-builder.html#adding-calculated-fields
You behavior will have to add the formatter in your beforeFind() to the query. You can add a custom find that adds it as well. Example code (taken from the docs):
$query->formatResults(function (\Cake\Collection\CollectionInterface $results) {
return $results->map(function ($row) {
$row['age'] = $row['birth_date']->diff(new \DateTime)->y;
return $row;
});
});
Count the results there.
Using map/reduce
https://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html#map-reduce
More often than not, find operations require post-processing the data that is found in the database. While entities’ getter methods can take care of most of the virtual property generation or special data formatting, sometimes you need to change the data structure in a more fundamental way.
You behavior will have to add the mapper/reducer in your beforeFind() to the query. You can add a custom find that adds it as well. Example code (taken from the docs):
$articlesByStatus = $articles->find()
->where(['author_id' => 1])
->mapReduce($mapper, $reducer);
Check the above link for a detailed explanation including examples of how to create a mapper and reducer.

View with small modifications for different types of article

I have one view for a type of article, I need to extends this view and if user open article type "Special" I need to show some new fields in this view.
I don't want to create a separate view for this type of form because differs only one field.
Also I need to save in database, in field "type" one different value if "Special" article is saved.
Please suggest me how I can do this.
Well, you should simply use a if statement in your view to display, or not, additional fields, and use beforeSave method in your model to handle your type attribute.
protected function beforeSave()
{
// if ('Special' article)
// $this->type = 'value';
parent::beforeSave();
}
You should also consider using scenario.
I resolved this by adding a public variable in Article model, in controller I set the variable with one string, and on view I verify this string.

CakePHP 2.x multiple select box

I create by $this->Form->input('field') automatic populate multiple select box.
But how to use code above to select options in edit action which saved values?
make sure your form is created with the correct model
eg `$this->Form->create('Article');
pass a variable as the singular of your model from the Controller via $this->set(). For example, if your model is "Article", then pass a variable with the data:
$this->set('article', $article);
it will populate automatically
Next time you ask a question on StackOverflow, provide information on what you've tried, what worked, what didn't, what you've search for but couldn't find...etc, so it doesn't feel as though we're just writing code for you.
Update (per additional info in comments):
For HABTM, create your field with the model:
$this->Form->input('PartnerState');
Then pass a variable camelCase plural:
$this->set('partnerStates', $partnerStates);

Backbone.js post model

I have a model which id is a code that must be written by the user and this code will be the primary key on the DB.
So, to create a new register I need to write the code but when i call the save() method I'm expecting one POST, but because de idAttribute has an value, I get always one PUT.
This is my model very simplified
Course = Backbone.Model.extend({
idAttribute: "courseCode"
});
As Jack commented, you should be using id, not idAttribute. (Unless we've misunderstood, and courseCode is the name of the primary key on the server; in that case, carry on.)
Whether the model is persisted using POST or PUT when you call save() depends on the isNew() function. The default implementation compares id to null to determine if the model has been saved yet or not. You will need to implement your own isNew function to determine whether to create or update your record. For example, you could try something like this:
Course = Backbone.Model.extend({
isNew: function() {
return !this.has('uniquePropertySetByServer');
}
});
As Jack mentioned, it sounds like your first issue might be using idAttribute when you should be using id. That aside, your issue with PUTs can be solved two ways.
The first is to tell Backbone to use a POST instead. One way to do this is to overrite isNew on your model, as Asheley Ross suggested. Alternatively you could let Backbone use it's native isNew method, and simply force it to treat your model as new by deleting it's ID attribute (as the native isNew check is just this.id == null). You can then either pass the ID in a separate attribute, or store in as a property (not an attribute) on the model and use the model's toJSON method to add it back in just before sending it to the server.
A second way, which will let you avoid all HTTP operations except GET and POST is to set Backbone.emulateHTTPBackbone = true;. This will make Backbone use a POST instead, and just pass an extra request header X-HTTP-Method-Override with the actual type (eg. PUT).

CakePHP actsAs Translate and $Model::find()

I have attached the Translate behavior to one of my models and I have some shortcomings regarding this:
1) If I don't save data in all fields passed as params when attaching the behavior to the model, $Model::find() method doesn't get the inserted rows.
public $actsAs = array(
'Translate' => array(
'title' => 'title_Translation',
'description' => 'description_Translation',
'description_long' => 'description_long_Translation'
)
);
Ex: if i pass to $Model::save() method only a value for 'title', the data is saved, even in the i18n table, but the $Model::find() doesn't get anything. I must pass data for all the fields.
Can I force it to retrieve those records ?
2) How can I get all the records in the admin side of the application (regardless of the language in which a record is saved) in order to list them so the user can alter it (edit data, save data in multiple languages)? Right now, I can only get the records that correspond to the current language (read from Configure or set explicitly)..
Thank you!
I kind of solved it, I copied the TranslateBehavior to app/Model/Behavior (just to avoid problems on future upgrades and keep the original one just in case) then I changed the _addJoin(...) method of the behavior, just changed the join type from INNER to LEFT on line 255 (I use cake 2.2.3).
Now if a record exist it is always retrieved, even if translated fields are missing.
Don't see any drawbacks besides the need to check if the translation field is empty.
OK, I might be a bit late, but anyway...
1) Cake uses an INNER JOIN when fetching a row and it's associated translations, so basically there's no easy way around this. You have to make sure you save every translatable field, every time - even if you just save it as blank. The only alternative would be to go hacking round the core to make it use a left join rather than an inner join - but don't do that.
2) The cookbook explains how to fetch all records here: http://book.cakephp.org/2.0/en/core-libraries/behaviors/translate.html#retrieve-all-translation-records-for-a-field
Now, probably most of the time you want to get just one translation, so you don't want to modify the definition of your $actsAs['Translate'] array in your model. So what I did, was set up a method in AppModel.php which modifies the $actsAs['Translate'] array on the fly:
/*
* See http://book.cakephp.org/2.0/en/core-libraries/behaviors/translate.html#using-the-bindtranslation-method
* This is for making it so we fetch all translations, as opposed to just that of the current locale.
* Used for eg. editing (multiple translations) via the admin interface.
*/
public function bindAllTranslations(){
$translatableFields = $this->actsAs['Translate'];
$keyValueFields = array();
foreach($translatableFields as $field){
$keyValueFields[$field] = $field.'Translation';
}
$this->bindTranslation($keyValueFields,false); // false means it will be changed for all future DB transactions in this page request - and won't be reset after the next transaction.
}
So, if it's an admin method (or any other situation you want all translations) you call that code before doing a find:
$this->MyModel->bindAllTranslations();
$this->MyModel->find('all');
Hope that helps!
Not exactly sure if it will help in your case, but you can also use
array to set locale before you call find()
$this->YourModel->locale = array("ENG", "GER", "JAP");
This way you will always get all records even if they don't have all possible translations.
Thanks a lot eleonzx, I'm having this problem since a decade, and with your simple answer I can now move forward ! So thanks again.
And maybe this code can help a lot of people :
in my AppController beforeFilter method I call _setLanguage
private function _setLanguage() {
if($this->Session->read('Config.language')){
$locale = $this->Session->read('Config.language');
$this->{$this->modelClass}->setLocale($locale);
}else{
$this->{$this->modelClass}->Behaviors->disable('Translate');
}
}
With the else condition I disable the Translate Behavior on the fly to get the original contents if there is no locale set in the session (I use basic links to switch between languages).

Resources