CakePHP actsAs Translate and $Model::find() - cakephp

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).

Related

How to create a whitelist of updatable fields in a CakePHP's model?

I want to create a whitelist of fields that I want to be updatable in CakePHP. I know that I can pass a fieldList array in the call to Model::save(), but this isn't what I'm looking for. What I want is that every model "publish" a list of the valid fields, so if I call the Model::save() method without a fieldList and with data that mustn't be updatable (like the ownerId) this won't be updated.
What can I do to get this behavior? Maybe override the Model::save method in every Model to call at the "original" Model::save with the whitelist? I think this is a good idea, because I don't pollute all the controllers with lots of duplicated whitelists...
Thanks for your help!
Well, thanks you all for your answers, but I was wrong: I don't need to add this functionality.
The problem I was trying to solve was a security problem, I was trying to avoid form tampering (I've discovered the name just now), and as I am a novice CakePHP user, I didn't know that CakePHP already manages this problem.
The answer to my question is very easy: I must use CakePHP's Security Plugin and use the Form Tampering prevention (http://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#form-tampering-prevention).
Can‘t say I’ve ever needed to do this in CakePHP, but CakePHP only saves the fields you pass it.
If you really need to create a white-list and you’re certain you only ever want these fields saving and never any others in your database (although I don’t understand why you have columns for them if you never touch them) then you could emulate this behavior in a model callback method:
<?php
class User extends AppModel {
public function beforeSave($options = array()) {
$whitelist = array('first_name', 'last_name', 'email');
foreach ($this->data[$this->alias] as $field => $value) {
if (!in_array($field, $whitelist)) {
unset($this->data[$this->alias][$field]);
}
}
return true;
}
}
This will just unset any data not in the $whitelist array, but if you really don’t want a column being updated then don’t pass a value for it.

Write to multiple tables in joomla component?

I'm trying to create a component (front end) that uses multiple tables. I found 1 or 2 post that partially answer to the question but none really does. The point seems always simple and evident for the one who knows how to do it but it is never really explained (or I missed the right post).
In my component, the user enters data in one view that need to be stored in two tables:
the standard Joomla User table i.e. # __users
an additional table to store data that are not included in Joomla i.e. # __users_complements
I'm a beginner, so maybe I'm wrong, but I understood that the standard functions of joomla can only save results of a form in one table .
In my case, I guess that I have to override the standard functions in my model: com_component / model / my_model.php.
1) I'm confused because I do not really understand which function must be overrided: save ()? store ()? other?
2) Let's say I override the save() function, should I rewrite all the code to save data (explode the data array and create all the update queries) or should I create 2 standard table objects.
In this case, (2 objects) it seems weird to send each time the whole data array to the parent function as I know that a part is for table 1 and the other part for the table 2. I should be able to split before don't I?
3) Should I create 2 models and manage those models from my controller when I get back data from the form and call the save function of the model?
Could you help me to clarify how to do this saving in multiple tables?
An example with code will be very much appreciated.
Thank you
I finally made it. As I spent many hours on this and found that a lot of people where looking for an answer, here is how I did.
I suppose you know how to create a component, using the standard MVC structure:
Component entry point
Component controller
Eventually component router
Component view
Component model
Component controller
In model components\my_component\models\my_model.php create your own save function
public function save($data)
{
// Initialise variables.
$userId = (!empty($data['id'])) ? $data['id'] : (int)$this->getState('user.id');
$user = JFactory::getUser();
$table_one = $this->getTable('TableOne', 'MyComponentTable', array());
$table_two = $this->getTable('TableTwo', 'MyComponentTable', array());
// Bind the data.
if (!$table_one->bind($data))
{
$this->setError(JText::sprintf('USERS PROFILE BIND FAILED', $user->getError()));
return false;
}
if (!$table_two->bind($data))
{
$this->setError(JText::sprintf('USERS PROFILE BIND FAILED', $user->getError()));
return false;
}
// Store the data.
if (!$table_one->save($data))
{
$this->setError($user->getError());
return false;
}
if (!$table_two->save($data))
{
$this->setError($user->getError());
return false;
}
return $user->id;
}
Of course, you need the getTable function called in the save function
public function getTable($type = 'TableOne', $prefix = 'MyComponentTable', $config = array())
{
// call the administrator\components\com_mycomponent\tables\__tablename__.php
$this->addTablePath(JPATH_COMPONENT_ADMINISTRATOR . '/tables');
return JTable::getInstance($type, $prefix, $config);
}
And it works! So simple!
Of course, as I said in my question the whole $data is sent to the parent save() function to with data that are not necessary for table_one or table_two. It works this way with the standard joomla structure (no hack or direct query in the code).
Hope it helps.
There may be those out there who disagree with the way that the following method disrupts the MVC structure just a bit, but I've found it to be the simplest for me.
Typically, you have a model that fits one of the tables. In your example with pushing data to the users table as well as one in your component, I would add the following to the model for the table in your component:
public function save($data) {
if (!parent::save($data)) {
return false;
}
// add necessary code to save to the users table, since there isn't a standard way to do this that I'm aware of
// sometimes I will grab another model even
require_once(JPATH_BASE . '/administrator/components/com_users/models/user.php');
$other_model = $this->getInstance('user', 'UsersModel');
$other_model->save($data);
return true;
}
The first part of this function should save the data to the components table just like normal. But you can tack what you need on to the rest of the component to make whatever you like happen.
I would almost guarantee that there is a better way to chain models (and I've seen some of the changes happening in the Joomla Platform core that will lead to better ways in the future), but this should get you going for now.
In addition, for prompt 3, I would handle in the controller if you need to sometimes save just one table and sometimes save both. I've found that the save functions are pretty safe to run even when parts aren't loaded, so I usually just let it run.

Can I deal with Records in an easier way in CakePHP?

I started to tackle CakePHP and read through the documentation, but two things still seem a bit clumsy to me.
I know other Frameworks where I have a certain record which I'd like to store, but CakePHP suggests me to do it anonymously:
$this->Foo->create(array(...));
$this->Foo->save();
Why can't I tell CakePHP which Record to save, just like in every other framework:
$foo = $this->Foo->create(array(...));
$foo->save();
I would like to iterate through a whole RecordSet inside of a Controller. Why do I need to iterate using
$foos = $this->Foos->find('all');
foreach($foos as $foo){
$foo['Foo'] // ... here we have $foo.
I don't understand why find() returns a 2-dimensional array and there are only records in the inner array. Why isn't this directly an array of records?
$this->Foo is an instance of your Foo model. When you call methods on it, you are calling methods on the active record (if there is one) of that instance of the Foo model. So in terms of telling Cake which record to save, you don't need to - Cake knows to save the current active record.
Here's the code you pasted with comments, which might help.
// Prepare this instance of the Foo model to save a new record
$this->Foo->create(array(...));
// Save the new record that we have just prepared
$this->Foo->save();
And the other way...
// Call the create method on this instance of the Foo model, and return what?
// Return another instance of the Foo model?
// Why not just continue using the instance we already have, ie, $this->Foo
$foo = $this->Foo->create(array(...));
// Call the save method on the duplicate instance of the Foo model that was
// returned from the create method?
$foo->save();
// Why did 'create' need to return a duplicate instance of the model to do a save???
// Why not call the save on the same instance of the Foo model that we used to call the create?
Point 2. This is basically for consistency. Often, you'll be returning data from multiple tables, linked to one another. Lets say tables Foo and Bar have a 1 to 1 relationship, and you're getting Foo records, along with their associated Bar records.The array returned will need Foo and Bar keys, eg: inside of your foreach loop, $foo might contain:
$foo['Foo']['column1'], $foo['Foo']['column2'], $foo['Bar']['column1'], $foo['Bar']['column2']
To be consistent, when you only fetch from one table, it still returns in the form $foo['Foo']['column1'], just like it would if you fetched joined data from multiple tables.
EDIT: In response to your comment, say you have the code:
$foos = $this->Foos->find('all');
Say you wanted to call some model method on each row of your returned array, there are a few ways you could do it. One way is something like:
// This is code for the controller
$this->Car->find('all');
foreach($cars as $car){
$this->Car->driveTwoMiles($car); // the driveTwoMiles would be in your model class
}
So in your model, you'd have a method:
// This would be a method in your model class
function driveTwoMiles($car){
$this->id = $car['Car']['id']; // set the active record
// we are now inside the model, so $this->id is the same as calling $this->Car->id from the controller
// Do whatever you want here. You have an active record, and your $car variable, holding data
$this->Post->saveField('distance_driven', $car['Car']['distance_driven']+2);
}
Also, for cases where you just want to update one record, not many, you can just do a "read" rather than a "find('all')" - more info in the links below.
I would highly recommend reading all the way through these pages in the cake cook book:
http://book.cakephp.org/2.0/en/models/retrieving-your-data.html - Retrieving Data
http://book.cakephp.org/2.0/en/models/saving-your-data.html - Saving data
http://book.cakephp.org/2.0/en/models/deleting-data.html - Deleting data
All contain really important foundational info on how to work with Cake Models. Spend the time to really understand it now, and you'll save yourself countless headaches and code re-factors in future!

How to change a ARO parent in CakePHP

I am having trouble updating the aros table in my CakePHP app. Here is the code I've got:
$aro = new Aro();
$targetAro = $aro->find('first', array('conditions' => array('foreign_key' => intval($user['User']['id']))));
$newParent = $aro->find('first', array('conditions' => array('alias' => $userUpdate->level)));
$targetAro['Aro']['parent_id'] = $newParent['Aro']['id'];
$aro->updateAll($targetAro);
I've put in debug log statements to make sure that I'm getting the correct results from the find calls. But when I check the database after the updateAll call, the data is unchanged. Any ideas what might be going on?
I also tried calling $aro->setParent but I couldn't get that to work either.
I found the problem by tracing through the code that does a similar thing in the cake console. Instead of using $aro->updateAll($targetAro) I should be using $aro->saveAll($targetAro).
updateAll() is used to save changes to a number of entities. The first argument should be an array of values. saveAll() is used when you just want to save the changes to a single entity, as is the case in the code above.
Althought it seems like I should, I couldn't find error output anywhere in the logs or the script output because of the incorrect arguments I was passing to updateAll().

Getting related data using Wizard Component

The short version of this question is:
How can I take data that only exists in an array (ie: not saved in a model yet) and relate it to a value in a $this->find('list') array from a model?
The long version of this question is:
I'm using CakePHP and the Wizard Component for a 3-step application form.
The 3 form steps are contact, course and details.
After these steps, there is a 'review' step which I want to display all of the submissions from the previous 3 form steps for the user to check one last time before pressing submit. For the most part, this works very well. I just need to do the following in the controller:
function _prepareReview() {
$contact = $this->Wizard->read('contact');
$course = $this->Wizard->read('course');
$details = $this->Wizard->read('details');
$this->set(compact('contact','course','details'));
}
Then, in review.ctp I can reference things like $contact['Contact']['firstname']; to get the person's firstname etc.
However, the problem is getting data from 'related' models. For example, there's a "Nationality" field which is just an ID. In the 'details' step, I use find('list') to get a list of all nationalities from the Nationality model as a dropdown menu which displays properly and then Cake saves the corresponding ID as it should do.
But, when I come to the 'review' step in the Wizard Component, I only get the actual ID from the Wizard Component's array. I couldn't really expect to get anything else.
I can't see any obvious way to access $details[Detail][Nationality][name] (or something like this) from the context of the Wizard Component because setting recursive doesn't work because the data isn't actually in the model at this stage, it's just an array of form data.
So, in other words, I have data in an array (NOT coming from a model, but from a form subsmission) as follows:
Array
(
[Details] => Array
(
[firstname] => Test
[nationality_id] => 3
)
)
Then I have the following coming from a $this->Detail->Nationality->find('list') which looks like this:
Array
(
[0] => American
[1] => Australian
[2] => British
[3] => Canadian
)
So how can I get $details['Details']['nationality_id']; from the Wizard Component to display 'Canadian' instead of '3' for example? How to I make the relationship when only one of the arrays is coming from a model? I only need this to momentarily confirm all of the data to the user. The id '3' will, of course, be written to the Model once the user presses submit, and this is already working as it should do.
Or is this a completely crazy way of doing things and I should look at a totally different approach such as saving the data first? I don't really want to save it until the user clicks the final submit, though.
I can see what you are getting at here - CakePHP isn't automatically querying these related models for you (as you aren't pulling from the database) but you can't help but think you are missing out on some of the framework's free functionality.
If you were still using FormHelper::input() it would automatically select the correct option (given you perform a Model::find('list') and passed the options list to the view first), but I'm assuming you wish for the review screen to be free of form inputs (disabled or not).
The most straightforward approach would be to simply perform the same Model::find('list') calls you do for each step in the wizard, set the data from each to the view, and print out the appropriate value manually:
// controller
$nationalities = $this->Review->Details->Nationality->find('list');
$this->set(compact(/*..., */ 'nationalities'));
// view
<?php echo $nationalities['Nationality'][$details['Detail']['nationality_id']]; ?>
outputs 'Canadian' (the value for $nationalities['Nationality'][3])
It might be possible to get CakePHP to do it for you by calling DboSource::queryAssociation() just right - if your up for the challenge - but is probably overkill for this particular problem.
// model
$db =& ConnectionManager::getDataSource($this->useDbConfig);
$data = $db->queryAssociation($model, $linkModel, $type, $association, $assocData, $queryData, $external, $resultSet, $recursive, $stack) {

Resources