I have a form which looks like this:
Delete
[Publisher One ] []
[Publisher Two ] []
[Publisher Three] []
Add[ ]
So basically, every Publisher appears on the page in its own field. I can modify any of the Publisher names, delete any of the Publishers, or add a new publisher, all on one form, simply by saveAll-ing the form. I know that this will not hold up under 10,000 rows, but I am using CakePHP to remake an existing tool, and I am sure there will be a manageable number of rows.
The problem is that on first load, I have to pre-populate the fields. Now, from the CakePHP book, I am supposed to create the form with Model.n.field. However, the data I pull using a $this->Model->find('all') is in $data[n][Model][field] form. Am I going to have to mangle the data myself to get it in $data[Model][n][field] form, or is there an easy way to do that from within the find command, or perhaps a helper function to turn it from one into the other?
The Set class might be able to do the reprocessing for you - see CakeBook and an example. Also other Set class methods might be useful for you.
The difficulties in moving data in different forms around in Cake isn't the smoothest, I agree. But why don't you just do this:
pull your data in the controller. $data is an array with the nth record as the first index; then Modelname, then fieldname (sorry for the confusing wording -- this is just how you've done it above).
in your view:
foreach( $data as $publisher-id => $record )
{
echo '<tr><td>' . $record['Model']['publisher_name'] . '</td>';
echo '<td>' . $form->input( 'Model.publisher_id', array( 'value' => $model['Modelname']['publisher_id']) ) . '</td></tr>';
}
So I don't really think you need to mangle the data, or the arrays returned from the find operation. Try to just think about how you're going to output it, and you have to be a little clever about it.
Related
Whenever I'm displaying a user's name in my view file, I use:
echo $user['User']['name'];
The users table also has a 'company_name' field. If the user has a company name listed (in most cases, but not all), I would like to display their company name (and if they do not, I want to use the 'name' field). The code would look something like:
if(isset($user['User']['company_name']) && $user['User']['company_name'] != '') {
echo $user['User']['company_name'];
} else {
echo $user['User']['name'];
}
What is the proper way to handle this throughout multiple view files? Should I create a helper for it?
Being that this particular use case is such a small bit of presentation logic it would make more sense to me to build this within the scope of an Element rather than a Helper.
Now if at some point down the road (or now even) you decide that there is more functionality that you would like to extend similar to this presentation logic, perhaps in the form of a UsersHelper that would define various pieces of display logic relevant to the current user, then it would be wise to encapsulate that functionality within a Helper.
Here's a good example to get you started with Elements:
// In src/Template/Element/name_display.ctp
if(isset($user['User']['company_name']) && $user['User']['company_name'] != '') {
echo $user['User']['company_name'];
} else {
echo $user['User']['name'];
}
In your view file:
// Echo this wherever you need the name displayed
echo $this->element('name_display', [
'user' => $user // Sets the $user viewVar in the element
]);
And for sake of completeness, in your controller:
$this->set('user', $user);
In your spare time it would be a good idea just to check out the manual on some of the core helpers that are included with Cake to get an idea of how they are constructed to get a better idea of use cases in which you would want to create a Helper.
2.x Elements Documentation
3.x Elements Documentation
2.x Helpers Documentation
3.x Helpers Documentation
Good luck!
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).
I have scenario where I have 3 different models data being saved from one form, by means of the saveAll pass through method in CakePHP 2.x
The models on the form are:
Project
ProjectImage
ProjectAttachment
1 and 2 are saving perfectly, but I am having problems with the ProjectAttachment fields. If you look at the following image, the fields in question are at the bottom right, highlighted by a red frame: http://img.skitch.com/20120417-bnarwihc9mqm1b49cjy2bp13cf.jpg
Here is the situation:
I have named the fields as follows: *ProjectAttachment.0.project_id*, ProjectAttachment.0.name .... *ProjectAttachment.6.project_id* etc where the numbers are relative to the already present amount of attachments the user has added, as there is no limit. So if the user has ALREADY added 6 documents, the field would be called ProjectAttachment.7.id and so on. This is because of the naming convention when using the saveAll method.
I have made use of two hidden fields, one to store the user ID, the other to store the project ID.
The problem is that if the user does not fill in the Document Title and select a file to upload, the form fails! If I remove all validation rules, the form no longer fails BUT a blank ProjectAttachment record is inserted.
I suspect the problem may also be that the project_id and user_id fields (that are hidden) already have values in them?
I gave it some thought, and came up with a simple concept: In my controller, before the saveAll call, I would check to see if a blank Document Title field was submitted, and if so, I would completely eliminate the relevant array entry from the $this->request->data['ProjectAttachment'] array, but this did not seem to work.
To clarify, I am not looking for validation rules. If the user decides they only want to upload images and not touch the ProjectAttachment form, them the saveAll operation must not fail, it must simple not attempt to save the blank fields in the form, if this is at all possible.
Any suggestions?
Kind regards,
Simon
Well it seems that the solution was far simpler than I had initially thought! I was partially on the right track, and I had to unset the variable ProjectArray key in the $this->request->data array if I had encountered any blank fields.
I did this as follows:
if(!empty($this->request->data['ProjectAttachment'])){
foreach($this->request->data['ProjectAttachment'] as $key => $value){
if(is_array($value) && array_key_exists('upload_file', $value)){
if($value['upload_file']['name'] == ''){ // Blank document file
unset($this->request->data['ProjectAttachment']);
}
}
}
}
I hope someone finds this somehow useful in some form or another.
Kind regards,
Simon
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) {
I'm using CakePHP 1.2 and I'm just wondering if there is any side affect on passing the $this->data to the View from the Controller.
Ex:
// inside PostsController, I have this code:
$this->data['Posts'] = $this->Post->find('all');
instead of :
$posts = $this->Post->find('all');
$this->set(compact('posts'));
// inside the /posts/view, I access it like this:
<?php foreach ($this->data['Posts'] as $post) {....};?>
By doing this, I skipped the $this->set() from the controller all together. Does this violate any MVC pattern or any security issue that I might have overlook? I saw that using the Auth Component, $this->data contains the [_Token] array.
Thanks
You need to be aware of the different places that Cake Helpers automagically look for data, since that is were it makes a real difference. The Form Helper will fill in fields automatically based on the contents of $this->data. That's how form data persists when validation fails. OTOH, a <select> elements options array is automatically taken from the pluralized field name,
e.g. $form->select('Model.foo_id') will take its options from $foos if set.
As such, $this->data has its special place and shouldn't be used lightly, just as named variables have their use and shouldn't be ignored. Use both as appropriate. If you don't want to auto-set Form Helper content, set() your variables. IMHO it's also more readable to assign a variable name that hints at the data it contains. All your views operating on $this->data is less clear than one view operating on $foo and another on $bar.
In CakePHP 2.x you should use $this->request->data instead if plain $this->data, otherwise you might end up getting this error:
Indirect modification of overloaded property View::$data has no effect
$controller->data is meant for data posted to the control from view file.
$view->data is for general data.
I would avoid doing it to keep myself sane. besides you are typing more in view.
There is no good reason for setting $this->data directly except when working with forms.
Why break convention - Controller:set is there for a reason. If you want to pass data to the view for display or display logic purposes you should use the function provided instead of trying to co-opt Controller:data for unintended purposes.
Everything is easier from within CakePHP if you follow the rules and do things the expected, correct way.
In cakephp version 2.*, error occurs when you try to set data on $this->data